"use client"; import React, { useState, useMemo, useEffect } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; // Accordion 제거 - 단순 섹션으로 변경 import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, GripVertical } from "lucide-react"; import { cn } from "@/lib/utils"; import { SplitPanelLayoutConfig, AdditionalTabConfig } from "./types"; import { TableInfo, ColumnInfo } from "@/types/screen"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { tableTypeApi } from "@/lib/api/screen"; import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel"; interface SplitPanelLayoutConfigPanelProps { config: SplitPanelLayoutConfig; onChange: (config: SplitPanelLayoutConfig) => void; tables?: TableInfo[]; // 전체 테이블 목록 (선택적) screenTableName?: string; // 현재 화면의 테이블명 (좌측 패널에서 사용) menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요) } /** * 그룹핑 기준 컬럼 선택 컴포넌트 */ const GroupByColumnsSelector: React.FC<{ tableName?: string; selectedColumns: string[]; onChange: (columns: string[]) => void; }> = ({ tableName, selectedColumns, onChange }) => { const [columns, setColumns] = useState([]); // ColumnTypeInfo 타입 const [loading, setLoading] = useState(false); useEffect(() => { if (!tableName) { setColumns([]); return; } const loadColumns = async () => { setLoading(true); try { const { tableManagementApi } = await import("@/lib/api/tableManagement"); const response = await tableManagementApi.getColumnList(tableName); if (response.success && response.data && response.data.columns) { setColumns(response.data.columns); } } catch (error) { console.error("컬럼 정보 로드 실패:", error); } finally { setLoading(false); } }; loadColumns(); }, [tableName]); const toggleColumn = (columnName: string) => { const newSelection = selectedColumns.includes(columnName) ? selectedColumns.filter((c) => c !== columnName) : [...selectedColumns, columnName]; onChange(newSelection); }; if (!tableName) { return (

먼저 우측 패널의 테이블을 선택하세요

); } return (
{loading ? (

로딩 중...

) : columns.length === 0 ? (

컬럼을 찾을 수 없습니다

) : (
{columns.map((col) => (
toggleColumn(col.columnName)} />
))}
)}

선택된 컬럼: {selectedColumns.length > 0 ? selectedColumns.join(", ") : "없음"}
같은 값을 가진 모든 레코드를 함께 불러옵니다

); }; /** * 화면 선택 Combobox 컴포넌트 */ const ScreenSelector: React.FC<{ value?: number; onChange: (screenId?: number) => void; }> = ({ value, onChange }) => { const [open, setOpen] = useState(false); const [screens, setScreens] = useState>([]); const [loading, setLoading] = useState(false); useEffect(() => { const loadScreens = async () => { setLoading(true); try { const { screenApi } = await import("@/lib/api/screen"); const response = await screenApi.getScreens({ page: 1, size: 1000 }); setScreens( response.data.map((s) => ({ screenId: s.screenId, screenName: s.screenName, screenCode: s.screenCode })), ); } catch (error) { console.error("화면 목록 로드 실패:", error); } finally { setLoading(false); } }; loadScreens(); }, []); const selectedScreen = screens.find((s) => s.screenId === value); return ( 화면을 찾을 수 없습니다. {screens.map((screen) => ( { onChange(screen.screenId === value ? undefined : screen.screenId); setOpen(false); }} className="text-xs" >
{screen.screenName} {screen.screenCode}
))}
); }; /** * 추가 탭 설정 패널 (우측 패널과 동일한 구조) */ interface AdditionalTabConfigPanelProps { tab: AdditionalTabConfig; tabIndex: number; config: SplitPanelLayoutConfig; updateRightPanel: (updates: Partial) => void; availableRightTables: TableInfo[]; leftTableColumns: ColumnInfo[]; menuObjid?: number; // 공유 컬럼 로드 상태 loadedTableColumns: Record; loadTableColumns: (tableName: string) => Promise; loadingColumns: Record; } const AdditionalTabConfigPanel: React.FC = ({ tab, tabIndex, config, updateRightPanel, availableRightTables, leftTableColumns, menuObjid, loadedTableColumns, loadTableColumns, loadingColumns, }) => { // 탭 테이블 변경 시 컬럼 로드 useEffect(() => { if (tab.tableName && !loadedTableColumns[tab.tableName] && !loadingColumns[tab.tableName]) { loadTableColumns(tab.tableName); } }, [tab.tableName, loadedTableColumns, loadingColumns, loadTableColumns]); // 현재 탭의 컬럼 목록 const tabColumns = useMemo(() => { return tab.tableName ? loadedTableColumns[tab.tableName] || [] : []; }, [tab.tableName, loadedTableColumns]); // 로딩 상태 const loadingTabColumns = tab.tableName ? loadingColumns[tab.tableName] || false : false; // 탭 업데이트 헬퍼 const updateTab = (updates: Partial) => { const newTabs = [...(config.rightPanel?.additionalTabs || [])]; newTabs[tabIndex] = { ...tab, ...updates }; updateRightPanel({ additionalTabs: newTabs }); }; return (
{tab.label || `탭 ${tabIndex + 1}`} {tab.tableName && ( ({tab.tableName}) )}
{/* ===== 1. 기본 정보 ===== */}
updateTab({ label: e.target.value })} placeholder="탭 이름" className="h-8 text-xs" />
updateTab({ title: e.target.value })} placeholder="패널 제목" className="h-8 text-xs" />
updateTab({ panelHeaderHeight: parseInt(e.target.value) || 48 })} placeholder="48" className="h-8 w-24 text-xs" />
{/* ===== 2. 테이블 선택 ===== */}
테이블을 찾을 수 없습니다. {availableRightTables.map((table) => ( updateTab({ tableName: table.tableName, columns: [] })} > {table.displayName || table.tableName} ))}
{/* ===== 3. 표시 모드 ===== */}
{/* 요약 설정 (목록 모드) */} {tab.displayMode === "list" && (
updateTab({ summaryColumnCount: parseInt(e.target.value) || 3 })} min={1} max={10} className="h-8 text-xs" />
updateTab({ summaryShowLabel: !!checked })} />
)}
{/* ===== 4. 컬럼 매핑 (조인 키) ===== */}

