"use client"; import React, { useCallback, useMemo, useRef, useState } from "react"; import { ChevronLeft, ChevronRight, Calendar, Plus, Loader2, ZoomIn, ZoomOut, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { TimelineSchedulerComponentProps, ScheduleItem, ZoomLevel, DragEvent, ResizeEvent, } from "./types"; import { useTimelineData } from "./hooks/useTimelineData"; import { TimelineHeader, ResourceRow } from "./components"; import { zoomLevelOptions, defaultTimelineSchedulerConfig } from "./config"; /** * v2-timeline-scheduler 메인 컴포넌트 * * 간트차트 형태의 일정/계획 시각화 및 편집 컴포넌트 */ export function TimelineSchedulerComponent({ config, isDesignMode = false, formData, externalSchedules, externalResources, isLoading: externalLoading, error: externalError, componentId, onDragEnd, onResizeEnd, onScheduleClick, onCellClick, onAddSchedule, }: TimelineSchedulerComponentProps) { const containerRef = useRef(null); // 드래그/리사이즈 상태 const [dragState, setDragState] = useState<{ schedule: ScheduleItem; startX: number; startY: number; } | null>(null); const [resizeState, setResizeState] = useState<{ schedule: ScheduleItem; direction: "start" | "end"; startX: number; } | null>(null); // 타임라인 데이터 훅 const { schedules, resources, isLoading: hookLoading, error: hookError, zoomLevel, setZoomLevel, viewStartDate, viewEndDate, goToPrevious, goToNext, goToToday, updateSchedule, } = useTimelineData(config, externalSchedules, externalResources); const isLoading = externalLoading ?? hookLoading; const error = externalError ?? hookError; // 설정값 const rowHeight = config.rowHeight || defaultTimelineSchedulerConfig.rowHeight!; const headerHeight = config.headerHeight || defaultTimelineSchedulerConfig.headerHeight!; const resourceColumnWidth = config.resourceColumnWidth || defaultTimelineSchedulerConfig.resourceColumnWidth!; const cellWidthConfig = config.cellWidth || defaultTimelineSchedulerConfig.cellWidth!; const cellWidth = cellWidthConfig[zoomLevel] || 60; // 리소스가 없으면 스케줄의 resourceId로 자동 생성 const effectiveResources = useMemo(() => { if (resources.length > 0) { return resources; } // 스케줄에서 고유한 resourceId 추출하여 자동 리소스 생성 const uniqueResourceIds = new Set(); schedules.forEach((schedule) => { if (schedule.resourceId) { uniqueResourceIds.add(schedule.resourceId); } }); return Array.from(uniqueResourceIds).map((id) => ({ id, name: id, // resourceId를 이름으로 사용 })); }, [resources, schedules]); // 리소스별 스케줄 그룹화 const schedulesByResource = useMemo(() => { const grouped = new Map(); effectiveResources.forEach((resource) => { grouped.set(resource.id, []); }); schedules.forEach((schedule) => { const list = grouped.get(schedule.resourceId); if (list) { list.push(schedule); } else { // 리소스가 없는 스케줄은 첫 번째 리소스에 할당 const firstResource = effectiveResources[0]; if (firstResource) { const firstList = grouped.get(firstResource.id); if (firstList) { firstList.push(schedule); } } } }); return grouped; }, [schedules, effectiveResources]); // 줌 레벨 변경 const handleZoomIn = useCallback(() => { const levels: ZoomLevel[] = ["month", "week", "day"]; const currentIdx = levels.indexOf(zoomLevel); if (currentIdx < levels.length - 1) { setZoomLevel(levels[currentIdx + 1]); } }, [zoomLevel, setZoomLevel]); const handleZoomOut = useCallback(() => { const levels: ZoomLevel[] = ["month", "week", "day"]; const currentIdx = levels.indexOf(zoomLevel); if (currentIdx > 0) { setZoomLevel(levels[currentIdx - 1]); } }, [zoomLevel, setZoomLevel]); // 스케줄 클릭 핸들러 const handleScheduleClick = useCallback( (schedule: ScheduleItem) => { const resource = effectiveResources.find((r) => r.id === schedule.resourceId); if (resource && onScheduleClick) { onScheduleClick({ schedule, resource }); } }, [effectiveResources, onScheduleClick] ); // 빈 셀 클릭 핸들러 const handleCellClick = useCallback( (resourceId: string, date: Date) => { if (onCellClick) { onCellClick({ resourceId, date: date.toISOString().split("T")[0], }); } }, [onCellClick] ); // 드래그 시작 const handleDragStart = useCallback( (schedule: ScheduleItem, e: React.MouseEvent) => { setDragState({ schedule, startX: e.clientX, startY: e.clientY, }); }, [] ); // 드래그 종료 const handleDragEnd = useCallback(() => { if (dragState) { // TODO: 드래그 결과 계산 및 업데이트 setDragState(null); } }, [dragState]); // 리사이즈 시작 const handleResizeStart = useCallback( (schedule: ScheduleItem, direction: "start" | "end", e: React.MouseEvent) => { setResizeState({ schedule, direction, startX: e.clientX, }); }, [] ); // 리사이즈 종료 const handleResizeEnd = useCallback(() => { if (resizeState) { // TODO: 리사이즈 결과 계산 및 업데이트 setResizeState(null); } }, [resizeState]); // 추가 버튼 클릭 const handleAddClick = useCallback(() => { if (onAddSchedule && effectiveResources.length > 0) { onAddSchedule( effectiveResources[0].id, new Date().toISOString().split("T")[0] ); } }, [onAddSchedule, effectiveResources]); // 디자인 모드 플레이스홀더 if (isDesignMode) { return (

타임라인 스케줄러

{config.selectedTable ? `테이블: ${config.selectedTable}` : "테이블을 선택하세요"}

); } // 로딩 상태 if (isLoading) { return (
로딩 중...
); } // 에러 상태 if (error) { return (

오류 발생

{error}

); } // 스케줄 데이터 없음 if (schedules.length === 0) { return (

스케줄 데이터가 없습니다

좌측 테이블에서 품목을 선택하거나,
스케줄 생성 버튼을 눌러 스케줄을 생성하세요

); } return (
{/* 툴바 */} {config.showToolbar !== false && (
{/* 네비게이션 */}
{config.showNavigation !== false && ( <> )} {/* 현재 날짜 범위 표시 */} {viewStartDate.getFullYear()}년 {viewStartDate.getMonth() + 1}월{" "} {viewStartDate.getDate()}일 ~{" "} {viewEndDate.getMonth() + 1}월 {viewEndDate.getDate()}일
{/* 오른쪽 컨트롤 */}
{/* 줌 컨트롤 */} {config.showZoomControls !== false && (
{zoomLevelOptions.find((o) => o.value === zoomLevel)?.label}
)} {/* 추가 버튼 */} {config.showAddButton !== false && config.editable && ( )}
)} {/* 타임라인 본문 */}
{/* 헤더 */} {/* 리소스 행들 */}
{effectiveResources.map((resource) => ( ))}
); }