From e6cd8806e3683943754b97fbd6075db44957cc75 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Thu, 18 Sep 2025 17:17:06 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0:=20INSERT?= =?UTF-8?q?=20=EC=95=A1=EC=85=98=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EB=A7=A4=ED=95=91=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EC=9A=B4=20InsertFieldMappingPanel=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataflow/ConnectionSetupModal.tsx | 22 + .../connection/ActionFieldMappings.tsx | 24 ++ .../connection/ColumnTableSection.tsx | 334 +++++++++++++++ .../dataflow/connection/DataSaveSettings.tsx | 4 + .../connection/InsertFieldMappingPanel.tsx | 395 ++++++++++++++++++ frontend/lib/api/dataflow.ts | 4 +- frontend/lib/api/screen.ts | 4 +- 7 files changed, 783 insertions(+), 4 deletions(-) create mode 100644 frontend/components/dataflow/connection/ColumnTableSection.tsx create mode 100644 frontend/components/dataflow/connection/InsertFieldMappingPanel.tsx diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx index b8d37f8a..b0c27fe9 100644 --- a/frontend/components/dataflow/ConnectionSetupModal.tsx +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -553,6 +553,28 @@ export const ConnectionSetupModal: React.FC = ({ return true; // DELETE는 필드 매핑 검증 생략 } + // INSERT 액션의 경우 모든 TO 테이블 컬럼이 매핑되거나 기본값이 있어야 함 + if (action.actionType === "insert") { + // TO 테이블의 모든 컬럼을 찾기 + const toTableName = action.fieldMappings[0]?.targetTable; + if (!toTableName) return false; + + const toTableColumns = tableColumnsCache[toTableName] || []; + if (toTableColumns.length === 0) return false; + + // 모든 TO 컬럼이 매핑되거나 기본값이 있는지 확인 + return toTableColumns.every((column) => { + const mapping = action.fieldMappings.find((m) => m.targetField === column.columnName); + if (!mapping) return false; + + // 소스 매핑 또는 기본값 중 하나는 있어야 함 + const hasSource = mapping.sourceTable && mapping.sourceField; + const hasDefault = mapping.defaultValue && mapping.defaultValue.trim(); + + return hasSource || hasDefault; + }); + } + return action.fieldMappings.every((mapping) => { // 타겟은 항상 필요 if (!mapping.targetTable || !mapping.targetField) return false; diff --git a/frontend/components/dataflow/connection/ActionFieldMappings.tsx b/frontend/components/dataflow/connection/ActionFieldMappings.tsx index d75d804b..dcd56cf6 100644 --- a/frontend/components/dataflow/connection/ActionFieldMappings.tsx +++ b/frontend/components/dataflow/connection/ActionFieldMappings.tsx @@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Plus, Trash2 } from "lucide-react"; import { TableInfo, ColumnInfo } from "@/lib/api/dataflow"; import { DataSaveSettings } from "@/types/connectionTypes"; +import { InsertFieldMappingPanel } from "./InsertFieldMappingPanel"; interface ActionFieldMappingsProps { action: DataSaveSettings["actions"][0]; @@ -16,6 +17,10 @@ interface ActionFieldMappingsProps { onSettingsChange: (settings: DataSaveSettings) => void; availableTables: TableInfo[]; tableColumnsCache: { [tableName: string]: ColumnInfo[] }; + fromTableColumns?: ColumnInfo[]; + toTableColumns?: ColumnInfo[]; + fromTableName?: string; + toTableName?: string; } export const ActionFieldMappings: React.FC = ({ @@ -25,7 +30,26 @@ export const ActionFieldMappings: React.FC = ({ onSettingsChange, availableTables, tableColumnsCache, + fromTableColumns = [], + toTableColumns = [], + fromTableName, + toTableName, }) => { + // INSERT 액션일 때는 새로운 패널 사용 + if (action.actionType === "insert" && fromTableColumns.length > 0 && toTableColumns.length > 0) { + return ( + + ); + } const addFieldMapping = () => { const newActions = [...settings.actions]; newActions[actionIndex].fieldMappings.push({ diff --git a/frontend/components/dataflow/connection/ColumnTableSection.tsx b/frontend/components/dataflow/connection/ColumnTableSection.tsx new file mode 100644 index 00000000..0edbd0a6 --- /dev/null +++ b/frontend/components/dataflow/connection/ColumnTableSection.tsx @@ -0,0 +1,334 @@ +"use client"; + +import React, { useMemo } from "react"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { ColumnInfo } from "@/lib/api/dataflow"; +import { getInputTypeForDataType } from "@/utils/connectionUtils"; + +interface ColumnMapping { + toColumnName: string; + fromColumnName?: string; + defaultValue?: string; +} + +interface ColumnTableSectionProps { + type: "from" | "to"; + tableName: string; + columns: ColumnInfo[]; + selectedColumn: string | null; + onColumnClick: (columnName: string) => void; + searchTerm: string; + onSearchChange: (term: string) => void; + dataTypeFilter: string; + onDataTypeFilterChange: (filter: string) => void; + showMappedOnly: boolean; + onShowMappedOnlyChange: (show: boolean) => void; + showUnmappedOnly: boolean; + onShowUnmappedOnlyChange: (show: boolean) => void; + columnMappings: ColumnMapping[]; + onDefaultValueChange?: (columnName: string, value: string) => void; + onRemoveMapping?: (columnName: string) => void; + isColumnClickable: (column: ColumnInfo) => boolean; + oppositeSelectedColumn?: string | null; + oppositeColumns?: ColumnInfo[]; +} + +export const ColumnTableSection: React.FC = ({ + type, + tableName, + columns, + selectedColumn, + onColumnClick, + searchTerm, + onSearchChange, + dataTypeFilter, + onDataTypeFilterChange, + showMappedOnly, + onShowMappedOnlyChange, + showUnmappedOnly, + onShowUnmappedOnlyChange, + columnMappings, + onDefaultValueChange, + onRemoveMapping, + isColumnClickable, + oppositeSelectedColumn, + oppositeColumns, +}) => { + const isFromTable = type === "from"; + + // 데이터 타입 목록 추출 + const dataTypes = useMemo(() => { + const types = new Set(columns.map((col) => col.dataType).filter((type): type is string => !!type)); + return Array.from(types).sort(); + }, [columns]); + + // 필터링된 컬럼 목록 + const filteredColumns = useMemo(() => { + return columns.filter((column) => { + // 검색어 필터 + const matchesSearch = searchTerm === "" || column.columnName.toLowerCase().includes(searchTerm.toLowerCase()); + + // 데이터 타입 필터 + const matchesDataType = dataTypeFilter === "" || column.dataType === dataTypeFilter; + + // 매핑 상태 필터 + const isMapped = isFromTable + ? columnMappings.some((mapping) => mapping.fromColumnName === column.columnName) + : (() => { + const mapping = columnMappings.find((mapping) => mapping.toColumnName === column.columnName); + return !!mapping?.fromColumnName || !!(mapping?.defaultValue && mapping.defaultValue.trim()); + })(); + + const matchesMappingFilter = + (!showMappedOnly && !showUnmappedOnly) || (showMappedOnly && isMapped) || (showUnmappedOnly && !isMapped); + + return matchesSearch && matchesDataType && matchesMappingFilter; + }); + }, [columns, searchTerm, dataTypeFilter, showMappedOnly, showUnmappedOnly, columnMappings, isFromTable]); + + const mappedCount = columns.filter((column) => + isFromTable + ? columnMappings.some((mapping) => mapping.fromColumnName === column.columnName) + : (() => { + const mapping = columnMappings.find((mapping) => mapping.toColumnName === column.columnName); + return !!mapping?.fromColumnName || !!(mapping?.defaultValue && mapping.defaultValue.trim()); + })(), + ).length; + + return ( +
+ {/* 헤더 */} +
+

+ {isFromTable ? "From" : "To"}: {tableName} ({columns.length}/{columns.length}) +

+
+ + {/* 검색 및 필터 */} +
+
+ onSearchChange(e.target.value)} + className="h-8 text-xs" + /> +
+ +
+ + +
+
+
+
+ + {/* 컬럼 리스트 */} +
+ {filteredColumns.map((column) => { + const isSelected = selectedColumn === column.columnName; + const isClickable = isColumnClickable(column); + + if (isFromTable) { + // FROM 테이블 렌더링 + const isMapped = columnMappings.some((mapping) => mapping.fromColumnName === column.columnName); + const mappedToColumn = columnMappings.find( + (mapping) => mapping.fromColumnName === column.columnName, + )?.toColumnName; + + // 선택된 TO 컬럼과 타입 호환성 체크 (이미 매핑된 컬럼은 제외) + const isTypeCompatible = + !oppositeSelectedColumn || + isMapped || + oppositeColumns?.find((col) => col.columnName === oppositeSelectedColumn)?.dataType === column.dataType; + + return ( +
onColumnClick(column.columnName) : undefined} + className={`border-b border-gray-200 px-3 py-2 text-xs transition-colors ${ + isSelected + ? "bg-gray-200 text-gray-800" + : isMapped + ? "bg-gray-100 text-gray-700" + : oppositeSelectedColumn && !isTypeCompatible + ? "cursor-not-allowed bg-red-50 text-red-400 opacity-60" + : isClickable + ? "cursor-pointer hover:bg-gray-50" + : "cursor-not-allowed bg-gray-100 text-gray-400" + }`} + > +
+
+
+ {column.columnName} + {isSelected && } + {isMapped && } + {oppositeSelectedColumn && !isTypeCompatible && ( + + ⚠ + + )} +
+
+ {column.dataType} + {oppositeSelectedColumn && !isTypeCompatible && ( + (호환 불가) + )} +
+ {isMapped && mappedToColumn && ( +
→ {mappedToColumn}
+ )} +
+
+
+ ); + } else { + // TO 테이블 렌더링 + const mapping = columnMappings.find((m) => m.toColumnName === column.columnName); + const isMapped = !!mapping?.fromColumnName; + const hasDefaultValue = !!(mapping?.defaultValue && mapping.defaultValue.trim()); + + // 선택된 FROM 컬럼과 타입 호환성 체크 (이미 매핑된 컬럼은 제외) + const isTypeCompatible = + !oppositeSelectedColumn || + isMapped || + oppositeColumns?.find((col) => col.columnName === oppositeSelectedColumn)?.dataType === column.dataType; + + return ( +
+ {/* 컬럼 정보 행 */} +
onColumnClick(column.columnName) : undefined} + className={`px-3 py-2 text-xs ${ + isClickable && isTypeCompatible + ? "cursor-pointer hover:bg-gray-50" + : oppositeSelectedColumn && !isTypeCompatible + ? "cursor-not-allowed" + : hasDefaultValue + ? "cursor-not-allowed" + : "" + }`} + > +
+
+ {column.columnName} + {isSelected && } + {oppositeSelectedColumn && !isTypeCompatible && ( + + ⚠ + + )} +
+
+ {column.dataType} + {oppositeSelectedColumn && !isTypeCompatible && ( + (호환 불가) + )} +
+ + {isMapped && ( +
+ ← {mapping.fromColumnName} + +
+ )} + + {!isMapped && onDefaultValueChange && ( +
+ onDefaultValueChange(column.columnName, e.target.value)} + className="h-6 border-gray-200 text-xs focus:border-green-400 focus:ring-0" + onClick={(e) => e.stopPropagation()} + disabled={isSelected || !!oppositeSelectedColumn} + /> +
+ )} +
+
+
+ ); + } + })} +
+ + {/* 하단 통계 */} +
+
+ + {isFromTable ? "매핑됨" : "설정됨"}: {mappedCount}/{columns.length} + + + 표시: {filteredColumns.length} + +
+
+
+ ); +}; diff --git a/frontend/components/dataflow/connection/DataSaveSettings.tsx b/frontend/components/dataflow/connection/DataSaveSettings.tsx index b9483e8a..8fd3937e 100644 --- a/frontend/components/dataflow/connection/DataSaveSettings.tsx +++ b/frontend/components/dataflow/connection/DataSaveSettings.tsx @@ -156,6 +156,10 @@ export const DataSaveSettings: React.FC = ({ onSettingsChange={onSettingsChange} availableTables={availableTables} tableColumnsCache={tableColumnsCache} + fromTableColumns={fromTableColumns} + toTableColumns={toTableColumns} + fromTableName={fromTableName} + toTableName={toTableName} /> )} diff --git a/frontend/components/dataflow/connection/InsertFieldMappingPanel.tsx b/frontend/components/dataflow/connection/InsertFieldMappingPanel.tsx new file mode 100644 index 00000000..8118147c --- /dev/null +++ b/frontend/components/dataflow/connection/InsertFieldMappingPanel.tsx @@ -0,0 +1,395 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { ColumnInfo } from "@/lib/api/dataflow"; +import { DataSaveSettings } from "@/types/connectionTypes"; +import { ColumnTableSection } from "./ColumnTableSection"; + +interface InsertFieldMappingPanelProps { + action: DataSaveSettings["actions"][0]; + actionIndex: number; + settings: DataSaveSettings; + onSettingsChange: (settings: DataSaveSettings) => void; + fromTableColumns: ColumnInfo[]; + toTableColumns: ColumnInfo[]; + fromTableName?: string; + toTableName?: string; +} + +interface ColumnMapping { + toColumnName: string; + fromColumnName?: string; + defaultValue?: string; +} + +export const InsertFieldMappingPanel: React.FC = ({ + action, + actionIndex, + settings, + onSettingsChange, + fromTableColumns, + toTableColumns, + fromTableName, + toTableName, +}) => { + const [selectedFromColumn, setSelectedFromColumn] = useState(null); + const [selectedToColumn, setSelectedToColumn] = useState(null); + const [columnMappings, setColumnMappings] = useState([]); + + // 검색 및 필터링 상태 (FROM과 TO 독립적) + const [fromSearchTerm, setFromSearchTerm] = useState(""); + const [toSearchTerm, setToSearchTerm] = useState(""); + const [fromDataTypeFilter, setFromDataTypeFilter] = useState(""); + const [toDataTypeFilter, setToDataTypeFilter] = useState(""); + + // FROM 테이블 필터 + const [fromShowMappedOnly, setFromShowMappedOnly] = useState(false); + const [fromShowUnmappedOnly, setFromShowUnmappedOnly] = useState(false); + + // TO 테이블 필터 + const [toShowMappedOnly, setToShowMappedOnly] = useState(false); + const [toShowUnmappedOnly, setToShowUnmappedOnly] = useState(false); + + // 기존 매핑 데이터를 columnMappings로 변환 + useEffect(() => { + const mappings: ColumnMapping[] = toTableColumns.map((toCol) => { + const existingMapping = action.fieldMappings.find((mapping) => mapping.targetField === toCol.columnName); + + return { + toColumnName: toCol.columnName, + fromColumnName: existingMapping?.sourceField || undefined, + defaultValue: existingMapping?.defaultValue || "", + }; + }); + + setColumnMappings(mappings); + }, [action.fieldMappings, toTableColumns]); + + // columnMappings 변경 시 settings 업데이트 + const updateSettings = (newMappings: ColumnMapping[]) => { + const newActions = [...settings.actions]; + + // 새로운 fieldMappings 생성 + const fieldMappings = newMappings + .filter((mapping) => mapping.fromColumnName || (mapping.defaultValue && mapping.defaultValue.trim())) + .map((mapping) => ({ + sourceTable: mapping.fromColumnName ? fromTableName || "" : "", + sourceField: mapping.fromColumnName || "", + targetTable: toTableName || "", + targetField: mapping.toColumnName, + defaultValue: mapping.defaultValue || "", + transformFunction: "", + })); + + newActions[actionIndex].fieldMappings = fieldMappings; + onSettingsChange({ ...settings, actions: newActions }); + }; + + // FROM 컬럼 클릭 핸들러 + const handleFromColumnClick = (columnName: string) => { + if (selectedFromColumn === columnName) { + setSelectedFromColumn(null); + } else { + setSelectedFromColumn(columnName); + + // TO 컬럼이 이미 선택되어 있으면 매핑 시도 + if (selectedToColumn) { + const fromColumn = fromTableColumns.find((col) => col.columnName === columnName); + const toColumn = toTableColumns.find((col) => col.columnName === selectedToColumn); + + if (fromColumn && toColumn) { + // 데이터 타입 호환성 체크 + if (fromColumn.dataType !== toColumn.dataType) { + alert(`데이터 타입이 다릅니다. FROM: ${fromColumn.dataType}, TO: ${toColumn.dataType}`); + return; + } + + // 매핑 생성 + createMapping(columnName, selectedToColumn); + setSelectedFromColumn(null); + setSelectedToColumn(null); + } + } + } + }; + + // 공통 매핑 생성 함수 + const createMapping = (fromColumnName: string, toColumnName: string) => { + const newMappings = columnMappings.map((mapping) => { + if (mapping.toColumnName === toColumnName) { + return { + ...mapping, + fromColumnName: fromColumnName, + defaultValue: "", // 매핑이 설정되면 기본값 초기화 + }; + } + return mapping; + }); + + setColumnMappings(newMappings); + updateSettings(newMappings); + }; + + // TO 컬럼 클릭 핸들러 + const handleToColumnClick = (toColumnName: string) => { + const currentMapping = columnMappings.find((m) => m.toColumnName === toColumnName); + + // 이미 매핑된 컬럼인 경우 처리하지 않음 + if (currentMapping?.fromColumnName) return; + + if (selectedToColumn === toColumnName) { + setSelectedToColumn(null); + } else { + setSelectedToColumn(toColumnName); + + // FROM 컬럼이 이미 선택되어 있으면 매핑 시도 + if (selectedFromColumn) { + const fromColumn = fromTableColumns.find((col) => col.columnName === selectedFromColumn); + const toColumn = toTableColumns.find((col) => col.columnName === toColumnName); + + if (fromColumn && toColumn) { + // 데이터 타입 호환성 체크 + if (fromColumn.dataType !== toColumn.dataType) { + alert(`데이터 타입이 다릅니다. FROM: ${fromColumn.dataType}, TO: ${toColumn.dataType}`); + return; + } + + // 매핑 생성 + createMapping(selectedFromColumn, toColumnName); + setSelectedFromColumn(null); + setSelectedToColumn(null); + } + } + } + }; + + // 기본값 변경 핸들러 + const handleDefaultValueChange = (toColumnName: string, value: string) => { + const newMappings = columnMappings.map((mapping) => { + if (mapping.toColumnName === toColumnName) { + return { + ...mapping, + fromColumnName: value.trim() ? undefined : mapping.fromColumnName, // 기본값이 있으면 매핑 제거 + defaultValue: value, + }; + } + return mapping; + }); + + setColumnMappings(newMappings); + updateSettings(newMappings); + }; + + // 매핑 제거 핸들러 + const handleRemoveMapping = (toColumnName: string) => { + const newMappings = columnMappings.map((mapping) => { + if (mapping.toColumnName === toColumnName) { + return { + ...mapping, + fromColumnName: undefined, + defaultValue: "", + }; + } + return mapping; + }); + + setColumnMappings(newMappings); + updateSettings(newMappings); + }; + + // FROM 컬럼이 클릭 가능한지 확인 (데이터 타입 호환성 + 1대1 매핑 제약) + const isFromColumnClickable = (fromColumn: ColumnInfo) => { + // 이미 다른 TO 컬럼과 매핑된 FROM 컬럼은 클릭 불가 + const isAlreadyMapped = columnMappings.some((mapping) => mapping.fromColumnName === fromColumn.columnName); + if (isAlreadyMapped) return false; + + if (!selectedToColumn) return true; // TO가 선택되지 않았으면 모든 FROM 클릭 가능 + + const toColumn = toTableColumns.find((col) => col.columnName === selectedToColumn); + if (!toColumn) return true; + + return fromColumn.dataType === toColumn.dataType; + }; + + // TO 컬럼이 클릭 가능한지 확인 (데이터 타입 호환성) + const isToColumnClickable = (toColumn: ColumnInfo) => { + const currentMapping = columnMappings.find((m) => m.toColumnName === toColumn.columnName); + + // 이미 매핑된 컬럼은 클릭 불가 + if (currentMapping?.fromColumnName) return false; + + // 기본값이 설정된 컬럼은 클릭 불가 + if (currentMapping?.defaultValue && currentMapping.defaultValue.trim()) return false; + + if (!selectedFromColumn) return true; // FROM이 선택되지 않았으면 모든 TO 클릭 가능 + + const fromColumn = fromTableColumns.find((col) => col.columnName === selectedFromColumn); + if (!fromColumn) return true; + + return fromColumn.dataType === toColumn.dataType; + }; + + return ( +
+ {/* 헤더 섹션 */} + + +

+ 양쪽 테이블의 컬럼을 클릭하여 매핑하거나, 대상 컬럼에 기본값을 입력하세요. 같은 데이터 타입의 컬럼만 매핑 + 가능합니다. 하나의 FROM 컬럼은 하나의 TO 컬럼에만 매핑 가능합니다. +

+
+
+ +
+ + + +
+ + {/* 빠른 필터 액션 */} + + + 빠른 필터: + + + +
+ + FROM: {fromTableColumns.length} + + + TO: {toTableColumns.length} + +
+
+
+ + {/* 매핑 통계 */} + + +
+
+
+
매핑 진행 상황
+
+ 총 {toTableColumns.length}개 컬럼 중{" "} + + {columnMappings.filter((m) => m.fromColumnName || (m.defaultValue && m.defaultValue.trim())).length} + 개 + {" "} + 완료 +
+
+
+
+
+ {Math.round( + (columnMappings.filter((m) => m.fromColumnName || (m.defaultValue && m.defaultValue.trim())).length / + toTableColumns.length) * + 100, + )} + % +
+
완료율
+
+
+
+ m.fromColumnName || (m.defaultValue && m.defaultValue.trim())).length / + toTableColumns.length) * + 100 + } + className="h-2" + /> +
+
+
+
+ ); +}; diff --git a/frontend/lib/api/dataflow.ts b/frontend/lib/api/dataflow.ts index 9c702ca2..c40f1a3b 100644 --- a/frontend/lib/api/dataflow.ts +++ b/frontend/lib/api/dataflow.ts @@ -358,7 +358,7 @@ export class DataFlowAPI { } /** - * 테이블 컬럼 정보 조회 + * 테이블 컬럼 정보 조회 (모든 컬럼) */ static async getTableColumns(tableName: string): Promise { try { @@ -369,7 +369,7 @@ export class DataFlowAPI { total: number; totalPages: number; }> - >(`/table-management/tables/${tableName}/columns`); + >(`/table-management/tables/${tableName}/columns?size=1000`); if (!response.data.success) { throw new Error(response.data.message || "컬럼 정보 조회에 실패했습니다."); diff --git a/frontend/lib/api/screen.ts b/frontend/lib/api/screen.ts index 4accf9da..11a91479 100644 --- a/frontend/lib/api/screen.ts +++ b/frontend/lib/api/screen.ts @@ -220,9 +220,9 @@ export const tableTypeApi = { } }, - // 테이블 컬럼 정보 조회 + // 테이블 컬럼 정보 조회 (모든 컬럼) getColumns: async (tableName: string): Promise => { - const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); + const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=1000`); // 새로운 API 응답 구조에 맞게 수정: { columns, total, page, size, totalPages } const data = response.data.data || response.data; return data.columns || data || []; -- 2.43.0