Files
sam-react-prod/src/components/settings/WorkScheduleManagement/index.tsx
유병철 835c06ce94 feat(WEB): 입력 컴포넌트 공통화 및 UI 개선
- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가
- MobileCard 컴포넌트 통합 (ListMobileCard 제거)
- IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈)
- IntegratedDetailTemplate 타이틀 중복 수정
- 문서 시스템 컴포넌트 추가
- 헤더 벨 아이콘 포커스 스타일 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 20:56:17 +09:00

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>
);
}