"use client"; /** * V2SplitPanelLayout 설정 패널 * 토스식 단계별 UX: 관계타입 카드선택 -> 레이아웃 -> 좌측패널 -> 우측패널 -> 추가탭 -> 고급설정 * 기존 SplitPanelLayoutConfigPanel의 모든 기능을 자체 UI로 완전 구현 */ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Slider } from "@/components/ui/slider"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Separator } from "@/components/ui/separator"; import { Database, Link2, GripVertical, X, Check, ChevronsUpDown, Settings, ChevronDown, Loader2, Columns3, PanelLeft, PanelRight, Layers, Plus, Trash2, ArrowRight, SplitSquareHorizontal, Eye, List, LayoutGrid, Search, Pencil, FileText, } from "lucide-react"; import { cn } from "@/lib/utils"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { tableTypeApi } from "@/lib/api/screen"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel"; import { DndContext, closestCenter, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import type { SplitPanelLayoutConfig, AdditionalTabConfig, } from "@/lib/registry/components/v2-split-panel-layout/types"; import type { TableInfo, ColumnInfo } from "@/types/screen"; // ─── DnD 정렬 가능한 컬럼 행 ─── function SortableColumnRow({ id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove, }: { id: string; col: { name: string; label: string; width?: number; format?: any; }; index: number; isNumeric: boolean; isEntityJoin?: boolean; onLabelChange: (value: string) => void; onWidthChange: (value: number) => void; onFormatChange: (checked: boolean) => void; onRemove: () => 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 && ( )}
); } // ─── 섹션 헤더 컴포넌트 ─── function SectionHeader({ icon: Icon, title, description, }: { icon: React.ComponentType<{ className?: string }>; title: string; description?: string; }) { return (

{title}

{description && (

{description}

)}
); } // ─── 수평 Switch Row (토스 패턴) ─── function SwitchRow({ label, description, checked, onCheckedChange, }: { label: string; description?: string; checked: boolean; onCheckedChange: (checked: boolean) => void; }) { return (

{label}

