"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, Move, Settings2, PanelLeft, PanelRight, Layers, ChevronRight, Link2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { PanelInlineComponent } from "./types"; 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { tableTypeApi } from "@/lib/api/screen"; import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { DndContext, closestCenter, type DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; // 드래그 가능한 컬럼 아이템 function SortableColumnRow({ id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove, onShowInSummaryChange, onShowInDetailChange, }: { id: string; col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean }; index: number; isNumeric: boolean; isEntityJoin?: boolean; onLabelChange: (value: string) => void; onWidthChange: (value: number) => void; onFormatChange: (checked: boolean) => void; onRemove: () => void; onShowInSummaryChange?: (checked: boolean) => void; onShowInDetailChange?: (checked: boolean) => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), transition }; return (
{isEntityJoin ? ( ) : ( #{index + 1} )} onLabelChange(e.target.value)} placeholder="라벨" className="h-6 min-w-0 flex-1 text-xs" /> onWidthChange(parseInt(e.target.value) || 100)} placeholder="너비" className="h-6 w-14 shrink-0 text-xs" /> {isNumeric && ( )} {/* 헤더/상세 표시 토글 */} {onShowInSummaryChange && ( )} {onShowInDetailChange && ( )}
); } 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; // Entity 조인 컬럼 (테이블별) entityJoinColumns?: Record; joinTables: Array<{ tableName: string; currentDisplayColumn: string; joinConfig?: any; availableColumns: Array<{ columnName: string; columnLabel: string; dataType: string; inputType?: string; description?: string }> }>; }>; } const AdditionalTabConfigPanel: React.FC = ({ tab, tabIndex, config, updateRightPanel, availableRightTables, leftTableColumns, menuObjid, loadedTableColumns, loadTableColumns, loadingColumns, entityJoinColumns: entityJoinColumnsMap, }) => { // 탭 테이블 변경 시 컬럼 로드 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" />
{/* ===== 2. 테이블 선택 ===== */}

테이블 설정

테이블을 찾을 수 없습니다. {availableRightTables.map((table) => ( updateTab({ tableName: table.tableName, columns: [] })} > {table.displayName || table.tableName} {table.displayName && ({table.tableName})} ))}
{/* ===== 3. 표시 모드 + 요약 설정 ===== */}

표시 설정

{/* 요약 설정 (목록 모드) */} {(tab.displayMode || "list") === "list" && (
updateTab({ summaryColumnCount: parseInt(e.target.value) || 3 })} className="bg-white" />

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

컬럼명 표시 여부

updateTab({ summaryShowLabel: checked as boolean })} />
)}
{/* ===== 4. 컬럼 매핑 (연결 키) ===== */}

컬럼 매핑 (연결 키)

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

{/* ===== 5. 기능 버튼 ===== */}

기능 버튼

