"use client"; /** * BOM 하위품목 편집기 설정 패널 * * 토스식 단계별 UX: * - 기본 탭: 저장 테이블 → 트리 구조 → 엔티티 선택 → 기능 옵션(고급) * - 컬럼 탭: 소스 표시 컬럼 + 저장 입력 컬럼 + 선택된 컬럼 상세 */ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Database, Link2, Trash2, GripVertical, ArrowRight, ChevronDown, ChevronRight, Eye, EyeOff, Check, ChevronsUpDown, GitBranch, } from "lucide-react"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { tableTypeApi } from "@/lib/api/screen"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { cn } from "@/lib/utils"; // ─── 타입 (리피터와 동일한 패턴) ─── interface TableRelation { tableName: string; tableLabel: string; foreignKeyColumn: string; referenceColumn: string; } interface ColumnOption { columnName: string; displayName: string; inputType?: string; detailSettings?: { codeGroup?: string; referenceTable?: string; referenceColumn?: string; displayColumn?: string; format?: string; }; } interface EntityColumnOption { columnName: string; displayName: string; referenceTable?: string; referenceColumn?: string; displayColumn?: string; } interface BomColumnConfig { key: string; title: string; width?: string; visible?: boolean; editable?: boolean; hidden?: boolean; inputType?: string; isSourceDisplay?: boolean; detailSettings?: { codeGroup?: string; referenceTable?: string; referenceColumn?: string; displayColumn?: string; format?: string; }; } interface BomItemEditorConfig { useCustomTable?: boolean; mainTableName?: string; foreignKeyColumn?: string; foreignKeySourceColumn?: string; parentKeyColumn?: string; dataSource?: { sourceTable?: string; foreignKey?: string; referenceKey?: string; displayColumn?: string; }; columns: BomColumnConfig[]; features?: { showAddButton?: boolean; showDeleteButton?: boolean; inlineEdit?: boolean; showRowNumber?: boolean; maxDepth?: number; }; } interface V2BomItemEditorConfigPanelProps { config: BomItemEditorConfig; onChange: (config: BomItemEditorConfig) => void; currentTableName?: string; screenTableName?: string; } // ─── 메인 패널 ─── export function V2BomItemEditorConfigPanel({ config: propConfig, onChange, currentTableName: propCurrentTableName, screenTableName, }: V2BomItemEditorConfigPanelProps) { const currentTableName = screenTableName || propCurrentTableName; const config: BomItemEditorConfig = useMemo( () => { const { columns: propColumns, ...rest } = propConfig || {} as BomItemEditorConfig; return { ...rest, columns: propColumns || [], dataSource: { ...propConfig?.dataSource }, features: { showAddButton: true, showDeleteButton: true, inlineEdit: false, showRowNumber: false, maxDepth: 3, ...propConfig?.features, }, }; }, [propConfig], ); // ─── 상태 ─── const [currentTableColumns, setCurrentTableColumns] = useState([]); const [entityColumns, setEntityColumns] = useState([]); const [sourceTableColumns, setSourceTableColumns] = useState([]); const [allTables, setAllTables] = useState<{ tableName: string; displayName: string }[]>([]); const [relatedTables, setRelatedTables] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [loadingSourceColumns, setLoadingSourceColumns] = useState(false); const [loadingTables, setLoadingTables] = useState(false); const [loadingRelations, setLoadingRelations] = useState(false); const [tableComboboxOpen, setTableComboboxOpen] = useState(false); const [expandedColumn, setExpandedColumn] = useState(null); // ─── 업데이트 헬퍼 (리피터 패턴) ─── const updateConfig = useCallback( (updates: Partial) => { onChange({ ...config, ...updates }); }, [config, onChange], ); const updateFeatures = useCallback( (field: string, value: any) => { updateConfig({ features: { ...config.features, [field]: value } }); }, [config.features, updateConfig], ); // ─── 전체 테이블 목록 로드 (리피터 동일) ─── useEffect(() => { const loadTables = async () => { setLoadingTables(true); try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setAllTables( response.data.map((t: any) => ({ tableName: t.tableName || t.table_name, displayName: t.displayName || t.table_label || t.tableName || t.table_name, })), ); } } catch (error) { console.error("테이블 목록 로드 실패:", error); } finally { setLoadingTables(false); } }; loadTables(); }, []); // ─── 연관 테이블 로드 (리피터 동일) ─── useEffect(() => { const loadRelatedTables = async () => { const baseTable = currentTableName || config.mainTableName; if (!baseTable) { setRelatedTables([]); return; } setLoadingRelations(true); try { const { apiClient } = await import("@/lib/api/client"); const allRelations: TableRelation[] = []; if (currentTableName) { const response = await apiClient.get( `/table-management/columns/${currentTableName}/referenced-by`, ); if (response.data.success && response.data.data) { allRelations.push( ...response.data.data.map((rel: any) => ({ tableName: rel.tableName || rel.table_name, tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name, foreignKeyColumn: rel.columnName || rel.column_name, referenceColumn: rel.referenceColumn || rel.reference_column || "id", })), ); } } if (config.mainTableName && config.mainTableName !== currentTableName) { const response2 = await apiClient.get( `/table-management/columns/${config.mainTableName}/referenced-by`, ); if (response2.data.success && response2.data.data) { response2.data.data.forEach((rel: any) => { const tn = rel.tableName || rel.table_name; if (!allRelations.some((r) => r.tableName === tn)) { allRelations.push({ tableName: tn, tableLabel: rel.tableLabel || rel.table_label || tn, foreignKeyColumn: rel.columnName || rel.column_name, referenceColumn: rel.referenceColumn || rel.reference_column || "id", }); } }); } } setRelatedTables(allRelations); } catch (error) { console.error("연관 테이블 로드 실패:", error); setRelatedTables([]); } finally { setLoadingRelations(false); } }; loadRelatedTables(); }, [currentTableName, config.mainTableName]); // ─── 저장 테이블 선택 (리피터 동일) ─── const handleSaveTableSelect = useCallback( (tableName: string) => { if (!tableName || tableName === currentTableName) { updateConfig({ useCustomTable: false, mainTableName: undefined, foreignKeyColumn: undefined, foreignKeySourceColumn: undefined, }); return; } const relation = relatedTables.find((r) => r.tableName === tableName); if (relation) { updateConfig({ useCustomTable: true, mainTableName: tableName, foreignKeyColumn: relation.foreignKeyColumn, foreignKeySourceColumn: relation.referenceColumn, }); } else { updateConfig({ useCustomTable: true, mainTableName: tableName, foreignKeyColumn: undefined, foreignKeySourceColumn: "id", }); } }, [currentTableName, relatedTables, updateConfig], ); // ─── 저장 테이블 컬럼 로드 (리피터 동일) ─── const targetTableForColumns = config.useCustomTable && config.mainTableName ? config.mainTableName : currentTableName; useEffect(() => { const loadColumns = async () => { if (!targetTableForColumns) { setCurrentTableColumns([]); setEntityColumns([]); return; } setLoadingColumns(true); try { const columnData = await tableTypeApi.getColumns(targetTableForColumns); const cols: ColumnOption[] = []; const entityCols: EntityColumnOption[] = []; for (const c of columnData) { let detailSettings: any = null; if (c.detailSettings) { try { detailSettings = typeof c.detailSettings === "string" ? JSON.parse(c.detailSettings) : c.detailSettings; } catch { // ignore } } const col: ColumnOption = { columnName: c.columnName || c.column_name, displayName: c.displayName || c.columnLabel || c.columnName || c.column_name, inputType: c.inputType || c.input_type, detailSettings: detailSettings ? { codeGroup: detailSettings.codeGroup, referenceTable: detailSettings.referenceTable, referenceColumn: detailSettings.referenceColumn, displayColumn: detailSettings.displayColumn, format: detailSettings.format, } : undefined, }; cols.push(col); if (col.inputType === "entity") { const refTable = detailSettings?.referenceTable || c.referenceTable; if (refTable) { entityCols.push({ columnName: col.columnName, displayName: col.displayName, referenceTable: refTable, referenceColumn: detailSettings?.referenceColumn || c.referenceColumn || "id", displayColumn: detailSettings?.displayColumn || c.displayColumn, }); } } } setCurrentTableColumns(cols); setEntityColumns(entityCols); } catch (error) { console.error("컬럼 로드 실패:", error); setCurrentTableColumns([]); setEntityColumns([]); } finally { setLoadingColumns(false); } }; loadColumns(); }, [targetTableForColumns]); // ─── 소스(엔티티) 테이블 컬럼 로드 (리피터 동일) ─── useEffect(() => { const loadSourceColumns = async () => { const sourceTable = config.dataSource?.sourceTable; if (!sourceTable) { setSourceTableColumns([]); return; } setLoadingSourceColumns(true); try { const columnData = await tableTypeApi.getColumns(sourceTable); setSourceTableColumns( columnData.map((c: any) => ({ columnName: c.columnName || c.column_name, displayName: c.displayName || c.columnLabel || c.columnName || c.column_name, inputType: c.inputType || c.input_type, })), ); } catch (error) { console.error("소스 테이블 컬럼 로드 실패:", error); setSourceTableColumns([]); } finally { setLoadingSourceColumns(false); } }; loadSourceColumns(); }, [config.dataSource?.sourceTable]); // ─── 엔티티 컬럼 선택 시 소스 테이블 자동 설정 (리피터 동일) ─── const handleEntityColumnSelect = (columnName: string) => { const selectedEntity = entityColumns.find((c) => c.columnName === columnName); if (selectedEntity) { updateConfig({ dataSource: { ...config.dataSource, sourceTable: selectedEntity.referenceTable || "", foreignKey: selectedEntity.columnName, referenceKey: selectedEntity.referenceColumn || "id", displayColumn: selectedEntity.displayColumn, }, }); } }; // ─── 컬럼 토글 (리피터 동일) ─── const toggleInputColumn = (column: ColumnOption) => { const exists = config.columns.findIndex((c) => c.key === column.columnName && !c.isSourceDisplay); if (exists >= 0) { updateConfig({ columns: config.columns.filter((c) => c.key !== column.columnName || c.isSourceDisplay) }); } else { const newCol: BomColumnConfig = { key: column.columnName, title: column.displayName, width: "auto", visible: true, editable: true, inputType: column.inputType || "text", detailSettings: column.detailSettings, }; updateConfig({ columns: [...config.columns, newCol] }); } }; const toggleSourceDisplayColumn = (column: ColumnOption) => { const exists = config.columns.some((c) => c.key === column.columnName && c.isSourceDisplay); if (exists) { updateConfig({ columns: config.columns.filter((c) => c.key !== column.columnName) }); } else { const newCol: BomColumnConfig = { key: column.columnName, title: column.displayName, width: "auto", visible: true, editable: false, isSourceDisplay: true, }; updateConfig({ columns: [...config.columns, newCol] }); } }; const isColumnAdded = (columnName: string) => config.columns.some((c) => c.key === columnName && !c.isSourceDisplay); const isSourceColumnSelected = (columnName: string) => config.columns.some((c) => c.key === columnName && c.isSourceDisplay); const updateColumnProp = (key: string, field: keyof BomColumnConfig, value: any) => { updateConfig({ columns: config.columns.map((col) => (col.key === key ? { ...col, [field]: value } : col)), }); }; const inputableColumns = useMemo(() => { const fkColumn = config.dataSource?.foreignKey; return currentTableColumns.filter( (col) => col.columnName !== fkColumn && col.inputType !== "entity", ); }, [currentTableColumns, config.dataSource?.foreignKey]); // ─── 렌더링 ─── return (
기본 컬럼 {/* ─── 기본 설정 탭 ─── */} {/* 저장 대상 테이블 */}
BOM 데이터를 어디에 저장하나요?

