"use client"; /** * V2 타임라인 스케줄러 설정 패널 * 토스식 단계별 UX: 스케줄 데이터 설정 -> 소스 데이터 설정 -> 리소스 설정 -> 표시 설정(접힘) -> 고급 설정(접힘) */ import React, { useState, useEffect } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Settings, ChevronDown, Check, ChevronsUpDown, Database, Users, Layers } from "lucide-react"; import { cn } from "@/lib/utils"; import { tableTypeApi } from "@/lib/api/screen"; import type { TimelineSchedulerConfig, ScheduleType, SourceDataConfig, ResourceFieldMapping, FieldMapping, ZoomLevel } from "@/lib/registry/components/v2-timeline-scheduler/types"; import { zoomLevelOptions, scheduleTypeOptions } from "@/lib/registry/components/v2-timeline-scheduler/config"; interface V2TimelineSchedulerConfigPanelProps { config: TimelineSchedulerConfig; onChange: (config: Partial) => void; } interface TableInfo { tableName: string; displayName: string; } interface ColumnInfo { columnName: string; displayName: string; } export const V2TimelineSchedulerConfigPanel: React.FC = ({ config, onChange, }) => { const [tables, setTables] = useState([]); const [sourceColumns, setSourceColumns] = useState([]); const [resourceColumns, setResourceColumns] = useState([]); const [scheduleColumns, setScheduleColumns] = useState([]); const [loading, setLoading] = useState(false); const [sourceTableOpen, setSourceTableOpen] = useState(false); const [resourceTableOpen, setResourceTableOpen] = useState(false); const [customTableOpen, setCustomTableOpen] = useState(false); const [displayOpen, setDisplayOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); useEffect(() => { const loadTables = async () => { setLoading(true); try { const tableList = await tableTypeApi.getTables(); if (Array.isArray(tableList)) { setTables( tableList.map((t: any) => ({ tableName: t.table_name || t.tableName, displayName: t.display_name || t.displayName || t.table_name || t.tableName, })) ); } } catch (err) { console.error("테이블 목록 로드 오류:", err); } finally { setLoading(false); } }; loadTables(); }, []); useEffect(() => { const loadSourceColumns = async () => { if (!config.sourceConfig?.tableName) { setSourceColumns([]); return; } try { const columns = await tableTypeApi.getColumns(config.sourceConfig.tableName); if (Array.isArray(columns)) { setSourceColumns( columns.map((col: any) => ({ columnName: col.column_name || col.columnName, displayName: col.display_name || col.displayName || col.column_name || col.columnName, })) ); } } catch (err) { console.error("소스 컬럼 로드 오류:", err); setSourceColumns([]); } }; loadSourceColumns(); }, [config.sourceConfig?.tableName]); useEffect(() => { const loadResourceColumns = async () => { if (!config.resourceTable) { setResourceColumns([]); return; } try { const columns = await tableTypeApi.getColumns(config.resourceTable); if (Array.isArray(columns)) { setResourceColumns( columns.map((col: any) => ({ columnName: col.column_name || col.columnName, displayName: col.display_name || col.displayName || col.column_name || col.columnName, })) ); } } catch (err) { console.error("리소스 컬럼 로드 오류:", err); setResourceColumns([]); } }; loadResourceColumns(); }, [config.resourceTable]); // 커스텀 테이블 또는 schedule_mng 컬럼 로드 useEffect(() => { const loadScheduleColumns = async () => { const tableName = config.useCustomTable && config.customTableName ? config.customTableName : "schedule_mng"; try { const columns = await tableTypeApi.getColumns(tableName); if (Array.isArray(columns)) { setScheduleColumns( columns.map((col: any) => ({ columnName: col.column_name || col.columnName, displayName: col.display_name || col.displayName || col.column_name || col.columnName, })) ); } } catch (err) { console.error("스케줄 테이블 컬럼 로드 오류:", err); setScheduleColumns([]); } }; loadScheduleColumns(); }, [config.useCustomTable, config.customTableName]); const updateConfig = (updates: Partial) => { onChange({ ...config, ...updates }); }; const updateSourceConfig = (updates: Partial) => { updateConfig({ sourceConfig: { ...config.sourceConfig, ...updates, }, }); }; const updateFieldMapping = (field: string, value: string) => { updateConfig({ fieldMapping: { ...config.fieldMapping, id: config.fieldMapping?.id || "id", resourceId: config.fieldMapping?.resourceId || "resource_id", title: config.fieldMapping?.title || "title", startDate: config.fieldMapping?.startDate || "start_date", endDate: config.fieldMapping?.endDate || "end_date", [field]: value, }, }); }; const updateResourceFieldMapping = (field: string, value: string) => { updateConfig({ resourceFieldMapping: { ...config.resourceFieldMapping, id: config.resourceFieldMapping?.id || "id", name: config.resourceFieldMapping?.name || "name", [field]: value, }, }); }; return (
{/* ─── 1단계: 스케줄 데이터 테이블 설정 ─── */}

스케줄 데이터 테이블

스케줄 데이터를 저장/조회할 테이블을 설정해요

{/* 스케줄 타입 */}
스케줄 타입
{/* 커스텀 테이블 사용 여부 */}

커스텀 테이블 사용

OFF: schedule_mng 사용

updateConfig({ useCustomTable: v })} />
{/* 커스텀 테이블 선택 (Combobox) */} {config.useCustomTable && (
커스텀 테이블명 { return value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0; }} > 테이블을 찾을 수 없습니다. {tables.map((table) => ( { updateConfig({ customTableName: table.tableName, selectedTable: table.tableName }); setCustomTableOpen(false); }} className="text-xs" >
{table.displayName} {table.tableName}
))}
)} {/* 디자인 모드 표시용 테이블명 (selectedTable) */} {!config.useCustomTable && (
디자인 모드 표시 테이블 updateConfig({ selectedTable: e.target.value })} placeholder="schedule_mng" className="h-7 w-[140px] text-xs" />
)} {/* 스케줄 필드 매핑 */}