updateTab({ showSearch: !!checked })} />
updateTab({ showAdd: !!checked })} />
updateTab({ showEdit: !!checked })} />
updateTab({ showDelete: !!checked })} />
{/* ===== 6. 표시할 컬럼 - DnD + Entity 조인 통합 ===== */} {(() => { const selectedColumns = tab.columns || []; const filteredTabCols = tabColumns.filter((c) => !["company_code", "company_name"].includes(c.columnName)); const unselectedCols = filteredTabCols.filter((c) => !selectedColumns.some((sc) => sc.name === c.columnName)); const dbNumericTypes = ["numeric", "decimal", "integer", "bigint", "double precision", "real", "smallint", "int4", "int8", "float4", "float8"]; const inputNumericTypes = ["number", "decimal", "currency", "integer"]; const handleTabDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = selectedColumns.findIndex((c) => c.name === active.id); const newIndex = selectedColumns.findIndex((c) => c.name === over.id); if (oldIndex !== -1 && newIndex !== -1) { updateTab({ columns: arrayMove([...selectedColumns], oldIndex, newIndex) }); } } }; return (

표시할 컬럼 ({selectedColumns.length}개 선택)

{!tab.tableName ? (

테이블을 선택해주세요

) : loadingTabColumns ? (

컬럼을 불러오는 중...

) : ( <> {selectedColumns.length > 0 && ( c.name)} strategy={verticalListSortingStrategy}>
{selectedColumns.map((col, index) => { const colInfo = tabColumns.find((c) => c.columnName === col.name); const isNumeric = colInfo && ( dbNumericTypes.includes(colInfo.dataType?.toLowerCase() || "") || inputNumericTypes.includes(colInfo.input_type?.toLowerCase() || "") || inputNumericTypes.includes(colInfo.webType?.toLowerCase() || "") ); return ( { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], label: value }; updateTab({ columns: newColumns }); }} onWidthChange={(value) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], width: value }; updateTab({ columns: newColumns }); }} onFormatChange={(checked) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } }; updateTab({ columns: newColumns }); }} onRemove={() => updateTab({ columns: selectedColumns.filter((_, i) => i !== index) })} onShowInSummaryChange={(checked) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], showInSummary: checked }; updateTab({ columns: newColumns }); }} onShowInDetailChange={(checked) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], showInDetail: checked }; updateTab({ columns: newColumns }); }} /> ); })}
)} {selectedColumns.length > 0 && unselectedCols.length > 0 && (
미선택 컬럼
)}
{unselectedCols.map((column) => (
{ updateTab({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] }); }} > {column.columnLabel || column.columnName}
))}
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */} {(() => { const joinData = tab.tableName ? entityJoinColumnsMap?.[tab.tableName] : null; if (!joinData || joinData.joinTables.length === 0) return null; return joinData.joinTables.map((joinTable, tableIndex) => { const joinColumnsToShow = joinTable.availableColumns.filter((column) => { const matchingJoinColumn = joinData.availableColumns.find( (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, ); if (!matchingJoinColumn) return false; return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias); }); const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length; if (joinColumnsToShow.length === 0 && addedCount === 0) return null; return (
{joinTable.tableName} {addedCount > 0 && ( {addedCount}개 선택 )} {joinColumnsToShow.length}개 남음
{joinColumnsToShow.map((column, colIndex) => { const matchingJoinColumn = joinData.availableColumns.find( (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, ); if (!matchingJoinColumn) return null; return (
{ updateTab({ columns: [...selectedColumns, { name: matchingJoinColumn.joinAlias, label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, width: 100, isEntityJoin: true, joinInfo: { sourceTable: tab.tableName!, sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "", referenceTable: matchingJoinColumn.tableName, joinAlias: matchingJoinColumn.joinAlias, }, }], }); }} > {column.columnLabel || column.columnName}
); })} {joinColumnsToShow.length === 0 && (

모든 컬럼이 이미 추가되었습니다

)}
); }); })()} )}
); })()} {/* ===== 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 }); }} /> 필수
)) )}
)} {/* Entity 조인 컬럼은 표시 컬럼 목록에 통합됨 */} {/* ===== 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 }, }); }} />
))}
)} {/* ===== 10-1. 추가 버튼 설정 ===== */} {tab.showAdd && (

추가 버튼 설정

{tab.addButton?.mode === "modal" && (
{ updateTab({ addButton: { ...tab.addButton, enabled: true, mode: "modal", modalScreenId: screenId }, }); }} />
)}
{ updateTab({ addButton: { ...tab.addButton, enabled: true, buttonLabel: e.target.value || undefined }, }); }} placeholder="추가" className="h-7 text-xs" />
)} {/* ===== 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 [activeModal, setActiveModal] = useState(null); // 설정 모달 상태 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>({}); // Entity 조인 컬럼 (테이블별) const [entityJoinColumns, setEntityJoinColumns] = useState< Record< string, { availableColumns: Array<{ tableName: string; columnName: string; columnLabel: string; dataType: string; joinAlias: string; suggestedLabel: string; }>; joinTables: Array<{ tableName: string; currentDisplayColumn: string; joinConfig?: any; availableColumns: Array<{ columnName: string; columnLabel: string; dataType: string; inputType?: string; description?: string; }>; }>; } > >({}); const [loadingEntityJoins, setLoadingEntityJoins] = 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); // Entity 조인 컬럼 정보도 로드 await loadEntityJoinColumns(tableName); } catch (error) { console.error(`테이블 ${tableName} 컬럼 로드 실패:`, error); setLoadedTableColumns((prev) => ({ ...prev, [tableName]: [] })); } finally { setLoadingColumns((prev) => ({ ...prev, [tableName]: false })); } }; // Entity 조인 컬럼 로드 const loadEntityJoinColumns = async (tableName: string) => { if (entityJoinColumns[tableName] || loadingEntityJoins[tableName]) return; setLoadingEntityJoins((prev) => ({ ...prev, [tableName]: true })); try { const result = await entityJoinApi.getEntityJoinColumns(tableName); console.log(`🔗 Entity 조인 컬럼 (${tableName}):`, result); setEntityJoinColumns((prev) => ({ ...prev, [tableName]: { availableColumns: result.availableColumns || [], joinTables: result.joinTables || [], }, })); } catch (error) { console.error(`❌ Entity 조인 컬럼 조회 실패 (${tableName}):`, error); setEntityJoinColumns((prev) => ({ ...prev, [tableName]: { availableColumns: [], joinTables: [] }, })); } finally { setLoadingEntityJoins((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 (
{/* ===== 간소화된 설정 메뉴 카드 ===== */}

