"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, Database, 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 || [])]; // undefined 값도 명시적으로 덮어쓰기 위해 Object.assign 대신 직접 처리 const updatedTab = { ...tab }; Object.keys(updates).forEach((key) => { (updatedTab as any)[key] = (updates as any)[key]; }); newTabs[tabIndex] = updatedTab; 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 [leftTableOpen, setLeftTableOpen] = useState(false); // 🆕 좌측 테이블 Combobox 상태 const [rightTableOpen, setRightTableOpen] = 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(() => { 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(); }, []); // 초기 로드 시 좌측 패널 테이블이 없으면 화면 테이블로 설정 useEffect(() => { if (screenTableName && !config.leftPanel?.tableName) { updateLeftPanel({ tableName: screenTableName }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [screenTableName]); // 테이블 컬럼 로드 함수 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); }; 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 = useMemo(() => { if (relationshipType === "detail") { return leftTableName; // 상세 모드에서는 좌측과 동일 } return config.rightPanel?.tableName || ""; }, [relationshipType, leftTableName, 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 (
{/* 관계 타입 선택 */}

패널 관계 타입

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

좌측 패널 설정 (마스터)

{/* 🆕 좌측 패널 테이블 선택 */}
테이블을 찾을 수 없습니다. {/* 화면 기본 테이블 */} {screenTableName && ( { updateLeftPanel({ tableName: screenTableName, columns: [] }); setLeftTableOpen(false); }} className="text-xs" > {allTables.find((t) => (t.tableName || t.table_name) === screenTableName)?.tableLabel || allTables.find((t) => (t.tableName || t.table_name) === screenTableName)?.displayName || screenTableName} )} {/* 전체 테이블 */} {allTables .filter((t) => (t.tableName || t.table_name) !== screenTableName) .map((table) => { const tableName = table.tableName || table.table_name; const displayName = table.tableLabel || table.displayName || tableName; return ( { updateLeftPanel({ tableName, columns: [] }); setLeftTableOpen(false); }} className="text-xs" > {displayName} ); })} {config.leftPanel?.tableName && config.leftPanel?.tableName !== screenTableName && (

화면 기본 테이블이 아닌 다른 테이블의 데이터를 표시합니다.

)}
{ 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)

*/}
{/* 좌측 패널 표시 컬럼 설정 - 체크박스 방식 */}
{/* 컬럼 체크박스 목록 */}
{leftTableColumns.length === 0 ? (

컬럼 로딩 중...

) : ( leftTableColumns .filter((column) => !["company_code", "company_name"].includes(column.columnName)) .map((column) => { const isSelected = (config.leftPanel?.columns || []).some((c) => c.name === column.columnName); return (
{ const currentColumns = config.leftPanel?.columns || []; if (isSelected) { // 제거 const newColumns = currentColumns.filter((c) => c.name !== column.columnName); updateLeftPanel({ columns: newColumns }); } else { // 추가 const newColumn = { name: column.columnName, label: column.columnLabel || column.columnName, width: 100, }; updateLeftPanel({ columns: [...currentColumns, newColumn] }); } }} > { const currentColumns = config.leftPanel?.columns || []; if (isSelected) { const newColumns = currentColumns.filter((c) => c.name !== column.columnName); updateLeftPanel({ columns: newColumns }); } else { const newColumn = { name: column.columnName, label: column.columnLabel || column.columnName, width: 100, }; updateLeftPanel({ columns: [...currentColumns, newColumn] }); } }} className="pointer-events-none h-3.5 w-3.5 shrink-0" /> {column.columnLabel || column.columnName}
); }) )}
{/* 선택된 컬럼 상세 설정 */} {(config.leftPanel?.columns || []).length > 0 && (
{(config.leftPanel?.columns || []).map((col, index) => { const column = leftTableColumns.find((c) => c.columnName === col.name); const isTableMode = config.leftPanel?.displayMode === "table"; // 숫자 타입 판별 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() || "")); return (
{ const newColumns = [...(config.leftPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], label: e.target.value }; updateLeftPanel({ columns: newColumns }); }} placeholder="제목" className="h-6 flex-1 text-xs" /> { const newColumns = [...(config.leftPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], width: parseInt(e.target.value) || 100 }; updateLeftPanel({ columns: newColumns }); }} placeholder="너비" className="h-6 w-14 text-xs" /> {/* 숫자 타입: 천단위 구분자 체크박스 */} {isNumeric && ( )}
); })}
)}
{/* 좌측 패널 데이터 필터링 */}