스케줄 필드 매핑

ID 필드 *
리소스 ID 필드 *
제목 필드 *
시작일 필드 *
종료일 필드 *
상태 필드
진행률 필드
색상 필드
{/* ─── 2단계: 소스 데이터 설정 ─── */}

소스 데이터 설정

스케줄 자동 생성 시 참조할 원본 데이터를 설정해요

{/* 소스 테이블 Combobox */}
소스 테이블 (수주/작업요청 등) { return value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0; }} > 테이블을 찾을 수 없습니다. {tables.map((table) => ( { updateSourceConfig({ tableName: table.tableName }); setSourceTableOpen(false); }} className="text-xs" >
{table.displayName} {table.tableName}
))}
{/* 소스 필드 매핑 (테이블 선택 시) */} {config.sourceConfig?.tableName && (

필드 매핑

기준일 (마감일/납기일) *

스케줄 종료일로 사용돼요

수량 필드
그룹 필드 (품목코드)
그룹명 필드 (품목명)
)}
{/* ─── 3단계: 리소스 설정 ─── */}

리소스 설정 (설비/작업자)

타임라인 Y축에 표시할 리소스를 설정해요

{/* 리소스 테이블 Combobox */}
리소스 테이블 { return value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0; }} > 테이블을 찾을 수 없습니다. {tables.map((table) => ( { updateConfig({ resourceTable: table.tableName }); setResourceTableOpen(false); }} className="text-xs" >
{table.displayName} {table.tableName}
))}
{/* 리소스 필드 매핑 */} {config.resourceTable && (

리소스 필드

ID 필드
이름 필드
그룹 필드
)}
{/* ─── 4단계: 표시 설정 (Collapsible) ─── */}
{/* 줌 레벨 */}
기본 줌 레벨
{/* 초기 표시 날짜 */}
초기 표시 날짜