분할 패널 설정

{[ { id: "basic", title: "기본 설정", desc: `${relationshipType === "detail" ? "1건 상세보기" : "연관 목록"} | 비율 ${config.splitRatio || 30}%`, icon: Settings2, }, { id: "left", title: "좌측 패널", desc: config.leftPanel?.tableName || screenTableName || "미설정", icon: PanelLeft, }, { id: "right", title: "우측 패널", desc: config.rightPanel?.tableName || "미설정", icon: PanelRight, }, { id: "tabs", title: "추가 탭", desc: `${config.rightPanel?.additionalTabs?.length || 0}개 탭`, icon: Layers, }, ].map((item) => ( ))}
{/* ===== 기본 설정 모달 ===== */} !open && setActiveModal(null)}> 기본 설정 패널 관계 타입 및 레이아웃을 설정합니다
{/* 관계 타입 선택 */}

우측 패널 표시 방식

좌측 항목 선택 시 우측에 어떤 형태로 데이터를 보여줄지 설정합니다

{/* 레이아웃 설정 */}

레이아웃

updateConfig({ splitRatio: value[0] })} min={20} max={80} step={5} />
updateConfig({ resizable: checked })} />
updateConfig({ autoLoad: checked })} />
{/* ===== 좌측 패널 모달 ===== */} !open && setActiveModal(null)}> 좌측 패널 설정 마스터 데이터 표시 및 필터링을 설정합니다
{/* 좌측 패널 설정 */}

좌측 패널 설정 (마스터)

{/* 🆕 좌측 패널 테이블 선택 */}
테이블을 찾을 수 없습니다. {/* 화면 기본 테이블 */} {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)