좌측 패널 데이터 필터링

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

({ 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" && (

우측 테이블에서 좌측 테이블을 참조하는 컬럼을 선택하세요

{config.rightPanel?.relation?.foreignKey && (

선택된 컬럼의 엔티티 설정이 자동으로 적용됩니다.

)}
)} {/* 우측 패널 표시 컬럼 설정 - 체크박스 방식 */}
{/* 컬럼 체크박스 목록 */}
{rightTableColumns.length === 0 ? (

테이블을 선택해주세요

) : ( rightTableColumns .filter((column) => !["company_code", "company_name"].includes(column.columnName)) .map((column) => { const isSelected = (config.rightPanel?.columns || []).some((c) => c.name === column.columnName); return (
{ const currentColumns = config.rightPanel?.columns || []; if (isSelected) { const newColumns = currentColumns.filter((c) => c.name !== column.columnName); updateRightPanel({ columns: newColumns }); } else { const newColumn = { name: column.columnName, label: column.columnLabel || column.columnName, width: 100, }; updateRightPanel({ columns: [...currentColumns, newColumn] }); } }} > { const currentColumns = config.rightPanel?.columns || []; if (isSelected) { const newColumns = currentColumns.filter((c) => c.name !== column.columnName); updateRightPanel({ columns: newColumns }); } else { const newColumn = { name: column.columnName, label: column.columnLabel || column.columnName, width: 100, }; updateRightPanel({ columns: [...currentColumns, newColumn] }); } }} className="pointer-events-none h-3.5 w-3.5 shrink-0" /> {column.columnLabel || column.columnName}
); }) )}
{/* 선택된 컬럼 상세 설정 */} {(config.rightPanel?.columns || []).length > 0 && (
{(config.rightPanel?.columns || []).map((col, index) => { 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() || "")); return (
{ const newColumns = [...(config.rightPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], label: e.target.value }; updateRightPanel({ columns: newColumns }); }} placeholder="제목" className="h-6 flex-1 text-xs" /> { const newColumns = [...(config.rightPanel?.columns || [])]; newColumns[index] = { ...newColumns[index], width: parseInt(e.target.value) || 100 }; updateRightPanel({ columns: newColumns }); }} placeholder="너비" className="h-6 w-14 text-xs" /> {/* 숫자 타입: 천단위 구분자 체크박스 */} {isNumeric && ( )}
); })}
)}
{/* 우측 패널 데이터 필터링 */}

우측 패널 데이터 필터링

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

({ columnName: col.columnName, columnLabel: col.columnLabel || col.columnName, dataType: col.dataType || "text", input_type: (col as any).input_type, }) as any, )} config={config.rightPanel?.dataFilter} onConfigChange={(dataFilter) => updateRightPanel({ dataFilter })} menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 />
{/* 우측 패널 중복 제거 */}

중복 데이터 제거

같은 값을 가진 데이터를 하나로 통합하여 표시

{ if (checked) { updateRightPanel({ deduplication: { enabled: true, groupByColumn: "", keepStrategy: "latest", sortColumn: "start_date", }, }); } else { updateRightPanel({ deduplication: undefined }); } }} />
{config.rightPanel?.deduplication?.enabled && (
{/* 중복 제거 기준 컬럼 */}

이 컬럼의 값이 같은 데이터들 중 하나만 표시합니다

{/* 유지 전략 */}

{config.rightPanel?.deduplication?.keepStrategy === "latest" && "가장 최근에 추가된 데이터를 표시합니다"} {config.rightPanel?.deduplication?.keepStrategy === "earliest" && "가장 먼저 추가된 데이터를 표시합니다"} {config.rightPanel?.deduplication?.keepStrategy === "current_date" && "오늘 날짜 기준으로 유효한 기간의 데이터를 표시합니다"} {config.rightPanel?.deduplication?.keepStrategy === "base_price" && "기준단가(base_price)로 체크된 데이터를 표시합니다"}

{/* 정렬 기준 컬럼 (latest/earliest만) */} {(config.rightPanel?.deduplication?.keepStrategy === "latest" || config.rightPanel?.deduplication?.keepStrategy === "earliest") && (

이 컬럼의 값으로 최신/최초를 판단합니다 (보통 날짜 컬럼)

)}
)}
{/* 🆕 우측 패널 수정 버튼 설정 */}

