fix: TypeScript 타입 오류 수정 및 설정 페이지 추가

- BOMItem Omit 타입 시그니처 통일 (useTemplateManagement, SectionsTab, ItemMasterContext)
- HeadersInit → Record<string, string> 타입 변경
- Zustand useShallow 마이그레이션 (zustand/react/shallow)
- DataTable, ListPageTemplate 제네릭 타입 제약 추가
- 설정 관리 페이지 추가 (직급, 직책, 휴가정책, 근무일정, 권한)
- HR 관리 페이지 추가 (급여, 휴가)
- 단가관리 페이지 리팩토링

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-09 18:07:47 +09:00
parent 48dbba0e5f
commit ded0bc2439
98 changed files with 10608 additions and 1204 deletions

View File

@@ -0,0 +1,286 @@
'use client';
import { useState, useEffect } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Clock, Save } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { TimePicker } from '@/components/ui/time-picker';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { toast } from 'sonner';
import type {
WorkScheduleSettings,
EmploymentType,
DayOfWeek,
} from './types';
import {
DEFAULT_WORK_SCHEDULE,
EMPLOYMENT_TYPE_LABELS,
DAY_OF_WEEK_LABELS,
} from './types';
// 고용 형태별 기본 설정
const EMPLOYMENT_TYPE_DEFAULTS: Record<EmploymentType, Partial<WorkScheduleSettings>> = {
regular: {
workDays: ['mon', 'tue', 'wed', 'thu', 'fri'],
workStartTime: '09:00',
workEndTime: '18:00',
weeklyWorkHours: 40,
weeklyOvertimeHours: 12,
},
contract: {
workDays: ['mon', 'tue', 'wed', 'thu', 'fri'],
workStartTime: '09:00',
workEndTime: '18:00',
weeklyWorkHours: 40,
weeklyOvertimeHours: 12,
},
dispatch: {
workDays: ['mon', 'tue', 'wed', 'thu', 'fri'],
workStartTime: '09:00',
workEndTime: '18:00',
weeklyWorkHours: 40,
weeklyOvertimeHours: 12,
},
outsourcing: {
workDays: ['mon', 'tue', 'wed', 'thu', 'fri'],
workStartTime: '09:00',
workEndTime: '18:00',
weeklyWorkHours: 40,
weeklyOvertimeHours: 12,
},
partTime: {
workDays: ['mon', 'tue', 'wed'],
workStartTime: '10:00',
workEndTime: '15:00',
weeklyWorkHours: 15,
weeklyOvertimeHours: 0,
},
};
export function WorkScheduleManagement() {
// 현재 선택된 고용 형태
const [selectedEmploymentType, setSelectedEmploymentType] = useState<EmploymentType>('regular');
// 근무 설정
const [settings, setSettings] = useState<WorkScheduleSettings>(DEFAULT_WORK_SCHEDULE);
// 고용 형태 변경 시 기본값 로드
useEffect(() => {
const defaults = EMPLOYMENT_TYPE_DEFAULTS[selectedEmploymentType];
setSettings(prev => ({
...prev,
employmentType: selectedEmploymentType,
...defaults,
}));
}, [selectedEmploymentType]);
// 근무일 토글
const toggleWorkDay = (day: DayOfWeek) => {
setSettings(prev => ({
...prev,
workDays: prev.workDays.includes(day)
? prev.workDays.filter(d => d !== day)
: [...prev.workDays, day],
}));
};
// 저장
const handleSave = () => {
// 실제로는 API 호출
console.log('저장할 설정:', settings);
toast.success(`${EMPLOYMENT_TYPE_LABELS[selectedEmploymentType]} 근무 설정이 저장되었습니다.`);
};
const ALL_DAYS: DayOfWeek[] = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
return (
<PageLayout>
<PageHeader
title="근무관리"
description="고용 형태별 근무 시간을 설정합니다."
icon={Clock}
/>
<div className="space-y-6">
{/* 고용 형태 선택 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"> </CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label htmlFor="employment-type"> </Label>
<Select
value={selectedEmploymentType}
onValueChange={(value: EmploymentType) => setSelectedEmploymentType(value)}
>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(EMPLOYMENT_TYPE_LABELS).map(([key, label]) => (
<SelectItem key={key} value={key}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* 주간 근무일 설정 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"> </CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-4">
{ALL_DAYS.map((day) => (
<label
key={day}
className="flex items-center gap-2 cursor-pointer"
>
<Checkbox
checked={settings.workDays.includes(day)}
onCheckedChange={() => toggleWorkDay(day)}
/>
<span className="text-sm font-medium">
{DAY_OF_WEEK_LABELS[day]}
</span>
</label>
))}
</div>
</CardContent>
</Card>
{/* 1일 기준 근로시간 */}
<Card>
<CardHeader>
<CardTitle className="text-lg">1 </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<Label> </Label>
<TimePicker
value={settings.workStartTime}
onChange={(value) => setSettings(prev => ({ ...prev, workStartTime: value }))}
className="w-40"
minuteStep={1}
/>
</div>
<div className="space-y-2">
<Label> </Label>
<TimePicker
value={settings.workEndTime}
onChange={(value) => setSettings(prev => ({ ...prev, workEndTime: value }))}
className="w-40"
minuteStep={1}
/>
</div>
</div>
</CardContent>
</Card>
{/* 주당 근로시간 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="weekly-hours"> </Label>
<div className="flex items-center gap-2">
<Input
id="weekly-hours"
type="number"
min={0}
max={52}
value={settings.weeklyWorkHours}
onChange={(e) =>
setSettings(prev => ({ ...prev, weeklyWorkHours: parseInt(e.target.value) || 0 }))
}
className="w-24"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="overtime-hours"> </Label>
<div className="flex items-center gap-2">
<Input
id="overtime-hours"
type="number"
min={0}
max={52}
value={settings.weeklyOvertimeHours}
onChange={(e) =>
setSettings(prev => ({ ...prev, weeklyOvertimeHours: parseInt(e.target.value) || 0 }))
}
className="w-24"
/>
<span className="text-sm text-muted-foreground"></span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 1일 기준 휴게시간 */}
<Card>
<CardHeader>
<CardTitle className="text-lg">1 </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<Label> </Label>
<TimePicker
value={settings.breakStartTime}
onChange={(value) => setSettings(prev => ({ ...prev, breakStartTime: value }))}
className="w-40"
minuteStep={1}
/>
</div>
<div className="space-y-2">
<Label> </Label>
<TimePicker
value={settings.breakEndTime}
onChange={(value) => setSettings(prev => ({ ...prev, breakEndTime: value }))}
className="w-40"
minuteStep={1}
/>
</div>
</div>
</CardContent>
</Card>
{/* 저장 버튼 */}
<div className="flex justify-end">
<Button onClick={handleSave} size="lg">
<Save className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 안내 문구 */}
<p className="text-sm text-muted-foreground">
. .
</p>
</div>
</PageLayout>
);
}

