- CEO 대시보드 컴포넌트 추가 - AuthenticatedLayout 개선 - 각 모듈 actions.ts 에러 핸들링 개선 - API fetch-wrapper, refresh-token 로직 개선 - ReceivablesStatus 컴포넌트 업데이트 - globals.css 스타일 업데이트 - 기타 다수 컴포넌트 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
289 lines
9.2 KiB
TypeScript
289 lines
9.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { TimePicker } from '@/components/ui/time-picker';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import type { CalendarScheduleItem } from '../types';
|
|
|
|
// 색상 옵션
|
|
const COLOR_OPTIONS = [
|
|
{ value: 'green', label: '녹색', className: 'bg-green-500' },
|
|
{ value: 'blue', label: '파란색', className: 'bg-blue-500' },
|
|
{ value: 'red', label: '빨간색', className: 'bg-red-500' },
|
|
{ value: 'yellow', label: '노란색', className: 'bg-yellow-500' },
|
|
{ value: 'purple', label: '보라색', className: 'bg-purple-500' },
|
|
];
|
|
|
|
// 부서 목록 (목업)
|
|
const DEPARTMENT_OPTIONS = [
|
|
{ value: 'all', label: '전체' },
|
|
{ value: 'sales', label: '영업부' },
|
|
{ value: 'production', label: '생산부' },
|
|
{ value: 'quality', label: '품질부' },
|
|
{ value: 'management', label: '경영지원부' },
|
|
];
|
|
|
|
interface ScheduleFormData {
|
|
title: string;
|
|
department: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
isAllDay: boolean; // 종일 여부 (true: 종일, false: 시간 지정)
|
|
startTime: string;
|
|
endTime: string;
|
|
color: string;
|
|
content: string;
|
|
}
|
|
|
|
interface ScheduleDetailModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
schedule: CalendarScheduleItem | null;
|
|
onSave: (data: ScheduleFormData) => void;
|
|
onDelete?: (id: string) => void;
|
|
}
|
|
|
|
export function ScheduleDetailModal({
|
|
isOpen,
|
|
onClose,
|
|
schedule,
|
|
onSave,
|
|
onDelete,
|
|
}: ScheduleDetailModalProps) {
|
|
const isEditMode = schedule && schedule.id !== '';
|
|
|
|
const [formData, setFormData] = useState<ScheduleFormData>({
|
|
title: '',
|
|
department: 'all',
|
|
startDate: '',
|
|
endDate: '',
|
|
isAllDay: true, // 기본값: 종일
|
|
startTime: '09:00',
|
|
endTime: '10:00',
|
|
color: 'green',
|
|
content: '',
|
|
});
|
|
|
|
// schedule이 변경될 때 폼 데이터 초기화
|
|
useEffect(() => {
|
|
if (schedule) {
|
|
// 시간이 있으면 종일 아님, 없으면 종일
|
|
const hasTimeValue = !!(schedule.startTime || schedule.endTime);
|
|
setFormData({
|
|
title: schedule.title || '',
|
|
department: schedule.department || 'all',
|
|
startDate: schedule.startDate || '',
|
|
endDate: schedule.endDate || schedule.startDate || '',
|
|
isAllDay: !hasTimeValue, // 시간이 없으면 종일
|
|
startTime: schedule.startTime || '09:00',
|
|
endTime: schedule.endTime || '10:00',
|
|
color: schedule.color || 'green',
|
|
content: '',
|
|
});
|
|
}
|
|
}, [schedule]);
|
|
|
|
const handleFieldChange = useCallback(
|
|
(field: keyof ScheduleFormData, value: string | boolean) => {
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
},
|
|
[]
|
|
);
|
|
|
|
const handleSave = useCallback(() => {
|
|
onSave(formData);
|
|
onClose();
|
|
}, [formData, onSave, onClose]);
|
|
|
|
const handleDelete = useCallback(() => {
|
|
if (schedule?.id && onDelete) {
|
|
onDelete(schedule.id);
|
|
onClose();
|
|
}
|
|
}, [schedule, onDelete, onClose]);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
onClose();
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={(open) => !open && handleCancel()}>
|
|
<DialogContent className="w-[95vw] max-w-[480px] sm:max-w-[480px] p-6">
|
|
<DialogHeader className="pb-2">
|
|
<DialogTitle className="text-lg font-bold">일정 상세</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6 py-4">
|
|
{/* 제목 */}
|
|
<div className="flex items-center gap-6">
|
|
<label className="w-10 text-sm font-medium text-gray-700 shrink-0">
|
|
제목
|
|
</label>
|
|
<Input
|
|
value={formData.title}
|
|
onChange={(e) => handleFieldChange('title', e.target.value)}
|
|
placeholder="제목"
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
|
|
{/* 대상 (부서) */}
|
|
<div className="flex items-center gap-6">
|
|
<label className="w-10 text-sm font-medium text-gray-700 shrink-0">
|
|
대상
|
|
</label>
|
|
<Select
|
|
value={formData.department}
|
|
onValueChange={(value) => handleFieldChange('department', value)}
|
|
>
|
|
<SelectTrigger className="flex-1">
|
|
<SelectValue placeholder="부서명" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DEPARTMENT_OPTIONS.map((dept) => (
|
|
<SelectItem key={dept.value} value={dept.value}>
|
|
{dept.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 기간 */}
|
|
<div className="flex items-center gap-6">
|
|
<label className="w-10 text-sm font-medium text-gray-700 shrink-0">
|
|
기간
|
|
</label>
|
|
<div className="flex-1 flex items-center gap-2">
|
|
<Input
|
|
type="date"
|
|
value={formData.startDate}
|
|
onChange={(e) => handleFieldChange('startDate', e.target.value)}
|
|
className="flex-1"
|
|
/>
|
|
<span className="text-gray-400 px-1">~</span>
|
|
<Input
|
|
type="date"
|
|
value={formData.endDate}
|
|
onChange={(e) => handleFieldChange('endDate', e.target.value)}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 시간 */}
|
|
<div className="flex items-start gap-6">
|
|
<label className="w-10 text-sm font-medium text-gray-700 shrink-0 pt-2">
|
|
시간
|
|
</label>
|
|
<div className="flex-1 space-y-3">
|
|
{/* 종일 체크박스 */}
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="isAllDay"
|
|
checked={formData.isAllDay}
|
|
onCheckedChange={(checked) =>
|
|
handleFieldChange('isAllDay', checked === true)
|
|
}
|
|
/>
|
|
<label htmlFor="isAllDay" className="text-sm text-gray-600 cursor-pointer">
|
|
종일
|
|
</label>
|
|
</div>
|
|
{/* 시간 선택 (종일 체크 해제 시 표시) */}
|
|
{!formData.isAllDay && (
|
|
<div className="flex items-center gap-2">
|
|
<TimePicker
|
|
value={formData.startTime}
|
|
onChange={(value) => handleFieldChange('startTime', value)}
|
|
placeholder="시작 시간"
|
|
className="flex-1"
|
|
minuteStep={5}
|
|
/>
|
|
<span className="text-gray-400 px-1">~</span>
|
|
<TimePicker
|
|
value={formData.endTime}
|
|
onChange={(value) => handleFieldChange('endTime', value)}
|
|
placeholder="종료 시간"
|
|
className="flex-1"
|
|
minuteStep={5}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 색상 */}
|
|
<div className="flex items-center gap-6">
|
|
<label className="w-10 text-sm font-medium text-gray-700 shrink-0">
|
|
색상
|
|
</label>
|
|
<div className="flex gap-3">
|
|
{COLOR_OPTIONS.map((color) => (
|
|
<button
|
|
key={color.value}
|
|
type="button"
|
|
className={`w-8 h-8 rounded-full ${color.className} transition-all ${
|
|
formData.color === color.value
|
|
? 'ring-2 ring-offset-2 ring-gray-400'
|
|
: 'hover:scale-110'
|
|
}`}
|
|
onClick={() => handleFieldChange('color', color.value)}
|
|
title={color.label}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 내용 */}
|
|
<div className="flex items-start gap-6">
|
|
<label className="w-10 text-sm font-medium text-gray-700 shrink-0 pt-2">
|
|
내용
|
|
</label>
|
|
<Textarea
|
|
value={formData.content}
|
|
onChange={(e) => handleFieldChange('content', e.target.value)}
|
|
placeholder="내용"
|
|
className="flex-1 min-h-[120px] resize-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="flex gap-2 pt-2">
|
|
{isEditMode && onDelete && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleDelete}
|
|
className="bg-gray-800 text-white hover:bg-gray-900"
|
|
>
|
|
삭제
|
|
</Button>
|
|
)}
|
|
<Button
|
|
onClick={handleSave}
|
|
className="bg-gray-800 text-white hover:bg-gray-900"
|
|
>
|
|
{isEditMode ? '수정' : '등록'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |