"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 (
);
}
// ─── 섹션 헤더 컴포넌트 ───
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" && (
)}
{/* 추가 버튼 설정 */}
updateRightPanel({
addButton: {
...config.rightPanel?.addButton,
enabled:
config.rightPanel?.addButton?.enabled ?? true,
mode: checked ? "modal" : "auto",
},
})
}
/>
{config.rightPanel?.addButton?.mode === "modal" && (
)}
{/* 삭제 버튼 설정 */}
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할 테이블을 지정합니다
{/* 테이블 모드 설정 */}
{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, {
tableName,
columns: [],
});
if (tableName) loadTableColumns(tableName);
}}
/>
{/* 탭 표시 모드 */}
표시 모드
{/* 탭 연결 키 */}
{tab.tableName && (
연결 키
)}
{/* 탭 기능 토글 */}
updateTab(tabIndex, { showSearch: checked })
}
/>
updateTab(tabIndex, { showAdd: checked })
}
/>
updateTab(tabIndex, { showDelete: checked })
}
/>
)
)}
{/* 탭 추가 버튼 */}
{/* ═══════════════════════════════════════ */}
{/* 6단계: 고급 설정 (기본 접힘) */}
{/* ═══════════════════════════════════════ */}
updateConfig({ syncSelection: checked })
}
/>
{/* 최소 너비 설정 */}
{/* 좌측 패널 하위 항목 추가 설정 */}
{config.leftPanel?.showItemAddButton && (
)}
{/* 좌측 패널 테이블 모드 설정 */}
{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" && (
)}
updateLeftPanel({
addButton: {
...config.leftPanel?.addButton,
enabled:
config.leftPanel?.addButton?.enabled ?? true,
mode: checked ? "modal" : "auto",
},
})
}
/>
{config.leftPanel?.addButton?.mode === "modal" && (
)}
{/* 패널 헤더 높이 */}
);
};
V2SplitPanelLayoutConfigPanel.displayName = "V2SplitPanelLayoutConfigPanel";
export default V2SplitPanelLayoutConfigPanel;