"use client";
/**
* V2TableList 설정 패널
* 토스식 단계별 UX: 데이터 소스 -> 컬럼 선택 -> 조인 컬럼 -> 순서/라벨 -> 고급 설정(접힘)
* 기존 TableListConfigPanel의 모든 기능을 자체 UI로 완전 구현
*/
import React, { useState, useEffect, useMemo, useCallback, useRef } 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 { 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 {
Table2,
Database,
Link2,
GripVertical,
X,
Check,
ChevronsUpDown,
Lock,
Unlock,
Settings,
ChevronDown,
Loader2,
Columns3,
ArrowUpDown,
Filter,
LayoutGrid,
CheckSquare,
Wrench,
ScrollText,
} 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 { TableListConfig, ColumnConfig } from "@/lib/registry/components/v2-table-list/types";
// ─── DnD 정렬 가능한 컬럼 행 (접이식) ───
function SortableColumnRow({
id,
col,
index,
isEntityJoin,
onLabelChange,
onWidthChange,
onRemove,
}: {
id: string;
col: ColumnConfig;
index: number;
isEntityJoin?: boolean;
onLabelChange: (value: string) => void;
onWidthChange: (value: number) => void;
onRemove: () => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
const style = { transform: CSS.Transform.toString(transform), transition };
const [expanded, setExpanded] = useState(false);
return (
{isEntityJoin ? (
) : (
#{index + 1}
)}
{col.width && (
{col.width}px
)}
{expanded && (
onLabelChange(e.target.value)}
placeholder="표시명"
className="h-7 min-w-0 text-xs"
/>
onWidthChange(parseInt(e.target.value) || 100)}
placeholder="너비"
className="h-7 shrink-0 text-xs text-center"
/>
)}
);
}
// ─── 섹션 헤더 컴포넌트 ───
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}
}
);
}
interface V2TableListConfigPanelProps {
config: TableListConfig;
onChange: (config: Partial) => void;
screenTableName?: string;
tableColumns?: any[];
menuObjid?: number;
}
export const V2TableListConfigPanel: React.FC = ({
config: configProp,
onChange,
screenTableName,
tableColumns,
menuObjid,
}) => {
const config = configProp || ({} as TableListConfig);
// componentConfigChanged 이벤트 발행 래퍼
const handleChange = useCallback((newConfig: Partial) => {
onChange(newConfig);
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("componentConfigChanged", {
detail: { config: { ...config, ...newConfig } },
})
);
}
}, [onChange, config]);
// key-value 형태 업데이트 헬퍼
const updateField = useCallback((key: keyof TableListConfig, value: any) => {
handleChange({ ...config, [key]: value });
}, [handleChange, config]);
const updateNestedField = useCallback((parentKey: keyof TableListConfig, childKey: string, value: any) => {
const parentValue = config[parentKey] as any;
handleChange({
...config,
[parentKey]: { ...parentValue, [childKey]: value },
});
}, [handleChange, config]);
// ─── 상태 ───
const [availableTables, setAvailableTables] = useState>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
const [availableColumns, setAvailableColumns] = useState<
Array<{ columnName: string; dataType: string; label?: string; input_type?: string }>
>([]);
const [entityJoinColumns, setEntityJoinColumns] = useState<{
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;
}>;
}>;
}>({ availableColumns: [], joinTables: [] });
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
const [referenceTableColumns, setReferenceTableColumns] = useState<
Array<{ columnName: string; dataType: string; label?: string }>
>([]);
const [loadingReferenceColumns, setLoadingReferenceColumns] = useState(false);
const [entityDisplayConfigs, setEntityDisplayConfigs] = useState<
Record;
joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
selectedColumns: string[];
separator: string;
}>
>({});
// Collapsible 상태
const [advancedOpen, setAdvancedOpen] = useState(false);
const [entityDisplayOpen, setEntityDisplayOpen] = useState(false);
const [columnSelectOpen, setColumnSelectOpen] = useState(() => (config.columns?.length || 0) > 0);
const [entityJoinOpen, setEntityJoinOpen] = useState(false);
const [displayColumnsOpen, setDisplayColumnsOpen] = useState(() => (config.columns?.length || 0) > 0);
const [columnSearchText, setColumnSearchText] = useState("");
const [entityJoinSubOpen, setEntityJoinSubOpen] = useState>({});
// 이전 컬럼 개수 추적 (엔티티 감지용)
const prevColumnsLengthRef = useRef(0);
// ─── 실제 사용할 테이블 이름 계산 ───
const targetTableName = useMemo(() => {
if (config.useCustomTable && config.customTableName) {
return config.customTableName;
}
return config.selectedTable || screenTableName;
}, [config.useCustomTable, config.customTableName, config.selectedTable, screenTableName]);
// ─── 초기화: 화면 테이블명 자동 설정 ───
useEffect(() => {
if (screenTableName && !config.selectedTable) {
handleChange({
...config,
selectedTable: screenTableName,
columns: config.columns || [],
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenTableName]);
// ─── 테이블 목록 가져오기 ───
useEffect(() => {
const fetchTables = async () => {
setLoadingTables(true);
try {
const response = await tableTypeApi.getTables();
setAvailableTables(
response.map((table: any) => ({
tableName: table.tableName,
displayName: table.displayName || table.tableName,
})),
);
} catch (error) {
console.error("테이블 목록 가져오기 실패:", error);
} finally {
setLoadingTables(false);
}
};
fetchTables();
}, []);
// ─── 선택된 테이블의 컬럼 목록 설정 ───
useEffect(() => {
if (!targetTableName) {
setAvailableColumns([]);
return;
}
const isUsingDifferentTable = config.selectedTable && screenTableName && config.selectedTable !== screenTableName;
const shouldUseTableColumnsProp = !config.useCustomTable && !isUsingDifferentTable && tableColumns && tableColumns.length > 0;
if (shouldUseTableColumnsProp) {
const mappedColumns = tableColumns.map((column: any) => ({
columnName: column.columnName || column.name,
dataType: column.dataType || column.type || "text",
label: column.label || column.displayName || column.columnLabel || column.columnName || column.name,
input_type: column.input_type || column.inputType,
}));
setAvailableColumns(mappedColumns);
if (!config.selectedTable && screenTableName) {
handleChange({
...config,
selectedTable: screenTableName,
columns: config.columns || [],
});
}
} else {
const fetchColumns = async () => {
try {
const result = await tableManagementApi.getColumnList(targetTableName);
if (result.success && result.data) {
const columns = Array.isArray(result.data) ? result.data : result.data.columns;
if (columns && Array.isArray(columns)) {
setAvailableColumns(
columns.map((col: any) => ({
columnName: col.columnName,
dataType: col.dataType,
label: col.displayName || col.columnLabel || col.columnName,
input_type: col.input_type || col.inputType,
})),
);
} else {
setAvailableColumns([]);
}
} else {
setAvailableColumns([]);
}
} catch (error) {
console.error("컬럼 목록 가져오기 실패:", error);
setAvailableColumns([]);
}
};
fetchColumns();
}
}, [targetTableName, config.useCustomTable, tableColumns]);
// ─── Entity 조인 컬럼 정보 가져오기 ───
useEffect(() => {
const fetchEntityJoinColumns = async () => {
if (!targetTableName) {
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
return;
}
setLoadingEntityJoins(true);
try {
const result = await entityJoinApi.getEntityJoinColumns(targetTableName);
setEntityJoinColumns({
availableColumns: result.availableColumns || [],
joinTables: result.joinTables || [],
});
} catch (error) {
console.error("Entity 조인 컬럼 조회 오류:", error);
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
} finally {
setLoadingEntityJoins(false);
}
};
fetchEntityJoinColumns();
}, [targetTableName]);
// ─── 제외 필터용 참조 테이블 컬럼 가져오기 ───
useEffect(() => {
const fetchReferenceColumns = async () => {
const refTable = config.excludeFilter?.referenceTable;
if (!refTable) {
setReferenceTableColumns([]);
return;
}
setLoadingReferenceColumns(true);
try {
const result = await tableManagementApi.getColumnList(refTable);
if (result.success && result.data) {
const columns = result.data.columns || [];
setReferenceTableColumns(
columns.map((col: any) => ({
columnName: col.columnName || col.column_name,
dataType: col.dataType || col.data_type || "text",
label: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
})),
);
}
} catch (error) {
console.error("참조 테이블 컬럼 조회 오류:", error);
setReferenceTableColumns([]);
} finally {
setLoadingReferenceColumns(false);
}
};
fetchReferenceColumns();
}, [config.excludeFilter?.referenceTable]);
// ─── 엔티티 컬럼 자동 로드 ───
useEffect(() => {
const entityColumns = config.columns?.filter((col) => col.isEntityJoin && col.entityDisplayConfig);
if (!entityColumns || entityColumns.length === 0) return;
entityColumns.forEach((column) => {
if (entityDisplayConfigs[column.columnName]) return;
loadEntityDisplayConfig(column);
});
}, [config.columns]);
// ─── 엔티티 타입 컬럼 자동 감지 ───
useEffect(() => {
const currentLength = config.columns?.length || 0;
const prevLength = prevColumnsLengthRef.current;
if (!config.columns || !tableColumns || config.columns.length === 0) {
prevColumnsLengthRef.current = currentLength;
return;
}
if (currentLength === prevLength && prevLength > 0) return;
const updatedColumns = config.columns.map((column) => {
if (column.isEntityJoin) return column;
const tableColumn = tableColumns.find((tc: any) => tc.columnName === column.columnName);
if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) {
return {
...column,
isEntityJoin: true,
entityJoinInfo: {
sourceTable: config.selectedTable || screenTableName || "",
sourceColumn: column.columnName,
joinAlias: column.columnName,
},
entityDisplayConfig: {
displayColumns: [],
separator: " - ",
sourceTable: config.selectedTable || screenTableName || "",
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
},
};
}
return column;
});
const hasChanges = updatedColumns.some((col, index) => col.isEntityJoin !== config.columns![index].isEntityJoin);
if (hasChanges) {
updateField("columns", updatedColumns);
}
prevColumnsLengthRef.current = currentLength;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.columns?.length, tableColumns, config.selectedTable]);
// ─── 엔티티 컬럼의 표시 컬럼 정보 로드 ───
const loadEntityDisplayConfig = useCallback(async (column: ColumnConfig) => {
const configKey = column.columnName;
if (entityDisplayConfigs[configKey]) return;
if (!column.isEntityJoin) {
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: { sourceColumns: [], joinColumns: [], selectedColumns: [], separator: " - " },
}));
return;
}
const sourceTable =
column.entityDisplayConfig?.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
if (!sourceTable) {
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
sourceColumns: [],
joinColumns: [],
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
},
}));
return;
}
let joinTable = column.entityDisplayConfig?.joinTable;
if (!joinTable) {
try {
const columnList = await tableTypeApi.getColumns(sourceTable);
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
joinTable = columnInfo.reference_table || columnInfo.referenceTable;
const updatedDisplayConfig = {
...column.entityDisplayConfig,
sourceTable,
joinTable,
displayColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
};
const updatedColumns = config.columns?.map((col) =>
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedDisplayConfig } : col,
);
if (updatedColumns) updateField("columns", updatedColumns);
}
} catch (error) {
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
}
}
try {
const sourceResult = await entityJoinApi.getReferenceTableColumns(sourceTable);
const sourceColumns = sourceResult.columns || [];
let joinColumns: Array<{ columnName: string; displayName: string; dataType: string }> = [];
if (joinTable) {
try {
const joinResult = await entityJoinApi.getReferenceTableColumns(joinTable);
joinColumns = joinResult.columns || [];
} catch {
// 조인 테이블 로드 실패해도 소스 테이블 컬럼은 표시
}
}
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
sourceColumns,
joinColumns,
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
},
}));
} catch (error) {
console.error("엔티티 표시 컬럼 정보 로드 실패:", error);
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
sourceColumns: [],
joinColumns: [],
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
},
}));
}
}, [entityDisplayConfigs, config.selectedTable, config.columns, screenTableName, updateField]);
// ─── 엔티티 표시 컬럼 선택 토글 ───
const toggleEntityDisplayColumn = useCallback((columnName: string, selectedColumn: string) => {
const configKey = columnName;
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn)
? localConfig.selectedColumns.filter((col) => col !== selectedColumn)
: [...localConfig.selectedColumns, selectedColumn];
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: { ...prev[configKey], selectedColumns: newSelectedColumns },
}));
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === columnName && col.entityDisplayConfig) {
return { ...col, entityDisplayConfig: { ...col.entityDisplayConfig, displayColumns: newSelectedColumns } };
}
return col;
});
if (updatedColumns) updateField("columns", updatedColumns);
}, [entityDisplayConfigs, config.columns, updateField]);
// ─── 엔티티 표시 구분자 업데이트 ───
const updateEntityDisplaySeparator = useCallback((columnName: string, separator: string) => {
const configKey = columnName;
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: { ...prev[configKey], separator },
}));
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === columnName && col.entityDisplayConfig) {
return { ...col, entityDisplayConfig: { ...col.entityDisplayConfig, separator } };
}
return col;
});
if (updatedColumns) updateField("columns", updatedColumns);
}, [entityDisplayConfigs, config.columns, updateField]);
// ─── 컬럼 추가 ───
const addColumn = useCallback((columnName: string) => {
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
if (existingColumn) return;
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
const availableColumnInfo = availableColumns.find((col) => col.columnName === columnName);
const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName;
const newColumn: ColumnConfig = {
columnName,
displayName,
visible: true,
sortable: true,
searchable: true,
align: "left",
format: "text",
order: config.columns?.length || 0,
};
updateField("columns", [...(config.columns || []), newColumn]);
}, [config.columns, tableColumns, availableColumns, updateField]);
// ─── 조인 컬럼 추가 ───
const addEntityColumn = useCallback((joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
if (existingColumn) return;
const joinTableInfo = entityJoinColumns.joinTables?.find((jt: any) => jt.tableName === joinColumn.tableName);
const sourceColumn = (joinTableInfo as any)?.joinConfig?.sourceColumn || "";
const newColumn: ColumnConfig = {
columnName: joinColumn.joinAlias,
displayName: joinColumn.columnLabel,
visible: true,
sortable: true,
searchable: true,
align: "left",
format: "text",
order: config.columns?.length || 0,
isEntityJoin: false,
additionalJoinInfo: {
sourceTable: config.selectedTable || screenTableName || "",
sourceColumn,
referenceTable: joinColumn.tableName,
joinAlias: joinColumn.joinAlias,
},
};
updateField("columns", [...(config.columns || []), newColumn]);
}, [config.columns, entityJoinColumns.joinTables, config.selectedTable, screenTableName, updateField]);
// ─── 컬럼 제거 ───
const removeColumn = useCallback((columnName: string) => {
updateField("columns", config.columns?.filter((col) => col.columnName !== columnName) || []);
}, [config.columns, updateField]);
// ─── 컬럼 업데이트 ───
const updateColumn = useCallback((columnName: string, updates: Partial) => {
const updatedColumns = config.columns?.map((col) =>
col.columnName === columnName ? { ...col, ...updates } : col
) || [];
updateField("columns", updatedColumns);
}, [config.columns, updateField]);
// ─── 테이블 변경 핸들러 ───
const handleTableChange = useCallback((newTableName: string) => {
if (newTableName === targetTableName) return;
handleChange({
...config,
selectedTable: newTableName,
columns: [],
});
setTableComboboxOpen(false);
}, [targetTableName, handleChange, config]);
// ─── 렌더링 ───
return (
{/* ═══════════════════════════════════════ */}
{/* 1단계: 데이터 소스 (테이블 선택) */}
{/* ═══════════════════════════════════════ */}
테이블을 찾을 수 없습니다.
{availableTables.map((table) => (
handleTableChange(table.tableName)}
className="text-xs"
>
{table.displayName}
{table.displayName !== table.tableName && (
{table.tableName}
)}
))}
{screenTableName && targetTableName !== screenTableName && (
화면 기본 테이블({screenTableName})과 다른 테이블을 사용 중
)}
{/* ═══════════════════════════════════════ */}
{/* 2단계: 컬럼 선택 (Collapsible) */}
{/* ═══════════════════════════════════════ */}
{targetTableName && availableColumns.length > 0 && (
<>
setColumnSearchText(e.target.value)}
placeholder="컬럼 검색..."
className="h-7 text-xs"
/>
{availableColumns
.filter((column) => {
if (!columnSearchText) return true;
const search = columnSearchText.toLowerCase();
return (
column.columnName.toLowerCase().includes(search) ||
(column.label || "").toLowerCase().includes(search)
);
})
.map((column) => {
const isAdded = config.columns?.some((c) => c.columnName === column.columnName);
return (
{
if (isAdded) {
updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
} else {
addColumn(column.columnName);
}
}}
>
{
if (isAdded) {
updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
} else {
addColumn(column.columnName);
}
}}
className="pointer-events-none h-3.5 w-3.5"
/>
{column.label || column.columnName}
{isAdded && (
)}
{column.input_type || column.dataType}
);
})}
{/* Entity 조인 컬럼 (Collapsible) */}
{entityJoinColumns.joinTables.length > 0 && (
{entityJoinColumns.joinTables.map((joinTable, tableIndex) => {
const addedCount = joinTable.availableColumns.filter((col) => {
const match = entityJoinColumns.availableColumns.find(
(jc) => jc.tableName === joinTable.tableName && jc.columnName === col.columnName,
);
return match && config.columns?.some((c) => c.columnName === match.joinAlias);
}).length;
const isSubOpen = entityJoinSubOpen[tableIndex] ?? false;
return (
setEntityJoinSubOpen((prev) => ({ ...prev, [tableIndex]: open }))}>
{joinTable.availableColumns.map((column, colIndex) => {
const matchingJoinColumn = entityJoinColumns.availableColumns.find(
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
);
const isAlreadyAdded = config.columns?.some(
(col) => col.columnName === matchingJoinColumn?.joinAlias,
);
if (!matchingJoinColumn) return null;
return (
{
if (isAlreadyAdded) {
updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
} else {
addEntityColumn(matchingJoinColumn);
}
}}
>
{
if (isAlreadyAdded) {
updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
} else {
addEntityColumn(matchingJoinColumn);
}
}}
className="pointer-events-none h-3.5 w-3.5"
/>
{column.columnLabel}
{column.inputType || column.dataType}
);
})}
);
})}
)}
>
)}
{/* 테이블 미선택 또는 컬럼 없음 안내 */}
{!targetTableName && (
테이블이 선택되지 않았습니다
위 데이터 소스에서 테이블을 선택하세요
)}
{targetTableName && availableColumns.length === 0 && (
컬럼 정보를 불러오는 중...
현재 화면 테이블: {screenTableName}
)}
{/* ═══════════════════════════════════════ */}
{/* 3단계: 표시할 컬럼 (Collapsible + DnD) */}
{/* ═══════════════════════════════════════ */}
{config.columns && config.columns.length > 0 && (
드래그하여 순서 변경, 클릭하여 표시명/너비 수정
{
const { active, over } = event;
if (!over || active.id === over.id) return;
const columns = [...(config.columns || [])];
const oldIndex = columns.findIndex((c) => c.columnName === active.id);
const newIndex = columns.findIndex((c) => c.columnName === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
const reordered = arrayMove(columns, oldIndex, newIndex);
reordered.forEach((col, idx) => { col.order = idx; });
updateField("columns", reordered);
}
}}
>
c.columnName)}
strategy={verticalListSortingStrategy}
>
{(config.columns || []).map((column, idx) => {
const resolvedLabel =
column.displayName && column.displayName !== column.columnName
? column.displayName
: availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName;
const colWithLabel = { ...column, displayName: resolvedLabel };
return (
updateColumn(column.columnName, { displayName: value })}
onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
onRemove={() => removeColumn(column.columnName)}
/>
);
})}
)}
{/* ═══════════════════════════════════════ */}
{/* 엔티티 컬럼 표시 설정 (접이식) */}
{/* ═══════════════════════════════════════ */}
{config.columns?.some((col) => col.isEntityJoin) && (
{config.columns
?.filter((col) => col.isEntityJoin && col.entityDisplayConfig)
.map((column) => (
{column.displayName || column.columnName}
{entityDisplayConfigs[column.columnName] ? (
{/* 구분자 */}
구분자
updateEntityDisplaySeparator(column.columnName, e.target.value)}
className="h-6 w-20 text-xs"
placeholder=" - "
/>
{/* 표시 컬럼 선택 */}
{entityDisplayConfigs[column.columnName].sourceColumns.length === 0 &&
entityDisplayConfigs[column.columnName].joinColumns.length === 0 ? (
표시 가능한 컬럼이 없습니다.
{!column.entityDisplayConfig?.joinTable && (
테이블 타입 관리에서 참조 테이블을 설정하면 더 많은 컬럼을 선택할 수 있습니다.
)}
) : (
표시할 컬럼 선택
컬럼을 찾을 수 없습니다.
{entityDisplayConfigs[column.columnName].sourceColumns.length > 0 && (
{entityDisplayConfigs[column.columnName].sourceColumns.map((col) => (
toggleEntityDisplayColumn(column.columnName, col.columnName)}
className="text-xs"
>
{col.displayName}
))}
)}
{entityDisplayConfigs[column.columnName].joinColumns.length > 0 && (
{entityDisplayConfigs[column.columnName].joinColumns.map((col) => (
toggleEntityDisplayColumn(column.columnName, col.columnName)}
className="text-xs"
>
{col.displayName}
))}
)}
)}
{/* 참조 테이블 미설정 안내 */}
{!column.entityDisplayConfig?.joinTable &&
entityDisplayConfigs[column.columnName].sourceColumns.length > 0 && (
현재 기본 테이블 컬럼만 표시됩니다. 테이블 타입 관리에서 참조 테이블을 설정하면 조인된 테이블의 컬럼도 선택할 수 있습니다.
)}
{/* 선택된 컬럼 미리보기 */}
{entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && (
미리보기
{entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => (
{colName}
{idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && (
{entityDisplayConfigs[column.columnName].separator}
)}
))}
)}
) : (
컬럼 정보 로딩 중...
)}
{config.columns?.filter((c) => c.isEntityJoin && c.entityDisplayConfig).indexOf(column) !==
(config.columns?.filter((c) => c.isEntityJoin && c.entityDisplayConfig).length || 0) - 1 && (
)}
))}
)}
{/* ═══════════════════════════════════════ */}
{/* 4단계: 툴바 버튼 설정 (Switch 토글) */}
{/* ═══════════════════════════════════════ */}
updateNestedField("toolbar", "showEditMode", checked)}
/>
updateNestedField("toolbar", "showExcel", checked)}
/>
updateNestedField("toolbar", "showPdf", checked)}
/>
updateNestedField("toolbar", "showCopy", checked)}
/>
updateNestedField("toolbar", "showSearch", checked)}
/>
updateNestedField("toolbar", "showFilter", checked)}
/>
updateNestedField("toolbar", "showRefresh", checked)}
/>
updateNestedField("toolbar", "showPaginationRefresh", checked)}
/>
{/* ═══════════════════════════════════════ */}
{/* 5단계: 고급 설정 (기본 접힘) */}
{/* ═══════════════════════════════════════ */}
{/* 체크박스 설정 */}
체크박스
updateNestedField("checkbox", "enabled", checked)}
/>
{config.checkbox?.enabled && (
updateNestedField("checkbox", "selectAll", checked)}
/>
체크박스 위치
)}
{/* 기본 정렬 설정 */}
테이블 로드 시 기본 정렬 순서를 지정합니다
정렬 컬럼
{config.defaultSort?.columnName && (
정렬 방향
)}
{/* 가로 스크롤 */}
가로 스크롤 및 컬럼 고정
updateNestedField("horizontalScroll", "enabled", checked)}
/>
{config.horizontalScroll?.enabled && (
최대 표시 컬럼 수
이 수를 넘는 컬럼이 있으면 가로 스크롤이 생성됩니다
updateNestedField("horizontalScroll", "maxVisibleColumns", parseInt(e.target.value) || 8)}
min={3}
max={20}
className="h-7 w-[80px] text-xs"
/>
)}
{/* ═══════════════════════════════════════ */}
{/* 6단계: 데이터 필터링 */}
{/* ═══════════════════════════════════════ */}
({
columnName: col.columnName,
columnLabel: col.label || col.columnName,
dataType: col.dataType,
input_type: col.input_type,
}) as any,
)}
config={config.dataFilter}
onConfigChange={(dataFilter) => updateField("dataFilter", dataFilter)}
/>
);
};
V2TableListConfigPanel.displayName = "V2TableListConfigPanel";
export default V2TableListConfigPanel;