좌측 패널 선택 시 관련 데이터만 표시합니다

{/* ===== 5. 기능 버튼 ===== */}
updateTab({ showSearch: !!checked })} />
updateTab({ showAdd: !!checked })} />
updateTab({ showEdit: !!checked })} />
updateTab({ showDelete: !!checked })} />
{/* ===== 6. 표시 컬럼 설정 ===== */}

표시할 컬럼을 선택하세요. 선택하지 않으면 모든 컬럼이 표시됩니다.

{/* 테이블 미선택 상태 */} {!tab.tableName && (

먼저 테이블을 선택하세요

)} {/* 테이블 선택됨 - 컬럼 목록 */} {tab.tableName && (
{/* 로딩 상태 */} {loadingTabColumns && (

컬럼을 불러오는 중...

)} {/* 설정된 컬럼이 없을 때 */} {!loadingTabColumns && (tab.columns || []).length === 0 && (

설정된 컬럼이 없습니다

컬럼을 추가하지 않으면 모든 컬럼이 표시됩니다

)} {/* 설정된 컬럼 목록 */} {!loadingTabColumns && (tab.columns || []).length > 0 && ( (tab.columns || []).map((col, colIndex) => (
{/* 상단: 순서 변경 + 삭제 버튼 */}
#{colIndex + 1}
{/* 컬럼 선택 */}
{/* 라벨 + 너비 */}
{ const newColumns = [...(tab.columns || [])]; newColumns[colIndex] = { ...col, label: e.target.value }; updateTab({ columns: newColumns }); }} placeholder="표시 라벨" className="h-8 text-xs" />
{ const newColumns = [...(tab.columns || [])]; newColumns[colIndex] = { ...col, width: parseInt(e.target.value) || 100 }; updateTab({ columns: newColumns }); }} placeholder="100" className="h-8 text-xs" />
)) )}
)}
{/* ===== 7. 추가 모달 컬럼 설정 (showAdd일 때) ===== */} {tab.showAdd && (
{(tab.addModalColumns || []).length === 0 ? (

추가 모달에 표시할 컬럼을 설정하세요

) : ( (tab.addModalColumns || []).map((col, colIndex) => (
{ const newColumns = [...(tab.addModalColumns || [])]; newColumns[colIndex] = { ...col, label: e.target.value }; updateTab({ addModalColumns: newColumns }); }} placeholder="라벨" className="h-8 w-24 text-xs" />
{ const newColumns = [...(tab.addModalColumns || [])]; newColumns[colIndex] = { ...col, required: !!checked }; updateTab({ addModalColumns: newColumns }); }} /> 필수
)) )}
)} {/* ===== 8. 데이터 필터링 ===== */}
updateTab({ dataFilter })} menuObjid={menuObjid} />
{/* ===== 9. 중복 데이터 제거 ===== */}
{ if (checked) { updateTab({ deduplication: { enabled: true, groupByColumn: "", keepStrategy: "latest", sortColumn: "start_date", }, }); } else { updateTab({ deduplication: undefined }); } }} />
{tab.deduplication?.enabled && (
)}
{/* ===== 10. 수정 버튼 설정 ===== */} {tab.showEdit && (
{tab.editButton?.mode === "modal" && (
{ updateTab({ editButton: { ...tab.editButton, enabled: true, mode: "modal", modalScreenId: screenId }, }); }} />
)}
{ updateTab({ editButton: { ...tab.editButton, enabled: true, buttonLabel: e.target.value || undefined }, }); }} placeholder="수정" className="h-7 text-xs" />
{/* 그룹핑 기준 컬럼 */}

수정 시 같은 값을 가진 레코드를 함께 불러옵니다