*/}
{config.leftPanel?.displayMode === "custom" && (

화면 디자이너에서 좌측 패널에 컴포넌트를 드래그하여 배치하세요.

)}
{/* 🆕 커스텀 모드: 배치된 컴포넌트 목록 */} {config.leftPanel?.displayMode === "custom" && (
{!config.leftPanel?.components || config.leftPanel.components.length === 0 ? (

디자인 화면에서 컴포넌트를 드래그하여 추가하세요

) : (
{config.leftPanel.components.map((comp: PanelInlineComponent) => (

{comp.label || comp.componentType}

{comp.componentType} | 위치: ({comp.position?.x || 0}, {comp.position?.y || 0}) | 크기: {comp.size?.width || 0}x{comp.size?.height || 0}

))}
)}
)} {/* 좌측 패널 표시 컬럼 설정 - 드래그앤드롭 (커스텀 모드가 아닐 때만) */} {config.leftPanel?.displayMode !== "custom" && (() => { const selectedColumns = config.leftPanel?.columns || []; const filteredTableColumns = leftTableColumns.filter((c) => !["company_code", "company_name"].includes(c.columnName)); const unselectedColumns = filteredTableColumns.filter((c) => !selectedColumns.some((sc) => sc.name === c.columnName)); const dbNumericTypes = ["numeric", "decimal", "integer", "bigint", "double precision", "real", "smallint", "int4", "int8", "float4", "float8"]; const inputNumericTypes = ["number", "decimal", "currency", "integer"]; const handleLeftDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = selectedColumns.findIndex((c) => c.name === active.id); const newIndex = selectedColumns.findIndex((c) => c.name === over.id); if (oldIndex !== -1 && newIndex !== -1) { updateLeftPanel({ columns: arrayMove([...selectedColumns], oldIndex, newIndex) }); } } }; return (
{leftTableColumns.length === 0 ? (

컬럼 로딩 중...

) : ( <> {/* 선택된 컬럼 - 드래그앤드롭 정렬 */} {selectedColumns.length > 0 && ( c.name)} strategy={verticalListSortingStrategy}>
{selectedColumns.map((col, index) => { const colInfo = leftTableColumns.find((c) => c.columnName === col.name); const isNumeric = colInfo && ( dbNumericTypes.includes(colInfo.dataType?.toLowerCase() || "") || inputNumericTypes.includes(colInfo.input_type?.toLowerCase() || "") || inputNumericTypes.includes(colInfo.webType?.toLowerCase() || "") ); return ( { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], label: value }; updateLeftPanel({ columns: newColumns }); }} onWidthChange={(value) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], width: value }; updateLeftPanel({ columns: newColumns }); }} onFormatChange={(checked) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } }; updateLeftPanel({ columns: newColumns }); }} onRemove={() => updateLeftPanel({ columns: selectedColumns.filter((_, i) => i !== index) })} /> ); })}
)} {/* 구분선 */} {selectedColumns.length > 0 && unselectedColumns.length > 0 && (
미선택 컬럼
)} {/* 미선택 컬럼 - 클릭으로 추가 */}
{unselectedColumns.map((column) => (
{ updateLeftPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] }); }} > {column.columnLabel || column.columnName}
))}
{/* 좌측 패널 - Entity 조인 컬럼 아코디언 */} {(() => { const leftTable = config.leftPanel?.tableName || screenTableName; const joinData = leftTable ? entityJoinColumns[leftTable] : null; if (!joinData || joinData.joinTables.length === 0) return null; return joinData.joinTables.map((joinTable, tableIndex) => { const joinColumnsToShow = joinTable.availableColumns.filter((column) => { const matchingJoinColumn = joinData.availableColumns.find( (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, ); if (!matchingJoinColumn) return false; return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias); }); const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length; if (joinColumnsToShow.length === 0 && addedCount === 0) return null; return (
{joinTable.tableName} {addedCount > 0 && ( {addedCount}개 선택 )} {joinColumnsToShow.length}개 남음
{joinColumnsToShow.map((column, colIndex) => { const matchingJoinColumn = joinData.availableColumns.find( (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, ); if (!matchingJoinColumn) return null; return (
{ updateLeftPanel({ columns: [...selectedColumns, { name: matchingJoinColumn.joinAlias, label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, width: 100, isEntityJoin: true, joinInfo: { sourceTable: leftTable!, sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "", referenceTable: matchingJoinColumn.tableName, joinAlias: matchingJoinColumn.joinAlias, }, }], }); }} > {column.columnLabel || column.columnName}
); })} {joinColumnsToShow.length === 0 && (

모든 컬럼이 이미 추가되었습니다

)}
); }); })()} )}
); })()}
{/* 좌측 패널 데이터 필터링 */}

좌측 패널 데이터 필터링

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

