"use client"; /** * BOM 하위품목 편집기 설정 패널 * * V2RepeaterConfigPanel 구조를 기반으로 구현: * - 기본 탭: 저장 테이블 + 엔티티 선택 + 트리 설정 + 기능 옵션 * - 컬럼 탭: 소스 표시 컬럼 + 저장 입력 컬럼 + 선택된 컬럼 상세 */ 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 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( () => ({ columns: [], ...propConfig, 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)), }); }; // FK 컬럼 제외한 입력 가능 컬럼 const inputableColumns = useMemo(() => { const fkColumn = config.dataSource?.foreignKey; return currentTableColumns.filter( (col) => col.columnName !== fkColumn && col.inputType !== "entity", ); }, [currentTableColumns, config.dataSource?.foreignKey]); // ─── 렌더링 ─── return (
기본 컬럼 {/* ─── 기본 설정 탭 ─── */} {/* 저장 대상 테이블 (리피터 동일) */}

{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 컬럼을 직접 입력하세요.

updateConfig({ foreignKeyColumn: e.target.value })} placeholder="예: bom_id" className="h-7 text-xs" />
updateConfig({ foreignKeySourceColumn: e.target.value })} placeholder="id" className="h-7 text-xs" />
)}
{/* 트리 구조 설정 (BOM 전용) */}

계층 구조를 위한 자기 참조 FK 컬럼을 선택하세요

{currentTableColumns.length > 0 ? ( ) : (

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

)} {/* 최대 깊이 */}
updateFeatures("maxDepth", parseInt(e.target.value) || 3)} className="h-7 w-20 text-xs" />
{/* 엔티티 선택 (리피터 모달 모드와 동일) */}

품목 검색 시 참조할 엔티티를 선택하세요 (FK만 저장됨)

{entityColumns.length > 0 ? ( ) : (

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

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

선택된 엔티티

검색 테이블: {config.dataSource.sourceTable}

저장 컬럼: {config.dataSource.foreignKey} (FK)

)}
{/* 기능 옵션 */}
updateFeatures("showAddButton", !!checked)} />
updateFeatures("showDeleteButton", !!checked)} />
updateFeatures("inlineEdit", !!checked)} />
updateFeatures("showRowNumber", !!checked)} />
{/* 메인 화면 테이블 참고 */} {currentTableName && ( <>

{currentTableName}

컬럼 {currentTableColumns.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} 표시
))}
)} )} {/* 저장 테이블 컬럼 (입력용) */}
저장 테이블 ({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.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 && ( updateColumnProp(col.key, "editable", !!checked) } title="편집 가능" /> )}
{/* 확장 상세 */} {!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;