- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가 - MobileCard 컴포넌트 통합 (ListMobileCard 제거) - IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈) - IntegratedDetailTemplate 타이틀 중복 수정 - 문서 시스템 컴포넌트 추가 - 헤더 벨 아이콘 포커스 스타일 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { PageLayout } from '@/components/organisms/PageLayout';
|
|
import { PageHeader } from '@/components/organisms/PageHeader';
|
|
import { Clock, Save, Loader2 } from 'lucide-react';
|
|
import { getWorkSetting, updateWorkSetting } from './actions';
|
|
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 { QuantityInput } from '@/components/ui/quantity-input';
|
|
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() {
|
|
// 현재 선택된 고용 형태 (UI 전용 - 고용 형태별 기본값 표시용)
|
|
const [selectedEmploymentType, setSelectedEmploymentType] = useState<EmploymentType>('regular');
|
|
|
|
// 근무 설정
|
|
const [settings, setSettings] = useState<WorkScheduleSettings>(DEFAULT_WORK_SCHEDULE);
|
|
|
|
// 로딩 상태
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
// API에서 설정 로드
|
|
const loadData = useCallback(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const result = await getWorkSetting();
|
|
if (result.success && result.data) {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
workDays: result.data!.workDays as DayOfWeek[],
|
|
workStartTime: result.data!.workStartTime,
|
|
workEndTime: result.data!.workEndTime,
|
|
weeklyWorkHours: result.data!.weeklyWorkHours,
|
|
weeklyOvertimeHours: result.data!.weeklyOvertimeHours,
|
|
breakStartTime: result.data!.breakStartTime,
|
|
breakEndTime: result.data!.breakEndTime,
|
|
}));
|
|
} else if (result.error) {
|
|
toast.error(result.error);
|
|
}
|
|
} catch {
|
|
toast.error('설정을 불러오는데 실패했습니다.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// 초기 로드
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
// 고용 형태 변경 시 기본값 로드 (UI 전용 - 로컬 기본값 표시)
|
|
const handleEmploymentTypeChange = (type: EmploymentType) => {
|
|
setSelectedEmploymentType(type);
|
|
const defaults = EMPLOYMENT_TYPE_DEFAULTS[type];
|
|
setSettings(prev => ({
|
|
...prev,
|
|
employmentType: type,
|
|
...defaults,
|
|
}));
|
|
};
|
|
|
|
// 근무일 토글
|
|
const toggleWorkDay = (day: DayOfWeek) => {
|
|
setSettings(prev => ({
|
|
...prev,
|
|
workDays: prev.workDays.includes(day)
|
|
? prev.workDays.filter(d => d !== day)
|
|
: [...prev.workDays, day],
|
|
}));
|
|
};
|
|
|
|
// 저장
|
|
const handleSave = async () => {
|
|
setIsSaving(true);
|
|
try {
|
|
const result = await updateWorkSetting({
|
|
workDays: settings.workDays,
|
|
workStartTime: settings.workStartTime,
|
|
workEndTime: settings.workEndTime,
|
|
weeklyWorkHours: settings.weeklyWorkHours,
|
|
weeklyOvertimeHours: settings.weeklyOvertimeHours,
|
|
breakStartTime: settings.breakStartTime,
|
|
breakEndTime: settings.breakEndTime,
|
|
});
|
|
|
|
if (result.success) {
|
|
toast.success('근무 설정이 저장되었습니다.');
|
|
} else {
|
|
toast.error(result.error || '저장에 실패했습니다.');
|
|
}
|
|
} catch {
|
|
toast.error('서버 오류가 발생했습니다.');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
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) => handleEmploymentTypeChange(value)}
|
|
disabled={isLoading}
|
|
>
|
|
<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">
|
|
<QuantityInput
|
|
id="weekly-hours"
|
|
min={0}
|
|
max={52}
|
|
value={settings.weeklyWorkHours}
|
|
onChange={(value) =>
|
|
setSettings(prev => ({ ...prev, weeklyWorkHours: 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">
|
|
<QuantityInput
|
|
id="overtime-hours"
|
|
min={0}
|
|
max={52}
|
|
value={settings.weeklyOvertimeHours}
|
|
onChange={(value) =>
|
|
setSettings(prev => ({ ...prev, weeklyOvertimeHours: 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" disabled={isLoading || isSaving}>
|
|
{isSaving ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
저장 중...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="h-4 w-4 mr-2" />
|
|
저장
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 안내 문구 */}
|
|
<p className="text-sm text-muted-foreground">
|
|
※ 근무 설정은 회사 전체에 적용됩니다. 설정 변경 후 반드시 저장 버튼을 클릭하세요.
|
|
</p>
|
|
</div>
|
|
</PageLayout>
|
|
);
|
|
}
|