"use client"; /** * V2 품목별 라우팅 설정 패널 * 토스식 단계별 UX: 데이터 소스 -> 모달 연동 -> 공정 컬럼 -> 레이아웃(접힘) */ import React, { useState, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 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, Plus, Trash2, Check, ChevronsUpDown, Database, Monitor, Columns, } from "lucide-react"; import { cn } from "@/lib/utils"; import type { ItemRoutingConfig, ProcessColumnDef } from "@/lib/registry/components/v2-item-routing/types"; import { defaultConfig } from "@/lib/registry/components/v2-item-routing/config"; interface V2ItemRoutingConfigPanelProps { config: Partial; onChange: (config: Partial) => void; } interface TableInfo { tableName: string; displayName?: string; } interface ColumnInfo { columnName: string; displayName?: string; dataType?: string; } interface ScreenInfo { screenId: number; screenName: string; screenCode: string; } // ─── 테이블 Combobox ─── function TableCombobox({ value, onChange, tables, loading, }: { value: string; onChange: (v: string) => void; tables: TableInfo[]; loading: boolean; }) { const [open, setOpen] = useState(false); const selected = tables.find((t) => t.tableName === value); return ( 테이블을 찾을 수 없습니다. {tables.map((t) => ( { onChange(t.tableName); setOpen(false); }} className="text-xs" >
{t.displayName || t.tableName} {t.displayName && {t.tableName}}
))}
); } // ─── 컬럼 Combobox ─── function ColumnCombobox({ value, onChange, tableName, placeholder, }: { value: string; onChange: (v: string) => void; tableName: string; placeholder?: string; }) { const [open, setOpen] = useState(false); const [columns, setColumns] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { if (!tableName) { setColumns([]); return; } const load = async () => { setLoading(true); try { const { tableManagementApi } = await import("@/lib/api/tableManagement"); const res = await tableManagementApi.getColumnList(tableName); if (res.success && res.data?.columns) { setColumns(res.data.columns); } } catch { /* ignore */ } finally { setLoading(false); } }; load(); }, [tableName]); const selected = columns.find((c) => c.columnName === value); return ( 컬럼을 찾을 수 없습니다. {columns.map((c) => ( { onChange(c.columnName); setOpen(false); }} className="text-xs" >
{c.displayName || c.columnName} {c.displayName && {c.columnName}}
))}
); } // ─── 화면 Combobox ─── function ScreenCombobox({ value, onChange, }: { value?: number; onChange: (v?: number) => void; }) { const [open, setOpen] = useState(false); const [screens, setScreens] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { const load = async () => { setLoading(true); try { const { screenApi } = await import("@/lib/api/screen"); const res = await screenApi.getScreens({ page: 1, size: 1000 }); if (res.data) { setScreens( res.data.map((s: any) => ({ screenId: s.screenId, screenName: s.screenName || `화면 ${s.screenId}`, screenCode: s.screenCode || "", })) ); } } catch { /* ignore */ } finally { setLoading(false); } }; load(); }, []); const selected = screens.find((s) => s.screenId === value); return ( 화면을 찾을 수 없습니다. {screens.map((s) => ( { onChange(s.screenId); setOpen(false); }} className="text-xs" >
{s.screenName} ID: {s.screenId}
))}
); } // ─── 메인 컴포넌트 ─── export const V2ItemRoutingConfigPanel: React.FC = ({ config: configProp, onChange, }) => { const [tables, setTables] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [dataSourceOpen, setDataSourceOpen] = useState(false); const [layoutOpen, setLayoutOpen] = useState(false); const config: ItemRoutingConfig = { ...defaultConfig, ...configProp, dataSource: { ...defaultConfig.dataSource, ...configProp?.dataSource }, modals: { ...defaultConfig.modals, ...configProp?.modals }, processColumns: configProp?.processColumns?.length ? configProp.processColumns : defaultConfig.processColumns, }; useEffect(() => { const loadTables = async () => { setLoadingTables(true); try { const { tableManagementApi } = await import("@/lib/api/tableManagement"); const res = await tableManagementApi.getTableList(); if (res.success && res.data) { setTables( res.data.map((t: any) => ({ tableName: t.tableName, displayName: t.displayName || t.tableName, })) ); } } catch { /* ignore */ } finally { setLoadingTables(false); } }; loadTables(); }, []); const dispatchConfigEvent = (newConfig: Partial) => { if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { detail: { config: { ...config, ...newConfig } }, }) ); } }; const update = (partial: Partial) => { const merged = { ...configProp, ...partial }; onChange(merged); dispatchConfigEvent(partial); }; const updateDataSource = (field: string, value: string) => { const newDataSource = { ...config.dataSource, [field]: value }; const partial = { dataSource: newDataSource }; onChange({ ...configProp, ...partial }); dispatchConfigEvent(partial); }; const updateModals = (field: string, value?: number) => { const newModals = { ...config.modals, [field]: value }; const partial = { modals: newModals }; onChange({ ...configProp, ...partial }); dispatchConfigEvent(partial); }; // 공정 컬럼 관리 const addColumn = () => { update({ processColumns: [ ...config.processColumns, { name: "", label: "새 컬럼", width: 100, align: "left" as const }, ], }); }; const removeColumn = (idx: number) => { update({ processColumns: config.processColumns.filter((_, i) => i !== idx) }); }; const updateColumn = (idx: number, field: keyof ProcessColumnDef, value: string | number) => { const next = [...config.processColumns]; next[idx] = { ...next[idx], [field]: value }; update({ processColumns: next }); }; return (
{/* ─── 1단계: 모달 연동 ─── */}

