From 9dbedbe42c6f58cb61fdfcf14c3f4de6d8127d49 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 12 Mar 2026 02:45:25 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260311174249-88g7 round-1 --- .../config-panels/V2TableListConfigPanel.tsx | 1343 ++++++++++++++++- 1 file changed, 1327 insertions(+), 16 deletions(-) diff --git a/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx b/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx index 14f07f96..489cd1dc 100644 --- a/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx @@ -2,14 +2,141 @@ /** * V2TableList 설정 패널 - * 기존 TableListConfigPanel의 모든 복잡한 로직(테이블 선택, 컬럼 DnD, 엔티티 조인, - * 데이터 필터, 페이지네이션, 툴바, 체크박스, 정렬, 가로 스크롤 등)을 유지하면서 - * componentConfigChanged 이벤트를 추가하여 실시간 업데이트 지원 + * 토스식 단계별 UX: 데이터 소스 -> 컬럼 선택 -> 조인 컬럼 -> 순서/라벨 -> 고급 설정(접힘) + * 기존 TableListConfigPanel의 모든 기능을 자체 UI로 완전 구현 */ -import React from "react"; -import { TableListConfigPanel } from "@/lib/registry/components/v2-table-list/TableListConfigPanel"; -import type { TableListConfig } from "@/lib/registry/components/v2-table-list/types"; +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 }; + + 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" + /> + +
+ ); +} + +// ─── 섹션 헤더 컴포넌트 ─── +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; @@ -20,15 +147,17 @@ interface V2TableListConfigPanelProps { } export const V2TableListConfigPanel: React.FC = ({ - config, + config: configProp, onChange, screenTableName, tableColumns, menuObjid, }) => { - const handleChange = (newConfig: Partial) => { - onChange(newConfig); + const config = configProp || ({} as TableListConfig); + // componentConfigChanged 이벤트 발행 래퍼 + const handleChange = useCallback((newConfig: Partial) => { + onChange(newConfig); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { @@ -36,15 +165,1197 @@ export const V2TableListConfigPanel: React.FC = ({ }) ); } - }; + }, [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 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단계: 컬럼 선택 */} + {/* ═══════════════════════════════════════ */} + {targetTableName && availableColumns.length > 0 && ( + <> +
+ + + +
+ {availableColumns.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 조인 컬럼 */} + {entityJoinColumns.joinTables.length > 0 && ( +
+ + +
+ {entityJoinColumns.joinTables.map((joinTable, tableIndex) => ( +
+
+ + {joinTable.tableName} + + {joinTable.currentDisplayColumn} + +
+
+ {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단계: 선택된 컬럼 순서 (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)} + /> +
+
); };