{tabColumns.map((col) => (
{ const current = tab.editButton?.groupByColumns || []; const newColumns = checked ? [...current, col.columnName] : current.filter((c) => c !== col.columnName); updateTab({ editButton: { ...tab.editButton, enabled: true, groupByColumns: newColumns }, }); }} />
))}
)} {/* ===== 11. 삭제 버튼 설정 ===== */} {tab.showDelete && (
{ updateTab({ deleteButton: { ...tab.deleteButton, enabled: true, buttonLabel: e.target.value || undefined }, }); }} placeholder="삭제" className="h-7 text-xs" />
{ updateTab({ deleteButton: { ...tab.deleteButton, enabled: true, confirmMessage: e.target.value || undefined }, }); }} placeholder="정말 삭제하시겠습니까?" className="h-7 text-xs" />
)} {/* ===== 탭 삭제 버튼 ===== */}
); }; /** * SplitPanelLayout 설정 패널 */ export const SplitPanelLayoutConfigPanel: React.FC = ({ config, onChange, tables = [], // 기본값 빈 배열 (현재 화면 테이블만) screenTableName, // 현재 화면의 테이블명 menuObjid, // 🆕 메뉴 OBJID }) => { const [rightTableOpen, setRightTableOpen] = useState(false); const [leftColumnOpen, setLeftColumnOpen] = useState(false); const [rightColumnOpen, setRightColumnOpen] = useState(false); const [loadedTableColumns, setLoadedTableColumns] = useState>({}); const [loadingColumns, setLoadingColumns] = useState>({}); const [allTables, setAllTables] = useState([]); // 조인 모드용 전체 테이블 목록 // 엔티티 참조 테이블 컬럼 type EntityRefTable = { tableName: string; columns: ColumnInfo[] }; const [entityReferenceTables, setEntityReferenceTables] = useState>({}); // 🆕 입력 필드용 로컬 상태 const [isUserEditing, setIsUserEditing] = useState(false); const [localTitles, setLocalTitles] = useState({ left: config.leftPanel?.title || "", right: config.rightPanel?.title || "", }); // 관계 타입 const relationshipType = config.rightPanel?.relation?.type || "detail"; // config 변경 시 로컬 타이틀 동기화 (사용자가 입력 중이 아닐 때만) useEffect(() => { if (!isUserEditing) { setLocalTitles({ left: config.leftPanel?.title || "", right: config.rightPanel?.title || "", }); } }, [config.leftPanel?.title, config.rightPanel?.title, isUserEditing]); // 조인 모드일 때만 전체 테이블 목록 로드 useEffect(() => { if (relationshipType === "join") { const loadAllTables = async () => { try { const { tableManagementApi } = await import("@/lib/api/tableManagement"); const response = await tableManagementApi.getTableList(); if (response.success && response.data) { console.log("✅ 분할패널 조인 모드: 전체 테이블 목록 로드", response.data.length, "개"); setAllTables(response.data); } } catch (error) { console.error("❌ 전체 테이블 목록 로드 실패:", error); } }; loadAllTables(); } else { // 상세 모드일 때는 기본 테이블만 사용 setAllTables([]); } }, [relationshipType]); // screenTableName이 변경되면 좌측 패널 테이블을 항상 화면 테이블로 설정 useEffect(() => { if (screenTableName) { // 좌측 패널은 항상 현재 화면의 테이블 사용 if (config.leftPanel?.tableName !== screenTableName) { updateLeftPanel({ tableName: screenTableName }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [screenTableName]); // 좌측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가 useEffect(() => { const leftTableName = config.leftPanel?.tableName || screenTableName; if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showAdd) { const currentAddModalColumns = config.leftPanel?.addModalColumns || []; const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns); // PK가 추가되었으면 업데이트 if (updatedColumns.length !== currentAddModalColumns.length) { console.log(`🔄 좌측 패널: PK 컬럼 자동 추가 (${leftTableName})`); updateLeftPanel({ addModalColumns: updatedColumns }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showAdd]); // 좌측 패널 하위 항목 추가 모달 PK 자동 추가 useEffect(() => { const leftTableName = config.leftPanel?.tableName || screenTableName; if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showItemAddButton) { const currentAddModalColumns = config.leftPanel?.itemAddConfig?.addModalColumns || []; const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns); // PK가 추가되었으면 업데이트 if (updatedColumns.length !== currentAddModalColumns.length) { console.log(`🔄 좌측 패널 하위 항목 추가: PK 컬럼 자동 추가 (${leftTableName})`); updateLeftPanel({ itemAddConfig: { ...config.leftPanel?.itemAddConfig, addModalColumns: updatedColumns, parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "", sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "", }, }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showItemAddButton]); // 우측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가 useEffect(() => { const rightTableName = config.rightPanel?.tableName; if (rightTableName && loadedTableColumns[rightTableName] && config.rightPanel?.showAdd) { const currentAddModalColumns = config.rightPanel?.addModalColumns || []; const updatedColumns = ensurePrimaryKeysInAddModal(rightTableName, currentAddModalColumns); // PK가 추가되었으면 업데이트 if (updatedColumns.length !== currentAddModalColumns.length) { console.log(`🔄 우측 패널: PK 컬럼 자동 추가 (${rightTableName})`); updateRightPanel({ addModalColumns: updatedColumns }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [config.rightPanel?.tableName, loadedTableColumns, config.rightPanel?.showAdd]); // 테이블 컬럼 로드 함수 const loadTableColumns = async (tableName: string) => { if (loadedTableColumns[tableName] || loadingColumns[tableName]) { return; // 이미 로드되었거나 로딩 중 } setLoadingColumns((prev) => ({ ...prev, [tableName]: true })); try { const columnsResponse = await tableTypeApi.getColumns(tableName); console.log(`📊 테이블 ${tableName} 컬럼 응답:`, columnsResponse); const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => ({ tableName: col.tableName || tableName, columnName: col.columnName || col.column_name, columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType, webType: col.webType || col.web_type, input_type: col.inputType || col.input_type, widgetType: col.widgetType || col.widget_type || col.webType || col.web_type, isNullable: col.isNullable || col.is_nullable, required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", columnDefault: col.columnDefault || col.column_default, characterMaximumLength: col.characterMaximumLength || col.character_maximum_length, isPrimaryKey: col.isPrimaryKey || false, // PK 여부 추가 codeCategory: col.codeCategory || col.code_category, codeValue: col.codeValue || col.code_value, referenceTable: col.referenceTable || col.reference_table, // 🆕 참조 테이블 referenceColumn: col.referenceColumn || col.reference_column, // 🆕 참조 컬럼 displayColumn: col.displayColumn || col.display_column, // 🆕 표시 컬럼 })); console.log(`✅ 테이블 ${tableName} 컬럼 ${columns.length}개 로드됨:`, columns); setLoadedTableColumns((prev) => ({ ...prev, [tableName]: columns })); // 🆕 엔티티 타입 컬럼의 참조 테이블 컬럼도 로드 await loadEntityReferenceColumns(tableName, columns); } catch (error) { console.error(`테이블 ${tableName} 컬럼 로드 실패:`, error); setLoadedTableColumns((prev) => ({ ...prev, [tableName]: [] })); } finally { setLoadingColumns((prev) => ({ ...prev, [tableName]: false })); } }; // 🆕 엔티티 참조 테이블의 컬럼 로드 const loadEntityReferenceColumns = async (sourceTableName: string, columns: ColumnInfo[]) => { const entityColumns = columns.filter( (col) => (col.input_type === "entity" || col.webType === "entity") && col.referenceTable, ); if (entityColumns.length === 0) { return; } console.log( `🔗 테이블 ${sourceTableName}의 엔티티 참조 ${entityColumns.length}개 발견:`, entityColumns.map((c) => `${c.columnName} -> ${c.referenceTable}`), ); const referenceTableData: Array<{ tableName: string; columns: ColumnInfo[] }> = []; // 각 참조 테이블의 컬럼 로드 for (const entityCol of entityColumns) { const refTableName = entityCol.referenceTable!; // 이미 로드했으면 스킵 if (referenceTableData.some((t) => t.tableName === refTableName)) continue; try { const refColumnsResponse = await tableTypeApi.getColumns(refTableName); const refColumns: ColumnInfo[] = (refColumnsResponse || []).map((col: any) => ({ tableName: col.tableName || refTableName, columnName: col.columnName || col.column_name, columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType, input_type: col.inputType || col.input_type, })); referenceTableData.push({ tableName: refTableName, columns: refColumns }); console.log(` ✅ 참조 테이블 ${refTableName} 컬럼 ${refColumns.length}개 로드됨`); } catch (error) { console.error(` ❌ 참조 테이블 ${refTableName} 컬럼 로드 실패:`, error); } } // 참조 테이블 정보 저장 setEntityReferenceTables((prev) => ({ ...prev, [sourceTableName]: referenceTableData, })); console.log(`✅ [엔티티 참조] ${sourceTableName}의 참조 테이블 저장 완료:`, { sourceTableName, referenceTableCount: referenceTableData.length, referenceTables: referenceTableData.map((t) => `${t.tableName}(${t.columns.length}개)`), }); }; // 좌측/우측 테이블이 변경되면 해당 테이블의 컬럼 로드 useEffect(() => { if (config.leftPanel?.tableName) { loadTableColumns(config.leftPanel.tableName); } }, [config.leftPanel?.tableName]); useEffect(() => { if (config.rightPanel?.tableName) { loadTableColumns(config.rightPanel.tableName); } }, [config.rightPanel?.tableName]); // 🆕 좌측/우측 테이블이 모두 선택되면 엔티티 관계 자동 감지 const [autoDetectedRelations, setAutoDetectedRelations] = useState< Array<{ leftColumn: string; rightColumn: string; direction: "left_to_right" | "right_to_left"; inputType: string; displayColumn?: string; }> >([]); const [isDetectingRelations, setIsDetectingRelations] = useState(false); useEffect(() => { const detectRelations = async () => { const leftTable = config.leftPanel?.tableName || screenTableName; const rightTable = config.rightPanel?.tableName; // 조인 모드이고 양쪽 테이블이 모두 있을 때만 감지 if (relationshipType !== "join" || !leftTable || !rightTable) { setAutoDetectedRelations([]); return; } setIsDetectingRelations(true); try { const { tableManagementApi } = await import("@/lib/api/tableManagement"); const response = await tableManagementApi.getTableEntityRelations(leftTable, rightTable); if (response.success && response.data?.relations) { console.log("🔍 엔티티 관계 자동 감지:", response.data.relations); setAutoDetectedRelations(response.data.relations); // 감지된 관계가 있고, 현재 설정된 키가 없으면 자동으로 첫 번째 관계를 설정 const currentKeys = config.rightPanel?.relation?.keys || []; if (response.data.relations.length > 0 && currentKeys.length === 0) { // 첫 번째 관계만 자동 설정 (사용자가 추가로 설정 가능) const firstRel = response.data.relations[0]; console.log("✅ 첫 번째 엔티티 관계 자동 설정:", firstRel); updateRightPanel({ relation: { ...config.rightPanel?.relation, type: "join", useMultipleKeys: true, keys: [ { leftColumn: firstRel.leftColumn, rightColumn: firstRel.rightColumn, }, ], }, }); } } } catch (error) { console.error("❌ 엔티티 관계 감지 실패:", error); setAutoDetectedRelations([]); } finally { setIsDetectingRelations(false); } }; detectRelations(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [config.leftPanel?.tableName, config.rightPanel?.tableName, screenTableName, relationshipType]); console.log("🔧 SplitPanelLayoutConfigPanel 렌더링"); console.log(" - config:", config); console.log(" - tables:", tables); console.log(" - tablesCount:", tables.length); console.log(" - screenTableName:", screenTableName); console.log(" - leftTable:", config.leftPanel?.tableName); console.log(" - rightTable:", config.rightPanel?.tableName); const updateConfig = (updates: Partial) => { const newConfig = { ...config, ...updates }; console.log("🔄 Config 업데이트:", newConfig); onChange(newConfig); }; // PK 컬럼을 추가 모달에 자동으로 포함시키는 함수 const ensurePrimaryKeysInAddModal = ( tableName: string, existingColumns: Array<{ name: string; label: string; required?: boolean }> = [], ) => { const tableColumns = loadedTableColumns[tableName]; if (!tableColumns) { console.warn(`⚠️ 테이블 ${tableName}의 컬럼 정보가 로드되지 않음`); return existingColumns; } // PK 컬럼 찾기 const pkColumns = tableColumns.filter((col) => col.isPrimaryKey); console.log( `🔑 테이블 ${tableName}의 PK 컬럼:`, pkColumns.map((c) => c.columnName), ); // 자동으로 처리되는 컬럼 (백엔드에서 자동 추가) const autoHandledColumns = ["company_code", "company_name"]; // 기존 컬럼 이름 목록 const existingColumnNames = existingColumns.map((col) => col.name); // PK 컬럼을 맨 앞에 추가 (이미 있거나 자동 처리되는 컬럼은 제외) const pkColumnsToAdd = pkColumns .filter((col) => !existingColumnNames.includes(col.columnName)) .filter((col) => !autoHandledColumns.includes(col.columnName)) // 자동 처리 컬럼 제외 .map((col) => ({ name: col.columnName, label: col.columnLabel || col.columnName, required: true, // PK는 항상 필수 })); if (pkColumnsToAdd.length > 0) { console.log( `✅ PK 컬럼 ${pkColumnsToAdd.length}개 자동 추가:`, pkColumnsToAdd.map((c) => c.name), ); } return [...pkColumnsToAdd, ...existingColumns]; }; const updateLeftPanel = (updates: Partial) => { const newConfig = { ...config, leftPanel: { ...config.leftPanel, ...updates }, }; console.log("🔄 Left Panel 업데이트:", newConfig); onChange(newConfig); }; const updateRightPanel = (updates: Partial) => { const newConfig = { ...config, rightPanel: { ...config.rightPanel, ...updates }, }; console.log("🔄 Right Panel 업데이트:", newConfig); onChange(newConfig); }; // 좌측 테이블명 const leftTableName = config.leftPanel?.tableName || screenTableName || ""; // 좌측 테이블 컬럼 (로드된 컬럼 사용) const leftTableColumns = useMemo(() => { return leftTableName ? loadedTableColumns[leftTableName] || [] : []; }, [loadedTableColumns, leftTableName]); // 우측 테이블명 const rightTableName = config.rightPanel?.tableName || ""; // 우측 테이블 컬럼 (로드된 컬럼 사용) const rightTableColumns = useMemo(() => { return rightTableName ? loadedTableColumns[rightTableName] || [] : []; }, [loadedTableColumns, rightTableName]); // 테이블 데이터 로딩 상태 확인 if (!tables || tables.length === 0) { return (

테이블 데이터를 불러올 수 없습니다.

화면에 테이블이 연결되지 않았거나 테이블 목록이 로드되지 않았습니다.

); } // 조인 모드에서 우측 테이블 선택 시 사용할 테이블 목록 const availableRightTables = relationshipType === "join" ? allTables : tables; console.log("📊 분할패널 테이블 목록 상태:"); console.log(" - relationshipType:", relationshipType); console.log(" - allTables:", allTables.length, "개"); console.log(" - availableRightTables:", availableRightTables.length, "개"); return (
{/* 관계 타입 선택 */}