({ 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 전달 />
{/* 좌측 패널 버튼 설정 */}

좌측 패널 버튼 설정

{/* 버튼 표시 체크박스 */}
updateLeftPanel({ showSearch: !!checked })} />
updateLeftPanel({ showAdd: !!checked })} />
updateLeftPanel({ showEdit: !!checked })} />
updateLeftPanel({ showDelete: !!checked })} />
{/* 추가 버튼 상세 설정 */} {config.leftPanel?.showAdd && (

추가 버튼 설정

{config.leftPanel?.addButton?.mode === "modal" && (
updateLeftPanel({ addButton: { ...config.leftPanel?.addButton, enabled: true, mode: "modal", modalScreenId: screenId }, }) } />
)}
updateLeftPanel({ addButton: { ...config.leftPanel?.addButton, enabled: true, mode: config.leftPanel?.addButton?.mode || "auto", buttonLabel: e.target.value || undefined, }, }) } placeholder="추가" className="h-7 text-xs" />
)} {/* 수정 버튼 상세 설정 */} {(config.leftPanel?.showEdit ?? true) && (

수정 버튼 설정

{config.leftPanel?.editButton?.mode === "modal" && (
updateLeftPanel({ editButton: { ...config.leftPanel?.editButton, enabled: true, mode: "modal", modalScreenId: screenId }, }) } />
)}
updateLeftPanel({ editButton: { ...config.leftPanel?.editButton, enabled: true, mode: config.leftPanel?.editButton?.mode || "auto", buttonLabel: e.target.value || undefined, }, }) } placeholder="수정" className="h-7 text-xs" />
)}
{/* ===== 우측 패널 모달 ===== */} !open && setActiveModal(null)}> 우측 패널 설정 상세/필터 데이터 표시, 버튼, 중복 제거를 설정합니다
{/* 우측 패널 설정 */}

우측 패널 설정 ({relationshipType === "detail" ? "1건 상세보기" : "연관 목록"})

