Files
sam-react-prod/src/components/hr/CalendarManagement/BulkRegistrationDialog.tsx
김보곤 31f6f7c29f fix: [calendar] 대량 등록 다이얼로그 기존 데이터 표시 기능 추가
- BulkRegistrationDialog에 schedules prop 추가
- 다이얼로그 열릴 때 기존 등록 데이터를 텍스트로 변환하여 표시
- MNG 대량 등록과 동일한 동작
2026-02-26 19:27:52 +09:00

209 lines
7.6 KiB
TypeScript

'use client';
import { useState, useMemo, useEffect } from 'react';
import { Loader2, Info } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import type { BulkScheduleItem, CalendarSchedule, CalendarScheduleType } from './types';
import { SCHEDULE_TYPE_LABELS, SCHEDULE_TYPE_BADGE_COLORS } from './types';
import { bulkCreateCalendarSchedules } from './actions';
interface BulkRegistrationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
schedules?: CalendarSchedule[];
}
const TYPE_KEYWORD_MAP: Record<string, CalendarScheduleType> = {
'공휴일': 'publicHoliday',
'세무일정': 'taxDeadline',
'회사지정': 'companyEvent',
'회사일정': 'companyEvent',
'대체휴일': 'substituteHoliday',
'임시휴일': 'temporaryHoliday',
};
function parseBulkText(text: string): BulkScheduleItem[] {
const lines = text.split('\n').filter((line) => line.trim());
const items: BulkScheduleItem[] = [];
const datePattern = /^(\d{4}-\d{2}-\d{2})(?:~(\d{4}-\d{2}-\d{2}))?\s+(.+)$/;
for (const line of lines) {
const match = line.trim().match(datePattern);
if (!match) continue;
const startDate = match[1];
const endDate = match[2] || startDate;
let nameAndType = match[3].trim();
let type: CalendarScheduleType = 'publicHoliday';
const typeMatch = nameAndType.match(/\[(.+?)\]\s*$/);
if (typeMatch) {
const keyword = typeMatch[1];
if (TYPE_KEYWORD_MAP[keyword]) {
type = TYPE_KEYWORD_MAP[keyword];
}
nameAndType = nameAndType.replace(/\[.+?\]\s*$/, '').trim();
}
const isValidDate = (d: string) => !isNaN(new Date(d).getTime());
items.push({
name: nameAndType,
type,
startDate,
endDate,
isValid: isValidDate(startDate) && isValidDate(endDate) && nameAndType.length > 0,
});
}
return items;
}
function schedulesToText(schedules: CalendarSchedule[]): string {
return schedules.map((s) => {
const datePart = s.startDate === s.endDate ? s.startDate : `${s.startDate}~${s.endDate}`;
const typePart = s.type !== 'publicHoliday' ? ` [${SCHEDULE_TYPE_LABELS[s.type]}]` : '';
return `${datePart} ${s.name}${typePart}`;
}).join('\n');
}
export function BulkRegistrationDialog({ open, onOpenChange, onSuccess, schedules }: BulkRegistrationDialogProps) {
const [text, setText] = useState('');
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
if (open) {
if (schedules && schedules.length > 0) {
setText(schedulesToText(schedules));
} else {
setText('');
}
}
}, [open, schedules]);
const parsedItems = useMemo(() => parseBulkText(text), [text]);
const validItems = parsedItems.filter((item) => item.isValid);
const handleSubmit = async () => {
if (validItems.length === 0) {
toast.error('등록할 일정이 없습니다.');
return;
}
setIsSaving(true);
try {
const result = await bulkCreateCalendarSchedules(validItems);
if (result.success) {
toast.success(`${validItems.length}건의 일정이 등록되었습니다.`);
setText('');
onOpenChange(false);
onSuccess();
} else {
toast.error(result.error || '대량 등록에 실패했습니다.');
}
} catch {
toast.error('등록 중 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="rounded-md border border-blue-200 bg-blue-50 p-3 text-sm">
<div className="flex items-start gap-2">
<Info className="h-4 w-4 mt-0.5 text-blue-600 shrink-0" />
<div className="text-blue-800 space-y-1">
<p className="font-medium"> </p>
<ul className="list-disc list-inside space-y-0.5 text-xs">
<li><code className="bg-blue-100 px-1 rounded">YYYY-MM-DD </code> - </li>
<li><code className="bg-blue-100 px-1 rounded">YYYY-MM-DD~YYYY-MM-DD </code> - </li>
<li><code className="bg-blue-100 px-1 rounded">YYYY-MM-DD []</code> - </li>
</ul>
<p className="text-xs text-blue-600">
유형: 공휴일, , , , ( )
</p>
</div>
</div>
</div>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder={`2026-01-01 신정\n2026-01-28~2026-01-30 설날\n2026-03-25 부가세 신고 [세무일정]\n2026-05-05 어린이날`}
rows={8}
className="font-mono text-sm"
/>
{parsedItems.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium">
: {validItems.length}
{parsedItems.length !== validItems.length && (
<span className="text-destructive ml-1">
( {parsedItems.length - validItems.length})
</span>
)}
</p>
<div className="max-h-[200px] overflow-y-auto border rounded-md">
<table className="w-full text-sm">
<thead className="bg-muted sticky top-0">
<tr>
<th className="text-left p-2 font-medium"></th>
<th className="text-left p-2 font-medium"></th>
<th className="text-left p-2 font-medium"></th>
</tr>
</thead>
<tbody>
{parsedItems.map((item, idx) => (
<tr
key={idx}
className={!item.isValid ? 'bg-red-50 text-red-600' : 'hover:bg-muted/50'}
>
<td className="p-2">{item.name || '-'}</td>
<td className="p-2 whitespace-nowrap">
{item.startDate === item.endDate
? item.startDate
: `${item.startDate} ~ ${item.endDate}`}
</td>
<td className="p-2">
<Badge className={SCHEDULE_TYPE_BADGE_COLORS[item.type]} variant="secondary">
{SCHEDULE_TYPE_LABELS[item.type]}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isSaving}>
</Button>
<Button onClick={handleSubmit} disabled={isSaving || validItems.length === 0}>
{isSaving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{validItems.length > 0 ? `${validItems.length}건 등록` : '등록'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}