{description && (

{description}

)}
); } // ─── 관계 타입 카드 정의 ─── const RELATION_CARDS = [ { value: "detail" as const, icon: Eye, title: "선택 시 표시", description: "좌측 선택 시에만 우측 데이터 표시", }, { value: "join" as const, icon: Link2, title: "연관 목록", description: "미선택 시 전체 / 선택 시 필터링", }, ] as const; // ─── 표시 모드 카드 정의 ─── const DISPLAY_MODE_CARDS = [ { value: "list" as const, icon: List, title: "목록", description: "리스트 형태로 표시", }, { value: "table" as const, icon: LayoutGrid, title: "테이블", description: "테이블 그리드로 표시", }, { value: "custom" as const, icon: FileText, title: "커스텀", description: "자유 배치 모드", }, ] as const; // ─── 패널 컬럼 설정 서브 컴포넌트 ─── const PanelColumnSection: React.FC<{ panelKey: "leftPanel" | "rightPanel"; columns: SplitPanelLayoutConfig["leftPanel"]["columns"]; availableColumns: ColumnInfo[]; entityJoinData: { 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; }>; }>; }; loadingEntityJoins: boolean; tableName: string; onColumnsChange: ( columns: SplitPanelLayoutConfig["leftPanel"]["columns"] ) => void; }> = ({ columns, availableColumns, entityJoinData, loadingEntityJoins, tableName, onColumnsChange, }) => { const currentColumns = columns || []; const addColumn = (colInfo: ColumnInfo) => { if (currentColumns.some((c) => c.name === colInfo.columnName)) return; onColumnsChange([ ...currentColumns, { name: colInfo.columnName, label: colInfo.displayName || colInfo.columnName, width: 120, }, ]); }; const removeColumn = (name: string) => { onColumnsChange(currentColumns.filter((c) => c.name !== name)); }; const updateColumn = ( name: string, updates: Partial<(typeof currentColumns)[0]> ) => { onColumnsChange( currentColumns.map((c) => (c.name === name ? { ...c, ...updates } : c)) ); }; const addEntityColumn = ( joinCol: (typeof entityJoinData.availableColumns)[0] ) => { if (currentColumns.some((c) => c.name === joinCol.joinAlias)) return; onColumnsChange([ ...currentColumns, { name: joinCol.joinAlias, label: joinCol.columnLabel, width: 120, isEntityJoin: true, joinInfo: { sourceTable: tableName, sourceColumn: joinCol.joinAlias.split("_")[0] || "", referenceTable: joinCol.tableName, joinAlias: joinCol.joinAlias, }, }, ]); }; const isNumericType = (name: string) => { const col = availableColumns.find((c) => c.columnName === name); if (!col) return false; const dt = (col.dataType || "").toLowerCase(); return ( dt.includes("int") || dt.includes("numeric") || dt.includes("decimal") || dt.includes("float") || dt.includes("double") ); }; return (
{/* 컬럼 선택 체크박스 리스트 */} {availableColumns.length > 0 && (
컬럼 선택
{availableColumns.map((col) => { const isAdded = currentColumns.some( (c) => c.name === col.columnName ); return (
{ if (isAdded) removeColumn(col.columnName); else addColumn(col); }} > {col.displayName || col.columnName} {col.input_type || col.dataType}
); })}
)} {/* Entity 조인 컬럼 */} {entityJoinData.joinTables.length > 0 && (
Entity 조인 컬럼 {loadingEntityJoins && ( )}
{entityJoinData.joinTables.map((joinTable, idx) => (
{joinTable.tableName} {joinTable.currentDisplayColumn}
{joinTable.availableColumns.map((jCol, jIdx) => { const matchingJoinColumn = entityJoinData.availableColumns.find( (jc) => jc.tableName === joinTable.tableName && jc.columnName === jCol.columnName ); if (!matchingJoinColumn) return null; const isAdded = currentColumns.some( (c) => c.name === matchingJoinColumn.joinAlias ); return (
{ if (isAdded) removeColumn(matchingJoinColumn.joinAlias); else addEntityColumn(matchingJoinColumn); }} > {jCol.columnLabel} {jCol.inputType || jCol.dataType}
); })}
))}
)} {/* 선택된 컬럼 DnD 정렬 */} {currentColumns.length > 0 && (
선택된 컬럼 ({currentColumns.length}개)
{ const { active, over } = event; if (!over || active.id === over.id) return; const cols = [...currentColumns]; const oldIdx = cols.findIndex((c) => c.name === active.id); const newIdx = cols.findIndex((c) => c.name === over.id); if (oldIdx !== -1 && newIdx !== -1) { onColumnsChange(arrayMove(cols, oldIdx, newIdx)); } }} > c.name)} strategy={verticalListSortingStrategy} >
{currentColumns.map((col, idx) => ( updateColumn(col.name, { label: v })} onWidthChange={(v) => updateColumn(col.name, { width: v })} onFormatChange={(checked) => updateColumn(col.name, { format: { ...col.format, thousandSeparator: checked, }, }) } onRemove={() => removeColumn(col.name)} /> ))}
)}
); }; // ─── 테이블 Combobox ─── const TableCombobox: React.FC<{ value: string; allTables: Array<{ tableName: string; displayName: string }>; screenTableName?: string; loading: boolean; onChange: (tableName: string) => void; }> = ({ value, allTables, screenTableName, loading, onChange }) => { const [open, setOpen] = useState(false); return ( 테이블을 찾을 수 없습니다. {screenTableName && ( { onChange(screenTableName); setOpen(false); }} className="text-xs" > {allTables.find((t) => t.tableName === screenTableName) ?.displayName || screenTableName} )} {allTables .filter((t) => t.tableName !== screenTableName) .map((table) => ( { onChange(table.tableName); setOpen(false); }} className="text-xs" >
{table.displayName} {table.displayName !== table.tableName && ( {table.tableName} )}
))}
); }; // ─── 메인 컴포넌트 ─── interface V2SplitPanelLayoutConfigPanelProps { config: SplitPanelLayoutConfig; onChange: (config: SplitPanelLayoutConfig) => void; tables?: TableInfo[]; screenTableName?: string; menuObjid?: number; } export const V2SplitPanelLayoutConfigPanel: React.FC< V2SplitPanelLayoutConfigPanelProps > = ({ config, onChange, tables, screenTableName, menuObjid }) => { // ─── 상태 ─── const [allTables, setAllTables] = useState< Array<{ tableName: string; displayName: string }> >([]); const [loadingTables, setLoadingTables] = useState(false); const [loadedTableColumns, setLoadedTableColumns] = useState< Record >({}); const [loadingColumns, setLoadingColumns] = useState< Record >({}); 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; }>; }>; } > >({}); const [loadingEntityJoins, setLoadingEntityJoins] = useState< Record >({}); // Collapsible 상태 const [leftPanelOpen, setLeftPanelOpen] = useState(false); const [rightPanelOpen, setRightPanelOpen] = useState(false); const [tabsOpen, setTabsOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false); const [leftColumnsOpen, setLeftColumnsOpen] = useState(false); const [rightColumnsOpen, setRightColumnsOpen] = useState(false); const [leftFilterOpen, setLeftFilterOpen] = useState(false); const [rightFilterOpen, setRightFilterOpen] = useState(false); // ─── 파생 값 ─── const relationshipType = config.rightPanel?.relation?.type || "detail"; const leftTableName = config.leftPanel?.tableName || screenTableName || ""; const rightTableName = config.rightPanel?.tableName || ""; const leftTableColumns = useMemo( () => (leftTableName ? loadedTableColumns[leftTableName] || [] : []), [loadedTableColumns, leftTableName] ); const rightTableColumns = useMemo( () => (rightTableName ? loadedTableColumns[rightTableName] || [] : []), [loadedTableColumns, rightTableName] ); const leftEntityJoins = useMemo( () => entityJoinColumns[leftTableName] || { availableColumns: [], joinTables: [], }, [entityJoinColumns, leftTableName] ); const rightEntityJoins = useMemo( () => entityJoinColumns[rightTableName] || { availableColumns: [], joinTables: [], }, [entityJoinColumns, rightTableName] ); // ─── 이벤트 발행 래퍼 ─── const handleChange = useCallback( (newConfig: SplitPanelLayoutConfig) => { onChange(newConfig); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { detail: { config: newConfig }, }) ); } }, [onChange] ); const updateConfig = useCallback( (updates: Partial) => { handleChange({ ...config, ...updates }); }, [handleChange, config] ); const updateLeftPanel = useCallback( (updates: Partial) => { handleChange({ ...config, leftPanel: { ...config.leftPanel, ...updates }, }); }, [handleChange, config] ); const updateRightPanel = useCallback( (updates: Partial) => { handleChange({ ...config, rightPanel: { ...config.rightPanel, ...updates }, }); }, [handleChange, config] ); // ─── 테이블 목록 로드 ─── useEffect(() => { const loadAllTables = async () => { setLoadingTables(true); try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setAllTables( response.data.map((t: any) => ({ tableName: t.tableName || t.table_name, displayName: t.tableLabel || t.displayName || t.tableName || t.table_name, })) ); } } catch (error) { console.error("테이블 목록 로드 실패:", error); } finally { setLoadingTables(false); } }; loadAllTables(); }, []); // 좌측 테이블 초기값 설정 useEffect(() => { if (screenTableName && !config.leftPanel?.tableName) { updateLeftPanel({ tableName: screenTableName }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [screenTableName]); // ─── 테이블 컬럼 로드 ─── const loadTableColumns = useCallback( async (tableName: string) => { if (loadedTableColumns[tableName] || loadingColumns[tableName]) return; setLoadingColumns((prev) => ({ ...prev, [tableName]: true })); try { const columnsResponse = await tableTypeApi.getColumns(tableName); const cols = (columnsResponse || []).map((col: any) => ({ tableName: col.tableName || tableName, columnName: col.columnName || col.column_name, displayName: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType || "", dbType: col.dbType || col.dataType || col.data_type || "", webType: col.webType || col.web_type || "text", inputType: col.inputType || "direct", input_type: col.input_type || col.inputType, isNullable: col.isNullable === true || col.isNullable === "Y", isPrimaryKey: col.isPrimaryKey ?? false, referenceTable: col.referenceTable || col.reference_table, })) as ColumnInfo[]; setLoadedTableColumns((prev) => ({ ...prev, [tableName]: cols })); await loadEntityJoinColumnsForTable(tableName); } catch (error) { console.error(`테이블 ${tableName} 컬럼 로드 실패:`, error); setLoadedTableColumns((prev) => ({ ...prev, [tableName]: [] })); } finally { setLoadingColumns((prev) => ({ ...prev, [tableName]: false })); } }, [loadedTableColumns, loadingColumns] ); const loadEntityJoinColumnsForTable = useCallback( async (tableName: string) => { if (entityJoinColumns[tableName] || loadingEntityJoins[tableName]) return; setLoadingEntityJoins((prev) => ({ ...prev, [tableName]: true })); try { const result = await entityJoinApi.getEntityJoinColumns(tableName); 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 })); } }, [entityJoinColumns, loadingEntityJoins] ); // 좌측/우측 테이블 변경 시 컬럼 로드 useEffect(() => { if (leftTableName) loadTableColumns(leftTableName); }, [leftTableName, loadTableColumns]); useEffect(() => { if (rightTableName) loadTableColumns(rightTableName); }, [rightTableName, loadTableColumns]); // ─── 추가 탭 관리 ─── const addTab = useCallback(() => { const currentTabs = config.rightPanel?.additionalTabs || []; const newTab: AdditionalTabConfig = { tabId: `tab_${Date.now()}`, label: `탭 ${currentTabs.length + 1}`, title: `탭 ${currentTabs.length + 1}`, }; updateRightPanel({ additionalTabs: [...currentTabs, newTab], }); }, [config.rightPanel?.additionalTabs, updateRightPanel]); const updateTab = useCallback( (tabIndex: number, updates: Partial) => { const newTabs = [...(config.rightPanel?.additionalTabs || [])]; newTabs[tabIndex] = { ...newTabs[tabIndex], ...updates }; updateRightPanel({ additionalTabs: newTabs }); }, [config.rightPanel?.additionalTabs, updateRightPanel] ); const removeTab = useCallback( (tabIndex: number) => { const newTabs = config.rightPanel?.additionalTabs?.filter((_, i) => i !== tabIndex) || []; updateRightPanel({ additionalTabs: newTabs }); }, [config.rightPanel?.additionalTabs, updateRightPanel] ); // ─── 렌더링 ─── return (
{/* ═══════════════════════════════════════ */} {/* 1단계: 관계 타입 선택 (카드 UI) */} {/* ═══════════════════════════════════════ */}
{RELATION_CARDS.map((card) => { const Icon = card.icon; const isSelected = relationshipType === card.value; return ( ); })}
{/* ═══════════════════════════════════════ */} {/* 2단계: 레이아웃 설정 */} {/* ═══════════════════════════════════════ */}
좌측 패널 너비 {config.splitRatio || 30}%
updateConfig({ splitRatio: value[0] })} min={20} max={80} step={5} />
updateConfig({ resizable: checked })} /> updateConfig({ autoLoad: checked })} />
{/* ═══════════════════════════════════════ */} {/* 3단계: 좌측 패널 (접이식) */} {/* ═══════════════════════════════════════ */}
{/* 좌측 패널 제목 */}
updateLeftPanel({ title: e.target.value })} placeholder="좌측 패널 제목" className="h-8 text-xs" />
{/* 좌측 테이블 선택 */}
updateLeftPanel({ tableName, columns: [] }) } /> {screenTableName && leftTableName !== screenTableName && (
기본 테이블({screenTableName})과 다름
)}
{/* 표시 모드 */}
{DISPLAY_MODE_CARDS.map((card) => { const Icon = card.icon; const currentMode = config.leftPanel?.displayMode || "list"; const isSelected = currentMode === card.value; return ( ); })}
{/* 좌측 패널 기능 토글 */}
updateLeftPanel({ showSearch: checked }) } /> updateLeftPanel({ showAdd: checked }) } /> updateLeftPanel({ showEdit: checked }) } /> updateLeftPanel({ showDelete: checked }) } /> updateLeftPanel({ showItemAddButton: checked }) } />
{/* 좌측 패널 컬럼 설정 (접이식) */} {config.leftPanel?.displayMode !== "custom" && (
{loadingColumns[leftTableName] ? (
컬럼 로딩 중...
) : leftTableColumns.length === 0 ? (

테이블을 선택하면 컬럼이 표시됩니다

) : ( updateLeftPanel({ columns }) } /> )}
)} {/* 좌측 패널 데이터 필터 (접이식) */}
updateLeftPanel({ dataFilter }) } />
{/* ═══════════════════════════════════════ */} {/* 4단계: 우측 패널 (접이식) */} {/* ═══════════════════════════════════════ */}
{/* 우측 패널 제목 */}
updateRightPanel({ title: e.target.value })} placeholder="우측 패널 제목" className="h-8 text-xs" />
{/* 우측 테이블 선택 */}
updateRightPanel({ tableName, columns: [] }) } />
{/* 표시 모드 */}
{DISPLAY_MODE_CARDS.map((card) => { const Icon = card.icon; const currentMode = config.rightPanel?.displayMode || "list"; const isSelected = currentMode === card.value; return ( ); })}
{/* 연결 키 설정 */} {rightTableName && (
테이블 연결 키

좌측 패널과 우측 패널을 연결할 컬럼을 설정합니다

{/* 기존 키 목록 */} {(config.rightPanel?.relation?.keys || []).map( (key, idx) => (
) )} {/* 키가 없을 때 단일키 호환 */} {(!config.rightPanel?.relation?.keys || config.rightPanel.relation.keys.length === 0) && (
)}
)} {/* 우측 패널 기능 토글 */}
updateRightPanel({ showSearch: checked }) } /> updateRightPanel({ showAdd: checked }) } /> updateRightPanel({ showEdit: checked }) } /> updateRightPanel({ showDelete: checked }) } />
{/* 우측 패널 컬럼 설정 (접이식) */} {config.rightPanel?.displayMode !== "custom" && (
{loadingColumns[rightTableName] ? (
컬럼 로딩 중...
) : rightTableColumns.length === 0 ? (

테이블을 선택하면 컬럼이 표시됩니다

) : ( updateRightPanel({ columns }) } /> )}
)} {/* 우측 패널 데이터 필터 (접이식) */}
updateRightPanel({ dataFilter }) } />
{/* 우측 패널 추가 설정 (접이식) */}
{/* 중복 제거 */} updateRightPanel({ deduplication: { ...config.rightPanel?.deduplication, enabled: checked, groupByColumn: config.rightPanel?.deduplication?.groupByColumn || "", keepStrategy: config.rightPanel?.deduplication?.keepStrategy || "latest", }, }) } /> {config.rightPanel?.deduplication?.enabled && (
기준 컬럼
유지 전략
)} {/* 수정 버튼 설정 */} updateRightPanel({ editButton: { ...config.rightPanel?.editButton, enabled: config.rightPanel?.editButton?.enabled ?? true, mode: checked ? "modal" : "auto", }, }) } /> {config.rightPanel?.editButton?.mode === "modal" && (
모달 화면 ID updateRightPanel({ editButton: { ...config.rightPanel?.editButton!, modalScreenId: parseInt(e.target.value) || undefined, }, }) } placeholder="화면 ID" className="h-7 w-[100px] text-xs" />
)} {/* 추가 버튼 설정 */} updateRightPanel({ addButton: { ...config.rightPanel?.addButton, enabled: config.rightPanel?.addButton?.enabled ?? true, mode: checked ? "modal" : "auto", }, }) } /> {config.rightPanel?.addButton?.mode === "modal" && (
모달 화면 ID updateRightPanel({ addButton: { ...config.rightPanel?.addButton!, modalScreenId: parseInt(e.target.value) || undefined, }, }) } placeholder="화면 ID" className="h-7 w-[100px] text-xs" />
)} {/* 삭제 버튼 설정 */} updateRightPanel({ deleteButton: { ...config.rightPanel?.deleteButton, enabled: config.rightPanel?.deleteButton?.enabled ?? true, confirmMessage: checked ? "정말 삭제하시겠습니까?" : undefined, }, }) } /> {config.rightPanel?.deleteButton?.confirmMessage && (
updateRightPanel({ deleteButton: { ...config.rightPanel?.deleteButton!, confirmMessage: e.target.value, }, }) } placeholder="삭제 확인 메시지" className="h-7 text-xs" />
)} {/* 추가 시 대상 테이블 (N:M 관계) */}
추가 대상 설정 (N:M)