모달 연동

버전 추가/공정 추가·수정 시 열리는 화면을 설정해요

버전 추가 화면 updateModals("versionAddScreenId", v)} />
공정 추가 화면 updateModals("processAddScreenId", v)} />
공정 수정 화면 updateModals("processEditScreenId", v)} />
{/* ─── 2단계: 공정 테이블 컬럼 ─── */}

공정 테이블 컬럼

공정 순서 테이블에 표시할 컬럼을 설정해요

{config.processColumns.map((col, idx) => (
updateColumn(idx, "name", e.target.value)} className="h-7 w-24 text-[10px]" placeholder="컬럼명" /> updateColumn(idx, "label", e.target.value)} className="h-7 flex-1 text-[10px]" placeholder="표시명" /> updateColumn(idx, "width", parseInt(e.target.value) || 100)} className="h-7 w-14 text-[10px]" placeholder="너비" />
))}
{/* ─── 3단계: 데이터 소스 (Collapsible) ─── */}
품목 테이블 updateDataSource("itemTable", v)} tables={tables} loading={loadingTables} />
품목명 컬럼 updateDataSource("itemNameColumn", v)} tableName={config.dataSource.itemTable} placeholder="품목명" />
품목코드 컬럼 updateDataSource("itemCodeColumn", v)} tableName={config.dataSource.itemTable} placeholder="품목코드" />
라우팅 버전 테이블 updateDataSource("routingVersionTable", v)} tables={tables} loading={loadingTables} />
품목 FK 컬럼 updateDataSource("routingVersionFkColumn", v)} tableName={config.dataSource.routingVersionTable} placeholder="FK 컬럼" />
버전명 컬럼 updateDataSource("routingVersionNameColumn", v)} tableName={config.dataSource.routingVersionTable} placeholder="버전명" />
라우팅 상세 테이블 updateDataSource("routingDetailTable", v)} tables={tables} loading={loadingTables} />
버전 FK 컬럼 updateDataSource("routingDetailFkColumn", v)} tableName={config.dataSource.routingDetailTable} placeholder="FK 컬럼" />
공정 마스터 테이블 updateDataSource("processTable", v)} tables={tables} loading={loadingTables} />
공정명 컬럼 updateDataSource("processNameColumn", v)} tableName={config.dataSource.processTable} placeholder="공정명" />
공정코드 컬럼 updateDataSource("processCodeColumn", v)} tableName={config.dataSource.processTable} placeholder="공정코드" />
{/* ─── 4단계: 레이아웃 & 기타 (Collapsible) ─── */}
좌측 패널 비율 (%)

품목 목록 패널의 너비

update({ splitRatio: parseInt(e.target.value) || 40 })} className="h-7 w-[80px] text-xs" />
좌측 패널 제목 update({ leftPanelTitle: e.target.value })} placeholder="품목 목록" className="h-7 w-[140px] text-xs" />
우측 패널 제목 update({ rightPanelTitle: e.target.value })} placeholder="공정 순서" className="h-7 w-[140px] text-xs" />
버전 추가 버튼 텍스트 update({ versionAddButtonText: e.target.value })} placeholder="+ 라우팅 버전 추가" className="h-7 w-[140px] text-xs" />
공정 추가 버튼 텍스트 update({ processAddButtonText: e.target.value })} placeholder="+ 공정 추가" className="h-7 w-[140px] text-xs" />

첫 번째 버전 자동 선택

품목 선택 시 첫 버전을 자동으로 선택해요

update({ autoSelectFirstVersion: checked })} />

읽기 전용

추가/수정/삭제 버튼을 숨겨요

update({ readonly: checked })} />
); }; V2ItemRoutingConfigPanel.displayName = "V2ItemRoutingConfigPanel"; export default V2ItemRoutingConfigPanel;