{ 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})} ))}
)}
{config.rightPanel?.displayMode === "custom" && (

화면 디자이너에서 우측 패널에 컴포넌트를 드래그하여 배치하세요.

)}
{/* 🆕 커스텀 모드: 배치된 컴포넌트 목록 */} {config.rightPanel?.displayMode === "custom" && (
{!config.rightPanel?.components || config.rightPanel.components.length === 0 ? (

디자인 화면에서 컴포넌트를 드래그하여 추가하세요

) : (
{config.rightPanel.components.map((comp: PanelInlineComponent) => (

{comp.label || comp.componentType}

{comp.componentType} | 위치: ({comp.position?.x || 0}, {comp.position?.y || 0}) | 크기: {comp.size?.width || 0}x{comp.size?.height || 0}

))}
)}
)} {/* 요약 표시 설정 (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 }); }} />
)} {/* 필터 연결 컬럼 제거됨 - Entity 조인이 자동으로 관계를 처리 */} {/* 우측 패널 표시 컬럼 설정 - 드래그앤드롭 */} {(() => { const selectedColumns = config.rightPanel?.columns || []; const filteredTableColumns = rightTableColumns.filter((c) => !["company_code", "company_name"].includes(c.columnName)); const unselectedColumns = filteredTableColumns.filter((c) => !selectedColumns.some((sc) => sc.name === c.columnName)); const dbNumericTypes = ["numeric", "decimal", "integer", "bigint", "double precision", "real", "smallint", "int4", "int8", "float4", "float8"]; const inputNumericTypes = ["number", "decimal", "currency", "integer"]; const handleRightDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = selectedColumns.findIndex((c) => c.name === active.id); const newIndex = selectedColumns.findIndex((c) => c.name === over.id); if (oldIndex !== -1 && newIndex !== -1) { updateRightPanel({ columns: arrayMove([...selectedColumns], oldIndex, newIndex) }); } } }; return (
{rightTableColumns.length === 0 ? (

테이블을 선택해주세요

) : ( <> {selectedColumns.length > 0 && ( c.name)} strategy={verticalListSortingStrategy}>
{selectedColumns.map((col, index) => { const colInfo = rightTableColumns.find((c) => c.columnName === col.name); const isNumeric = colInfo && ( dbNumericTypes.includes(colInfo.dataType?.toLowerCase() || "") || inputNumericTypes.includes(colInfo.input_type?.toLowerCase() || "") || inputNumericTypes.includes(colInfo.webType?.toLowerCase() || "") ); return ( { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], label: value }; updateRightPanel({ columns: newColumns }); }} onWidthChange={(value) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], width: value }; updateRightPanel({ columns: newColumns }); }} onFormatChange={(checked) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } }; updateRightPanel({ columns: newColumns }); }} onRemove={() => updateRightPanel({ columns: selectedColumns.filter((_, i) => i !== index) })} onShowInSummaryChange={(checked) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], showInSummary: checked }; updateRightPanel({ columns: newColumns }); }} onShowInDetailChange={(checked) => { const newColumns = [...selectedColumns]; newColumns[index] = { ...newColumns[index], showInDetail: checked }; updateRightPanel({ columns: newColumns }); }} /> ); })}
)} {selectedColumns.length > 0 && unselectedColumns.length > 0 && (
미선택 컬럼
)}
{unselectedColumns.map((column) => (
{ updateRightPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] }); }} > {column.columnLabel || column.columnName}
))}
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */} {(() => { const rightTable = config.rightPanel?.tableName; const joinData = rightTable ? entityJoinColumns[rightTable] : null; if (!joinData || joinData.joinTables.length === 0) return null; return joinData.joinTables.map((joinTable, tableIndex) => { const joinColumnsToShow = joinTable.availableColumns.filter((column) => { const matchingJoinColumn = joinData.availableColumns.find( (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, ); if (!matchingJoinColumn) return false; return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias); }); const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length; if (joinColumnsToShow.length === 0 && addedCount === 0) return null; return (
{joinTable.tableName} {addedCount > 0 && ( {addedCount}개 선택 )} {joinColumnsToShow.length}개 남음
{joinColumnsToShow.map((column, colIndex) => { const matchingJoinColumn = joinData.availableColumns.find( (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, ); if (!matchingJoinColumn) return null; return (
{ updateRightPanel({ columns: [...selectedColumns, { name: matchingJoinColumn.joinAlias, label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel, width: 100, isEntityJoin: true, joinInfo: { sourceTable: rightTable!, sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "", referenceTable: matchingJoinColumn.tableName, joinAlias: matchingJoinColumn.joinAlias, }, }], }); }} > {column.columnLabel || column.columnName}
); })} {joinColumnsToShow.length === 0 && (

모든 컬럼이 이미 추가되었습니다

)}
); }); })()} )}
); })()}
{/* 우측 패널 Entity 조인 컬럼은 표시 컬럼 목록에 통합됨 */} {/* 우측 패널 데이터 필터링 */}

우측 패널 데이터 필터링

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

({ 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", }, }); }} /> )}
)}
{/* 🆕 우측 패널 추가 버튼 설정 */} {config.rightPanel?.showAdd && (

추가 버튼 설정

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

{config.rightPanel?.addButton?.mode === "modal" ? "지정한 화면을 모달로 열어 데이터를 추가합니다" : "내장 폼으로 데이터를 추가합니다"}

{config.rightPanel?.addButton?.mode === "modal" && (
updateRightPanel({ addButton: { ...config.rightPanel?.addButton!, modalScreenId: screenId, }, }) } />
)}
updateRightPanel({ addButton: { ...config.rightPanel?.addButton!, buttonLabel: e.target.value, enabled: true, mode: config.rightPanel?.addButton?.mode || "auto", }, }) } placeholder="추가" className="h-8 text-xs" />
)} {/* 🆕 우측 패널 삭제 버튼 설정 */}

삭제 버튼 설정

{ 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" />
)}
{/* ===== 추가 탭 모달 ===== */} !open && setActiveModal(null)}> 추가 탭 설정 우측 패널에 다른 테이블 데이터를 탭으로 추가합니다

추가 탭

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

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

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

)}
); };