비워두면 오늘 기준

updateConfig({ initialDate: e.target.value || undefined })} className="h-7 w-[140px] text-xs" />
{/* 높이 */}
높이 (px) updateConfig({ height: parseInt(e.target.value) || 500 })} className="h-7 w-[100px] text-xs" />
{/* 최대 높이 */}
최대 높이 (px) { const val = e.target.value ? parseInt(e.target.value) : undefined; updateConfig({ maxHeight: val }); }} placeholder="제한 없음" className="h-7 w-[100px] text-xs" />
{/* 행 높이 */}
행 높이 (px) updateConfig({ rowHeight: parseInt(e.target.value) || 50 })} className="h-7 w-[100px] text-xs" />
{/* 헤더 높이 */}
헤더 높이 (px) updateConfig({ headerHeight: parseInt(e.target.value) || 60 })} className="h-7 w-[100px] text-xs" />
{/* 리소스 컬럼 너비 */}
리소스 컬럼 너비 (px) updateConfig({ resourceColumnWidth: parseInt(e.target.value) || 150 })} className="h-7 w-[100px] text-xs" />
{/* 셀 너비 (줌 레벨별) */}
셀 너비 (줌 레벨별, px)
updateConfig({ cellWidth: { ...config.cellWidth, day: parseInt(e.target.value) || 60 } })} className="h-7 text-xs" />
updateConfig({ cellWidth: { ...config.cellWidth, week: parseInt(e.target.value) || 120 } })} className="h-7 text-xs" />
updateConfig({ cellWidth: { ...config.cellWidth, month: parseInt(e.target.value) || 40 } })} className="h-7 text-xs" />
{/* Switch 토글들 */}

편집 가능

스케줄을 직접 수정할 수 있어요

updateConfig({ editable: v })} />

드래그 이동

스케줄을 드래그해서 날짜를 변경해요

updateConfig({ draggable: v })} />

리사이즈

스케줄의 기간을 늘이거나 줄여요

updateConfig({ resizable: v })} />

오늘 표시선

오늘 날짜에 빨간 세로선을 표시해요

updateConfig({ showTodayLine: v })} />

진행률 표시

각 스케줄의 진행률 바를 보여줘요

updateConfig({ showProgress: v })} />

충돌 표시

스케줄 겹침 시 경고를 표시해요

updateConfig({ showConflicts: v })} />

툴바 표시

상단의 네비게이션/줌 컨트롤을 보여줘요

updateConfig({ showToolbar: v })} />

줌 컨트롤 표시

줌 레벨 변경 버튼을 보여줘요

updateConfig({ showZoomControls: v })} />

네비게이션 표시

이전/다음/오늘 버튼을 보여줘요

updateConfig({ showNavigation: v })} />

추가 버튼 표시

스케줄 추가 버튼을 보여줘요

updateConfig({ showAddButton: v })} />
{/* ─── 5단계: 고급 설정 - 상태별 색상 (Collapsible) ─── */}
{[ { key: "planned", label: "계획됨", defaultColor: "#3b82f6" }, { key: "in_progress", label: "진행중", defaultColor: "#f59e0b" }, { key: "completed", label: "완료", defaultColor: "#10b981" }, { key: "delayed", label: "지연", defaultColor: "#ef4444" }, { key: "cancelled", label: "취소", defaultColor: "#6b7280" }, ].map((status) => (
{status.label}
updateConfig({ statusColors: { ...config.statusColors, [status.key]: e.target.value, }, }) } className="h-7 w-7 cursor-pointer rounded border" /> updateConfig({ statusColors: { ...config.statusColors, [status.key]: e.target.value, }, }) } className="h-7 w-[80px] text-xs" />
))}
); }; V2TimelineSchedulerConfigPanel.displayName = "V2TimelineSchedulerConfigPanel"; export default V2TimelineSchedulerConfigPanel;