- BulkRegistrationDialog에 schedules prop 추가 - 다이얼로그 열릴 때 기존 등록 데이터를 텍스트로 변환하여 표시 - MNG 대량 등록과 동일한 동작
209 lines
7.6 KiB
TypeScript
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>
|
|
);
|
|
}
|