수정 버튼 설정

우측 리스트의 수정 버튼 동작 방식 설정

{ updateRightPanel({ editButton: { enabled: checked, mode: config.rightPanel?.editButton?.mode || "auto", buttonLabel: config.rightPanel?.editButton?.buttonLabel, buttonVariant: config.rightPanel?.editButton?.buttonVariant, }, }); }} />
{(config.rightPanel?.editButton?.enabled ?? true) && (
{/* 수정 모드 */}

{config.rightPanel?.editButton?.mode === "modal" ? "지정한 화면을 모달로 열어 데이터를 수정합니다" : "현재 위치에서 직접 데이터를 수정합니다"}

{/* 모달 화면 선택 (modal 모드일 때만) */} {config.rightPanel?.editButton?.mode === "modal" && (
updateRightPanel({ editButton: { ...config.rightPanel?.editButton!, modalScreenId: screenId, }, }) } />

수정 버튼 클릭 시 열릴 화면을 선택하세요

)} {/* 버튼 라벨 */}
updateRightPanel({ editButton: { ...config.rightPanel?.editButton!, buttonLabel: e.target.value, enabled: config.rightPanel?.editButton?.enabled ?? true, mode: config.rightPanel?.editButton?.mode || "auto", }, }) } className="h-8 text-xs" placeholder="수정" />
{/* 버튼 스타일 */}
{/* 🆕 그룹핑 기준 컬럼 설정 (modal 모드일 때만 표시) */} {config.rightPanel?.editButton?.mode === "modal" && ( { updateRightPanel({ editButton: { ...config.rightPanel?.editButton!, groupByColumns: columns, enabled: config.rightPanel?.editButton?.enabled ?? true, mode: config.rightPanel?.editButton?.mode || "auto", }, }); }} /> )}
)}
{/* 🆕 우측 패널 삭제 버튼 설정 */}

삭제 버튼 설정

{ updateRightPanel({ deleteButton: { enabled: checked, buttonLabel: config.rightPanel?.deleteButton?.buttonLabel, buttonVariant: config.rightPanel?.deleteButton?.buttonVariant, }, }); }} />
{(config.rightPanel?.deleteButton?.enabled ?? true) && (
{/* 버튼 라벨 */}
{ updateRightPanel({ deleteButton: { ...config.rightPanel?.deleteButton!, buttonLabel: e.target.value || undefined, enabled: config.rightPanel?.deleteButton?.enabled ?? true, }, }); }} className="h-8 text-xs" />
{/* 버튼 스타일 */}
{/* 삭제 확인 메시지 */}
{ updateRightPanel({ deleteButton: { ...config.rightPanel?.deleteButton!, confirmMessage: e.target.value || undefined, enabled: config.rightPanel?.deleteButton?.enabled ?? true, }, }); }} className="h-8 text-xs" />
)}
{/* ======================================== */} {/* 추가 탭 설정 (우측 패널과 동일한 구조) */} {/* ======================================== */}

추가 탭

우측 패널에 다른 테이블 데이터를 탭으로 추가합니다

{/* 추가된 탭 목록 */} {(config.rightPanel?.additionalTabs?.length || 0) > 0 ? ( {config.rightPanel?.additionalTabs?.map((tab, tabIndex) => ( ))} ) : (

추가된 탭이 없습니다. [탭 추가] 버튼을 클릭하여 새 탭을 추가하세요.

)}
{/* 레이아웃 설정 */}
updateConfig({ splitRatio: value[0] })} min={20} max={80} step={5} />
updateConfig({ resizable: checked })} />
updateConfig({ autoLoad: checked })} />
); };