패널 관계 타입

{/* 좌측 패널 설정 */}

좌측 패널 설정 (마스터)

{ setIsUserEditing(true); setLocalTitles((prev) => ({ ...prev, left: e.target.value })); }} onBlur={() => { setIsUserEditing(false); updateLeftPanel({ title: localTitles.left }); }} placeholder="좌측 패널 제목" />
updateLeftPanel({ panelHeaderHeight: parseInt(e.target.value) || 48 })} placeholder="48" min={32} max={120} />

패널 상단 헤더의 높이 (기본: 48px)

{config.leftPanel?.tableName || screenTableName || "테이블이 지정되지 않음"}

좌측 패널은 현재 화면의 테이블 데이터를 표시합니다

updateLeftPanel({ showSearch: checked })} />
updateLeftPanel({ showAdd: checked })} />
updateLeftPanel({ showItemAddButton: checked })} />
{/* 항목별 + 버튼 설정 (하위 항목 추가) */} {config.leftPanel?.showItemAddButton && (

+ 버튼 클릭 시 선택된 항목의 하위 항목을 추가합니다 (예: 부서 → 하위 부서)

{/* 현재 항목의 값을 가져올 컬럼 (sourceColumn) */}

선택된 항목의 어떤 컬럼 값을 사용할지 (예: dept_code)

컬럼을 찾을 수 없습니다. {leftTableColumns .filter((column) => !["company_code", "company_name"].includes(column.columnName)) .map((column) => ( { updateLeftPanel({ itemAddConfig: { ...config.leftPanel?.itemAddConfig, sourceColumn: value, parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "", }, }); }} className="text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ))}
{/* 상위 항목 ID를 저장할 컬럼 (parentColumn) */}

하위 항목에서 상위 항목 ID를 저장할 컬럼 (예: parent_dept_code)

컬럼을 찾을 수 없습니다. {leftTableColumns .filter((column) => !["company_code", "company_name"].includes(column.columnName)) .map((column) => ( { updateLeftPanel({ itemAddConfig: { ...config.leftPanel?.itemAddConfig, parentColumn: value, sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "", }, }); }} className="text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ))}
{/* 하위 항목 추가 모달 컬럼 설정 */}

하위 항목 추가 시 입력받을 필드를 선택하세요

{(config.leftPanel?.itemAddConfig?.addModalColumns || []).length === 0 ? (

설정된 컬럼이 없습니다

) : ( (config.leftPanel?.itemAddConfig?.addModalColumns || []).map((col, index) => { const column = leftTableColumns.find((c) => c.columnName === col.name); const isPK = column?.isPrimaryKey || false; return (
{isPK && ( PK )}
컬럼을 찾을 수 없습니다. {leftTableColumns .filter((column) => !["company_code", "company_name"].includes(column.columnName)) .map((column) => ( { const newColumns = [ ...(config.leftPanel?.itemAddConfig?.addModalColumns || []), ]; newColumns[index] = { ...newColumns[index], name: value, label: column.columnLabel || value, }; updateLeftPanel({ itemAddConfig: { ...config.leftPanel?.itemAddConfig, addModalColumns: newColumns, parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "", sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "", }, }); }} className="text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ))}
); }) )}
)} {/* 좌측 패널 표시 컬럼 설정 */}

좌측 패널에 표시할 컬럼을 선택하세요. 선택하지 않으면 모든 컬럼이 표시됩니다.

{/* 선택된 컬럼 목록 */}
{(config.leftPanel?.columns || []).length === 0 ? (

설정된 컬럼이 없습니다

컬럼을 추가하지 않으면 모든 컬럼이 표시됩니다

) : ( (config.leftPanel?.columns || []).map((col, index) => { const isTableMode = config.leftPanel?.displayMode === "table"; return (
{/* 순서 변경 버튼 */}
컬럼을 찾을 수 없습니다.
{/* 기본 테이블 컬럼 */} {leftTableColumns.map((column) => ( { const newColumns = [...(config.leftPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], name: value, label: column.columnLabel || value, }; updateLeftPanel({ columns: newColumns }); // Popover 닫기 document.body.click(); }} className="text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ))} {/* 🆕 엔티티 참조 테이블 컬럼 */} {leftTableName && entityReferenceTables[leftTableName]?.map((refTable) => ( {refTable.columns.map((column) => { const fullColumnName = `${refTable.tableName}.${column.columnName}`; return ( { const newColumns = [...(config.leftPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], name: value, label: column.columnLabel || column.columnName, }; updateLeftPanel({ columns: newColumns }); // Popover 닫기 document.body.click(); }} className="pl-6 text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ); })} ))}
{/* 테이블 모드 전용 옵션 */} {isTableMode && (
{ const newColumns = [...(config.leftPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], width: parseInt(e.target.value) || 100, }; updateLeftPanel({ columns: newColumns }); }} className="h-7 text-xs" />
)} {/* 🆕 날짜 타입 포맷 설정 (좌측) */} {(() => { const column = leftTableColumns.find((c) => c.columnName === col.name); const dbDateTypes = [ "date", "timestamp", "timestamptz", "timestamp with time zone", "timestamp without time zone", "time", "timetz", ]; const inputDateTypes = ["date", "datetime", "time"]; const isDate = column && (dbDateTypes.includes(column.dataType?.toLowerCase() || "") || inputDateTypes.includes(column.input_type?.toLowerCase() || "") || inputDateTypes.includes(column.webType?.toLowerCase() || "")); if (!isDate) return null; return (
); })()} {/* 🆕 숫자 타입 포맷 설정 (좌측) */} {(() => { const column = leftTableColumns.find((c) => c.columnName === col.name); const dbNumericTypes = [ "numeric", "decimal", "integer", "bigint", "double precision", "real", "smallint", "int4", "int8", "float4", "float8", ]; const inputNumericTypes = ["number", "decimal", "currency", "integer"]; const isNumeric = column && (dbNumericTypes.includes(column.dataType?.toLowerCase() || "") || inputNumericTypes.includes(column.input_type?.toLowerCase() || "") || inputNumericTypes.includes(column.webType?.toLowerCase() || "")); if (!isNumeric) return null; return (
{ const newColumns = [...(config.leftPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", decimalPlaces: e.target.value ? parseInt(e.target.value) : undefined, }, }; updateLeftPanel({ columns: newColumns }); }} className="h-6 w-12 text-xs" />
); })()}
); }) )}
{/* 좌측 패널 추가 모달 컬럼 설정 */} {config.leftPanel?.showAdd && (

추가 버튼 클릭 시 모달에 표시될 입력 필드를 선택하세요

{(config.leftPanel?.addModalColumns || []).length === 0 ? (

설정된 컬럼이 없습니다

) : ( (config.leftPanel?.addModalColumns || []).map((col, index) => { // 현재 컬럼이 PK인지 확인 const column = leftTableColumns.find((c) => c.columnName === col.name); const isPK = column?.isPrimaryKey || false; return (
{isPK && ( PK )}
컬럼을 찾을 수 없습니다. {leftTableColumns .filter((column) => !["company_code", "company_name"].includes(column.columnName)) .map((column) => ( { const newColumns = [...(config.leftPanel?.addModalColumns || [])]; newColumns[index] = { ...newColumns[index], name: value, label: column.columnLabel || value, }; updateLeftPanel({ addModalColumns: newColumns }); }} className="text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ))}
); }) )}
)}
{/* 좌측 패널 데이터 필터링 */}

좌측 패널 데이터 필터링

특정 컬럼 값으로 좌측 패널 데이터를 필터링합니다

({ columnName: col.columnName, columnLabel: col.columnLabel || col.columnName, dataType: col.dataType || "text", input_type: (col as any).input_type, }) as any, )} config={config.leftPanel?.dataFilter} onConfigChange={(dataFilter) => updateLeftPanel({ dataFilter })} menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 />
{/* 우측 패널 설정 */}

우측 패널 설정 ({relationshipType === "detail" ? "상세" : "조인"})

{ setIsUserEditing(true); setLocalTitles((prev) => ({ ...prev, right: e.target.value })); }} onBlur={() => { setIsUserEditing(false); updateRightPanel({ title: localTitles.right }); }} placeholder="우측 패널 제목" />
updateRightPanel({ panelHeaderHeight: parseInt(e.target.value) || 48 })} placeholder="48" min={32} max={120} />

패널 상단 헤더의 높이 (기본: 48px)

{/* 관계 타입에 따라 테이블 선택 UI 변경 */} {relationshipType === "detail" ? ( // 상세 모드: 좌측과 동일한 테이블 (자동 설정)

{config.leftPanel?.tableName || screenTableName || "테이블이 지정되지 않음"}

상세 모드에서는 좌측과 동일한 테이블을 사용합니다

) : ( // 조인 모드: 전체 테이블에서 선택 가능
테이블을 찾을 수 없습니다. {availableRightTables.map((table) => ( { updateRightPanel({ tableName: table.tableName }); setRightTableOpen(false); }} > {table.displayName || table.tableName} {table.displayName && ({table.tableName})} ))}
)}
{/* 요약 표시 설정 (LIST 모드에서만) */} {(config.rightPanel?.displayMode || "list") === "list" && (
{ const value = parseInt(e.target.value) || 3; updateRightPanel({ summaryColumnCount: value }); }} className="bg-white" />

접기 전에 표시할 컬럼 개수 (기본: 3개)

컬럼명 표시 여부

{ updateRightPanel({ summaryShowLabel: checked as boolean }); }} />
)} {/* 엔티티 관계 자동 감지 (읽기 전용) - 조인 모드에서만 표시 */} {relationshipType !== "detail" && (

테이블 타입관리에서 정의된 엔티티 관계입니다

{isDetectingRelations ? (
관계 감지 중...
) : autoDetectedRelations.length > 0 ? (
{autoDetectedRelations.map((rel, index) => (
{leftTableName}.{rel.leftColumn} {rightTableName}.{rel.rightColumn} {rel.inputType === "entity" ? "엔티티" : "카테고리"}
))}

테이블 타입관리에서 엔티티/카테고리 설정을 변경하면 자동으로 적용됩니다

) : config.rightPanel?.tableName ? (

감지된 엔티티 관계가 없습니다

테이블 타입관리에서 엔티티 타입과 참조 테이블을 설정하세요

) : (

우측 테이블을 선택하면 관계를 자동 감지합니다

)}
)}
updateRightPanel({ showSearch: checked })} />
updateRightPanel({ showAdd: checked })} />
{/* 우측 패널 표시 컬럼 설정 */}

우측 패널에 표시할 컬럼을 선택하세요. 선택하지 않으면 모든 컬럼이 표시됩니다.

{/* 선택된 컬럼 목록 */}
{(config.rightPanel?.columns || []).length === 0 ? (

설정된 컬럼이 없습니다

컬럼을 추가하지 않으면 모든 컬럼이 표시됩니다

) : ( (config.rightPanel?.columns || []).map((col, index) => { const isTableMode = config.rightPanel?.displayMode === "table"; return (
{/* 순서 변경 버튼 */}
컬럼을 찾을 수 없습니다.
{/* 기본 테이블 컬럼 */} {rightTableColumns.map((column) => ( { const newColumns = [...(config.rightPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], name: value, label: column.columnLabel || value, }; updateRightPanel({ columns: newColumns }); // Popover 닫기 document.body.click(); }} className="text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ))} {/* 🆕 엔티티 참조 테이블 컬럼 */} {rightTableName && entityReferenceTables[rightTableName]?.map((refTable) => ( {refTable.columns.map((column) => { const fullColumnName = `${refTable.tableName}.${column.columnName}`; return ( { const newColumns = [...(config.rightPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], name: value, label: column.columnLabel || column.columnName, }; updateRightPanel({ columns: newColumns }); // Popover 닫기 document.body.click(); }} className="pl-6 text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ); })} ))}
{/* 테이블 모드 전용 옵션 */} {isTableMode && (
{ const newColumns = [...(config.rightPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], width: parseInt(e.target.value) || 100, }; updateRightPanel({ columns: newColumns }); }} className="h-7 text-xs" />
)} {/* LIST 모드: 볼드 설정 */} {!isTableMode && (
)} {/* 🆕 날짜 타입 포맷 설정 */} {(() => { // 컬럼 타입 확인 (DB 타입 + 입력 타입 모두 체크) const column = rightTableColumns.find((c) => c.columnName === col.name); const dbDateTypes = [ "date", "timestamp", "timestamptz", "timestamp with time zone", "timestamp without time zone", "time", "timetz", ]; const inputDateTypes = ["date", "datetime", "time"]; const isDate = column && (dbDateTypes.includes(column.dataType?.toLowerCase() || "") || inputDateTypes.includes(column.input_type?.toLowerCase() || "") || inputDateTypes.includes(column.webType?.toLowerCase() || "")); if (!isDate) return null; return (
{/* 미리보기 */}

미리보기:

{(() => { const now = new Date(); const format = col.format?.dateFormat || "YYYY-MM-DD"; if (format === "relative") return "3일 전"; return format .replace("YYYY", String(now.getFullYear())) .replace("MM", String(now.getMonth() + 1).padStart(2, "0")) .replace("DD", String(now.getDate()).padStart(2, "0")) .replace("HH", String(now.getHours()).padStart(2, "0")) .replace("mm", String(now.getMinutes()).padStart(2, "0")) .replace("ss", String(now.getSeconds()).padStart(2, "0")); })()}

); })()} {/* 🆕 숫자 타입 포맷 설정 */} {(() => { // 컬럼 타입 확인 (DB 타입 + 입력 타입 모두 체크) const column = rightTableColumns.find((c) => c.columnName === col.name); const dbNumericTypes = [ "numeric", "decimal", "integer", "bigint", "double precision", "real", "smallint", "int4", "int8", "float4", "float8", ]; const inputNumericTypes = ["number", "decimal", "currency", "integer"]; const isNumeric = column && (dbNumericTypes.includes(column.dataType?.toLowerCase() || "") || inputNumericTypes.includes(column.input_type?.toLowerCase() || "") || inputNumericTypes.includes(column.webType?.toLowerCase() || "")); if (!isNumeric) return null; return (
{/* 천 단위 구분자 */} {/* 소수점 자릿수 */}
{ const newColumns = [...(config.rightPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, decimalPlaces: e.target.value ? parseInt(e.target.value) : undefined, }, }; updateRightPanel({ columns: newColumns }); }} className="h-7 text-xs" />
{/* 접두사 */}
{ const newColumns = [...(config.rightPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, prefix: e.target.value || undefined, }, }; updateRightPanel({ columns: newColumns }); }} className="h-7 text-xs" />
{/* 접미사 */}
{ const newColumns = [...(config.rightPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, suffix: e.target.value || undefined, }, }; updateRightPanel({ columns: newColumns }); }} className="h-7 text-xs" />
{/* 미리보기 */} {(col.format?.thousandSeparator || col.format?.prefix || col.format?.suffix || col.format?.decimalPlaces !== undefined) && (

미리보기:

{col.format?.prefix || ""} {(1234567.89).toLocaleString("ko-KR", { minimumFractionDigits: col.format?.decimalPlaces ?? 0, maximumFractionDigits: col.format?.decimalPlaces ?? 10, useGrouping: col.format?.thousandSeparator ?? false, })} {col.format?.suffix || ""}

)}
); })()}
); }) )}
{/* 우측 패널 추가 모달 컬럼 설정 */} {config.rightPanel?.showAdd && (

추가 버튼 클릭 시 모달에 표시될 입력 필드를 선택하세요

{(config.rightPanel?.addModalColumns || []).length === 0 ? (

설정된 컬럼이 없습니다

) : ( (config.rightPanel?.addModalColumns || []).map((col, index) => { // 현재 컬럼이 PK인지 확인 const column = rightTableColumns.find((c) => c.columnName === col.name); const isPK = column?.isPrimaryKey || false; return (
{isPK && ( PK )}
컬럼을 찾을 수 없습니다. {rightTableColumns .filter((column) => !["company_code", "company_name"].includes(column.columnName)) .map((column) => ( { const newColumns = [...(config.rightPanel?.addModalColumns || [])]; newColumns[index] = { ...newColumns[index], name: value, label: column.columnLabel || value, }; updateRightPanel({ addModalColumns: newColumns }); }} className="text-xs" > {column.columnLabel || column.columnName} ({column.columnName}) ))}
); }) )}
{/* 중계 테이블 설정 */}

중계 테이블을 사용하여 다대다 관계를 구현합니다

{ const addConfig = config.rightPanel?.addConfig || {}; updateRightPanel({ addConfig: { ...addConfig, targetTable: e.target.value, }, }); }} placeholder="예: user_dept" className="mt-1 h-8 text-xs" />

데이터가 실제로 저장될 중계 테이블명

{ const addConfig = config.rightPanel?.addConfig || {}; updateRightPanel({ addConfig: { ...addConfig, leftPanelColumn: e.target.value, }, }); }} placeholder="예: dept_code" className="mt-1 h-8 text-xs" />

좌측 패널에서 선택한 항목의 어떤 컬럼값을 가져올지

{ const addConfig = config.rightPanel?.addConfig || {}; updateRightPanel({ addConfig: { ...addConfig, targetColumn: e.target.value, }, }); }} placeholder="예: dept_code" className="mt-1 h-8 text-xs" />

중계 테이블의 어떤 컬럼에 좌측값을 저장할지