추가 버튼 클릭 시 실제 INSERT할 테이블을 지정합니다

대상 테이블 updateRightPanel({ addConfig: { ...config.rightPanel?.addConfig, targetTable: e.target.value || undefined, }, }) } placeholder="미설정 시 우측 테이블" className="h-7 w-[160px] text-xs" />
좌측값 컬럼 updateRightPanel({ addConfig: { ...config.rightPanel?.addConfig, leftPanelColumn: e.target.value || undefined, }, }) } placeholder="좌측 컬럼명" className="h-7 w-[160px] text-xs" />
대상 컬럼 updateRightPanel({ addConfig: { ...config.rightPanel?.addConfig, targetColumn: e.target.value || undefined, }, }) } placeholder="대상 컬럼명" className="h-7 w-[160px] text-xs" />
{/* 테이블 모드 설정 */} {config.rightPanel?.displayMode === "table" && ( <>
테이블 옵션 updateRightPanel({ tableConfig: { ...config.rightPanel?.tableConfig, showCheckbox: checked, }, }) } /> updateRightPanel({ tableConfig: { ...config.rightPanel?.tableConfig, showRowNumber: checked, }, }) } /> updateRightPanel({ tableConfig: { ...config.rightPanel?.tableConfig, striped: checked, }, }) } /> updateRightPanel({ tableConfig: { ...config.rightPanel?.tableConfig, stickyHeader: checked, }, }) } />
)}
{/* ═══════════════════════════════════════ */} {/* 5단계: 추가 탭 (접이식) */} {/* ═══════════════════════════════════════ */}
{/* 탭 목록 */} {(config.rightPanel?.additionalTabs || []).map( (tab, tabIndex) => (
{tab.label || `탭 ${tabIndex + 1}`}
updateTab(tabIndex, { label: e.target.value }) } placeholder="탭 이름" className="h-7 text-xs" />
updateTab(tabIndex, { title: e.target.value }) } placeholder="패널 제목" className="h-7 text-xs" />
{/* 탭 테이블 선택 */}
{ updateTab(tabIndex, { tableName, columns: [], }); if (tableName) loadTableColumns(tableName); }} />
{/* 탭 표시 모드 */}
표시 모드
{/* 탭 연결 키 */} {tab.tableName && (
연결 키
)} {/* 탭 기능 토글 */}
updateTab(tabIndex, { showSearch: checked }) } /> updateTab(tabIndex, { showAdd: checked }) } /> updateTab(tabIndex, { showDelete: checked }) } />
) )} {/* 탭 추가 버튼 */}
{/* ═══════════════════════════════════════ */} {/* 6단계: 고급 설정 (기본 접힘) */} {/* ═══════════════════════════════════════ */}
updateConfig({ syncSelection: checked }) } /> {/* 최소 너비 설정 */}
최소 너비 (px)
updateConfig({ minLeftWidth: parseInt(e.target.value) || 200, }) } className="h-7 text-xs" />
updateConfig({ minRightWidth: parseInt(e.target.value) || 300, }) } className="h-7 text-xs" />
{/* 좌측 패널 하위 항목 추가 설정 */} {config.leftPanel?.showItemAddButton && (
하위 항목 추가 설정
부모 컬럼 updateLeftPanel({ itemAddConfig: { ...config.leftPanel?.itemAddConfig, parentColumn: e.target.value, sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "", }, }) } placeholder="예: parent_dept_code" className="h-7 w-[160px] text-xs" />
소스 컬럼 updateLeftPanel({ itemAddConfig: { ...config.leftPanel?.itemAddConfig!, sourceColumn: e.target.value, }, }) } placeholder="예: dept_code" className="h-7 w-[160px] text-xs" />
)} {/* 좌측 패널 테이블 모드 설정 */} {config.leftPanel?.displayMode === "table" && (
좌측 테이블 옵션 updateLeftPanel({ tableConfig: { ...config.leftPanel?.tableConfig, showCheckbox: checked, }, }) } /> updateLeftPanel({ tableConfig: { ...config.leftPanel?.tableConfig, showRowNumber: checked, }, }) } /> updateLeftPanel({ tableConfig: { ...config.leftPanel?.tableConfig, striped: checked, }, }) } /> updateLeftPanel({ tableConfig: { ...config.leftPanel?.tableConfig, stickyHeader: checked, }, }) } />
)} {/* 좌측 패널 수정/추가 버튼 모달 설정 */}
좌측 버튼 모달 설정 updateLeftPanel({ editButton: { ...config.leftPanel?.editButton, enabled: config.leftPanel?.editButton?.enabled ?? true, mode: checked ? "modal" : "auto", }, }) } /> {config.leftPanel?.editButton?.mode === "modal" && (
모달 화면 ID updateLeftPanel({ editButton: { ...config.leftPanel?.editButton!, modalScreenId: parseInt(e.target.value) || undefined, }, }) } placeholder="화면 ID" className="h-7 w-[100px] text-xs" />
)} updateLeftPanel({ addButton: { ...config.leftPanel?.addButton, enabled: config.leftPanel?.addButton?.enabled ?? true, mode: checked ? "modal" : "auto", }, }) } /> {config.leftPanel?.addButton?.mode === "modal" && (
모달 화면 ID updateLeftPanel({ addButton: { ...config.leftPanel?.addButton!, modalScreenId: parseInt(e.target.value) || undefined, }, }) } placeholder="화면 ID" className="h-7 w-[100px] text-xs" />
)}
{/* 패널 헤더 높이 */}
패널 헤더 높이 (px)
updateLeftPanel({ panelHeaderHeight: parseInt(e.target.value) || undefined, }) } placeholder="자동" className="h-7 text-xs" />
updateRightPanel({ panelHeaderHeight: parseInt(e.target.value) || undefined, }) } placeholder="자동" className="h-7 text-xs" />
); }; V2SplitPanelLayoutConfigPanel.displayName = "V2SplitPanelLayoutConfigPanel"; export default V2SplitPanelLayoutConfigPanel;