{config.useCustomTable && config.mainTableName ? allTables.find((t) => t.tableName === config.mainTableName)?.displayName || config.mainTableName : currentTableName || "미설정"}

{config.useCustomTable && config.mainTableName && config.foreignKeyColumn && (

FK: {config.foreignKeyColumn} → {currentTableName}. {config.foreignKeySourceColumn || "id"}

)} {!config.useCustomTable && currentTableName && (

화면 메인 테이블

)}
{/* 테이블 Combobox */} 테이블을 찾을 수 없습니다. {currentTableName && ( { handleSaveTableSelect(currentTableName); setTableComboboxOpen(false); }} className="text-xs" > {currentTableName} (기본) )} {relatedTables.length > 0 && ( {relatedTables.map((rel) => ( { handleSaveTableSelect(rel.tableName); setTableComboboxOpen(false); }} className="text-xs" > {rel.tableLabel} ({rel.foreignKeyColumn}) ))} )} {allTables .filter( (t) => t.tableName !== currentTableName && !relatedTables.some((r) => r.tableName === t.tableName), ) .map((table) => ( { handleSaveTableSelect(table.tableName); setTableComboboxOpen(false); }} className="text-xs" > {table.displayName} ))} {/* FK 직접 입력 */} {config.useCustomTable && config.mainTableName && currentTableName && !relatedTables.some((r) => r.tableName === config.mainTableName) && (

화면 테이블({currentTableName})과의 엔티티 관계가 없어요. FK 컬럼을 직접 입력해주세요.

FK 컬럼 (저장 테이블)

updateConfig({ foreignKeyColumn: e.target.value })} placeholder="예: bom_id" className="h-7 text-xs" />

PK 컬럼 (화면 테이블)

updateConfig({ foreignKeySourceColumn: e.target.value })} placeholder="id" className="h-7 text-xs" />
)} {/* 화면 메인 테이블 참고 정보 */} {currentTableName && (

화면 메인 테이블

{currentTableName}

컬럼 {currentTableColumns.length}개 / 엔티티 {entityColumns.length}개

)}
{/* 트리 구조 설정 (BOM 전용) */}
트리 구조는 어떻게 만드나요?

