"use client"; import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { TableListConfig, ColumnConfig } from "./types"; import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; import { getFullImageUrl } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, RefreshCw, ArrowUp, ArrowDown, TableIcon, Settings, X, Layers, ChevronDown, } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { tableDisplayStore } from "@/stores/tableDisplayStore"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; import { SingleTableWithSticky } from "./SingleTableWithSticky"; import { CardModeRenderer } from "./CardModeRenderer"; import { TableOptionsModal } from "@/components/common/TableOptionsModal"; // ======================================== // 인터페이스 // ======================================== // 그룹화된 데이터 인터페이스 interface GroupedData { groupKey: string; groupValues: Record; items: any[]; count: number; } // ======================================== // 캐시 및 유틸리티 // ======================================== const tableColumnCache = new Map(); const tableInfoCache = new Map(); const TABLE_CACHE_TTL = 5 * 60 * 1000; // 5분 const cleanupTableCache = () => { const now = Date.now(); for (const [key, entry] of tableColumnCache.entries()) { if (now - entry.timestamp > TABLE_CACHE_TTL) { tableColumnCache.delete(key); } } for (const [key, entry] of tableInfoCache.entries()) { if (now - entry.timestamp > TABLE_CACHE_TTL) { tableInfoCache.delete(key); } } }; if (typeof window !== "undefined") { setInterval(cleanupTableCache, 10 * 60 * 1000); } const debounceTimers = new Map(); const activeRequests = new Map>(); const debouncedApiCall = (key: string, fn: (...args: T) => Promise, delay: number = 300) => { return (...args: T): Promise => { const activeRequest = activeRequests.get(key); if (activeRequest) { return activeRequest as Promise; } return new Promise((resolve, reject) => { const existingTimer = debounceTimers.get(key); if (existingTimer) { clearTimeout(existingTimer); } const timer = setTimeout(async () => { try { const requestPromise = fn(...args); activeRequests.set(key, requestPromise); const result = await requestPromise; resolve(result); } catch (error) { reject(error); } finally { debounceTimers.delete(key); activeRequests.delete(key); } }, delay); debounceTimers.set(key, timer); }); }; }; // ======================================== // Props 인터페이스 // ======================================== export interface TableListComponentProps { component: any; isDesignMode?: boolean; isSelected?: boolean; isInteractive?: boolean; onClick?: () => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: (e: React.DragEvent) => void; className?: string; style?: React.CSSProperties; formData?: Record; onFormDataChange?: (data: any) => void; config?: TableListConfig; size?: { width: number; height: number }; position?: { x: number; y: number; z?: number }; componentConfig?: any; selectedScreen?: any; onZoneComponentDrop?: any; onZoneClick?: any; tableName?: string; onRefresh?: () => void; onClose?: () => void; screenId?: string; userId?: string; // 사용자 ID (컬럼 순서 저장용) onSelectedRowsChange?: ( selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[], ) => void; onConfigChange?: (config: any) => void; refreshKey?: number; } // ======================================== // 메인 컴포넌트 // ======================================== export const TableListComponent: React.FC = ({ component, isDesignMode = false, isSelected = false, onClick, onDragStart, onDragEnd, config, className, style, onFormDataChange, componentConfig, onSelectedRowsChange, onConfigChange, refreshKey, tableName, userId, }) => { // ======================================== // 설정 및 스타일 // ======================================== const tableConfig = { ...config, ...component.config, ...componentConfig, } as TableListConfig; // selectedTable 안전하게 추출 (문자열인지 확인) let finalSelectedTable = componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName; // 디버그 로그 제거 (성능 최적화) // 객체인 경우 tableName 속성 추출 시도 if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) { console.warn("⚠️ selectedTable이 객체입니다:", finalSelectedTable); finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName; console.log("✅ 객체에서 추출한 테이블명:", finalSelectedTable); } tableConfig.selectedTable = finalSelectedTable; // 디버그 로그 제거 (성능 최적화) const buttonColor = component.style?.labelColor || "#212121"; const buttonTextColor = component.config?.buttonTextColor || "#ffffff"; const gridColumns = component.gridColumns || 1; let calculatedWidth: string; if (isDesignMode) { if (gridColumns === 1) { calculatedWidth = "400px"; } else if (gridColumns === 2) { calculatedWidth = "800px"; } else { calculatedWidth = "100%"; } } else { calculatedWidth = "100%"; } const componentStyle: React.CSSProperties = { width: calculatedWidth, height: isDesignMode ? "auto" : "100%", minHeight: isDesignMode ? "300px" : "100%", display: "flex", flexDirection: "column", backgroundColor: "hsl(var(--background))", overflow: "hidden", ...style, // width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어) width: "100%", }; // ======================================== // 상태 관리 // ======================================== const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [totalItems, setTotalItems] = useState(0); const [searchTerm, setSearchTerm] = useState(""); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [columnLabels, setColumnLabels] = useState>({}); const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); const [displayColumns, setDisplayColumns] = useState([]); const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState< Record >({}); const [categoryMappings, setCategoryMappings] = useState< Record> >({}); const [categoryMappingsKey, setCategoryMappingsKey] = useState(0); // 강제 리렌더링용 const [searchValues, setSearchValues] = useState>({}); const [selectedRows, setSelectedRows] = useState>(new Set()); const [columnWidths, setColumnWidths] = useState>({}); const [refreshTrigger, setRefreshTrigger] = useState(0); const [columnOrder, setColumnOrder] = useState([]); const columnRefs = useRef>({}); const [isAllSelected, setIsAllSelected] = useState(false); const hasInitializedWidths = useRef(false); const isResizing = useRef(false); // 필터 설정 관련 상태 const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); const [visibleFilterColumns, setVisibleFilterColumns] = useState>(new Set()); // 그룹 설정 관련 상태 const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false); const [groupByColumns, setGroupByColumns] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); // 사용자 옵션 모달 관련 상태 const [isTableOptionsOpen, setIsTableOptionsOpen] = useState(false); const [showGridLines, setShowGridLines] = useState(true); const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table"); const [frozenColumns, setFrozenColumns] = useState([]); // 🆕 초기 로드 시 localStorage에서 컬럼 순서 불러오기 useEffect(() => { if (!tableConfig.selectedTable || !userId) return; const userKey = userId || "guest"; const storageKey = `table_column_order_${tableConfig.selectedTable}_${userKey}`; const savedOrder = localStorage.getItem(storageKey); if (savedOrder) { try { const parsedOrder = JSON.parse(savedOrder); console.log("📂 localStorage에서 컬럼 순서 불러오기:", { storageKey, columnOrder: parsedOrder }); setColumnOrder(parsedOrder); // 부모 컴포넌트에 초기 컬럼 순서 전달 if (onSelectedRowsChange && parsedOrder.length > 0) { console.log("✅ 초기 컬럼 순서 전달:", parsedOrder); // 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬) const initialData = data.map((row: any) => { const reordered: any = {}; parsedOrder.forEach((colName: string) => { if (colName in row) { reordered[colName] = row[colName]; } }); // 나머지 컬럼 추가 Object.keys(row).forEach((key) => { if (!(key in reordered)) { reordered[key] = row[key]; } }); return reordered; }); console.log("📊 초기 화면 표시 데이터 전달:", { count: initialData.length, firstRow: initialData[0] }); // 전역 저장소에 데이터 저장 if (tableConfig.selectedTable) { tableDisplayStore.setTableData( tableConfig.selectedTable, initialData, parsedOrder.filter((col) => col !== "__checkbox__"), sortColumn, sortDirection, ); } onSelectedRowsChange([], [], sortColumn, sortDirection, parsedOrder, initialData); } } catch (error) { console.error("❌ 컬럼 순서 파싱 실패:", error); } } }, [tableConfig.selectedTable, userId, data.length]); // data.length 추가 (데이터 로드 후 실행) const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, { enableBatchLoading: true, preloadCommonCodes: true, maxBatchSize: 5, }); // ======================================== // 컬럼 라벨 가져오기 // ======================================== const fetchColumnLabels = useCallback(async () => { if (!tableConfig.selectedTable) return; try { const cacheKey = `columns_${tableConfig.selectedTable}`; const cached = tableColumnCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) { const labels: Record = {}; const meta: Record = {}; // 캐시된 inputTypes 맵 생성 const inputTypeMap: Record = {}; if (cached.inputTypes) { cached.inputTypes.forEach((col: any) => { inputTypeMap[col.columnName] = col.inputType; }); } cached.columns.forEach((col: any) => { labels[col.columnName] = col.displayName || col.comment || col.columnName; meta[col.columnName] = { webType: col.webType, codeCategory: col.codeCategory, inputType: inputTypeMap[col.columnName], // 캐시된 inputType 사용! }; }); setColumnLabels(labels); setColumnMeta(meta); return; } const columns = await tableTypeApi.getColumns(tableConfig.selectedTable); // 컬럼 입력 타입 정보 가져오기 const inputTypes = await tableTypeApi.getColumnInputTypes(tableConfig.selectedTable); const inputTypeMap: Record = {}; inputTypes.forEach((col: any) => { inputTypeMap[col.columnName] = col.inputType; }); tableColumnCache.set(cacheKey, { columns, inputTypes, timestamp: Date.now(), }); const labels: Record = {}; const meta: Record = {}; columns.forEach((col: any) => { labels[col.columnName] = col.displayName || col.comment || col.columnName; meta[col.columnName] = { webType: col.webType, codeCategory: col.codeCategory, inputType: inputTypeMap[col.columnName], }; }); setColumnLabels(labels); setColumnMeta(meta); } catch (error) { console.error("컬럼 라벨 가져오기 실패:", error); } }, [tableConfig.selectedTable]); // ======================================== // 테이블 라벨 가져오기 // ======================================== const fetchTableLabel = useCallback(async () => { if (!tableConfig.selectedTable) return; try { const cacheKey = `table_info_${tableConfig.selectedTable}`; const cached = tableInfoCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < TABLE_CACHE_TTL) { const tables = cached.tables || []; const tableInfo = tables.find((t: any) => t.tableName === tableConfig.selectedTable); const label = tableInfo?.displayName || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable; setTableLabel(label); return; } const tables = await tableTypeApi.getTables(); tableInfoCache.set(cacheKey, { tables, timestamp: Date.now(), }); const tableInfo = tables.find((t: any) => t.tableName === tableConfig.selectedTable); const label = tableInfo?.displayName || (tableInfo as any)?.comment || tableInfo?.description || tableConfig.selectedTable; setTableLabel(label); } catch (error) { console.error("테이블 라벨 가져오기 실패:", error); } }, [tableConfig.selectedTable]); // ======================================== // 카테고리 값 매핑 로드 // ======================================== useEffect(() => { const loadCategoryMappings = async () => { if (!tableConfig.selectedTable) return; // 로딩 중에는 매핑 로드하지 않음 (데이터 로드 완료 후에만 실행) if (loading) return; // columnMeta가 비어있으면 대기 const columnMetaKeys = Object.keys(columnMeta || {}); if (columnMetaKeys.length === 0) { console.log("⏳ [TableList] columnMeta 로딩 대기 중..."); return; } try { const categoryColumns = Object.entries(columnMeta) .filter(([_, meta]) => meta.inputType === "category") .map(([columnName, _]) => columnName); if (categoryColumns.length === 0) { console.log("⚠️ [TableList] 카테고리 컬럼 없음:", { table: tableConfig.selectedTable, columnMetaKeys, columnMeta, }); return; } console.log("🔄 [TableList] 카테고리 매핑 로드 시작:", { table: tableConfig.selectedTable, categoryColumns, dataLength: data.length, loading, }); const mappings: Record> = {}; for (const columnName of categoryColumns) { try { const apiClient = (await import("@/lib/api/client")).apiClient; const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); if (response.data.success && response.data.data) { const mapping: Record = {}; response.data.data.forEach((item: any) => { mapping[item.valueCode] = { label: item.valueLabel, color: item.color, }; }); mappings[columnName] = mapping; console.log(`✅ [TableList] 카테고리 매핑 로드 [${columnName}]:`, mapping); } } catch (error) { console.error(`❌ [TableList] 카테고리 값 로드 실패 [${columnName}]:`, error); } } console.log("📊 [TableList] 전체 카테고리 매핑:", mappings); setCategoryMappings(mappings); setCategoryMappingsKey((prev) => prev + 1); // 리렌더링 트리거 } catch (error) { console.error("TableListComponent 카테고리 매핑 로드 실패:", error); } }; loadCategoryMappings(); }, [tableConfig.selectedTable, columnMeta, loading]); // loading이 false가 될 때마다 갱신! // ======================================== // 데이터 가져오기 // ======================================== const fetchTableDataInternal = useCallback(async () => { if (!tableConfig.selectedTable || isDesignMode) { setData([]); setTotalPages(0); setTotalItems(0); return; } // 테이블명 확인 로그 (개발 시에만) // console.log("🔍 fetchTableDataInternal - selectedTable:", tableConfig.selectedTable); // console.log("🔍 selectedTable 타입:", typeof tableConfig.selectedTable); // console.log("🔍 전체 tableConfig:", tableConfig); setLoading(true); setError(null); try { const page = tableConfig.pagination?.currentPage || currentPage; const pageSize = localPageSize; const sortBy = sortColumn || undefined; const sortOrder = sortDirection; const search = searchTerm || undefined; const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; const entityJoinColumns = (tableConfig.columns || []) .filter((col) => col.additionalJoinInfo) .map((col) => ({ sourceTable: col.additionalJoinInfo!.sourceTable, sourceColumn: col.additionalJoinInfo!.sourceColumn, joinAlias: col.additionalJoinInfo!.joinAlias, referenceTable: col.additionalJoinInfo!.referenceTable, })); const hasEntityJoins = entityJoinColumns.length > 0; let response; if (hasEntityJoins) { response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page, size: pageSize, sortBy, sortOrder, search: filters, enableEntityJoin: true, additionalJoinColumns: entityJoinColumns, }); } else { response = await tableTypeApi.getTableData(tableConfig.selectedTable, { page, size: pageSize, sortBy, sortOrder, search: filters, }); } setData(response.data || []); setTotalPages(response.totalPages || 0); setTotalItems(response.total || 0); setError(null); } catch (err: any) { console.error("데이터 가져오기 실패:", err); setData([]); setTotalPages(0); setTotalItems(0); setError(err.message || "데이터를 불러오지 못했습니다."); } finally { setLoading(false); } }, [ tableConfig.selectedTable, tableConfig.pagination?.currentPage, tableConfig.columns, currentPage, localPageSize, sortColumn, sortDirection, searchTerm, searchValues, isDesignMode, ]); const fetchTableDataDebounced = useCallback( (...args: Parameters) => { const key = `fetchData_${tableConfig.selectedTable}_${currentPage}_${sortColumn}_${sortDirection}`; return debouncedApiCall(key, fetchTableDataInternal, 300)(...args); }, [fetchTableDataInternal, tableConfig.selectedTable, currentPage, sortColumn, sortDirection], ); // ======================================== // 이벤트 핸들러 // ======================================== const handlePageChange = (newPage: number) => { if (newPage < 1 || newPage > totalPages) return; setCurrentPage(newPage); if (tableConfig.pagination) { tableConfig.pagination.currentPage = newPage; } if (onConfigChange) { onConfigChange({ ...tableConfig, pagination: { ...tableConfig.pagination, currentPage: newPage } }); } }; const handleSort = (column: string) => { console.log("🔄 정렬 클릭:", { column, currentSortColumn: sortColumn, currentSortDirection: sortDirection }); let newSortColumn = column; let newSortDirection: "asc" | "desc" = "asc"; if (sortColumn === column) { newSortDirection = sortDirection === "asc" ? "desc" : "asc"; setSortDirection(newSortDirection); } else { setSortColumn(column); setSortDirection("asc"); newSortColumn = column; newSortDirection = "asc"; } console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection }); console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange); // 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달 if (onSelectedRowsChange) { const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index))); // 1단계: 데이터를 정렬 const sortedData = [...data].sort((a, b) => { const aVal = a[newSortColumn]; const bVal = b[newSortColumn]; // null/undefined 처리 if (aVal == null && bVal == null) return 0; if (aVal == null) return 1; if (bVal == null) return -1; // 숫자 비교 (문자열이어도 숫자로 변환 가능하면 숫자로 비교) const aNum = Number(aVal); const bNum = Number(bVal); // 둘 다 유효한 숫자이고, 원본 값이 빈 문자열이 아닌 경우 if (!isNaN(aNum) && !isNaN(bNum) && aVal !== "" && bVal !== "") { return newSortDirection === "desc" ? bNum - aNum : aNum - bNum; } // 문자열 비교 (대소문자 구분 없이, 숫자 포함 문자열도 자연스럽게 정렬) const aStr = String(aVal).toLowerCase(); const bStr = String(bVal).toLowerCase(); // 자연스러운 정렬 (숫자 포함 문자열) const comparison = aStr.localeCompare(bStr, undefined, { numeric: true, sensitivity: "base" }); return newSortDirection === "desc" ? -comparison : comparison; }); // 2단계: 정렬된 데이터를 컬럼 순서대로 재정렬 const reorderedData = sortedData.map((row: any) => { const reordered: any = {}; visibleColumns.forEach((col) => { if (col.columnName in row) { reordered[col.columnName] = row[col.columnName]; } }); // 나머지 컬럼 추가 Object.keys(row).forEach((key) => { if (!(key in reordered)) { reordered[key] = row[key]; } }); return reordered; }); console.log("✅ 정렬 정보 전달:", { selectedRowsCount: selectedRows.size, selectedRowsDataCount: selectedRowsData.length, sortBy: newSortColumn, sortOrder: newSortDirection, columnOrder: columnOrder.length > 0 ? columnOrder : undefined, tableDisplayDataCount: reorderedData.length, firstRowAfterSort: reorderedData[0]?.[newSortColumn], lastRowAfterSort: reorderedData[reorderedData.length - 1]?.[newSortColumn], }); onSelectedRowsChange( Array.from(selectedRows), selectedRowsData, newSortColumn, newSortDirection, columnOrder.length > 0 ? columnOrder : undefined, reorderedData, ); // 전역 저장소에 정렬된 데이터 저장 if (tableConfig.selectedTable) { const cleanColumnOrder = ( columnOrder.length > 0 ? columnOrder : visibleColumns.map((c) => c.columnName) ).filter((col) => col !== "__checkbox__"); tableDisplayStore.setTableData( tableConfig.selectedTable, reorderedData, cleanColumnOrder, newSortColumn, newSortDirection, ); } } else { console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!"); } }; const handleSearchValueChange = (columnName: string, value: any) => { setSearchValues((prev) => ({ ...prev, [columnName]: value })); }; const handleAdvancedSearch = () => { setCurrentPage(1); fetchTableDataDebounced(); }; const handleClearAdvancedFilters = useCallback(() => { console.log("🔄 필터 초기화 시작", { 이전searchValues: searchValues }); // 상태를 초기화하고 useEffect로 데이터 새로고침 setSearchValues({}); setCurrentPage(1); // 강제로 데이터 새로고침 트리거 setRefreshTrigger((prev) => prev + 1); }, [searchValues]); const handleRefresh = () => { fetchTableDataDebounced(); }; const getRowKey = (row: any, index: number) => { return row.id || row.uuid || `row-${index}`; }; const handleRowSelection = (rowKey: string, checked: boolean) => { const newSelectedRows = new Set(selectedRows); if (checked) { newSelectedRows.add(rowKey); } else { newSelectedRows.delete(rowKey); } setSelectedRows(newSelectedRows); const selectedRowsData = data.filter((row, index) => newSelectedRows.has(getRowKey(row, index))); if (onSelectedRowsChange) { onSelectedRowsChange(Array.from(newSelectedRows), selectedRowsData, sortColumn || undefined, sortDirection); } if (onFormDataChange) { onFormDataChange({ selectedRows: Array.from(newSelectedRows), selectedRowsData, }); } const allRowsSelected = data.every((row, index) => newSelectedRows.has(getRowKey(row, index))); setIsAllSelected(allRowsSelected && data.length > 0); }; const handleSelectAll = (checked: boolean) => { if (checked) { const allKeys = data.map((row, index) => getRowKey(row, index)); const newSelectedRows = new Set(allKeys); setSelectedRows(newSelectedRows); setIsAllSelected(true); if (onSelectedRowsChange) { onSelectedRowsChange(Array.from(newSelectedRows), data, sortColumn || undefined, sortDirection); } if (onFormDataChange) { onFormDataChange({ selectedRows: Array.from(newSelectedRows), selectedRowsData: data, }); } } else { setSelectedRows(new Set()); setIsAllSelected(false); if (onSelectedRowsChange) { onSelectedRowsChange([], [], sortColumn || undefined, sortDirection); } if (onFormDataChange) { onFormDataChange({ selectedRows: [], selectedRowsData: [] }); } } }; const handleRowClick = (row: any, index: number, e: React.MouseEvent) => { // 체크박스 클릭은 무시 (이미 handleRowSelection에서 처리됨) const target = e.target as HTMLElement; if (target.closest('input[type="checkbox"]')) { return; } // 행 선택/해제 토글 const rowKey = getRowKey(row, index); const isCurrentlySelected = selectedRows.has(rowKey); handleRowSelection(rowKey, !isCurrentlySelected); console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected }); }; // 컬럼 드래그앤드롭 기능 제거됨 (테이블 옵션 모달에서 컬럼 순서 변경 가능) const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); onClick?.(); }; // ======================================== // 컬럼 관련 // ======================================== const visibleColumns = useMemo(() => { let cols = (tableConfig.columns || []).filter((col) => col.visible !== false); if (tableConfig.checkbox?.enabled) { const checkboxCol: ColumnConfig = { columnName: "__checkbox__", displayName: "", visible: true, sortable: false, searchable: false, width: 50, align: "center", order: -1, }; if (tableConfig.checkbox.position === "right") { cols = [...cols, checkboxCol]; } else { cols = [checkboxCol, ...cols]; } } // columnOrder 상태가 있으면 그 순서대로 정렬 if (columnOrder.length > 0) { const orderedCols = columnOrder .map((colName) => cols.find((c) => c.columnName === colName)) .filter(Boolean) as ColumnConfig[]; // columnOrder에 없는 새로운 컬럼들 추가 const remainingCols = cols.filter((c) => !columnOrder.includes(c.columnName)); console.log("🔄 columnOrder 기반 정렬:", { columnOrder, orderedColsCount: orderedCols.length, remainingColsCount: remainingCols.length, }); return [...orderedCols, ...remainingCols]; } return cols.sort((a, b) => (a.order || 0) - (b.order || 0)); }, [tableConfig.columns, tableConfig.checkbox, columnOrder]); // 🆕 visibleColumns가 변경될 때마다 현재 컬럼 순서를 부모에게 전달 const lastColumnOrderRef = useRef(""); useEffect(() => { console.log("🔍 [컬럼 순서 전달 useEffect] 실행됨:", { hasCallback: !!onSelectedRowsChange, visibleColumnsLength: visibleColumns.length, visibleColumnsNames: visibleColumns.map((c) => c.columnName), }); if (!onSelectedRowsChange) { console.warn("⚠️ onSelectedRowsChange 콜백이 없습니다!"); return; } if (visibleColumns.length === 0) { console.warn("⚠️ visibleColumns가 비어있습니다!"); return; } const currentColumnOrder = visibleColumns.map((col) => col.columnName).filter((name) => name !== "__checkbox__"); // 체크박스 컬럼 제외 console.log("🔍 [컬럼 순서] 체크박스 제외 후:", currentColumnOrder); // 컬럼 순서가 실제로 변경되었을 때만 전달 (무한 루프 방지) const columnOrderString = currentColumnOrder.join(","); console.log("🔍 [컬럼 순서] 비교:", { current: columnOrderString, last: lastColumnOrderRef.current, isDifferent: columnOrderString !== lastColumnOrderRef.current, }); if (columnOrderString === lastColumnOrderRef.current) { console.log("⏭️ 컬럼 순서 변경 없음, 전달 스킵"); return; } lastColumnOrderRef.current = columnOrderString; console.log("📊 현재 화면 컬럼 순서 전달:", currentColumnOrder); // 선택된 행 데이터 가져오기 const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index))); // 화면에 표시된 데이터를 컬럼 순서대로 재정렬 const reorderedData = data.map((row: any) => { const reordered: any = {}; visibleColumns.forEach((col) => { if (col.columnName in row) { reordered[col.columnName] = row[col.columnName]; } }); // 나머지 컬럼 추가 Object.keys(row).forEach((key) => { if (!(key in reordered)) { reordered[key] = row[key]; } }); return reordered; }); onSelectedRowsChange( Array.from(selectedRows), selectedRowsData, sortColumn, sortDirection, currentColumnOrder, reorderedData, ); }, [visibleColumns.length, visibleColumns.map((c) => c.columnName).join(",")]); // 의존성 단순화 const getColumnWidth = (column: ColumnConfig) => { if (column.columnName === "__checkbox__") return 50; if (column.width) return column.width; switch (column.format) { case "date": return 120; case "number": case "currency": return 100; case "boolean": return 80; default: return 150; } }; const renderCheckboxHeader = () => { if (!tableConfig.checkbox?.selectAll) return null; return ; }; const renderCheckboxCell = (row: any, index: number) => { const rowKey = getRowKey(row, index); const isChecked = selectedRows.has(rowKey); return ( handleRowSelection(rowKey, checked as boolean)} aria-label={`행 ${index + 1} 선택`} /> ); }; const formatCellValue = useCallback( (value: any, column: ColumnConfig, rowData?: Record) => { if (value === null || value === undefined) return "-"; // 🎯 엔티티 컬럼 표시 설정이 있는 경우 if (column.entityDisplayConfig && rowData) { // displayColumns 또는 selectedColumns 둘 다 체크 const displayColumns = column.entityDisplayConfig.displayColumns || column.entityDisplayConfig.selectedColumns; const separator = column.entityDisplayConfig.separator; if (displayColumns && displayColumns.length > 0) { // 선택된 컬럼들의 값을 구분자로 조합 const values = displayColumns .map((colName) => { const cellValue = rowData[colName]; if (cellValue === null || cellValue === undefined) return ""; return String(cellValue); }) .filter((v) => v !== ""); // 빈 값 제외 return values.join(separator || " - "); } } const meta = columnMeta[column.columnName]; // inputType 기반 포맷팅 (columnMeta에서 가져온 inputType 우선) const inputType = meta?.inputType || column.inputType; // 🖼️ 이미지 타입: 작은 썸네일 표시 if (inputType === "image" && value && typeof value === "string") { const imageUrl = getFullImageUrl(value); return ( 이미지 { e.currentTarget.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Crect width='40' height='40' fill='%23f3f4f6'/%3E%3C/svg%3E"; }} /> ); } // 카테고리 타입: 배지로 표시 if (inputType === "category") { if (!value) return ""; const mapping = categoryMappings[column.columnName]; const categoryData = mapping?.[String(value)]; console.log(`🎨 [카테고리 배지] ${column.columnName}:`, { value, stringValue: String(value), mapping, categoryData, hasMapping: !!mapping, hasCategoryData: !!categoryData, }); // 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값과 기본색상 const displayLabel = categoryData?.label || String(value); const displayColor = categoryData?.color || "#64748b"; // 기본 slate 색상 const { Badge } = require("@/components/ui/badge"); return ( {displayLabel} ); } // 코드 타입: 코드 값 → 코드명 변환 if (inputType === "code" && meta?.codeCategory && value) { try { // optimizedConvertCode(categoryCode, codeValue) 순서 주의! const convertedValue = optimizedConvertCode(meta.codeCategory, value); // 변환에 성공했으면 변환된 코드명 반환 if (convertedValue && convertedValue !== value) { return convertedValue; } } catch (error) { console.error(`코드 변환 실패: ${column.columnName}, 카테고리: ${meta.codeCategory}, 값: ${value}`, error); } // 변환 실패 시 원본 코드 값 반환 return String(value); } // 숫자 타입 포맷팅 if (inputType === "number" || inputType === "decimal") { if (value !== null && value !== undefined && value !== "") { const numValue = typeof value === "string" ? parseFloat(value) : value; if (!isNaN(numValue)) { return numValue.toLocaleString("ko-KR"); } } return String(value); } switch (column.format) { case "number": if (value !== null && value !== undefined && value !== "") { const numValue = typeof value === "string" ? parseFloat(value) : value; if (!isNaN(numValue)) { return numValue.toLocaleString("ko-KR"); } } return String(value); case "date": if (value) { try { const date = new Date(value); return date.toLocaleDateString("ko-KR"); } catch { return value; } } return "-"; case "number": return typeof value === "number" ? value.toLocaleString() : value; case "currency": return typeof value === "number" ? `₩${value.toLocaleString()}` : value; case "boolean": return value ? "예" : "아니오"; default: return String(value); } }, [columnMeta, optimizedConvertCode], ); // ======================================== // useEffect 훅 // ======================================== // 필터 설정 localStorage 키 생성 const filterSettingKey = useMemo(() => { if (!tableConfig.selectedTable) return null; return `tableList_filterSettings_${tableConfig.selectedTable}`; }, [tableConfig.selectedTable]); // 그룹 설정 localStorage 키 생성 const groupSettingKey = useMemo(() => { if (!tableConfig.selectedTable) return null; return `tableList_groupSettings_${tableConfig.selectedTable}`; }, [tableConfig.selectedTable]); // 저장된 필터 설정 불러오기 useEffect(() => { if (!filterSettingKey || visibleColumns.length === 0) return; try { const saved = localStorage.getItem(filterSettingKey); if (saved) { const savedFilters = JSON.parse(saved); setVisibleFilterColumns(new Set(savedFilters)); } else { // 초기값: 빈 Set (아무것도 선택 안 함) setVisibleFilterColumns(new Set()); } } catch (error) { console.error("필터 설정 불러오기 실패:", error); setVisibleFilterColumns(new Set()); } }, [filterSettingKey, visibleColumns]); // 필터 설정 저장 const saveFilterSettings = useCallback(() => { if (!filterSettingKey) return; try { localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(visibleFilterColumns))); setIsFilterSettingOpen(false); toast.success("검색 필터 설정이 저장되었습니다"); // 검색 값 초기화 setSearchValues({}); } catch (error) { console.error("필터 설정 저장 실패:", error); toast.error("설정 저장에 실패했습니다"); } }, [filterSettingKey, visibleFilterColumns]); // 필터 컬럼 토글 const toggleFilterVisibility = useCallback((columnName: string) => { setVisibleFilterColumns((prev) => { const newSet = new Set(prev); if (newSet.has(columnName)) { newSet.delete(columnName); } else { newSet.add(columnName); } return newSet; }); }, []); // 전체 선택/해제 const toggleAllFilters = useCallback(() => { const filterableColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__"); const columnNames = filterableColumns.map((col) => col.columnName); if (visibleFilterColumns.size === columnNames.length) { // 전체 해제 setVisibleFilterColumns(new Set()); } else { // 전체 선택 setVisibleFilterColumns(new Set(columnNames)); } }, [visibleFilterColumns, visibleColumns]); // 표시할 필터 목록 (선택된 컬럼만) const activeFilters = useMemo(() => { return visibleColumns .filter((col) => col.columnName !== "__checkbox__" && visibleFilterColumns.has(col.columnName)) .map((col) => ({ columnName: col.columnName, label: columnLabels[col.columnName] || col.displayName || col.columnName, type: col.format || "text", })); }, [visibleColumns, visibleFilterColumns, columnLabels]); // 그룹 설정 저장 const saveGroupSettings = useCallback(() => { if (!groupSettingKey) return; try { localStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns)); setIsGroupSettingOpen(false); toast.success("그룹 설정이 저장되었습니다"); } catch (error) { console.error("그룹 설정 저장 실패:", error); toast.error("설정 저장에 실패했습니다"); } }, [groupSettingKey, groupByColumns]); // 그룹 컬럼 토글 const toggleGroupColumn = useCallback((columnName: string) => { setGroupByColumns((prev) => { if (prev.includes(columnName)) { return prev.filter((col) => col !== columnName); } else { return [...prev, columnName]; } }); }, []); // 사용자 옵션 저장 핸들러 const handleTableOptionsSave = useCallback( (config: { columns: Array<{ columnName: string; label: string; visible: boolean; width?: number; frozen?: boolean }>; showGridLines: boolean; viewMode: "table" | "card" | "grouped-card"; }) => { // 컬럼 순서 업데이트 const newColumnOrder = config.columns.map((col) => col.columnName); setColumnOrder(newColumnOrder); // 컬럼 너비 업데이트 const newWidths: Record = {}; config.columns.forEach((col) => { if (col.width) { newWidths[col.columnName] = col.width; } }); setColumnWidths(newWidths); // 틀고정 컬럼 업데이트 const newFrozenColumns = config.columns.filter((col) => col.frozen).map((col) => col.columnName); setFrozenColumns(newFrozenColumns); // 그리드선 표시 업데이트 setShowGridLines(config.showGridLines); // 보기 모드 업데이트 setViewMode(config.viewMode); // 컬럼 표시/숨기기 업데이트 const newDisplayColumns = displayColumns.map((col) => { const configCol = config.columns.find((c) => c.columnName === col.columnName); if (configCol) { return { ...col, visible: configCol.visible }; } return col; }); setDisplayColumns(newDisplayColumns); toast.success("테이블 옵션이 저장되었습니다"); }, [displayColumns], ); // 그룹 펼치기/접기 토글 const toggleGroupCollapse = useCallback((groupKey: string) => { setCollapsedGroups((prev) => { const newSet = new Set(prev); if (newSet.has(groupKey)) { newSet.delete(groupKey); } else { newSet.add(groupKey); } return newSet; }); }, []); // 그룹 해제 const clearGrouping = useCallback(() => { setGroupByColumns([]); setCollapsedGroups(new Set()); if (groupSettingKey) { localStorage.removeItem(groupSettingKey); } toast.success("그룹이 해제되었습니다"); }, [groupSettingKey]); // 데이터 그룹화 const groupedData = useMemo((): GroupedData[] => { if (groupByColumns.length === 0 || data.length === 0) return []; const grouped = new Map(); data.forEach((item) => { // 그룹 키 생성: "통화:KRW > 단위:EA" const keyParts = groupByColumns.map((col) => { const value = item[col]; const label = columnLabels[col] || col; return `${label}:${value !== null && value !== undefined ? value : "-"}`; }); const groupKey = keyParts.join(" > "); if (!grouped.has(groupKey)) { grouped.set(groupKey, []); } grouped.get(groupKey)!.push(item); }); return Array.from(grouped.entries()).map(([groupKey, items]) => { const groupValues: Record = {}; groupByColumns.forEach((col) => { groupValues[col] = items[0]?.[col]; }); return { groupKey, groupValues, items, count: items.length, }; }); }, [data, groupByColumns, columnLabels]); // 저장된 그룹 설정 불러오기 useEffect(() => { if (!groupSettingKey || visibleColumns.length === 0) return; try { const saved = localStorage.getItem(groupSettingKey); if (saved) { const savedGroups = JSON.parse(saved); setGroupByColumns(savedGroups); } } catch (error) { console.error("그룹 설정 불러오기 실패:", error); } }, [groupSettingKey, visibleColumns]); useEffect(() => { fetchColumnLabels(); fetchTableLabel(); }, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]); useEffect(() => { if (!isDesignMode && tableConfig.selectedTable) { fetchTableDataDebounced(); } }, [ tableConfig.selectedTable, currentPage, localPageSize, sortColumn, sortDirection, searchTerm, searchValues, // 필터 값 변경 시에도 데이터 새로고침 refreshKey, refreshTrigger, // 강제 새로고침 트리거 isDesignMode, fetchTableDataDebounced, ]); useEffect(() => { if (tableConfig.refreshInterval && !isDesignMode) { const interval = setInterval(() => { fetchTableDataDebounced(); }, tableConfig.refreshInterval * 1000); return () => clearInterval(interval); } }, [tableConfig.refreshInterval, isDesignMode]); // 초기 컬럼 너비 측정 (한 번만) useEffect(() => { if (!hasInitializedWidths.current && visibleColumns.length > 0) { // 약간의 지연을 두고 DOM이 완전히 렌더링된 후 측정 const timer = setTimeout(() => { const newWidths: Record = {}; let hasAnyWidth = false; visibleColumns.forEach((column) => { // 체크박스 컬럼은 제외 (고정 48px) if (column.columnName === "__checkbox__") return; const thElement = columnRefs.current[column.columnName]; if (thElement) { const measuredWidth = thElement.offsetWidth; if (measuredWidth > 0) { newWidths[column.columnName] = measuredWidth; hasAnyWidth = true; } } }); if (hasAnyWidth) { setColumnWidths(newWidths); hasInitializedWidths.current = true; } }, 100); return () => clearTimeout(timer); } }, [visibleColumns]); // ======================================== // 페이지네이션 JSX // ======================================== const paginationJSX = useMemo(() => { if (!tableConfig.pagination?.enabled || isDesignMode) return null; return (
{/* 중앙 페이지네이션 컨트롤 */}
{currentPage} / {totalPages || 1} 전체 {totalItems.toLocaleString()}개
{/* 우측 새로고침 버튼 */}
); }, [tableConfig.pagination, isDesignMode, currentPage, totalPages, totalItems, loading]); // ======================================== // 렌더링 // ======================================== const domProps = { onClick: handleClick, onDragStart: isDesignMode ? onDragStart : undefined, onDragEnd: isDesignMode ? onDragEnd : undefined, draggable: isDesignMode, className: cn(className, isDesignMode && "cursor-move"), style: componentStyle, }; // 카드 모드 if (tableConfig.displayMode === "card" && !isDesignMode) { return (
{paginationJSX}
); } // SingleTableWithSticky 모드 if (tableConfig.stickyHeader && !isDesignMode) { return (
{tableConfig.filter?.enabled && (
)} {/* 그룹 표시 배지 */} {groupByColumns.length > 0 && (
그룹:
{groupByColumns.map((col, idx) => ( {idx > 0 && } {columnLabels[col] || col} ))}
)}
) => { const column = visibleColumns.find((c) => c.columnName === columnName); return column ? formatCellValue(value, column, rowData) : String(value); }} getColumnWidth={getColumnWidth} containerWidth={calculatedWidth} />
{paginationJSX}
); } // 일반 테이블 모드 (네이티브 HTML 테이블) return ( <>
{/* 필터 */} {tableConfig.filter?.enabled && (
)} {/* 그룹 표시 배지 */} {groupByColumns.length > 0 && (
그룹:
{groupByColumns.map((col, idx) => ( {idx > 0 && } {columnLabels[col] || col} ))}
)} {/* 테이블 컨테이너 */}
{/* 스크롤 영역 */}
{/* 테이블 */} {/* 헤더 (sticky) */} {visibleColumns.map((column, columnIndex) => { const columnWidth = columnWidths[column.columnName]; const isFrozen = frozenColumns.includes(column.columnName); const frozenIndex = frozenColumns.indexOf(column.columnName); // 틀고정된 컬럼의 left 위치 계산 let leftPosition = 0; if (isFrozen && frozenIndex > 0) { for (let i = 0; i < frozenIndex; i++) { const frozenCol = frozenColumns[i]; const frozenColWidth = columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } return ( ); })} {/* 바디 (스크롤) */} {loading ? ( ) : error ? ( ) : data.length === 0 ? ( ) : groupByColumns.length > 0 && groupedData.length > 0 ? ( // 그룹화된 렌더링 groupedData.map((group) => { const isCollapsed = collapsedGroups.has(group.groupKey); return ( {/* 그룹 헤더 */} {/* 그룹 데이터 */} {!isCollapsed && group.items.map((row, index) => ( handleRowClick(row, index, e)} > {visibleColumns.map((column, colIndex) => { const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; const cellValue = row[mappedColumnName]; const meta = columnMeta[column.columnName]; const inputType = meta?.inputType || column.inputType; const isNumeric = inputType === "number" || inputType === "decimal"; const isFrozen = frozenColumns.includes(column.columnName); const frozenIndex = frozenColumns.indexOf(column.columnName); // 틀고정된 컬럼의 left 위치 계산 let leftPosition = 0; if (isFrozen && frozenIndex > 0) { for (let i = 0; i < frozenIndex; i++) { const frozenCol = frozenColumns[i]; const frozenColWidth = columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } return ( ); })} ))} ); }) ) : ( // 일반 렌더링 (그룹 없음) data.map((row, index) => ( handleRowClick(row, index, e)} > {visibleColumns.map((column, colIndex) => { const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; const cellValue = row[mappedColumnName]; const meta = columnMeta[column.columnName]; const inputType = meta?.inputType || column.inputType; const isNumeric = inputType === "number" || inputType === "decimal"; const isFrozen = frozenColumns.includes(column.columnName); const frozenIndex = frozenColumns.indexOf(column.columnName); // 틀고정된 컬럼의 left 위치 계산 let leftPosition = 0; if (isFrozen && frozenIndex > 0) { for (let i = 0; i < frozenIndex; i++) { const frozenCol = frozenColumns[i]; const frozenColWidth = columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } return ( ); })} )) )}
(columnRefs.current[column.columnName] = el)} className={cn( "text-foreground/90 relative h-8 overflow-hidden text-xs font-bold text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-sm", column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2", column.sortable !== false && column.columnName !== "__checkbox__" && "hover:bg-muted/70 cursor-pointer transition-colors", isFrozen && "sticky z-60 shadow-[2px_0_4px_rgba(0,0,0,0.1)]", )} style={{ textAlign: column.columnName === "__checkbox__" ? "center" : "center", width: column.columnName === "__checkbox__" ? "48px" : columnWidth ? `${columnWidth}px` : undefined, minWidth: column.columnName === "__checkbox__" ? "48px" : undefined, maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined, userSelect: "none", backgroundColor: "hsl(var(--muted))", ...(isFrozen && { left: `${leftPosition}px` }), }} onClick={() => { if (isResizing.current) return; if (column.sortable !== false && column.columnName !== "__checkbox__") { handleSort(column.columnName); } }} > {column.columnName === "__checkbox__" ? ( renderCheckboxHeader() ) : (
{columnLabels[column.columnName] || column.displayName} {column.sortable !== false && sortColumn === column.columnName && ( {sortDirection === "asc" ? "↑" : "↓"} )}
)} {/* 리사이즈 핸들 (체크박스 제외) */} {columnIndex < visibleColumns.length - 1 && column.columnName !== "__checkbox__" && (
e.stopPropagation()} // 정렬 클릭 방지 onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); const thElement = columnRefs.current[column.columnName]; if (!thElement) return; isResizing.current = true; const startX = e.clientX; const startWidth = columnWidth || thElement.offsetWidth; // 드래그 중 텍스트 선택 방지 document.body.style.userSelect = "none"; document.body.style.cursor = "col-resize"; const handleMouseMove = (moveEvent: MouseEvent) => { moveEvent.preventDefault(); const diff = moveEvent.clientX - startX; const newWidth = Math.max(80, startWidth + diff); // 직접 DOM 스타일 변경 (리렌더링 없음) if (thElement) { thElement.style.width = `${newWidth}px`; } }; const handleMouseUp = () => { // 최종 너비를 state에 저장 if (thElement) { const finalWidth = Math.max(80, thElement.offsetWidth); setColumnWidths((prev) => ({ ...prev, [column.columnName]: finalWidth })); } // 텍스트 선택 복원 document.body.style.userSelect = ""; document.body.style.cursor = ""; // 약간의 지연 후 리사이즈 플래그 해제 (클릭 이벤트가 먼저 처리되지 않도록) setTimeout(() => { isResizing.current = false; }, 100); document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }} /> )}
로딩 중...
오류 발생
{error}
데이터가 없습니다
조건을 변경하거나 새로운 데이터를 추가해보세요
toggleGroupCollapse(group.groupKey)} > {isCollapsed ? ( ) : ( )} {group.groupKey} ({group.count}건)
{column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) : formatCellValue(cellValue, column, row)}
{column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) : formatCellValue(cellValue, column, row)}
{/* 페이지네이션 */} {paginationJSX}
{/* 필터 설정 다이얼로그 */} 검색 필터 설정 검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다.
{/* 전체 선택/해제 */}
col.columnName !== "__checkbox__").length && visibleColumns.filter((col) => col.columnName !== "__checkbox__").length > 0 } onCheckedChange={toggleAllFilters} /> {visibleFilterColumns.size} / {visibleColumns.filter((col) => col.columnName !== "__checkbox__").length} 개
{/* 컬럼 목록 */}
{visibleColumns .filter((col) => col.columnName !== "__checkbox__") .map((col) => (
toggleFilterVisibility(col.columnName)} />
))}
{/* 선택된 컬럼 개수 안내 */}
{visibleFilterColumns.size === 0 ? ( 검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요 ) : ( {visibleFilterColumns.size}개의 검색 필터가 표시됩니다 )}
{/* 그룹 설정 다이얼로그 */} 그룹 설정 데이터를 그룹화할 컬럼을 선택하세요. 여러 컬럼을 선택하면 계층적으로 그룹화됩니다.
{/* 컬럼 목록 */}
{visibleColumns .filter((col) => col.columnName !== "__checkbox__") .map((col) => (
toggleGroupColumn(col.columnName)} />
))}
{/* 선택된 그룹 안내 */}
{groupByColumns.length === 0 ? ( 그룹화할 컬럼을 선택하세요 ) : ( 선택된 그룹:{" "} {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")} )}
{/* 테이블 옵션 모달 */} setIsTableOptionsOpen(false)} columns={visibleColumns.map((col) => ({ columnName: col.columnName, label: columnLabels[col.columnName] || col.displayName || col.columnName, visible: col.visible !== false, width: columnWidths[col.columnName], frozen: frozenColumns.includes(col.columnName), }))} onSave={handleTableOptionsSave} tableName={tableConfig.selectedTable || "table"} userId={userId} /> ); }; export const TableListWrapper: React.FC = (props) => { return ; };