View File

@@ -0,0 +1,51 @@
/**
* 근무관리 타입 정의 (PDF 56페이지 기준)
*/
// 고용 형태
export type EmploymentType = 'regular' | 'contract' | 'dispatch' | 'outsourcing' | 'partTime';
export const EMPLOYMENT_TYPE_LABELS: Record<EmploymentType, string> = {
regular: '정규직',
contract: '계약직',
dispatch: '파견직',
outsourcing: '용역직',
partTime: '시간제 근로자',
};
// 요일
export type DayOfWeek = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
export const DAY_OF_WEEK_LABELS: Record<DayOfWeek, string> = {
mon: '월',
tue: '화',
wed: '수',
thu: '목',
fri: '금',
sat: '토',
sun: '일',
};
// 근무 설정
export interface WorkScheduleSettings {
employmentType: EmploymentType;
workDays: DayOfWeek[]; // 주간 근무일
workStartTime: string; // 출근 시간 (HH:mm)
workEndTime: string; // 퇴근 시간 (HH:mm)
weeklyWorkHours: number; // 주당 기준 근로시간
weeklyOvertimeHours: number; // 주당 연장 근로시간
breakStartTime: string; // 휴게 시작 시간 (HH:mm)
breakEndTime: string; // 휴게 종료 시간 (HH:mm)
}
// 기본 설정값
export const DEFAULT_WORK_SCHEDULE: WorkScheduleSettings = {
employmentType: 'regular',
workDays: ['mon', 'tue', 'wed', 'thu', 'fri'],
workStartTime: '09:00',
workEndTime: '18:00',
weeklyWorkHours: 40,
weeklyOvertimeHours: 12,
breakStartTime: '12:00',
breakEndTime: '13:00',
};