계층 구조를 위한 자기 참조 FK 컬럼을 선택하면 부모-자식 관계가 만들어져요

{currentTableColumns.length > 0 ? (

부모 키 컬럼

) : (

{loadingColumns ? "컬럼 정보를 불러오고 있어요..." : "저장 테이블을 먼저 선택해주세요"}

)}
최대 트리 깊이 updateFeatures("maxDepth", parseInt(e.target.value) || 3)} className="h-7 w-[80px] text-xs" />
{/* 엔티티 선택 (품목 참조) */}
어떤 품목을 참조하나요?

품목 검색 시 참조할 엔티티를 선택하면 FK만 저장돼요

{entityColumns.length > 0 ? ( ) : (

{loadingColumns ? "컬럼 정보를 불러오고 있어요..." : !targetTableForColumns ? "저장 테이블을 먼저 선택해주세요" : "엔티티 타입 컬럼이 없어요"}

)} {config.dataSource?.sourceTable && (

선택된 엔티티

{config.dataSource.sourceTable}

{config.dataSource.foreignKey} 컬럼에 FK로 저장돼요

)}
{/* 기능 옵션 - Switch + 설명 텍스트 */}
기능 옵션

추가 버튼

하위 품목을 추가할 수 있어요

updateFeatures("showAddButton", checked)} />

