"use client"; /** * BOM 트리 뷰 설정 패널 * * V2BomItemEditorConfigPanel 구조 기반: * - 기본 탭: 디테일 테이블 + 엔티티 선택 + 트리 설정 * - 컬럼 탭: 소스 표시 컬럼 + 디테일 컬럼 + 선택된 컬럼 상세 */ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; 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 TreeColumnConfig { key: string; title: string; width?: string; visible?: boolean; hidden?: boolean; isSourceDisplay?: boolean; } interface BomTreeConfig { detailTable?: string; foreignKey?: string; parentKey?: string; historyTable?: string; versionTable?: string; dataSource?: { sourceTable?: string; foreignKey?: string; referenceKey?: string; displayColumn?: string; }; columns: TreeColumnConfig[]; features?: { showExpandAll?: boolean; showHeader?: boolean; showQuantity?: boolean; showLossRate?: boolean; showHistory?: boolean; showVersion?: boolean; }; } interface V2BomTreeConfigPanelProps { config: BomTreeConfig; onChange: (config: BomTreeConfig) => void; currentTableName?: string; screenTableName?: string; } export function V2BomTreeConfigPanel({ config: propConfig, onChange, currentTableName: propCurrentTableName, screenTableName, }: V2BomTreeConfigPanelProps) { const currentTableName = screenTableName || propCurrentTableName; const config: BomTreeConfig = useMemo( () => ({ columns: [], ...propConfig, dataSource: { ...propConfig?.dataSource }, features: { showExpandAll: true, showHeader: true, showQuantity: true, showLossRate: true, ...propConfig?.features, }, }), [propConfig], ); const [detailTableColumns, setDetailTableColumns] = 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; if (!baseTable) { setRelatedTables([]); return; } setLoadingRelations(true); try { const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.get( `/table-management/columns/${baseTable}/referenced-by`, ); if (response.data.success && response.data.data) { setRelatedTables( 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", })), ); } } catch (error) { console.error("연관 테이블 로드 실패:", error); setRelatedTables([]); } finally { setLoadingRelations(false); } }; loadRelatedTables(); }, [currentTableName]); // 디테일 테이블 선택 const handleDetailTableSelect = useCallback( (tableName: string) => { const relation = relatedTables.find((r) => r.tableName === tableName); updateConfig({ detailTable: tableName, foreignKey: relation?.foreignKeyColumn || config.foreignKey, }); }, [relatedTables, config.foreignKey, updateConfig], ); // 디테일 테이블 컬럼 로드 useEffect(() => { const loadColumns = async () => { if (!config.detailTable) { setDetailTableColumns([]); setEntityColumns([]); return; } setLoadingColumns(true); try { const columnData = await tableTypeApi.getColumns(config.detailTable); 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, }); } } } setDetailTableColumns(cols); setEntityColumns(entityCols); } catch (error) { console.error("컬럼 로드 실패:", error); setDetailTableColumns([]); setEntityColumns([]); } finally { setLoadingColumns(false); } }; loadColumns(); }, [config.detailTable]); // 소스(엔티티) 테이블 컬럼 로드 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 toggleDetailColumn = (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: TreeColumnConfig = { key: column.columnName, title: column.displayName, width: "auto", visible: true, }; 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: TreeColumnConfig = { key: column.columnName, title: column.displayName, width: "auto", visible: true, 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 TreeColumnConfig, value: any) => { updateConfig({ columns: config.columns.map((col) => (col.key === key ? { ...col, [field]: value } : col)), }); }; // FK/시스템 컬럼 제외한 표시 가능 컬럼 const displayableColumns = useMemo(() => { const fkColumn = config.dataSource?.foreignKey; const systemCols = ["id", "created_at", "updated_at", "created_by", "updated_by", "company_code", "created_date"]; return detailTableColumns.filter( (col) => col.columnName !== fkColumn && col.inputType !== "entity" && !systemCols.includes(col.columnName), ); }, [detailTableColumns, config.dataSource?.foreignKey]); // FK 후보 컬럼 const fkCandidateColumns = useMemo(() => { const systemCols = ["created_at", "updated_at", "created_by", "updated_by", "company_code", "created_date"]; return detailTableColumns.filter((c) => !systemCols.includes(c.columnName)); }, [detailTableColumns]); return (
기본 컬럼 {/* ─── 기본 설정 탭 ─── */} {/* 디테일 테이블 */}

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

{config.detailTable && config.foreignKey && (

FK: {config.foreignKey} → {currentTableName || "메인 테이블"}.id

)}
테이블을 찾을 수 없습니다. {relatedTables.length > 0 && ( {relatedTables.map((rel) => ( { handleDetailTableSelect(rel.tableName); setTableComboboxOpen(false); }} className="text-xs" > {rel.tableLabel} ({rel.foreignKeyColumn}) ))} )} {allTables .filter((t) => !relatedTables.some((r) => r.tableName === t.tableName)) .map((table) => ( { handleDetailTableSelect(table.tableName); setTableComboboxOpen(false); }} className="text-xs" > {table.displayName} ))}
{/* 트리 구조 설정 */}

메인 FK와 부모-자식 계층 FK를 선택하세요

{fkCandidateColumns.length > 0 ? (
) : (

{loadingColumns ? "로딩 중..." : "디테일 테이블을 먼저 선택하세요"}

)}
{/* 엔티티 선택 (품목 참조) */}

트리 노드에 표시할 품목 정보의 소스 엔티티

{entityColumns.length > 0 ? ( ) : (

{loadingColumns ? "로딩 중..." : !config.detailTable ? "디테일 테이블을 먼저 선택하세요" : "엔티티 타입 컬럼이 없습니다"}

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

선택된 엔티티

참조 테이블: {config.dataSource.sourceTable}

FK 컬럼: {config.dataSource.foreignKey}

)}
{/* 이력/버전 테이블 설정 */}

BOM 변경 이력과 버전 관리에 사용할 테이블을 선택하세요

updateFeatures("showHistory", !!checked)} />
{(config.features?.showHistory ?? true) && ( 테이블을 찾을 수 없습니다. {allTables.map((table) => ( updateConfig({ historyTable: table.tableName })} className="text-xs" > {table.displayName} ))} )}
updateFeatures("showVersion", !!checked)} />
{(config.features?.showVersion ?? true) && ( 테이블을 찾을 수 없습니다. {allTables.map((table) => ( updateConfig({ versionTable: table.tableName })} className="text-xs" > {table.displayName} ))} )}
{/* 표시 옵션 */}
updateFeatures("showExpandAll", !!checked)} />
updateFeatures("showHeader", !!checked)} />
updateFeatures("showQuantity", !!checked)} />
updateFeatures("showLossRate", !!checked)} />
{/* 메인 화면 테이블 참고 */} {currentTableName && ( <>

{currentTableName}

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

)}
{/* ─── 컬럼 설정 탭 ─── */}

트리 노드에 표시할 소스/디테일 컬럼을 선택하세요

{/* 소스 테이블 컬럼 (표시용) */} {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} 표시
))}
)} )} {/* 디테일 테이블 컬럼 */}
디테일 테이블 ({config.detailTable || "미선택"}) - 직접 컬럼
{loadingColumns ? (

로딩 중...

) : displayableColumns.length === 0 ? (

컬럼 정보가 없습니다

) : (
{displayableColumns.map((column) => (
toggleDetailColumn(column)} > toggleDetailColumn(column)} className="pointer-events-none h-3.5 w-3.5" /> {column.displayName} {column.inputType}
))}
)}
{/* 선택된 컬럼 상세 */} {config.columns.length > 0 && ( <>
{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 && expandedColumn === col.key && (
updateColumnProp(col.key, "width", e.target.value)} placeholder="auto, 100px, 20%" className="h-6 text-xs" />
)}
))}
)}
); } V2BomTreeConfigPanel.displayName = "V2BomTreeConfigPanel"; export default V2BomTreeConfigPanel;