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)}
+ />
+
+
);
};