삭제 버튼

선택한 품목을 삭제할 수 있어요

updateFeatures("showDeleteButton", checked)} />

인라인 편집

셀을 클릭하면 바로 수정할 수 있어요

updateFeatures("inlineEdit", checked)} />

행 번호

각 행에 순번을 표시해요

updateFeatures("showRowNumber", checked)} />
{/* ─── 컬럼 설정 탭 ─── */} {/* 통합 컬럼 선택 */}
어떤 컬럼을 표시하고 입력받을까요?

소스 테이블 컬럼은 표시용, 저장 테이블 컬럼은 입력용이에요

{/* 소스 테이블 컬럼 (표시용) */} {config.dataSource?.sourceTable && ( <>
소스 테이블 ({config.dataSource.sourceTable}) - 표시용
{loadingSourceColumns ? (

로딩 중...

) : sourceTableColumns.length === 0 ? (

컬럼 정보가 없어요

) : (
{sourceTableColumns.map((column) => (
toggleSourceDisplayColumn(column)} > toggleSourceDisplayColumn(column)} className="pointer-events-none h-3.5 w-3.5" /> {column.displayName} 표시
))}
)} )} {/* 저장 테이블 컬럼 (입력용) */}
저장 테이블 ({targetTableForColumns || "미선택"}) - 입력용
{loadingColumns ? (

로딩 중...

) : inputableColumns.length === 0 ? (

컬럼 정보가 없어요

) : (
{inputableColumns.map((column) => (
toggleInputColumn(column)} > toggleInputColumn(column)} className="pointer-events-none h-3.5 w-3.5" /> {column.displayName} {column.inputType}
))}
)}
{/* 선택된 컬럼 상세 */} {config.columns.length > 0 && (
선택된 컬럼 ({config.columns.length}) 드래그로 순서 변경
{config.columns.map((col, index) => (
e.dataTransfer.setData("columnIndex", String(index))} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); const fromIndex = parseInt(e.dataTransfer.getData("columnIndex"), 10); if (fromIndex !== index) { const newColumns = [...config.columns]; const [movedCol] = newColumns.splice(fromIndex, 1); newColumns.splice(index, 0, movedCol); updateConfig({ columns: newColumns }); } }} > {!col.isSourceDisplay && ( )} {col.isSourceDisplay ? ( ) : ( )} updateColumnProp(col.key, "title", e.target.value)} placeholder="제목" className="h-6 flex-1 text-xs" /> {!col.isSourceDisplay && ( )} {!col.isSourceDisplay && ( )}
{/* 확장 상세 */} {!col.isSourceDisplay && expandedColumn === col.key && (

컬럼 너비

updateColumnProp(col.key, "width", e.target.value)} placeholder="auto, 100px, 20%" className="h-6 text-xs" />
)}
))}
)}
); } V2BomItemEditorConfigPanel.displayName = "V2BomItemEditorConfigPanel"; export default V2BomItemEditorConfigPanel;