"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"; // ๐Ÿ†• RelatedDataButtons ์ „์—ญ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํƒ€์ž… ์„ ์–ธ declare global { interface Window { __relatedButtonsTargetTables?: Set; } } import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, RefreshCw, ArrowUp, ArrowDown, TableIcon, Settings, X, Layers, ChevronDown, Filter, Check, Download, FileSpreadsheet, Copy, ClipboardCopy, Edit, CheckSquare, Trash2, Lock, } from "lucide-react"; import * as XLSX from "xlsx"; import { FileText, ChevronRightIcon } 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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 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"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options"; import { useAuth } from "@/hooks/useAuth"; import { useScreenContextOptional } from "@/contexts/ScreenContext"; import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext"; import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer"; // ======================================== // ์ธํ„ฐํŽ˜์ด์Šค // ======================================== // ๊ทธ๋ฃนํ™”๋œ ๋ฐ์ดํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค interface GroupedData { groupKey: string; groupValues: Record; items: any[]; count: number; summary?: Record; // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ์†Œ๊ณ„ } // ======================================== // ์บ์‹œ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ // ======================================== 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); }); }; }; // ======================================== // Filter Builder ์ธํ„ฐํŽ˜์ด์Šค // ======================================== interface FilterCondition { id: string; column: string; operator: | "equals" | "notEquals" | "contains" | "notContains" | "startsWith" | "endsWith" | "greaterThan" | "lessThan" | "greaterOrEqual" | "lessOrEqual" | "isEmpty" | "isNotEmpty"; value: string; } interface FilterGroup { id: string; logic: "AND" | "OR"; conditions: FilterCondition[]; } // ======================================== // 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?: number | string; // ํ™”๋ฉด ID (ํ•„ํ„ฐ ์„ค์ • ์ €์žฅ์šฉ) userId?: string; // ์‚ฌ์šฉ์ž ID (์ปฌ๋Ÿผ ์ˆœ์„œ ์ €์žฅ์šฉ) onSelectedRowsChange?: ( selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[], ) => void; onConfigChange?: (config: any) => void; refreshKey?: number; // ํƒญ ๊ด€๋ จ ์ •๋ณด (ํƒญ ๋‚ด๋ถ€์˜ ํ…Œ์ด๋ธ”์—์„œ ์‚ฌ์šฉ) parentTabId?: string; // ๋ถ€๋ชจ ํƒญ ID parentTabsComponentId?: string; // ๋ถ€๋ชจ ํƒญ ์ปดํฌ๋„ŒํŠธ ID // ๐Ÿ†• ํ”„๋ฆฌ๋ทฐ์šฉ ํšŒ์‚ฌ ์ฝ”๋“œ (DynamicComponentRenderer์—์„œ ์ „๋‹ฌ, ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์˜ค๋ฒ„๋ผ์ด๋“œ ๊ฐ€๋Šฅ) companyCode?: string; } // ======================================== // ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ // ======================================== export const TableListComponent: React.FC = ({ component, isDesignMode = false, isSelected = false, onClick, onDragStart, onDragEnd, config, className, style, formData: propFormData, onFormDataChange, componentConfig, onSelectedRowsChange, onConfigChange, refreshKey, tableName, userId, screenId, parentTabId, parentTabsComponentId, companyCode, }) => { // ======================================== // ์„ค์ • ๋ฐ ์Šคํƒ€์ผ // ======================================== 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 = { position: "relative", display: "flex", flexDirection: "column", backgroundColor: "hsl(var(--background))", overflow: "hidden", boxSizing: "border-box", width: "100%", height: "100%", minHeight: isDesignMode ? "300px" : "100%", ...style, // style prop์ด ์œ„์˜ ๊ธฐ๋ณธ๊ฐ’๋“ค์„ ๋ฎ์–ด์”€ }; // ======================================== // ์ƒํƒœ ๊ด€๋ฆฌ // ======================================== // ์‚ฌ์šฉ์ž ์ •๋ณด (props์—์„œ ๋ฐ›๊ฑฐ๋‚˜ useAuth์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ) const { userId: authUserId } = useAuth(); const currentUserId = userId || authUserId; // ํ™”๋ฉด ์ปจํ…์ŠคํŠธ (๋ฐ์ดํ„ฐ ์ œ๊ณต์ž๋กœ ๋“ฑ๋ก) const screenContext = useScreenContextOptional(); // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ (๋ถ„ํ•  ํŒจ๋„ ๋‚ด๋ถ€์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก) const splitPanelContext = useSplitPanelContext(); // ๐Ÿ†• ScreenContext์—์„œ splitPanelPosition ๊ฐ€์ ธ์˜ค๊ธฐ (์ค‘์ฒฉ ํ™”๋ฉด์—์„œ๋„ ์ž‘๋™) const splitPanelPosition = screenContext?.splitPanelPosition; // ๐Ÿ†• ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ์ƒํƒœ (๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ๊ฐ’์œผ๋กœ ํ•„ํ„ฐ๋ง) const [linkedFilterValues, setLinkedFilterValues] = useState>({}); // ๐Ÿ†• RelatedDataButtons ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ํ•„ํ„ฐ ์ƒํƒœ const [relatedButtonFilter, setRelatedButtonFilter] = useState<{ filterColumn: string; filterValue: any; } | null>(null); // ๐Ÿ†• RelatedDataButtons๊ฐ€ ์ด ํ…Œ์ด๋ธ”์„ ๋Œ€์ƒ์œผ๋กœ ๋“ฑ๋ก๋˜์–ด ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ const [isRelatedButtonTarget, setIsRelatedButtonTarget] = useState(() => { // ์ดˆ๊ธฐ๊ฐ’: ์ „์—ญ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ ํ™•์ธ if (typeof window !== "undefined" && window.__relatedButtonsTargetTables) { return window.__relatedButtonsTargetTables.has(tableConfig.selectedTable || ""); } return false; }); // TableOptions Context const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions(); const [filters, setFilters] = useState([]); const [grouping, setGrouping] = useState([]); const [columnVisibility, setColumnVisibility] = useState([]); // filters๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด searchValues ์—…๋ฐ์ดํŠธ (์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰) useEffect(() => { const newSearchValues: Record = {}; filters.forEach((filter) => { if (filter.value) { newSearchValues[filter.columnName] = filter.value; } }); // console.log("๐Ÿ” [TableListComponent] filters โ†’ searchValues:", { // filters: filters.length, // searchValues: newSearchValues, // }); setSearchValues(newSearchValues); setCurrentPage(1); // ํ•„ํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ฒซ ํŽ˜์ด์ง€๋กœ }, [filters]); // grouping์ด ๋ณ€๊ฒฝ๋˜๋ฉด groupByColumns ์—…๋ฐ์ดํŠธ useEffect(() => { setGroupByColumns(grouping); }, [grouping]); // ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ localStorage์—์„œ ์ €์žฅ๋œ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ useEffect(() => { if (tableConfig.selectedTable && currentUserId) { const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`; const savedSettings = localStorage.getItem(storageKey); if (savedSettings) { try { const parsed = JSON.parse(savedSettings) as ColumnVisibility[]; setColumnVisibility(parsed); } catch (error) { console.error("์ €์žฅ๋œ ์ปฌ๋Ÿผ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ:", error); } } } }, [tableConfig.selectedTable, currentUserId]); // columnVisibility ๋ณ€๊ฒฝ ์‹œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ฐ ๊ฐ€์‹œ์„ฑ ์ ์šฉ useEffect(() => { if (columnVisibility.length > 0) { const newOrder = columnVisibility.map((cv) => cv.columnName).filter((name) => name !== "__checkbox__"); // ์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ setColumnOrder(newOrder); // localStorage์— ์ €์žฅ (์‚ฌ์šฉ์ž๋ณ„) if (tableConfig.selectedTable && currentUserId) { const storageKey = `table_column_visibility_${tableConfig.selectedTable}_${currentUserId}`; localStorage.setItem(storageKey, JSON.stringify(columnVisibility)); } } }, [columnVisibility, tableConfig.selectedTable, currentUserId]); // ๐Ÿ†• columnOrder๋ฅผ visibleColumns ์ด์ „์— ์ •์˜ (visibleColumns์—์„œ ์‚ฌ์šฉ) const [columnOrder, setColumnOrder] = useState([]); // ๐Ÿ†• visibleColumns๋ฅผ ์ƒ๋‹จ์—์„œ ์ •์˜ (๋‹ค๋ฅธ useCallback/useMemo์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด) const visibleColumns = useMemo(() => { let cols = (tableConfig.columns || []).filter((col) => col.visible !== false); // columnVisibility๊ฐ€ ์žˆ์œผ๋ฉด ๊ฐ€์‹œ์„ฑ ์ ์šฉ if (columnVisibility.length > 0) { cols = cols.filter((col) => { const visibilityConfig = columnVisibility.find((cv) => cv.columnName === col.columnName); return visibilityConfig ? visibilityConfig.visible : true; }); } // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ (๋‚˜์ค‘์— ์œ„์น˜ ๊ฒฐ์ •) // ๊ธฐ๋ณธ๊ฐ’: enabled๊ฐ€ undefined๋ฉด true๋กœ ์ฒ˜๋ฆฌ let checkboxCol: ColumnConfig | null = null; if (tableConfig.checkbox?.enabled ?? true) { checkboxCol = { columnName: "__checkbox__", displayName: "", visible: true, sortable: false, searchable: false, width: 40, align: "center" as const, order: -1, editable: false, // ์ฒดํฌ๋ฐ•์Šค๋Š” ํŽธ์ง‘ ๋ถˆ๊ฐ€ }; } // columnOrder๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ์ˆœ์„œ๋กœ ์ •๋ ฌ if (columnOrder.length > 0) { const orderMap = new Map(columnOrder.map((name, idx) => [name, idx])); cols = [...cols].sort((a, b) => { const aIdx = orderMap.get(a.columnName) ?? 9999; const bIdx = orderMap.get(b.columnName) ?? 9999; return aIdx - bIdx; }); } // ์ฒดํฌ๋ฐ•์Šค ์œ„์น˜ ๊ฒฐ์ • if (checkboxCol) { const checkboxPosition = tableConfig.checkbox?.position || "left"; if (checkboxPosition === "left") { return [checkboxCol, ...cols]; } else { return [...cols, checkboxCol]; } } return cols; }, [tableConfig.columns, tableConfig.checkbox, columnVisibility, columnOrder]); const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // ๐Ÿ†• ์ปฌ๋Ÿผ ํ—ค๋” ํ•„ํ„ฐ ์ƒํƒœ (์ƒ๋‹จ์—์„œ ์„ ์–ธ) const [headerFilters, setHeaderFilters] = useState>>({}); const [openFilterColumn, setOpenFilterColumn] = useState(null); // ๐Ÿ†• Filter Builder (๊ณ ๊ธ‰ ํ•„ํ„ฐ) ๊ด€๋ จ ์ƒํƒœ - filteredData๋ณด๋‹ค ๋จผ์ € ์ •์˜ํ•ด์•ผ ํ•จ const [filterGroups, setFilterGroups] = useState([]); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ์šฐ์ธก์— ์ด๋ฏธ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ํ•„ํ„ฐ๋ง (์ขŒ์ธก ํ…Œ์ด๋ธ”์—๋งŒ ์ ์šฉ) + ํ—ค๋” ํ•„ํ„ฐ const filteredData = useMemo(() => { let result = data; // 1. ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก์— ์žˆ๊ณ , ์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ํ•„ํ„ฐ๋ง if (splitPanelPosition === "left" && splitPanelContext?.addedItemIds && splitPanelContext.addedItemIds.size > 0) { const addedIds = splitPanelContext.addedItemIds; result = result.filter((row) => { const rowId = String(row.id || row.po_item_id || row.item_id || ""); return !addedIds.has(rowId); }); } // 2. ํ—ค๋” ํ•„ํ„ฐ ์ ์šฉ (joinColumnMapping ์‚ฌ์šฉ ์•ˆ ํ•จ - ์ง์ ‘ ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ) if (Object.keys(headerFilters).length > 0) { result = result.filter((row) => { return Object.entries(headerFilters).every(([columnName, values]) => { if (values.size === 0) return true; // ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ๋ช… ์‹œ๋„ const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : ""; return values.has(cellStr); }); }); } // 3. ๐Ÿ†• Filter Builder ์ ์šฉ if (filterGroups.length > 0) { result = result.filter((row) => { return filterGroups.every((group) => { const validConditions = group.conditions.filter( (c) => c.column && (c.operator === "isEmpty" || c.operator === "isNotEmpty" || c.value), ); if (validConditions.length === 0) return true; const evaluateCondition = (value: any, condition: (typeof group.conditions)[0]): boolean => { const strValue = value !== null && value !== undefined ? String(value).toLowerCase() : ""; const condValue = condition.value.toLowerCase(); switch (condition.operator) { case "equals": return strValue === condValue; case "notEquals": return strValue !== condValue; case "contains": return strValue.includes(condValue); case "notContains": return !strValue.includes(condValue); case "startsWith": return strValue.startsWith(condValue); case "endsWith": return strValue.endsWith(condValue); case "greaterThan": return parseFloat(strValue) > parseFloat(condValue); case "lessThan": return parseFloat(strValue) < parseFloat(condValue); case "greaterOrEqual": return parseFloat(strValue) >= parseFloat(condValue); case "lessOrEqual": return parseFloat(strValue) <= parseFloat(condValue); case "isEmpty": return strValue === "" || value === null || value === undefined; case "isNotEmpty": return strValue !== "" && value !== null && value !== undefined; default: return true; } }; if (group.logic === "AND") { return validConditions.every((cond) => evaluateCondition(row[cond.column], cond)); } else { return validConditions.some((cond) => evaluateCondition(row[cond.column], cond)); } }); }); } return result; }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]); 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 hasInitializedSort = useRef(false); 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 >({}); // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช… โ†’ inputType) const [joinedColumnMeta, setJoinedColumnMeta] = 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); // columnOrder๋Š” ์ƒ๋‹จ์—์„œ ์ •์˜๋จ (visibleColumns๋ณด๋‹ค ๋จผ์ € ํ•„์š”) 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 [focusedCell, setFocusedCell] = useState<{ rowIndex: number; colIndex: number } | null>(null); const tableContainerRef = useRef(null); // ๐Ÿ†• ์ธ๋ผ์ธ ์…€ ํŽธ์ง‘ ๊ด€๋ จ ์ƒํƒœ const [editingCell, setEditingCell] = useState<{ rowIndex: number; colIndex: number; columnName: string; originalValue: any; } | null>(null); const [editingValue, setEditingValue] = useState(""); const editInputRef = useRef(null); // ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘ ๊ด€๋ จ ์ƒํƒœ const [editMode, setEditMode] = useState<"immediate" | "batch">("immediate"); // ํŽธ์ง‘ ๋ชจ๋“œ const [pendingChanges, setPendingChanges] = useState< Map< string, { rowIndex: number; columnName: string; originalValue: any; newValue: any; primaryKeyValue: any; } > >(new Map()); // key: `${rowIndex}-${columnName}` const [localEditedData, setLocalEditedData] = useState>>({}); // ๋กœ์ปฌ ์ˆ˜์ • ๋ฐ์ดํ„ฐ // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ด€๋ จ ์ƒํƒœ const [validationErrors, setValidationErrors] = useState>(new Map()); // key: `${rowIndex}-${columnName}` // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ทœ์น™ ํƒ€์ž… type ValidationRule = { required?: boolean; min?: number; max?: number; minLength?: number; maxLength?: number; pattern?: RegExp; customMessage?: string; validate?: (value: any, row: any) => string | null; // ์ปค์Šคํ…€ ๊ฒ€์ฆ ํ•จ์ˆ˜ (์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋˜๋Š” null) }; // ๐Ÿ†• Cascading Lookups ๊ด€๋ จ ์ƒํƒœ const [cascadingOptions, setCascadingOptions] = useState>({}); const [loadingCascading, setLoadingCascading] = useState>({}); // ๐Ÿ†• Multi-Level Headers (Column Bands) ํƒ€์ž… type ColumnBand = { caption: string; columns: string[]; // ํฌํ•จ๋  ์ปฌ๋Ÿผ๋ช… ๋ฐฐ์—ด }; // ๊ทธ๋ฃน ์„ค์ • ๊ด€๋ จ ์ƒํƒœ const [groupByColumns, setGroupByColumns] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • ์ƒํƒœ const [groupSumConfig, setGroupSumConfig] = useState(null); // ๐Ÿ†• Master-Detail ๊ด€๋ จ ์ƒํƒœ const [expandedRows, setExpandedRows] = useState>(new Set()); // ํ™•์žฅ๋œ ํ–‰ ํ‚ค ๋ชฉ๋ก const [detailData, setDetailData] = useState>({}); // ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์บ์‹œ // ๐Ÿ†• Drag & Drop ์žฌ์ •๋ ฌ ๊ด€๋ จ ์ƒํƒœ const [draggedRowIndex, setDraggedRowIndex] = useState(null); const [dropTargetIndex, setDropTargetIndex] = useState(null); const [isDragEnabled, setIsDragEnabled] = useState((tableConfig as any).enableRowDrag ?? false); // ๐Ÿ†• Virtual Scrolling ๊ด€๋ จ ์ƒํƒœ const [isVirtualScrollEnabled] = useState((tableConfig as any).virtualScroll ?? false); const [scrollTop, setScrollTop] = useState(0); const ROW_HEIGHT = 40; // ๊ฐ ํ–‰์˜ ๋†’์ด (ํ”ฝ์…€) const OVERSCAN = 5; // ๋ฒ„ํผ๋กœ ์ถ”๊ฐ€ ๋ Œ๋”๋งํ•  ํ–‰ ์ˆ˜ const scrollContainerRef = useRef(null); // ๐Ÿ†• Column Reordering ๊ด€๋ จ ์ƒํƒœ const [draggedColumnIndex, setDraggedColumnIndex] = useState(null); const [dropTargetColumnIndex, setDropTargetColumnIndex] = useState(null); const [isColumnDragEnabled] = useState((tableConfig as any).enableColumnDrag ?? true); // ๐Ÿ†• State Persistence: ํ†ตํ•ฉ ์ƒํƒœ ํ‚ค const tableStateKey = useMemo(() => { if (!tableConfig.selectedTable) return null; return `tableState_${tableConfig.selectedTable}`; }, [tableConfig.selectedTable]); // ๐Ÿ†• Real-Time Updates ๊ด€๋ จ ์ƒํƒœ const [isRealTimeEnabled] = useState((tableConfig as any).realTimeUpdates ?? false); const [wsConnectionStatus, setWsConnectionStatus] = useState<"connecting" | "connected" | "disconnected">( "disconnected", ); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); // ๐Ÿ†• Context Menu ๊ด€๋ จ ์ƒํƒœ const [contextMenu, setContextMenu] = useState<{ x: number; y: number; rowIndex: number; colIndex: number; row: any; } | null>(null); // ์‚ฌ์šฉ์ž ์˜ต์…˜ ๋ชจ๋‹ฌ ๊ด€๋ จ ์ƒํƒœ const [isTableOptionsOpen, setIsTableOptionsOpen] = useState(false); const [showGridLines, setShowGridLines] = useState(true); const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table"); // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ ํ•ญ์ƒ ๊ธฐ๋ณธ ํ‹€๊ณ ์ • const [frozenColumns, setFrozenColumns] = useState( (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [], ); const [frozenColumnCount, setFrozenColumnCount] = useState(0); // ๐Ÿ†• Search Panel (ํ†ตํ•ฉ ๊ฒ€์ƒ‰) ๊ด€๋ จ ์ƒํƒœ const [globalSearchTerm, setGlobalSearchTerm] = useState(""); const [isSearchPanelOpen, setIsSearchPanelOpen] = useState(false); const [searchHighlights, setSearchHighlights] = useState>(new Set()); // "rowIndex-colIndex" ํ˜•์‹ // ๐Ÿ†• Filter Builder (๊ณ ๊ธ‰ ํ•„ํ„ฐ) ๊ด€๋ จ ์ƒํƒœ ์ถ”๊ฐ€ const [isFilterBuilderOpen, setIsFilterBuilderOpen] = useState(false); const [activeFilterCount, setActiveFilterCount] = useState(0); // ๐Ÿ†• ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ์ฒ˜๋ฆฌ (์…€๋ ‰ํŠธ๋ฐ•์Šค ๋“ฑ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ ๊ฐ’์œผ๋กœ ํ•„ํ„ฐ๋ง) useEffect(() => { const linkedFilters = tableConfig.linkedFilters; if (!linkedFilters || linkedFilters.length === 0 || !screenContext) { return; } // ์—ฐ๊ฒฐ๋œ ์†Œ์Šค ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ๊ฐ’์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ const checkLinkedFilters = () => { const newFilterValues: Record = {}; let hasChanges = false; linkedFilters.forEach((filter) => { if (filter.enabled === false) return; const sourceProvider = screenContext.getDataProvider(filter.sourceComponentId); if (sourceProvider) { const selectedData = sourceProvider.getSelectedData(); if (selectedData && selectedData.length > 0) { const sourceField = filter.sourceField || "value"; const value = selectedData[0][sourceField]; if (value !== linkedFilterValues[filter.targetColumn]) { newFilterValues[filter.targetColumn] = value; hasChanges = true; } else { newFilterValues[filter.targetColumn] = linkedFilterValues[filter.targetColumn]; } } } }); if (hasChanges) { console.log("๐Ÿ”— [TableList] ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ๊ฐ’ ๋ณ€๊ฒฝ:", newFilterValues); setLinkedFilterValues(newFilterValues); // searchValues์— ์—ฐ๊ฒฐ๋œ ํ•„ํ„ฐ ๊ฐ’ ๋ณ‘ํ•ฉ setSearchValues((prev) => ({ ...prev, ...newFilterValues, })); // ์ฒซ ํŽ˜์ด์ง€๋กœ ์ด๋™ setCurrentPage(1); } }; // ์ดˆ๊ธฐ ์ฒดํฌ checkLinkedFilters(); // ์ฃผ๊ธฐ์ ์œผ๋กœ ์ฒดํฌ (500ms๋งˆ๋‹ค) const intervalId = setInterval(checkLinkedFilters, 500); return () => { clearInterval(intervalId); }; }, [screenContext, tableConfig.linkedFilters, linkedFilterValues]); // DataProvidable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ const dataProvider: DataProvidable = { componentId: component.id, componentType: "table-list", getSelectedData: () => { // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ์—์„œ ์„ ํƒ๋œ ํ–‰๋งŒ ๋ฐ˜ํ™˜ (์šฐ์ธก์— ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ ์ œ์™ธ) const selectedData = filteredData.filter((row) => { const rowId = String(row.id || row[tableConfig.selectedTable + "_id"] || ""); return selectedRows.has(rowId); }); return selectedData; }, getAllData: () => { // ๐Ÿ†• ํ•„ํ„ฐ๋ง๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ return filteredData; }, clearSelection: () => { setSelectedRows(new Set()); setIsAllSelected(false); }, }; // DataReceivable ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ const dataReceiver: DataReceivable = { componentId: component.id, componentType: "table", receiveData: async (receivedData: any[], config: DataReceiverConfig) => { console.log("๐Ÿ“ฅ TableList ๋ฐ์ดํ„ฐ ์ˆ˜์‹ :", { componentId: component.id, receivedDataCount: receivedData.length, mode: config.mode, currentDataCount: data.length, }); try { let newData: any[] = []; switch (config.mode) { case "append": // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ newData = [...data, ...receivedData]; console.log("โœ… Append ๋ชจ๋“œ: ๊ธฐ์กด ๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€", { newDataCount: newData.length }); break; case "replace": // ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ์™„์ „ํžˆ ๊ต์ฒด newData = receivedData; console.log("โœ… Replace ๋ชจ๋“œ: ๋ฐ์ดํ„ฐ ๊ต์ฒด", { newDataCount: newData.length }); break; case "merge": // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€ ๋ณ‘ํ•ฉ (ID ๊ธฐ๋ฐ˜) const existingMap = new Map(data.map((item) => [item.id, item])); receivedData.forEach((item) => { if (item.id && existingMap.has(item.id)) { // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ existingMap.set(item.id, { ...existingMap.get(item.id), ...item }); } else { // ์ƒˆ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ existingMap.set(item.id || Date.now() + Math.random(), item); } }); newData = Array.from(existingMap.values()); console.log("โœ… Merge ๋ชจ๋“œ: ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ", { newDataCount: newData.length }); break; } // ์ƒํƒœ ์—…๋ฐ์ดํŠธ setData(newData); // ์ด ์•„์ดํ…œ ์ˆ˜ ์—…๋ฐ์ดํŠธ setTotalItems(newData.length); console.log("โœ… ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์™„๋ฃŒ:", { finalDataCount: newData.length }); } catch (error) { console.error("โŒ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  ์‹คํŒจ:", error); throw error; } }, getData: () => { return data; }, }; // ํ™”๋ฉด ์ปจํ…์ŠคํŠธ์— ๋ฐ์ดํ„ฐ ์ œ๊ณต์ž/์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก useEffect(() => { if (screenContext && component.id) { screenContext.registerDataProvider(component.id, dataProvider); screenContext.registerDataReceiver(component.id, dataReceiver); return () => { screenContext.unregisterDataProvider(component.id); screenContext.unregisterDataReceiver(component.id); }; } }, [screenContext, component.id, data, selectedRows]); // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ์— ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ์ž๋กœ ๋“ฑ๋ก // useSplitPanelPosition ํ›…์œผ๋กœ ์œ„์น˜ ๊ฐ€์ ธ์˜ค๊ธฐ (์ค‘์ฒฉ๋œ ํ™”๋ฉด์—์„œ๋„ ์ž‘๋™) const currentSplitPosition = splitPanelPosition || splitPanelContext?.getPositionByScreenId(screenId as number) || null; useEffect(() => { if (splitPanelContext && component.id && currentSplitPosition) { const splitPanelReceiver = { componentId: component.id, componentType: "table-list", receiveData: async (incomingData: any[], mode: "append" | "replace" | "merge") => { console.log("๐Ÿ“ฅ [TableListComponent] ๋ถ„ํ•  ํŒจ๋„์—์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ :", { count: incomingData.length, mode, position: currentSplitPosition, }); await dataReceiver.receiveData(incomingData, { targetComponentId: component.id, targetComponentType: "table-list", mode, mappingRules: [], }); }, }; splitPanelContext.registerReceiver(currentSplitPosition, component.id, splitPanelReceiver); return () => { splitPanelContext.unregisterReceiver(currentSplitPosition, component.id); }; } }, [splitPanelContext, component.id, currentSplitPosition, dataReceiver]); // ํ…Œ์ด๋ธ” ๋“ฑ๋ก (Context์— ๋“ฑ๋ก) const tableId = `table-list-${component.id}`; useEffect(() => { // tableConfig.columns๋ฅผ ์ง์ ‘ ์‚ฌ์šฉ (displayColumns๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์žˆ์Œ) const columnsToRegister = (tableConfig.columns || []).filter( (col) => col.visible !== false && col.columnName !== "__checkbox__", ); if (!tableConfig.selectedTable || !columnsToRegister || columnsToRegister.length === 0) { return; } // ์ปฌ๋Ÿผ์˜ ๊ณ ์œ  ๊ฐ’ ์กฐํšŒ ํ•จ์ˆ˜ const getColumnUniqueValues = async (columnName: string) => { console.log("๐Ÿ” [getColumnUniqueValues] ํ˜ธ์ถœ๋จ:", { columnName, dataLength: data.length, columnMeta: columnMeta[columnName], sampleData: data[0], }); const meta = columnMeta[columnName]; const inputType = meta?.inputType || "text"; // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ธ ๊ฒฝ์šฐ ์ „์ฒด ์ •์˜๋œ ๊ฐ’ ์กฐํšŒ (๋ฐฑ์—”๋“œ API) if (inputType === "category") { try { console.log("๐Ÿ” [getColumnUniqueValues] ์นดํ…Œ๊ณ ๋ฆฌ ์ „์ฒด ๊ฐ’ ์กฐํšŒ:", { tableName: tableConfig.selectedTable, columnName, }); // API ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ (์ฟ ํ‚ค ์ธ์ฆ ์ž๋™ ์ฒ˜๋ฆฌ) const { apiClient } = await import("@/lib/api/client"); const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`); if (response.data.success && response.data.data) { const categoryOptions = response.data.data.map((item: any) => ({ value: item.valueCode, // ์นด๋ฉœ์ผ€์ด์Šค label: item.valueLabel, // ์นด๋ฉœ์ผ€์ด์Šค })); console.log("โœ… [getColumnUniqueValues] ์นดํ…Œ๊ณ ๋ฆฌ ์ „์ฒด ๊ฐ’:", { columnName, count: categoryOptions.length, options: categoryOptions, }); return categoryOptions; } else { console.warn("โš ๏ธ [getColumnUniqueValues] ์‘๋‹ต ํ˜•์‹ ์˜ค๋ฅ˜:", response.data); } } catch (error: any) { console.error("โŒ [getColumnUniqueValues] ์นดํ…Œ๊ณ ๋ฆฌ ์กฐํšŒ ์‹คํŒจ:", { error: error.message, response: error.response?.data, status: error.response?.status, columnName, tableName: tableConfig.selectedTable, }); // ์—๋Ÿฌ ์‹œ ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ fallback } } // ์ผ๋ฐ˜ ํƒ€์ž… ๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ ์กฐํšŒ ์‹คํŒจ ์‹œ: ํ˜„์žฌ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ const isLabelType = ["category", "entity", "code"].includes(inputType); const labelField = isLabelType ? `${columnName}_name` : columnName; console.log("๐Ÿ” [getColumnUniqueValues] ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์กฐํšŒ:", { columnName, inputType, isLabelType, labelField, hasLabelField: data[0] && labelField in data[0], sampleLabelValue: data[0] ? data[0][labelField] : undefined, }); // ํ˜„์žฌ ๋กœ๋“œ๋œ ๋ฐ์ดํ„ฐ์—์„œ ๊ณ ์œ  ๊ฐ’ ์ถ”์ถœ const uniqueValuesMap = new Map(); // value -> label data.forEach((row) => { const value = row[columnName]; if (value !== null && value !== undefined && value !== "") { // ๋ฐฑ์—”๋“œ ์กฐ์ธ๋œ _name ํ•„๋“œ ์‚ฌ์šฉ (์—†์œผ๋ฉด ์›๋ณธ ๊ฐ’) const label = isLabelType && row[labelField] ? row[labelField] : String(value); uniqueValuesMap.set(String(value), label); } }); // Map์„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ๋ผ๋ฒจ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ const result = Array.from(uniqueValuesMap.entries()) .map(([value, label]) => ({ value: value, label: label, })) .sort((a, b) => a.label.localeCompare(b.label)); console.log("โœ… [getColumnUniqueValues] ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ๊ฒฐ๊ณผ:", { columnName, inputType, isLabelType, labelField, uniqueCount: result.length, values: result, }); return result; }; const registration = { tableId, label: tableLabel || tableConfig.selectedTable, tableName: tableConfig.selectedTable, dataCount: totalItems || data.length, // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๊ฑด์ˆ˜ ํฌํ•จ columns: columnsToRegister.map((col) => ({ columnName: col.columnName || col.field, columnLabel: columnLabels[col.columnName] || col.displayName || col.label || col.columnName || col.field, inputType: columnMeta[col.columnName]?.inputType || "text", visible: col.visible !== false, width: columnWidths[col.columnName] || col.width || 150, sortable: col.sortable !== false, filterable: col.searchable !== false, })), onFilterChange: setFilters, onGroupChange: setGrouping, onColumnVisibilityChange: setColumnVisibility, getColumnUniqueValues, // ๊ณ ์œ  ๊ฐ’ ์กฐํšŒ ํ•จ์ˆ˜ ๋“ฑ๋ก onGroupSumChange: setGroupSumConfig, // ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ์„ค์ • // ํ‹€๊ณ ์ • ์ปฌ๋Ÿผ ๊ด€๋ จ frozenColumnCount, // ํ˜„์žฌ ํ‹€๊ณ ์ • ์ปฌ๋Ÿผ ์ˆ˜ onFrozenColumnCountChange: (count: number) => { setFrozenColumnCount(count); // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ ํ•ญ์ƒ ํ‹€๊ณ ์ •์— ํฌํ•จ const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : []; // ํ‘œ์‹œ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ ์ค‘ ์ฒ˜์Œ N๊ฐœ๋ฅผ ํ‹€๊ณ ์ • ์ปฌ๋Ÿผ์œผ๋กœ ์„ค์ • const visibleCols = columnsToRegister .filter((col) => col.visible !== false) .map((col) => col.columnName || col.field); const newFrozenColumns = [...checkboxColumn, ...visibleCols.slice(0, count)]; setFrozenColumns(newFrozenColumns); }, // ํƒญ ๊ด€๋ จ ์ •๋ณด (ํƒญ ๋‚ด๋ถ€์˜ ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ) parentTabId, parentTabsComponentId, screenId: screenId ? Number(screenId) : undefined, }; registerTable(registration); return () => { unregisterTable(tableId); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ tableId, tableConfig.selectedTable, tableConfig.columns, columnLabels, columnMeta, // columnMeta๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์žฌ๋“ฑ๋ก (inputType ์ •๋ณด ํ•„์š”) columnWidths, tableLabel, data, // ๋ฐ์ดํ„ฐ ์ž์ฒด๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์žฌ๋“ฑ๋ก (๊ณ ์œ  ๊ฐ’ ์กฐํšŒ์šฉ) totalItems, // ์ „์ฒด ํ•ญ๋ชฉ ์ˆ˜๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์žฌ๋“ฑ๋ก registerTable, // unregisterTable์€ ์˜์กด์„ฑ์—์„œ ์ œ์™ธ - ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ // unregisterTable ํ•จ์ˆ˜๋Š” ์˜์กด์„ฑ์ด ์—†์–ด ์•ˆ์ •์ ์ž„ ]); // ๐ŸŽฏ ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ localStorage์—์„œ ์ •๋ ฌ ์ƒํƒœ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ useEffect(() => { if (!tableConfig.selectedTable || !userId || hasInitializedSort.current) return; const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`; const savedSort = localStorage.getItem(storageKey); if (savedSort) { try { const { column, direction } = JSON.parse(savedSort); if (column && direction) { setSortColumn(column); setSortDirection(direction); hasInitializedSort.current = true; console.log("๐Ÿ“‚ localStorage์—์„œ ์ •๋ ฌ ์ƒํƒœ ๋ณต์›:", { column, direction }); } } catch (error) { console.error("โŒ ์ •๋ ฌ ์ƒํƒœ ๋ณต์› ์‹คํŒจ:", error); } } }, [tableConfig.selectedTable, userId]); // ๐Ÿ†• ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ 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; }); // ์ „์—ญ ์ €์žฅ์†Œ์— ๋ฐ์ดํ„ฐ ์ €์žฅ if (tableConfig.selectedTable) { // ์ปฌ๋Ÿผ ๋ผ๋ฒจ ๋งคํ•‘ ์ƒ์„ฑ (tableConfig.columns ์‚ฌ์šฉ - visibleColumns๋Š” ์•„์ง ์ •์˜๋˜์ง€ ์•Š์Œ) const cols = (tableConfig.columns || []).filter((col) => col.visible !== false); const labels: Record = {}; cols.forEach((col) => { labels[col.columnName] = columnLabels[col.columnName] || col.columnName; }); tableDisplayStore.setTableData( tableConfig.selectedTable, initialData, parsedOrder.filter((col) => col !== "__checkbox__"), sortColumn, sortDirection, { filterConditions: Object.keys(searchValues).length > 0 ? searchValues : undefined, searchTerm: searchTerm || undefined, visibleColumns: cols.map((col) => col.columnName), columnLabels: labels, currentPage: currentPage, pageSize: localPageSize, totalItems: totalItems, }, ); } 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 { // ๐Ÿ”ฅ FIX: ์บ์‹œ ํ‚ค์— ํšŒ์‚ฌ ์ฝ”๋“œ ํฌํ•จ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ง€์›) const currentUser = JSON.parse(localStorage.getItem("currentUser") || "{}"); const companyCode = currentUser.companyCode || "UNKNOWN"; const cacheKey = `columns_${tableConfig.selectedTable}_${companyCode}`; 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]); // ======================================== // ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋งคํ•‘ ๋กœ๋“œ // ======================================== // ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ๋ชฉ๋ก ์ถ”์ถœ (useMemo๋กœ ์ตœ์ ํ™”) const categoryColumns = useMemo(() => { const cols = Object.entries(columnMeta) .filter(([_, meta]) => meta.inputType === "category") .map(([columnName, _]) => columnName); return cols; }, [columnMeta]); // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ๋กœ๋“œ (columnMeta ๋ณ€๊ฒฝ ์‹œ ์ฆ‰์‹œ ์‹คํ–‰) useEffect(() => { const loadCategoryMappings = async () => { if (!tableConfig.selectedTable) { return; } if (categoryColumns.length === 0) { setCategoryMappings({}); return; } try { const mappings: Record> = {}; const apiClient = (await import("@/lib/api/client")).apiClient; for (const columnName of categoryColumns) { try { // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ: "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•ํƒœ์ธ์ง€ ํ™•์ธ let targetTable = tableConfig.selectedTable; let targetColumn = columnName; if (columnName.includes(".")) { const parts = columnName.split("."); targetTable = parts[0]; // ์กฐ์ธ๋œ ํ…Œ์ด๋ธ”๋ช… (์˜ˆ: item_info) targetColumn = parts[1]; // ์‹ค์ œ ์ปฌ๋Ÿผ๋ช… (์˜ˆ: material) console.log("๐Ÿ”— [TableList] ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ์ง€:", { originalColumn: columnName, targetTable, targetColumn, }); } const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`); if (response.data.success && response.data.data && Array.isArray(response.data.data)) { const mapping: Record = {}; response.data.data.forEach((item: any) => { // valueCode๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ํ‚ค๋กœ ์‚ฌ์šฉ const key = String(item.valueCode); mapping[key] = { label: item.valueLabel, color: item.color, }; }); if (Object.keys(mapping).length > 0) { // ๐Ÿ†• ์›๋ž˜ ์ปฌ๋Ÿผ๋ช…(item_info.material)์œผ๋กœ ๋งคํ•‘ ์ €์žฅ mappings[columnName] = mapping; } else { console.warn(`โš ๏ธ [TableList] ๋งคํ•‘ ๋ฐ์ดํ„ฐ๊ฐ€ ๋น„์–ด์žˆ์Œ [${columnName}]`); } } else { console.warn(`โš ๏ธ [TableList] ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์—†์Œ [${columnName}]:`, { success: response.data.success, hasData: !!response.data.data, isArray: Array.isArray(response.data.data), response: response.data, }); } } catch (error: any) { console.error(`โŒ [TableList] ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋กœ๋“œ ์‹คํŒจ [${columnName}]:`, { error: error.message, stack: error.stack, response: error.response?.data, status: error.response?.status, }); } } // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ์˜ inputType ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ๋ฐ ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ๋กœ๋“œ // 1. "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•ํƒœ์˜ ์กฐ์ธ ์ปฌ๋Ÿผ ์ถ”์ถœ const joinedColumns = tableConfig.columns?.filter((col) => col.columnName?.includes(".")).map((col) => col.columnName) || []; // 2. additionalJoinInfo๊ฐ€ ์žˆ๋Š” ์ปฌ๋Ÿผ๋„ ์ถ”์ถœ (์˜ˆ: item_code_material โ†’ item_info.material) const additionalJoinColumns = tableConfig.columns ?.filter((col: any) => col.additionalJoinInfo?.referenceTable) .map((col: any) => ({ columnName: col.columnName, // ์˜ˆ: item_code_material referenceTable: col.additionalJoinInfo.referenceTable, // ์˜ˆ: item_info // joinAlias์—์„œ ์‹ค์ œ ์ปฌ๋Ÿผ๋ช… ์ถ”์ถœ (item_code_material โ†’ material) actualColumn: col.additionalJoinInfo.joinAlias?.replace(`${col.additionalJoinInfo.sourceColumn}_`, "") || col.columnName, })) || []; // ์กฐ์ธ ํ…Œ์ด๋ธ”๋ณ„๋กœ ๊ทธ๋ฃนํ™” const joinedTableColumns: Record = {}; // "ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" ํ˜•ํƒœ ์ฒ˜๋ฆฌ for (const joinedColumn of joinedColumns) { const parts = joinedColumn.split("."); if (parts.length !== 2) continue; const joinedTable = parts[0]; const joinedColumnName = parts[1]; if (!joinedTableColumns[joinedTable]) { joinedTableColumns[joinedTable] = []; } joinedTableColumns[joinedTable].push({ columnName: joinedColumn, actualColumn: joinedColumnName, }); } // additionalJoinInfo ํ˜•ํƒœ ์ฒ˜๋ฆฌ for (const col of additionalJoinColumns) { if (!joinedTableColumns[col.referenceTable]) { joinedTableColumns[col.referenceTable] = []; } joinedTableColumns[col.referenceTable].push({ columnName: col.columnName, // ์˜ˆ: item_code_material actualColumn: col.actualColumn, // ์˜ˆ: material }); } // ์กฐ์ธ๋œ ํ…Œ์ด๋ธ”๋ณ„๋กœ inputType ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ const newJoinedColumnMeta: Record = {}; for (const [joinedTable, columns] of Object.entries(joinedTableColumns)) { try { // ์กฐ์ธ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ inputType ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ด๋ฏธ import๋œ tableTypeApi ์‚ฌ์šฉ) const inputTypes = await tableTypeApi.getColumnInputTypes(joinedTable); console.log(`๐Ÿ“ก [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ” inputType ๋กœ๋“œ [${joinedTable}]:`, inputTypes); for (const col of columns) { const inputTypeInfo = inputTypes.find((it: any) => it.columnName === col.actualColumn); // ์ปฌ๋Ÿผ๋ช… ๊ทธ๋Œ€๋กœ ์ €์žฅ (item_code_material ๋˜๋Š” item_info.material) newJoinedColumnMeta[col.columnName] = { inputType: inputTypeInfo?.inputType, }; console.log( ` ๐Ÿ”— [${col.columnName}] (์‹ค์ œ: ${col.actualColumn}) inputType: ${inputTypeInfo?.inputType || "unknown"}`, ); // inputType์ด category์ธ ๊ฒฝ์šฐ ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ๋กœ๋“œ if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) { try { console.log(`๐Ÿ“ก [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ” ์นดํ…Œ๊ณ ๋ฆฌ ๋กœ๋“œ ์‹œ๋„ [${col.columnName}]:`, { url: `/table-categories/${joinedTable}/${col.actualColumn}/values`, }); const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`); if (response.data.success && response.data.data && Array.isArray(response.data.data)) { const mapping: Record = {}; response.data.data.forEach((item: any) => { const key = String(item.valueCode); mapping[key] = { label: item.valueLabel, color: item.color, }; }); if (Object.keys(mapping).length > 0) { mappings[col.columnName] = mapping; } } } catch (error) { console.log(`โ„น๏ธ [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ” ์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ (${col.columnName})`); } } } } catch (error) { console.error(`โŒ [TableList] ์กฐ์ธ ํ…Œ์ด๋ธ” inputType ๋กœ๋“œ ์‹คํŒจ [${joinedTable}]:`, error); } } // ์กฐ์ธ ์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ if (Object.keys(newJoinedColumnMeta).length > 0) { setJoinedColumnMeta(newJoinedColumnMeta); console.log("โœ… [TableList] ์กฐ์ธ ์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค์ •:", newJoinedColumnMeta); } // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ์—ฐ์‡„๊ด€๊ณ„ ๋งคํ•‘ ๋กœ๋“œ (category_value_cascading_mapping) try { const cascadingResponse = await apiClient.get( `/category-value-cascading/table/${tableConfig.selectedTable}/mappings`, ); if (cascadingResponse.data.success && cascadingResponse.data.data) { const cascadingMappings = cascadingResponse.data.data; // ๊ฐ ์ž์‹ ์ปฌ๋Ÿผ์— ๋Œ€ํ•œ ๋งคํ•‘ ์ถ”๊ฐ€ for (const [columnName, columnMappings] of Object.entries( cascadingMappings as Record>, )) { if (!mappings[columnName]) { mappings[columnName] = {}; } // ์—ฐ์‡„๊ด€๊ณ„ ๋งคํ•‘ ์ถ”๊ฐ€ for (const item of columnMappings) { mappings[columnName][item.code] = { label: item.label, color: undefined, // ์—ฐ์‡„๊ด€๊ณ„๋Š” ์ƒ‰์ƒ ์—†์Œ }; } } console.log("โœ… [TableList] ์นดํ…Œ๊ณ ๋ฆฌ ์—ฐ์‡„๊ด€๊ณ„ ๋งคํ•‘ ๋กœ๋“œ ์™„๋ฃŒ:", { tableName: tableConfig.selectedTable, cascadingColumns: Object.keys(cascadingMappings), }); } } catch (cascadingError: any) { // ์—ฐ์‡„๊ด€๊ณ„ ๋งคํ•‘์ด ์—†๋Š” ๊ฒฝ์šฐ ๋ฌด์‹œ (404 ๋“ฑ) if (cascadingError?.response?.status !== 404) { console.warn("โš ๏ธ [TableList] ์นดํ…Œ๊ณ ๋ฆฌ ์—ฐ์‡„๊ด€๊ณ„ ๋งคํ•‘ ๋กœ๋“œ ์‹คํŒจ:", cascadingError?.message); } } if (Object.keys(mappings).length > 0) { setCategoryMappings(mappings); setCategoryMappingsKey((prev) => prev + 1); } else { console.warn("โš ๏ธ [TableList] ๋งคํ•‘์ด ๋น„์–ด์žˆ์–ด ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์Šคํ‚ต"); } } catch (error) { console.error("โŒ [TableList] ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ๋กœ๋“œ ์‹คํŒจ:", error); } }; loadCategoryMappings(); }, [ tableConfig.selectedTable, categoryColumns.length, JSON.stringify(categoryColumns), JSON.stringify(tableConfig.columns), ]); // ๋” ๋ช…ํ™•ํ•œ ์˜์กด์„ฑ // ======================================== // ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ // ======================================== const fetchTableDataInternal = useCallback(async () => { if (!tableConfig.selectedTable || isDesignMode) { setData([]); setTotalPages(0); setTotalItems(0); return; } 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 linkedFilterValues: Record = {}; let hasLinkedFiltersConfigured = false; // ์—ฐ๊ฒฐ ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ let hasSelectedLeftData = false; // ์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€ if (splitPanelContext) { // ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ • ์—ฌ๋ถ€ ํ™•์ธ (ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” ํ•„ํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€) const linkedFiltersConfig = splitPanelContext.linkedFilters || []; hasLinkedFiltersConfigured = linkedFiltersConfig.some( (filter) => filter.targetColumn?.startsWith(tableConfig.selectedTable + ".") || filter.targetColumn === tableConfig.selectedTable, ); // ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์„ ํƒ ์—ฌ๋ถ€ ํ™•์ธ hasSelectedLeftData = splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0; const allLinkedFilters = splitPanelContext.getLinkedFilterValues(); // ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ํ•ด๋‹นํ•˜๋Š” ํ•„ํ„ฐ๋งŒ ์ถ”์ถœ (ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช… ํ˜•์‹์—์„œ) // ์—ฐ๊ฒฐ ํ•„ํ„ฐ๋Š” ์ฝ”๋“œ ๊ฐ’์ด๋ฏ€๋กœ ์ •ํ™•ํ•œ ๋งค์นญ(equals)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•จ for (const [key, value] of Object.entries(allLinkedFilters)) { if (key.includes(".")) { const [tableName, columnName] = key.split("."); if (tableName === tableConfig.selectedTable) { // ์—ฐ๊ฒฐ ํ•„ํ„ฐ๋Š” ์ฝ”๋“œ ๊ฐ’์ด๋ฏ€๋กœ equals ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ linkedFilterValues[columnName] = { value, operator: "equals" }; hasLinkedFiltersConfigured = true; // ์ด ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ํ•„ํ„ฐ๊ฐ€ ์žˆ์Œ } } else { // ํ…Œ์ด๋ธ”๋ช… ์—†์ด ์ปฌ๋Ÿผ๋ช…๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ (equals) linkedFilterValues[key] = { value, operator: "equals" }; } } // ๐Ÿ†• ์ž๋™ ์ปฌ๋Ÿผ ๋งค์นญ: linkedFilters๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์•„๋„ // ์šฐ์ธก ํ™”๋ฉด(splitPanelPosition === "right")์ด๊ณ  ์ขŒ์ธก ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์–ด ์žˆ์œผ๋ฉด // ๋™์ผํ•œ ์ปฌ๋Ÿผ๋ช…์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ ํ•„ํ„ฐ๋ง ์ ์šฉ if ( splitPanelPosition === "right" && hasSelectedLeftData && Object.keys(linkedFilterValues).length === 0 && !hasLinkedFiltersConfigured ) { const leftData = splitPanelContext.selectedLeftData!; const tableColumns = (tableConfig.columns || []).map((col) => col.columnName); // ์ขŒ์ธก ๋ฐ์ดํ„ฐ์˜ ์ปฌ๋Ÿผ ์ค‘ ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ๋™์ผํ•œ ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ for (const [colName, colValue] of Object.entries(leftData)) { // null, undefined, ๋นˆ ๋ฌธ์ž์—ด ์ œ์™ธ if (colValue === null || colValue === undefined || colValue === "") continue; // id, objid ๋“ฑ ๊ธฐ๋ณธ ํ‚ค๋Š” ์ œ์™ธ (๋„ˆ๋ฌด ์ผ๋ฐ˜์ ์ธ ์ปฌ๋Ÿผ๋ช…) if (colName === "id" || colName === "objid" || colName === "company_code") continue; // ํ˜„์žฌ ํ…Œ์ด๋ธ”์— ๋™์ผํ•œ ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ํ™•์ธ if (tableColumns.includes(colName)) { // ์ž๋™ ์ปฌ๋Ÿผ ๋งค์นญ๋„ equals ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ linkedFilterValues[colName] = { value: colValue, operator: "equals" }; hasLinkedFiltersConfigured = true; console.log(`๐Ÿ”— [TableList] ์ž๋™ ์ปฌ๋Ÿผ ๋งค์นญ: ${colName} = ${colValue}`); } } if (Object.keys(linkedFilterValues).length > 0) { console.log("๐Ÿ”— [TableList] ์ž๋™ ์ปฌ๋Ÿผ ๋งค์นญ ํ•„ํ„ฐ ์ ์šฉ:", linkedFilterValues); } } if (Object.keys(linkedFilterValues).length > 0) { console.log("๐Ÿ”— [TableList] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์ ์šฉ:", linkedFilterValues); } } // ๐Ÿ†• ์—ฐ๊ฒฐ ํ•„ํ„ฐ๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€๋งŒ ์ขŒ์ธก์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ // โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ (๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์Œ) if (hasLinkedFiltersConfigured && !hasSelectedLeftData) { console.log("โš ๏ธ [TableList] ์—ฐ๊ฒฐ ํ•„ํ„ฐ ์„ค์ •๋จ but ์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋ฏธ์„ ํƒ โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ"); setData([]); setTotalItems(0); setLoading(false); return; } // ๐Ÿ†• RelatedDataButtons ๋Œ€์ƒ์ด์ง€๋งŒ ์•„์ง ๋ฒ„ํŠผ์ด ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ // โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ (๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š์Œ) if (isRelatedButtonTarget && !relatedButtonFilter) { console.log("โš ๏ธ [TableList] RelatedDataButtons ๋Œ€์ƒ์ด์ง€๋งŒ ๋ฒ„ํŠผ ๋ฏธ์„ ํƒ โ†’ ๋นˆ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ"); setData([]); setTotalItems(0); setLoading(false); return; } // ๐Ÿ†• RelatedDataButtons ํ•„ํ„ฐ ๊ฐ’ ์ค€๋น„ const relatedButtonFilterValues: Record = {}; if (relatedButtonFilter) { relatedButtonFilterValues[relatedButtonFilter.filterColumn] = { value: relatedButtonFilter.filterValue, operator: "equals", }; console.log("๐Ÿ”— [TableList] RelatedDataButtons ํ•„ํ„ฐ ์ ์šฉ:", relatedButtonFilterValues); } // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ, ์—ฐ๊ฒฐ ํ•„ํ„ฐ, RelatedDataButtons ํ•„ํ„ฐ ๋ณ‘ํ•ฉ const filters = { ...(Object.keys(searchValues).length > 0 ? searchValues : {}), ...linkedFilterValues, ...relatedButtonFilterValues, // ๐Ÿ†• RelatedDataButtons ํ•„ํ„ฐ ์ถ”๊ฐ€ }; const hasFilters = Object.keys(filters).length > 0; // ๐Ÿ†• REST API ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ฒ˜๋ฆฌ const isRestApiTable = tableConfig.selectedTable.startsWith("restapi_") || tableConfig.selectedTable.startsWith("_restapi_"); let response: any; if (isRestApiTable) { // REST API ๋ฐ์ดํ„ฐ ์†Œ์Šค์ธ ๊ฒฝ์šฐ const connectionIdMatch = tableConfig.selectedTable.match(/restapi_(\d+)/); const connectionId = connectionIdMatch ? parseInt(connectionIdMatch[1]) : null; if (connectionId) { console.log("๐ŸŒ [TableList] REST API ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ˜ธ์ถœ", { connectionId }); // REST API ์—ฐ๊ฒฐ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ๋ฐ ๋ฐ์ดํ„ฐ ์กฐํšŒ const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection"); const restApiData = await ExternalRestApiConnectionAPI.fetchData( connectionId, undefined, // endpoint - ์—ฐ๊ฒฐ ์ •๋ณด์—์„œ ๊ฐ€์ ธ์˜ด "response", // jsonPath - ๊ธฐ๋ณธ๊ฐ’ response ); response = { data: restApiData.rows || [], total: restApiData.total || restApiData.rows?.length || 0, totalPages: Math.ceil((restApiData.total || restApiData.rows?.length || 0) / pageSize), }; console.log("โœ… [TableList] REST API ์‘๋‹ต:", { dataLength: response.data.length, total: response.total, }); } else { throw new Error("REST API ์—ฐ๊ฒฐ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); } } else { // ์ผ๋ฐ˜ DB ํ…Œ์ด๋ธ”์ธ ๊ฒฝ์šฐ (๊ธฐ์กด ๋กœ์ง) 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 screenEntityConfigs: Record = {}; (tableConfig.columns || []) .filter((col) => col.entityDisplayConfig && col.entityDisplayConfig.displayColumns?.length > 0) .forEach((col) => { screenEntityConfigs[col.columnName] = { displayColumns: col.entityDisplayConfig!.displayColumns, separator: col.entityDisplayConfig!.separator || " - ", sourceTable: col.entityDisplayConfig!.sourceTable || tableConfig.selectedTable, joinTable: col.entityDisplayConfig!.joinTable, }; }); // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ ์ฒ˜๋ฆฌ (๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ œ์™ธ) let excludeFilterParam: any = undefined; if (tableConfig.excludeFilter?.enabled) { const excludeConfig = tableConfig.excludeFilter; let filterValue: any = undefined; // ํ•„ํ„ฐ ๊ฐ’ ์†Œ์Šค์— ๋”ฐ๋ผ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (์šฐ์„ ์ˆœ์œ„: formData > URL > ๋ถ„ํ• ํŒจ๋„) if (excludeConfig.filterColumn && excludeConfig.filterValueField) { const fieldName = excludeConfig.filterValueField; // 1์ˆœ์œ„: props๋กœ ์ „๋‹ฌ๋ฐ›์€ formData์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ชจ๋‹ฌ์—์„œ ์‚ฌ์šฉ) if (propFormData && propFormData[fieldName]) { filterValue = propFormData[fieldName]; console.log("๐Ÿ”— [TableList] formData์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { field: fieldName, value: filterValue, }); } // 2์ˆœ์œ„: URL ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ else if (typeof window !== "undefined") { const urlParams = new URLSearchParams(window.location.search); filterValue = urlParams.get(fieldName); if (filterValue) { console.log("๐Ÿ”— [TableList] URL์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { field: fieldName, value: filterValue, }); } } // 3์ˆœ์œ„: ๋ถ„ํ•  ํŒจ๋„ ๋ถ€๋ชจ ๋ฐ์ดํ„ฐ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ if (!filterValue && splitPanelContext?.selectedLeftData) { filterValue = splitPanelContext.selectedLeftData[fieldName]; if (filterValue) { console.log("๐Ÿ”— [TableList] ๋ถ„ํ• ํŒจ๋„์—์„œ excludeFilter ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ:", { field: fieldName, value: filterValue, }); } } } if (filterValue || !excludeConfig.filterColumn) { excludeFilterParam = { enabled: true, referenceTable: excludeConfig.referenceTable, referenceColumn: excludeConfig.referenceColumn, sourceColumn: excludeConfig.sourceColumn, filterColumn: excludeConfig.filterColumn, filterValue: filterValue, }; console.log("๐Ÿšซ [TableList] ์ œ์™ธ ํ•„ํ„ฐ ์ ์šฉ:", excludeFilterParam); } } // ๐ŸŽฏ ํ•ญ์ƒ entityJoinApi ์‚ฌ์šฉ (writer ์ปฌ๋Ÿผ ์ž๋™ ์กฐ์ธ ์ง€์›) response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page, size: pageSize, sortBy, sortOrder, search: hasFilters ? filters : undefined, enableEntityJoin: true, additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined, screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // ๐ŸŽฏ ํ™”๋ฉด๋ณ„ ์—”ํ‹ฐํ‹ฐ ์„ค์ • ์ „๋‹ฌ dataFilter: tableConfig.dataFilter, // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ „๋‹ฌ excludeFilter: excludeFilterParam, // ๐Ÿ†• ์ œ์™ธ ํ•„ํ„ฐ ์ „๋‹ฌ companyCodeOverride: companyCode, // ๐Ÿ†• ํ”„๋ฆฌ๋ทฐ์šฉ ํšŒ์‚ฌ ์ฝ”๋“œ ์˜ค๋ฒ„๋ผ์ด๋“œ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) }); // ์‹ค์ œ ๋ฐ์ดํ„ฐ์˜ item_number๋งŒ ์ถ”์ถœํ•˜์—ฌ ์ค‘๋ณต ํ™•์ธ const itemNumbers = (response.data || []).map((item: any) => item.item_number); const uniqueItemNumbers = [...new Set(itemNumbers)]; // console.log("โœ… [TableList] API ์‘๋‹ต ๋ฐ›์Œ"); // console.log(` - dataLength: ${response.data?.length || 0}`); // console.log(` - total: ${response.total}`); // console.log(` - itemNumbers: ${JSON.stringify(itemNumbers)}`); // console.log(` - uniqueItemNumbers: ${JSON.stringify(uniqueItemNumbers)}`); // console.log(` - isDuplicated: ${itemNumbers.length !== uniqueItemNumbers.length}`); setData(response.data || []); setTotalPages(response.totalPages || 0); setTotalItems(response.total || 0); setError(null); // ๐ŸŽฏ Store์— ํ•„ํ„ฐ ์กฐ๊ฑด ์ €์žฅ (์—‘์…€ ๋‹ค์šด๋กœ๋“œ์šฉ) // tableConfig.columns ์‚ฌ์šฉ (visibleColumns๋Š” ์ด ์‹œ์ ์—์„œ ์•„์ง ์ •์˜๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ) const cols = (tableConfig.columns || []).filter((col) => col.visible !== false); const labels: Record = {}; cols.forEach((col) => { labels[col.columnName] = columnLabels[col.columnName] || col.columnName; }); tableDisplayStore.setTableData( tableConfig.selectedTable, response.data || [], cols.map((col) => col.columnName), sortBy, sortOrder, { filterConditions: filters, searchTerm: search, visibleColumns: cols.map((col) => col.columnName), columnLabels: labels, currentPage: page, pageSize: pageSize, totalItems: response.total || 0, }, ); } } 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, // ๐Ÿ†• ์šฐ์ธก ํ™”๋ฉด์ผ ๋•Œ๋งŒ selectedLeftData ๋ณ€๊ฒฝ์— ๋ฐ˜์‘ (์ขŒ์ธก ํ…Œ์ด๋ธ”์€ ์žฌ์กฐํšŒ ๋ถˆํ•„์š”) splitPanelPosition, currentSplitPosition, splitPanelContext?.selectedLeftData, // ๐Ÿ†• RelatedDataButtons ํ•„ํ„ฐ ์ถ”๊ฐ€ relatedButtonFilter, isRelatedButtonTarget, // ๐Ÿ†• ํ”„๋ฆฌ๋ทฐ์šฉ ํšŒ์‚ฌ ์ฝ”๋“œ ์˜ค๋ฒ„๋ผ์ด๋“œ companyCode, ]); 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"; } // ๐ŸŽฏ ์ •๋ ฌ ์ƒํƒœ๋ฅผ localStorage์— ์ €์žฅ (์‚ฌ์šฉ์ž๋ณ„) if (tableConfig.selectedTable && userId) { const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`; try { localStorage.setItem( storageKey, JSON.stringify({ column: newSortColumn, direction: newSortDirection, }), ); console.log("๐Ÿ’พ ์ •๋ ฌ ์ƒํƒœ ์ €์žฅ:", { column: newSortColumn, direction: newSortDirection }); } catch (error) { console.error("โŒ ์ •๋ ฌ ์ƒํƒœ ์ €์žฅ ์‹คํŒจ:", error); } } 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๋‹จ๊ณ„: ์ •๋ ฌ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ปฌ๋Ÿผ ์ˆœ์„œ๋Œ€๋กœ ์žฌ์ •๋ ฌ // tableConfig.columns ์‚ฌ์šฉ (visibleColumns๋Š” ์ด ์‹œ์ ์—์„œ ์•„์ง ์ •์˜๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ) const cols = (tableConfig.columns || []).filter((col) => col.visible !== false); const reorderedData = sortedData.map((row: any) => { const reordered: any = {}; cols.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 : cols.map((c) => c.columnName)).filter( (col) => col !== "__checkbox__", ); // ์ปฌ๋Ÿผ ๋ผ๋ฒจ ์ •๋ณด๋„ ํ•จ๊ป˜ ์ €์žฅ const labels: Record = {}; cols.forEach((col) => { labels[col.columnName] = columnLabels[col.columnName] || col.columnName; }); tableDisplayStore.setTableData( tableConfig.selectedTable, reorderedData, cleanColumnOrder, newSortColumn, newSortDirection, { filterConditions: Object.keys(searchValues).length > 0 ? searchValues : undefined, searchTerm: searchTerm || undefined, visibleColumns: cols.map((col) => col.columnName), columnLabels: labels, currentPage: currentPage, pageSize: localPageSize, totalItems: totalItems, }, ); } } 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, }); } // ๐Ÿ†• modalDataStore์— ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ž๋™ ์ €์žฅ (ํ…Œ์ด๋ธ”๋ช… ๊ธฐ๋ฐ˜ dataSourceId) if (tableConfig.selectedTable && selectedRowsData.length > 0) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { const modalItems = selectedRowsData.map((row, idx) => ({ id: getRowKey(row, idx), originalData: row, additionalData: {}, })); useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems); console.log("โœ… [TableList] modalDataStore์— ๋ฐ์ดํ„ฐ ์ €์žฅ:", { dataSourceId: tableConfig.selectedTable, count: modalItems.length, }); }); } else if (tableConfig.selectedTable && selectedRowsData.length === 0) { // ์„ ํƒ ํ•ด์ œ ์‹œ ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ import("@/stores/modalDataStore").then(({ useModalDataStore }) => { useModalDataStore.getState().clearData(tableConfig.selectedTable!); console.log("๐Ÿ—‘๏ธ [TableList] modalDataStore ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ:", tableConfig.selectedTable); }); } const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index))); setIsAllSelected(allRowsSelected && filteredData.length > 0); }; const handleSelectAll = (checked: boolean) => { if (checked) { const allKeys = filteredData.map((row, index) => getRowKey(row, index)); const newSelectedRows = new Set(allKeys); setSelectedRows(newSelectedRows); setIsAllSelected(true); if (onSelectedRowsChange) { onSelectedRowsChange(Array.from(newSelectedRows), filteredData, sortColumn || undefined, sortDirection); } if (onFormDataChange) { onFormDataChange({ selectedRows: Array.from(newSelectedRows), selectedRowsData: filteredData, }); } // ๐Ÿ†• modalDataStore์— ์ „์ฒด ๋ฐ์ดํ„ฐ ์ €์žฅ if (tableConfig.selectedTable && filteredData.length > 0) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { const modalItems = filteredData.map((row, idx) => ({ id: getRowKey(row, idx), originalData: row, additionalData: {}, })); useModalDataStore.getState().setData(tableConfig.selectedTable!, modalItems); console.log("โœ… [TableList] modalDataStore์— ์ „์ฒด ๋ฐ์ดํ„ฐ ์ €์žฅ:", { dataSourceId: tableConfig.selectedTable, count: modalItems.length, }); }); } } else { setSelectedRows(new Set()); setIsAllSelected(false); if (onSelectedRowsChange) { onSelectedRowsChange([], [], sortColumn || undefined, sortDirection); } if (onFormDataChange) { onFormDataChange({ selectedRows: [], selectedRowsData: [] }); } // ๐Ÿ†• modalDataStore ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ if (tableConfig.selectedTable) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { useModalDataStore.getState().clearData(tableConfig.selectedTable!); console.log("๐Ÿ—‘๏ธ [TableList] modalDataStore ์ „์ฒด ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ:", tableConfig.selectedTable); }); } } }; 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); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ์— ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ (์ขŒ์ธก ํ™”๋ฉด์ธ ๊ฒฝ์šฐ) // disableAutoDataTransfer๊ฐ€ true์ด๋ฉด ์ž๋™ ์ „๋‹ฌ ๋น„ํ™œ์„ฑํ™” (๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ๋งŒ ์ „๋‹ฌ) // currentSplitPosition์„ ์‚ฌ์šฉํ•˜์—ฌ ์ •ํ™•ํ•œ ์œ„์น˜ ํ™•์ธ (splitPanelPosition์ด ์—†์„ ์ˆ˜ ์žˆ์Œ) const effectiveSplitPosition = splitPanelPosition || currentSplitPosition; console.log("๐Ÿ”— [TableList] ํ–‰ ํด๋ฆญ - ๋ถ„ํ•  ํŒจ๋„ ์œ„์น˜ ํ™•์ธ:", { splitPanelPosition, currentSplitPosition, effectiveSplitPosition, hasSplitPanelContext: !!splitPanelContext, disableAutoDataTransfer: splitPanelContext?.disableAutoDataTransfer, }); if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) { if (!isCurrentlySelected) { // ์„ ํƒ๋œ ๊ฒฝ์šฐ: ๋ฐ์ดํ„ฐ ์ €์žฅ splitPanelContext.setSelectedLeftData(row); console.log("๐Ÿ”— [TableList] ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์ €์žฅ:", { row, parentDataMapping: splitPanelContext.parentDataMapping, }); } else { // ์„ ํƒ ํ•ด์ œ๋œ ๊ฒฝ์šฐ: ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” splitPanelContext.setSelectedLeftData(null); console.log("๐Ÿ”— [TableList] ๋ถ„ํ•  ํŒจ๋„ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”"); } } console.log("ํ–‰ ํด๋ฆญ:", { row, index, isSelected: !isCurrentlySelected }); }; // ๐Ÿ†• ์…€ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ (ํฌ์ปค์Šค ์„ค์ • + ํ–‰ ์„ ํƒ) const handleCellClick = (rowIndex: number, colIndex: number, e: React.MouseEvent) => { e.stopPropagation(); setFocusedCell({ rowIndex, colIndex }); // ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ์— ํฌ์ปค์Šค ์„ค์ • (ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ ์ˆ˜์‹ ์šฉ) tableContainerRef.current?.focus(); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋‚ด์—์„œ ์…€ ํด๋ฆญ ์‹œ์—๋„ ํ•ด๋‹น ํ–‰ ์„ ํƒ ์ฒ˜๋ฆฌ // filteredData์—์„œ ํ•ด๋‹น ํ–‰์˜ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ const row = filteredData[rowIndex]; if (!row) return; const rowKey = getRowKey(row, rowIndex); const isCurrentlySelected = selectedRows.has(rowKey); // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ๊ฐ€ ์žˆ๊ณ , ์ขŒ์ธก ํ™”๋ฉด์ธ ๊ฒฝ์šฐ์—๋งŒ ํ–‰ ์„ ํƒ ๋ฐ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ const effectiveSplitPosition = splitPanelPosition || currentSplitPosition; if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) { // ์ด๋ฏธ ์„ ํƒ๋œ ํ–‰๊ณผ ๋‹ค๋ฅธ ํ–‰์„ ํด๋ฆญํ•œ ๊ฒฝ์šฐ์—๋งŒ ์ฒ˜๋ฆฌ if (!isCurrentlySelected) { // ๊ธฐ์กด ์„ ํƒ ํ•ด์ œํ•˜๊ณ  ์ƒˆ ํ–‰ ์„ ํƒ setSelectedRows(new Set([rowKey])); setIsAllSelected(false); // ๋ถ„ํ•  ํŒจ๋„ ์ปจํ…์ŠคํŠธ์— ๋ฐ์ดํ„ฐ ์ €์žฅ splitPanelContext.setSelectedLeftData(row); // onSelectedRowsChange ์ฝœ๋ฐฑ ํ˜ธ์ถœ if (onSelectedRowsChange) { onSelectedRowsChange([rowKey], [row], sortColumn || undefined, sortDirection); } if (onFormDataChange) { onFormDataChange({ selectedRows: [rowKey], selectedRowsData: [row] }); } } } }; // ๐Ÿ†• ์…€ ๋”๋ธ”ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ (ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž…) - visibleColumns ์ •์˜ ํ›„ ์‚ฌ์šฉ const handleCellDoubleClick = useCallback( (rowIndex: number, colIndex: number, columnName: string, value: any) => { // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ ๋ถˆ๊ฐ€ if (columnName === "__checkbox__") return; // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ const column = visibleColumns.find((col) => col.columnName === columnName); if (column?.editable === false) { toast.warning(`'${column.displayName || columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); return; } setEditingCell({ rowIndex, colIndex, columnName, originalValue: value }); setEditingValue(value !== null && value !== undefined ? String(value) : ""); setFocusedCell({ rowIndex, colIndex }); }, [visibleColumns], ); // ๐Ÿ†• ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… placeholder (์‹ค์ œ ๊ตฌํ˜„์€ visibleColumns ์ •์˜ ํ›„) const startEditingRef = useRef<() => void>(() => {}); // ๐Ÿ†• ๊ฐ ์ปฌ๋Ÿผ์˜ ๊ณ ์œ ๊ฐ’ ๋ชฉ๋ก ๊ณ„์‚ฐ const columnUniqueValues = useMemo(() => { const result: Record = {}; if (data.length === 0) return result; (tableConfig.columns || []).forEach((column: { columnName: string }) => { if (column.columnName === "__checkbox__") return; const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; const values = new Set(); data.forEach((row) => { const val = row[mappedColumnName]; if (val !== null && val !== undefined && val !== "") { values.add(String(val)); } }); result[column.columnName] = Array.from(values).sort(); }); return result; }, [data, tableConfig.columns, joinColumnMapping]); // ๐Ÿ†• ํ—ค๋” ํ•„ํ„ฐ ํ† ๊ธ€ const toggleHeaderFilter = useCallback((columnName: string, value: string) => { setHeaderFilters((prev) => { const current = prev[columnName] || new Set(); const newSet = new Set(current); if (newSet.has(value)) { newSet.delete(value); } else { newSet.add(value); } return { ...prev, [columnName]: newSet }; }); }, []); // ๐Ÿ†• ํ—ค๋” ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” const clearHeaderFilter = useCallback((columnName: string) => { setHeaderFilters((prev) => { const newFilters = { ...prev }; delete newFilters[columnName]; return newFilters; }); setOpenFilterColumn(null); }, []); // ๐Ÿ†• ๋ชจ๋“  ํ—ค๋” ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” const clearAllHeaderFilters = useCallback(() => { setHeaderFilters({}); setOpenFilterColumn(null); }, []); // ๐Ÿ†• ๋ฐ์ดํ„ฐ ์š”์•ฝ (Total Summaries) ์„ค์ • // ํ˜•์‹: { columnName: { type: 'sum' | 'avg' | 'count' | 'min' | 'max', label?: string } } const summaryConfig = useMemo(() => { const config: Record = {}; // tableConfig์—์„œ summary ์„ค์ • ์ฝ๊ธฐ if (tableConfig.summaries) { tableConfig.summaries.forEach((summary: { columnName: string; type: string; label?: string }) => { config[summary.columnName] = { type: summary.type, label: summary.label }; }); } return config; }, [tableConfig.summaries]); // ๐Ÿ†• ์š”์•ฝ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ const summaryData = useMemo(() => { if (Object.keys(summaryConfig).length === 0 || data.length === 0) { return null; } const result: Record = {}; Object.entries(summaryConfig).forEach(([columnName, config]) => { const values = data .map((row) => { const mappedColumnName = joinColumnMapping[columnName] || columnName; const val = row[mappedColumnName]; return typeof val === "number" ? val : parseFloat(val); }) .filter((v) => !isNaN(v)); let value: number | string = 0; let label = config.label || ""; switch (config.type) { case "sum": value = values.reduce((acc, v) => acc + v, 0); label = label || "ํ•ฉ๊ณ„"; break; case "avg": value = values.length > 0 ? values.reduce((acc, v) => acc + v, 0) / values.length : 0; label = label || "ํ‰๊ท "; break; case "count": value = data.length; label = label || "๊ฐœ์ˆ˜"; break; case "min": value = values.length > 0 ? Math.min(...values) : 0; label = label || "์ตœ์†Œ"; break; case "max": value = values.length > 0 ? Math.max(...values) : 0; label = label || "์ตœ๋Œ€"; break; default: value = 0; } result[columnName] = { value, label }; }); return result; }, [data, summaryConfig, joinColumnMapping]); // ๐Ÿ†• ํŽธ์ง‘ ์ทจ์†Œ const cancelEditing = useCallback(() => { setEditingCell(null); setEditingValue(""); tableContainerRef.current?.focus(); }, []); // ๐Ÿ†• ํŽธ์ง‘ ์ €์žฅ (์ฆ‰์‹œ ์ €์žฅ ๋˜๋Š” ๋ฐฐ์น˜ ์ €์žฅ) const saveEditing = useCallback(async () => { if (!editingCell) return; const { rowIndex, columnName, originalValue } = editingCell; const newValue = editingValue; // ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜์œผ๋ฉด ๊ทธ๋ƒฅ ๋‹ซ๊ธฐ if (String(originalValue ?? "") === newValue) { setCellValidationError(rowIndex, columnName, null); // ์—๋Ÿฌ ์ดˆ๊ธฐํ™” cancelEditing(); return; } // ํ˜„์žฌ ํ–‰ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ const row = data[rowIndex]; if (!row || !tableConfig.selectedTable) { cancelEditing(); return; } // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํ–‰ const validationError = validateValue(newValue === "" ? null : newValue, columnName, row); if (validationError) { setCellValidationError(rowIndex, columnName, validationError); toast.error(validationError); // ํŽธ์ง‘ ์ƒํƒœ ์œ ์ง€ (์—๋Ÿฌ ์ˆ˜์ • ๊ฐ€๋Šฅํ•˜๋„๋ก) return; } // ์œ ํšจ์„ฑ ํ†ต๊ณผ ์‹œ ์—๋Ÿฌ ์ดˆ๊ธฐํ™” setCellValidationError(rowIndex, columnName, null); // ๊ธฐ๋ณธ ํ‚ค ํ•„๋“œ ์ฐพ๊ธฐ (id ๋˜๋Š” ์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ) const primaryKeyField = tableConfig.primaryKey || "id"; const primaryKeyValue = row[primaryKeyField]; if (primaryKeyValue === undefined || primaryKeyValue === null) { console.error("๊ธฐ๋ณธ ํ‚ค ๊ฐ’์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:", primaryKeyField); cancelEditing(); return; } // ๐Ÿ†• ๋ฐฐ์น˜ ๋ชจ๋“œ: ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ pending์— ์ €์žฅ if (editMode === "batch") { const changeKey = `${rowIndex}-${columnName}`; setPendingChanges((prev) => { const newMap = new Map(prev); newMap.set(changeKey, { rowIndex, columnName, originalValue, newValue: newValue === "" ? null : newValue, primaryKeyValue, }); return newMap; }); // ๋กœ์ปฌ ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ (ํ™”๋ฉด ํ‘œ์‹œ์šฉ) setLocalEditedData((prev) => ({ ...prev, [rowIndex]: { ...(prev[rowIndex] || {}), [columnName]: newValue === "" ? null : newValue, }, })); console.log("๐Ÿ“ ๋ฐฐ์น˜ ํŽธ์ง‘ ์ถ”๊ฐ€:", { columnName, newValue, pendingCount: pendingChanges.size + 1 }); cancelEditing(); return; } // ๐Ÿ†• ์ฆ‰์‹œ ๋ชจ๋“œ: ๋ฐ”๋กœ ์ €์žฅ try { const { apiClient } = await import("@/lib/api/client"); await apiClient.put("/dynamic-form/update-field", { tableName: tableConfig.selectedTable, keyField: primaryKeyField, keyValue: primaryKeyValue, updateField: columnName, updateValue: newValue === "" ? null : newValue, }); // ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ ํŠธ๋ฆฌ๊ฑฐ setRefreshTrigger((prev) => prev + 1); console.log("โœ… ์…€ ํŽธ์ง‘ ์ €์žฅ ์™„๋ฃŒ:", { columnName, newValue }); } catch (error) { console.error("โŒ ์…€ ํŽธ์ง‘ ์ €์žฅ ์‹คํŒจ:", error); } cancelEditing(); }, [ editingCell, editingValue, data, tableConfig.selectedTable, tableConfig.primaryKey, cancelEditing, editMode, pendingChanges.size, ]); // ๐Ÿ†• ๋ฐฐ์น˜ ์ €์žฅ: ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ•œ๋ฒˆ์— ์ €์žฅ const saveBatchChanges = useCallback(async () => { if (pendingChanges.size === 0) { toast.info("์ €์žฅํ•  ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์—†์Šต๋‹ˆ๋‹ค."); return; } try { const { apiClient } = await import("@/lib/api/client"); const primaryKeyField = tableConfig.primaryKey || "id"; // ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ const savePromises = Array.from(pendingChanges.values()).map((change) => apiClient.put("/dynamic-form/update-field", { tableName: tableConfig.selectedTable, keyField: primaryKeyField, keyValue: change.primaryKeyValue, updateField: change.columnName, updateValue: change.newValue, }), ); await Promise.all(savePromises); // ์ƒํƒœ ์ดˆ๊ธฐํ™” setPendingChanges(new Map()); setLocalEditedData({}); setRefreshTrigger((prev) => prev + 1); toast.success(`${pendingChanges.size}๊ฐœ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); console.log("โœ… ๋ฐฐ์น˜ ์ €์žฅ ์™„๋ฃŒ:", pendingChanges.size, "๊ฐœ"); } catch (error) { console.error("โŒ ๋ฐฐ์น˜ ์ €์žฅ ์‹คํŒจ:", error); toast.error("์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } }, [pendingChanges, tableConfig.selectedTable, tableConfig.primaryKey]); // ๐Ÿ†• ๋ฐฐ์น˜ ์ทจ์†Œ: ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋กค๋ฐฑ const cancelBatchChanges = useCallback(() => { if (pendingChanges.size === 0) return; setPendingChanges(new Map()); setLocalEditedData({}); toast.info("๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); console.log("๐Ÿ”„ ๋ฐฐ์น˜ ํŽธ์ง‘ ์ทจ์†Œ"); }, [pendingChanges.size]); // ๐Ÿ†• ํŠน์ • ์…€์ด ์ˆ˜์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ const isCellModified = useCallback( (rowIndex: number, columnName: string) => { return pendingChanges.has(`${rowIndex}-${columnName}`); }, [pendingChanges], ); // ๐Ÿ†• ์ˆ˜์ •๋œ ์…€ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (๋กœ์ปฌ ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์šฐ์„ ) const getDisplayValue = useCallback( (row: any, rowIndex: number, columnName: string) => { const localValue = localEditedData[rowIndex]?.[columnName]; if (localValue !== undefined) { return localValue; } return row[columnName]; }, [localEditedData], ); // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ•จ์ˆ˜ const validateValue = useCallback( (value: any, columnName: string, row: any): string | null => { // tableConfig.validation์—์„œ ์ปฌ๋Ÿผ๋ณ„ ๊ทœ์น™ ๊ฐ€์ ธ์˜ค๊ธฐ const rules = (tableConfig as any).validation?.[columnName] as ValidationRule | undefined; if (!rules) return null; const strValue = value !== null && value !== undefined ? String(value) : ""; const numValue = parseFloat(strValue); // ํ•„์ˆ˜ ๊ฒ€์‚ฌ if (rules.required && (!strValue || strValue.trim() === "")) { return rules.customMessage || "ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค."; } // ๊ฐ’์ด ๋น„์–ด์žˆ์œผ๋ฉด ๋‹ค๋ฅธ ๊ฒ€์‚ฌ ์Šคํ‚ต (required๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ) if (!strValue || strValue.trim() === "") return null; // ์ตœ์†Œ๊ฐ’ ๊ฒ€์‚ฌ if (rules.min !== undefined && !isNaN(numValue) && numValue < rules.min) { return rules.customMessage || `์ตœ์†Œ๊ฐ’์€ ${rules.min}์ž…๋‹ˆ๋‹ค.`; } // ์ตœ๋Œ€๊ฐ’ ๊ฒ€์‚ฌ if (rules.max !== undefined && !isNaN(numValue) && numValue > rules.max) { return rules.customMessage || `์ตœ๋Œ€๊ฐ’์€ ${rules.max}์ž…๋‹ˆ๋‹ค.`; } // ์ตœ์†Œ ๊ธธ์ด ๊ฒ€์‚ฌ if (rules.minLength !== undefined && strValue.length < rules.minLength) { return rules.customMessage || `์ตœ์†Œ ${rules.minLength}์ž ์ด์ƒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.`; } // ์ตœ๋Œ€ ๊ธธ์ด ๊ฒ€์‚ฌ if (rules.maxLength !== undefined && strValue.length > rules.maxLength) { return rules.customMessage || `์ตœ๋Œ€ ${rules.maxLength}์ž๊นŒ์ง€ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.`; } // ํŒจํ„ด ๊ฒ€์‚ฌ if (rules.pattern && !rules.pattern.test(strValue)) { return rules.customMessage || "์ž…๋ ฅ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; } // ์ปค์Šคํ…€ ๊ฒ€์ฆ if (rules.validate) { const customError = rules.validate(value, row); if (customError) return customError; } return null; }, [tableConfig], ); // ๐Ÿ†• ์…€ ์œ ํšจ์„ฑ ์—๋Ÿฌ ์—ฌ๋ถ€ ํ™•์ธ const getCellValidationError = useCallback( (rowIndex: number, columnName: string): string | null => { return validationErrors.get(`${rowIndex}-${columnName}`) || null; }, [validationErrors], ); // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ ์„ค์ • const setCellValidationError = useCallback((rowIndex: number, columnName: string, error: string | null) => { setValidationErrors((prev) => { const newMap = new Map(prev); const key = `${rowIndex}-${columnName}`; if (error) { newMap.set(key, error); } else { newMap.delete(key); } return newMap; }); }, []); // ๐Ÿ†• ๋ชจ๋“  ์œ ํšจ์„ฑ ์—๋Ÿฌ ์ดˆ๊ธฐํ™” const clearAllValidationErrors = useCallback(() => { setValidationErrors(new Map()); }, []); // ๐Ÿ†• Excel ๋‚ด๋ณด๋‚ด๊ธฐ ํ•จ์ˆ˜ const exportToExcel = useCallback( (exportAll: boolean = true) => { try { // ๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ์„ ํƒ (์„ ํƒ๋œ ํ–‰๋งŒ ๋˜๋Š” ์ „์ฒด) let exportData: any[]; if (exportAll) { exportData = filteredData; } else { // ์„ ํƒ๋œ ํ–‰๋งŒ ๋‚ด๋ณด๋‚ด๊ธฐ exportData = filteredData.filter((row, index) => { const rowKey = getRowKey(row, index); return selectedRows.has(rowKey); }); } if (exportData.length === 0) { toast.error(exportAll ? "๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." : "์„ ํƒ๋œ ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค."); return; } // ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ) const exportColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__"); // ํ—ค๋” ํ–‰ ์ƒ์„ฑ const headers = exportColumns.map((col) => columnLabels[col.columnName] || col.columnName); // ๋ฐ์ดํ„ฐ ํ–‰ ์ƒ์„ฑ const rows = exportData.map((row) => { return exportColumns.map((col) => { const mappedColumnName = joinColumnMapping[col.columnName] || col.columnName; const value = row[mappedColumnName]; // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘๋œ ๊ฐ’ ์ฒ˜๋ฆฌ if (categoryMappings[col.columnName] && value !== null && value !== undefined) { const mapping = categoryMappings[col.columnName][String(value)]; if (mapping) { return mapping.label; } } // null/undefined ์ฒ˜๋ฆฌ if (value === null || value === undefined) { return ""; } return value; }); }); // ์›Œํฌ์‹œํŠธ ์ƒ์„ฑ const wsData = [headers, ...rows]; const ws = XLSX.utils.aoa_to_sheet(wsData); // ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ž๋™ ์กฐ์ • const colWidths = exportColumns.map((col, idx) => { const headerLength = headers[idx]?.length || 10; const maxDataLength = Math.max(...rows.map((row) => String(row[idx] ?? "").length)); return { wch: Math.min(Math.max(headerLength, maxDataLength) + 2, 50) }; }); ws["!cols"] = colWidths; // ์›Œํฌ๋ถ ์ƒ์„ฑ const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, tableLabel || "๋ฐ์ดํ„ฐ"); // ํŒŒ์ผ๋ช… ์ƒ์„ฑ const fileName = `${tableLabel || tableConfig.selectedTable || "export"}_${new Date().toISOString().split("T")[0]}.xlsx`; // ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ XLSX.writeFile(wb, fileName); toast.success(`${exportData.length}๊ฐœ ํ–‰์ด Excel๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); console.log("โœ… Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์™„๋ฃŒ:", fileName); } catch (error) { console.error("โŒ Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ:", error); toast.error("Excel ๋‚ด๋ณด๋‚ด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } }, [ filteredData, selectedRows, visibleColumns, columnLabels, joinColumnMapping, categoryMappings, tableLabel, tableConfig.selectedTable, getRowKey, ], ); // ๐Ÿ†• ํ–‰ ํ™•์žฅ/์ถ•์†Œ ํ† ๊ธ€ const toggleRowExpand = useCallback( async (rowKey: string, row: any) => { setExpandedRows((prev) => { const newSet = new Set(prev); if (newSet.has(rowKey)) { newSet.delete(rowKey); } else { newSet.add(rowKey); // ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ (์•„์ง ์—†๋Š” ๊ฒฝ์šฐ) if (!detailData[rowKey] && (tableConfig as any).masterDetail?.detailTable) { loadDetailData(rowKey, row); } } return newSet; }); }, [detailData, tableConfig], ); // ๐Ÿ†• ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ const loadDetailData = useCallback( async (rowKey: string, row: any) => { const masterDetailConfig = (tableConfig as any).masterDetail; if (!masterDetailConfig?.detailTable) return; try { const { apiClient } = await import("@/lib/api/client"); // masterKey ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ const masterKeyField = masterDetailConfig.masterKey || "id"; const masterKeyValue = row[masterKeyField]; // ์ƒ์„ธ ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ const response = await apiClient.post(`/table-management/tables/${masterDetailConfig.detailTable}/data`, { page: 1, size: 100, search: { [masterDetailConfig.detailKey || masterKeyField]: masterKeyValue, }, autoFilter: true, }); const details = response.data?.data?.data || []; setDetailData((prev) => ({ ...prev, [rowKey]: details, })); console.log("โœ… ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์™„๋ฃŒ:", { rowKey, count: details.length }); } catch (error) { console.error("โŒ ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ:", error); setDetailData((prev) => ({ ...prev, [rowKey]: [], })); } }, [tableConfig], ); // ๐Ÿ†• ๋ชจ๋“  ํ–‰ ํ™•์žฅ/์ถ•์†Œ const expandAllRows = useCallback(() => { if (expandedRows.size === filteredData.length) { // ๋ชจ๋‘ ์ถ•์†Œ setExpandedRows(new Set()); } else { // ๋ชจ๋‘ ํ™•์žฅ const allKeys = new Set(filteredData.map((row, index) => getRowKey(row, index))); setExpandedRows(allKeys); } }, [expandedRows.size, filteredData, getRowKey]); // ๐Ÿ†• Multi-Level Headers: Band ์ •๋ณด ๊ณ„์‚ฐ const columnBandsInfo = useMemo(() => { const bands = (tableConfig as any).columnBands as ColumnBand[] | undefined; if (!bands || bands.length === 0) return null; // ๊ฐ band์˜ ์‹œ์ž‘ ์ธ๋ฑ์Šค์™€ colspan ๊ณ„์‚ฐ const bandInfo = bands .map((band) => { const visibleBandColumns = band.columns.filter((colName) => visibleColumns.some((vc) => vc.columnName === colName), ); const startIndex = visibleColumns.findIndex((vc) => visibleBandColumns.includes(vc.columnName)); return { caption: band.caption, columns: visibleBandColumns, colSpan: visibleBandColumns.length, startIndex, }; }) .filter((b) => b.colSpan > 0); // Band์— ํฌํ•จ๋˜์ง€ ์•Š์€ ์ปฌ๋Ÿผ ์ฐพ๊ธฐ const bandedColumns = new Set(bands.flatMap((b) => b.columns)); const unbandedColumns = visibleColumns .map((vc, idx) => ({ columnName: vc.columnName, index: idx })) .filter((c) => !bandedColumns.has(c.columnName)); return { bands: bandInfo, unbandedColumns, hasBands: bandInfo.length > 0, }; }, [tableConfig, visibleColumns]); // ๐Ÿ†• Cascading Lookups: ์—ฐ๊ณ„ ๋“œ๋กญ๋‹ค์šด ์˜ต์…˜ ๋กœ๋”ฉ const loadCascadingOptions = useCallback( async (columnName: string, parentColumnName: string, parentValue: any) => { const cascadingConfig = (tableConfig as any).cascadingLookups?.[columnName]; if (!cascadingConfig) return; const cacheKey = `${columnName}_${parentValue}`; // ์ด๋ฏธ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์Šคํ‚ต if (loadingCascading[cacheKey]) return; // ์ด๋ฏธ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์Šคํ‚ต if (cascadingOptions[cacheKey]) return; setLoadingCascading((prev) => ({ ...prev, [cacheKey]: true })); try { const { apiClient } = await import("@/lib/api/client"); // API์—์„œ ์—ฐ๊ณ„ ์˜ต์…˜ ๋กœ๋”ฉ const response = await apiClient.post(`/table-management/tables/${cascadingConfig.sourceTable}/data`, { page: 1, size: 1000, search: { [cascadingConfig.parentKeyField || parentColumnName]: parentValue, }, autoFilter: true, }); const items = response.data?.data?.data || []; const options = items.map((item: any) => ({ value: item[cascadingConfig.valueField || "id"], label: item[cascadingConfig.labelField || "name"], })); setCascadingOptions((prev) => ({ ...prev, [cacheKey]: options, })); console.log("โœ… Cascading options ๋กœ๋”ฉ ์™„๋ฃŒ:", { columnName, parentValue, count: options.length }); } catch (error) { console.error("โŒ Cascading options ๋กœ๋”ฉ ์‹คํŒจ:", error); setCascadingOptions((prev) => ({ ...prev, [cacheKey]: [], })); } finally { setLoadingCascading((prev) => ({ ...prev, [cacheKey]: false })); } }, [tableConfig, cascadingOptions, loadingCascading], ); // ๐Ÿ†• Cascading Lookups: ํŠน์ • ์ปฌ๋Ÿผ์˜ ์˜ต์…˜ ๊ฐ€์ ธ์˜ค๊ธฐ const getCascadingOptions = useCallback( (columnName: string, row: any): { value: string; label: string }[] => { const cascadingConfig = (tableConfig as any).cascadingLookups?.[columnName]; if (!cascadingConfig) return []; const parentValue = row[cascadingConfig.parentColumn]; if (parentValue === undefined || parentValue === null) return []; const cacheKey = `${columnName}_${parentValue}`; return cascadingOptions[cacheKey] || []; }, [tableConfig, cascadingOptions], ); // ๐Ÿ†• Virtual Scrolling: virtualScrollInfo๋Š” displayData ์ •์˜ ์ดํ›„๋กœ ์ด๋™๋จ (์•„๋ž˜ ์ฐธ์กฐ) // ๐Ÿ†• Virtual Scrolling: ์Šคํฌ๋กค ํ•ธ๋“ค๋Ÿฌ const handleVirtualScroll = useCallback( (e: React.UIEvent) => { if (!isVirtualScrollEnabled) return; setScrollTop(e.currentTarget.scrollTop); }, [isVirtualScrollEnabled], ); // ๐Ÿ†• State Persistence: ํ†ตํ•ฉ ์ƒํƒœ ์ €์žฅ const saveTableState = useCallback(() => { if (!tableStateKey) return; const state = { columnWidths, columnOrder, sortColumn, sortDirection, groupByColumns, frozenColumns, frozenColumnCount, // ํ‹€๊ณ ์ • ์ปฌ๋Ÿผ ์ˆ˜ ์ €์žฅ showGridLines, headerFilters: Object.fromEntries( Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set)]), ), pageSize: localPageSize, timestamp: Date.now(), }; try { localStorage.setItem(tableStateKey, JSON.stringify(state)); } catch (error) { console.error("โŒ ํ…Œ์ด๋ธ” ์ƒํƒœ ์ €์žฅ ์‹คํŒจ:", error); } }, [ tableStateKey, columnWidths, columnOrder, sortColumn, sortDirection, groupByColumns, frozenColumns, frozenColumnCount, showGridLines, headerFilters, localPageSize, ]); // ๐Ÿ†• State Persistence: ํ†ตํ•ฉ ์ƒํƒœ ๋ณต์› const loadTableState = useCallback(() => { if (!tableStateKey) return; try { const saved = localStorage.getItem(tableStateKey); if (!saved) return; const state = JSON.parse(saved); if (state.columnWidths) setColumnWidths(state.columnWidths); if (state.columnOrder) setColumnOrder(state.columnOrder); if (state.sortColumn !== undefined) setSortColumn(state.sortColumn); if (state.sortDirection) setSortDirection(state.sortDirection); if (state.groupByColumns) setGroupByColumns(state.groupByColumns); if (state.frozenColumns) { // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์ด ํ•ญ์ƒ ํฌํ•จ๋˜๋„๋ก ๋ณด์žฅ const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? "__checkbox__" : null; const restoredFrozenColumns = checkboxColumn && !state.frozenColumns.includes(checkboxColumn) ? [checkboxColumn, ...state.frozenColumns] : state.frozenColumns; setFrozenColumns(restoredFrozenColumns); } if (state.frozenColumnCount !== undefined) setFrozenColumnCount(state.frozenColumnCount); // ํ‹€๊ณ ์ • ์ปฌ๋Ÿผ ์ˆ˜ ๋ณต์› if (state.showGridLines !== undefined) setShowGridLines(state.showGridLines); if (state.headerFilters) { const filters: Record> = {}; Object.entries(state.headerFilters).forEach(([key, values]) => { filters[key] = new Set(values as string[]); }); setHeaderFilters(filters); } } catch (error) { console.error("โŒ ํ…Œ์ด๋ธ” ์ƒํƒœ ๋ณต์› ์‹คํŒจ:", error); } }, [tableStateKey]); // ๐Ÿ†• State Persistence: ์ƒํƒœ ์ดˆ๊ธฐํ™” const resetTableState = useCallback(() => { if (!tableStateKey) return; try { localStorage.removeItem(tableStateKey); setColumnWidths({}); setColumnOrder([]); setSortColumn(null); setSortDirection("asc"); setGroupByColumns([]); setFrozenColumns([]); setShowGridLines(true); setHeaderFilters({}); toast.success("ํ…Œ์ด๋ธ” ์„ค์ •์ด ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } catch (error) { console.error("โŒ ํ…Œ์ด๋ธ” ์ƒํƒœ ์ดˆ๊ธฐํ™” ์‹คํŒจ:", error); } }, [tableStateKey]); // ๐Ÿ†• State Persistence: ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์ƒํƒœ ๋ณต์› useEffect(() => { loadTableState(); }, [tableStateKey]); // loadTableState๋Š” ์˜์กด์„ฑ์—์„œ ์ œ์™ธ (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) // ๐Ÿ†• Real-Time Updates: WebSocket ์—ฐ๊ฒฐ const connectWebSocket = useCallback(() => { if (!isRealTimeEnabled || !tableConfig.selectedTable) return; const wsUrl = (tableConfig as any).wsUrl || `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws/table/${tableConfig.selectedTable}`; try { setWsConnectionStatus("connecting"); wsRef.current = new WebSocket(wsUrl); wsRef.current.onopen = () => { setWsConnectionStatus("connected"); console.log("โœ… WebSocket ์—ฐ๊ฒฐ๋จ:", tableConfig.selectedTable); }; wsRef.current.onmessage = (event) => { try { const message = JSON.parse(event.data); console.log("๐Ÿ“จ WebSocket ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ :", message); switch (message.type) { case "insert": // ์ƒˆ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ setRefreshTrigger((prev) => prev + 1); toast.info("์ƒˆ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); break; case "update": // ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ setRefreshTrigger((prev) => prev + 1); toast.info("๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); break; case "delete": // ๋ฐ์ดํ„ฐ ์‚ญ์ œ setRefreshTrigger((prev) => prev + 1); toast.info("๋ฐ์ดํ„ฐ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); break; case "refresh": // ์ „์ฒด ์ƒˆ๋กœ๊ณ ์นจ setRefreshTrigger((prev) => prev + 1); break; default: console.log("์•Œ ์ˆ˜ ์—†๋Š” ๋ฉ”์‹œ์ง€ ํƒ€์ž…:", message.type); } } catch (error) { console.error("WebSocket ๋ฉ”์‹œ์ง€ ํŒŒ์‹ฑ ์˜ค๋ฅ˜:", error); } }; wsRef.current.onclose = () => { setWsConnectionStatus("disconnected"); console.log("๐Ÿ”Œ WebSocket ์—ฐ๊ฒฐ ์ข…๋ฃŒ"); // ์ž๋™ ์žฌ์—ฐ๊ฒฐ (5์ดˆ ํ›„) if (isRealTimeEnabled) { reconnectTimeoutRef.current = setTimeout(() => { console.log("๐Ÿ”„ WebSocket ์žฌ์—ฐ๊ฒฐ ์‹œ๋„..."); connectWebSocket(); }, 5000); } }; wsRef.current.onerror = (error) => { console.error("โŒ WebSocket ์˜ค๋ฅ˜:", error); setWsConnectionStatus("disconnected"); }; } catch (error) { console.error("WebSocket ์—ฐ๊ฒฐ ์‹คํŒจ:", error); setWsConnectionStatus("disconnected"); } }, [isRealTimeEnabled, tableConfig.selectedTable]); // ๐Ÿ†• Real-Time Updates: ์—ฐ๊ฒฐ ๊ด€๋ฆฌ useEffect(() => { if (isRealTimeEnabled) { connectWebSocket(); } return () => { // ์ •๋ฆฌ if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } if (wsRef.current) { wsRef.current.close(); wsRef.current = null; } }; }, [isRealTimeEnabled, tableConfig.selectedTable]); // ๐Ÿ†• State Persistence: ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์ €์žฅ (๋””๋ฐ”์šด์Šค) useEffect(() => { const timeoutId = setTimeout(() => { saveTableState(); }, 1000); // 1์ดˆ ํ›„ ์ €์žฅ (๋””๋ฐ”์šด์Šค) return () => clearTimeout(timeoutId); }, [ columnWidths, columnOrder, sortColumn, sortDirection, groupByColumns, frozenColumns, showGridLines, headerFilters, ]); // ๐Ÿ†• Clipboard: ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ const handleCopy = useCallback(async () => { try { // ์„ ํƒ๋œ ํ–‰ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ let copyData: any[]; if (selectedRows.size > 0) { // ์„ ํƒ๋œ ํ–‰๋งŒ copyData = filteredData.filter((row, index) => { const rowKey = getRowKey(row, index); return selectedRows.has(rowKey); }); } else if (focusedCell) { // ํฌ์ปค์Šค๋œ ์…€๋งŒ const row = filteredData[focusedCell.rowIndex]; if (row) { const column = visibleColumns[focusedCell.colIndex]; const value = row[column?.columnName]; await navigator.clipboard.writeText(String(value ?? "")); toast.success("์…€ ๋ณต์‚ฌ๋จ"); return; } return; } else { toast.info("๋ณต์‚ฌํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); return; } // TSV ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ (ํƒญ์œผ๋กœ ๊ตฌ๋ถ„) const exportColumns = visibleColumns.filter((c) => c.columnName !== "__checkbox__"); const headers = exportColumns.map((c) => columnLabels[c.columnName] || c.columnName); const rows = copyData.map((row) => exportColumns .map((c) => { const value = row[c.columnName]; return value !== null && value !== undefined ? String(value).replace(/\t/g, " ").replace(/\n/g, " ") : ""; }) .join("\t"), ); const tsvContent = [headers.join("\t"), ...rows].join("\n"); await navigator.clipboard.writeText(tsvContent); toast.success(`${copyData.length}ํ–‰ ๋ณต์‚ฌ๋จ`); console.log("โœ… ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ:", copyData.length, "ํ–‰"); } catch (error) { console.error("โŒ ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ ์‹คํŒจ:", error); toast.error("๋ณต์‚ฌ ์‹คํŒจ"); } }, [selectedRows, filteredData, focusedCell, visibleColumns, columnLabels, getRowKey]); // ๐Ÿ†• ์ „์ฒด ํ–‰ ์„ ํƒ const handleSelectAllRows = useCallback(() => { if (selectedRows.size === filteredData.length) { // ์ „์ฒด ํ•ด์ œ setSelectedRows(new Set()); setIsAllSelected(false); } else { // ์ „์ฒด ์„ ํƒ const allKeys = new Set(filteredData.map((row, index) => getRowKey(row, index))); setSelectedRows(allKeys); setIsAllSelected(true); } }, [selectedRows.size, filteredData, getRowKey]); // ๐Ÿ†• Context Menu: ์—ด๊ธฐ const handleContextMenu = useCallback((e: React.MouseEvent, rowIndex: number, colIndex: number, row: any) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, rowIndex, colIndex, row, }); }, []); // ๐Ÿ†• Context Menu: ๋‹ซ๊ธฐ const closeContextMenu = useCallback(() => { setContextMenu(null); }, []); // ๐Ÿ†• Context Menu: ์™ธ๋ถ€ ํด๋ฆญ ์‹œ ๋‹ซ๊ธฐ useEffect(() => { if (contextMenu) { const handleClick = () => closeContextMenu(); document.addEventListener("click", handleClick); return () => document.removeEventListener("click", handleClick); } }, [contextMenu, closeContextMenu]); // ๐Ÿ†• Search Panel: ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ์‹คํ–‰ const executeGlobalSearch = useCallback( (term: string) => { if (!term.trim()) { setSearchHighlights(new Set()); return; } const lowerTerm = term.toLowerCase(); const highlights = new Set(); filteredData.forEach((row, rowIndex) => { visibleColumns.forEach((col, colIndex) => { const value = row[col.columnName]; if (value !== null && value !== undefined) { const strValue = String(value).toLowerCase(); if (strValue.includes(lowerTerm)) { highlights.add(`${rowIndex}-${colIndex}`); } } }); }); setSearchHighlights(highlights); // ์ฒซ ๋ฒˆ์งธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋กœ ํฌ์ปค์Šค ์ด๋™ if (highlights.size > 0) { const firstHighlight = Array.from(highlights)[0]; const [rowIdx, colIdx] = firstHighlight.split("-").map(Number); setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx }); toast.success(`${highlights.size}๊ฐœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ`); } else { toast.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); } }, [filteredData, visibleColumns], ); // ๐Ÿ†• Search Panel: ๋‹ค์Œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋กœ ์ด๋™ const goToNextSearchResult = useCallback(() => { if (searchHighlights.size === 0) return; const highlightArray = Array.from(searchHighlights).sort((a, b) => { const [aRow, aCol] = a.split("-").map(Number); const [bRow, bCol] = b.split("-").map(Number); if (aRow !== bRow) return aRow - bRow; return aCol - bCol; }); if (!focusedCell) { const [rowIdx, colIdx] = highlightArray[0].split("-").map(Number); setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx }); return; } const currentKey = `${focusedCell.rowIndex}-${focusedCell.colIndex}`; const currentIndex = highlightArray.indexOf(currentKey); const nextIndex = (currentIndex + 1) % highlightArray.length; const [rowIdx, colIdx] = highlightArray[nextIndex].split("-").map(Number); setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx }); }, [searchHighlights, focusedCell]); // ๐Ÿ†• Search Panel: ์ด์ „ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋กœ ์ด๋™ const goToPrevSearchResult = useCallback(() => { if (searchHighlights.size === 0) return; const highlightArray = Array.from(searchHighlights).sort((a, b) => { const [aRow, aCol] = a.split("-").map(Number); const [bRow, bCol] = b.split("-").map(Number); if (aRow !== bRow) return aRow - bRow; return aCol - bCol; }); if (!focusedCell) { const lastIdx = highlightArray.length - 1; const [rowIdx, colIdx] = highlightArray[lastIdx].split("-").map(Number); setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx }); return; } const currentKey = `${focusedCell.rowIndex}-${focusedCell.colIndex}`; const currentIndex = highlightArray.indexOf(currentKey); const prevIndex = currentIndex <= 0 ? highlightArray.length - 1 : currentIndex - 1; const [rowIdx, colIdx] = highlightArray[prevIndex].split("-").map(Number); setFocusedCell({ rowIndex: rowIdx, colIndex: colIdx }); }, [searchHighlights, focusedCell]); // ๐Ÿ†• Search Panel: ๊ฒ€์ƒ‰ ์ดˆ๊ธฐํ™” const clearGlobalSearch = useCallback(() => { setGlobalSearchTerm(""); setSearchHighlights(new Set()); setIsSearchPanelOpen(false); }, []); // ๐Ÿ†• Filter Builder: ์กฐ๊ฑด ์ถ”๊ฐ€ const addFilterCondition = useCallback((groupId: string, defaultColumn?: string) => { setFilterGroups((prev) => prev.map((group) => group.id === groupId ? { ...group, conditions: [ ...group.conditions, { id: `cond-${Date.now()}`, column: defaultColumn || "", operator: "contains" as const, value: "", }, ], } : group, ), ); }, []); // ๐Ÿ†• Filter Builder: ์กฐ๊ฑด ์‚ญ์ œ const removeFilterCondition = useCallback((groupId: string, conditionId: string) => { setFilterGroups((prev) => prev.map((group) => group.id === groupId ? { ...group, conditions: group.conditions.filter((c) => c.id !== conditionId), } : group, ), ); }, []); // ๐Ÿ†• Filter Builder: ์กฐ๊ฑด ์—…๋ฐ์ดํŠธ const updateFilterCondition = useCallback( (groupId: string, conditionId: string, field: keyof FilterCondition, value: string) => { setFilterGroups((prev) => prev.map((group) => group.id === groupId ? { ...group, conditions: group.conditions.map((c) => (c.id === conditionId ? { ...c, [field]: value } : c)), } : group, ), ); }, [], ); // ๐Ÿ†• Filter Builder: ๊ทธ๋ฃน ์ถ”๊ฐ€ const addFilterGroup = useCallback((defaultColumn?: string) => { setFilterGroups((prev) => [ ...prev, { id: `group-${Date.now()}`, logic: "AND" as const, conditions: [ { id: `cond-${Date.now()}`, column: defaultColumn || "", operator: "contains" as const, value: "", }, ], }, ]); }, []); // ๐Ÿ†• Filter Builder: ๊ทธ๋ฃน ์‚ญ์ œ const removeFilterGroup = useCallback((groupId: string) => { setFilterGroups((prev) => prev.filter((g) => g.id !== groupId)); }, []); // ๐Ÿ†• Filter Builder: ๊ทธ๋ฃน ๋กœ์ง ๋ณ€๊ฒฝ const updateGroupLogic = useCallback((groupId: string, logic: "AND" | "OR") => { setFilterGroups((prev) => prev.map((group) => (group.id === groupId ? { ...group, logic } : group))); }, []); // ๐Ÿ†• Filter Builder: ํ•„ํ„ฐ ์ ์šฉ const applyFilterBuilder = useCallback(() => { // ์œ ํšจํ•œ ์กฐ๊ฑด ๊ฐœ์ˆ˜ ๊ณ„์‚ฐ let validConditions = 0; filterGroups.forEach((group) => { group.conditions.forEach((cond) => { if (cond.column && (cond.operator === "isEmpty" || cond.operator === "isNotEmpty" || cond.value)) { validConditions++; } }); }); setActiveFilterCount(validConditions); setIsFilterBuilderOpen(false); toast.success(`${validConditions}๊ฐœ ํ•„ํ„ฐ ์กฐ๊ฑด ์ ์šฉ๋จ`); }, [filterGroups]); // ๐Ÿ†• Filter Builder: ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” const clearFilterBuilder = useCallback(() => { setFilterGroups([]); setActiveFilterCount(0); toast.info("ํ•„ํ„ฐ ์ดˆ๊ธฐํ™”๋จ"); }, []); // ๐Ÿ†• Filter Builder: ์กฐ๊ฑด ํ‰๊ฐ€ ํ•จ์ˆ˜ const evaluateCondition = useCallback((value: any, condition: FilterCondition): boolean => { const strValue = value !== null && value !== undefined ? String(value).toLowerCase() : ""; const condValue = condition.value.toLowerCase(); switch (condition.operator) { case "equals": return strValue === condValue; case "notEquals": return strValue !== condValue; case "contains": return strValue.includes(condValue); case "notContains": return !strValue.includes(condValue); case "startsWith": return strValue.startsWith(condValue); case "endsWith": return strValue.endsWith(condValue); case "greaterThan": return parseFloat(strValue) > parseFloat(condValue); case "lessThan": return parseFloat(strValue) < parseFloat(condValue); case "greaterOrEqual": return parseFloat(strValue) >= parseFloat(condValue); case "lessOrEqual": return parseFloat(strValue) <= parseFloat(condValue); case "isEmpty": return strValue === "" || value === null || value === undefined; case "isNotEmpty": return strValue !== "" && value !== null && value !== undefined; default: return true; } }, []); // ๐Ÿ†• Filter Builder: ํ–‰์ด ํ•„ํ„ฐ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธ const rowPassesFilterBuilder = useCallback( (row: any): boolean => { if (filterGroups.length === 0) return true; // ๋ชจ๋“  ๊ทธ๋ฃน์ด AND๋กœ ์—ฐ๊ฒฐ๋จ (๊ทธ๋ฃน ๊ฐ„) return filterGroups.every((group) => { const validConditions = group.conditions.filter( (c) => c.column && (c.operator === "isEmpty" || c.operator === "isNotEmpty" || c.value), ); if (validConditions.length === 0) return true; if (group.logic === "AND") { return validConditions.every((cond) => evaluateCondition(row[cond.column], cond)); } else { return validConditions.some((cond) => evaluateCondition(row[cond.column], cond)); } }); }, [filterGroups, evaluateCondition], ); // ๐Ÿ†• ์ปฌ๋Ÿผ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ const handleColumnDragStart = useCallback( (e: React.DragEvent, index: number) => { if (!isColumnDragEnabled) return; setDraggedColumnIndex(index); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", `col-${index}`); }, [isColumnDragEnabled], ); // ๐Ÿ†• ์ปฌ๋Ÿผ ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„ const handleColumnDragOver = useCallback( (e: React.DragEvent, index: number) => { if (!isColumnDragEnabled || draggedColumnIndex === null) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (index !== draggedColumnIndex) { setDropTargetColumnIndex(index); } }, [isColumnDragEnabled, draggedColumnIndex], ); // ๐Ÿ†• ์ปฌ๋Ÿผ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ const handleColumnDragEnd = useCallback(() => { setDraggedColumnIndex(null); setDropTargetColumnIndex(null); }, []); // ๐Ÿ†• ์ปฌ๋Ÿผ ๋“œ๋กญ const handleColumnDrop = useCallback( (e: React.DragEvent, targetIndex: number) => { e.preventDefault(); if (!isColumnDragEnabled || draggedColumnIndex === null || draggedColumnIndex === targetIndex) { handleColumnDragEnd(); return; } // ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ const newOrder = [...(columnOrder.length > 0 ? columnOrder : visibleColumns.map((c) => c.columnName))]; const [movedColumn] = newOrder.splice(draggedColumnIndex, 1); newOrder.splice(targetIndex, 0, movedColumn); setColumnOrder(newOrder); toast.info("์ปฌ๋Ÿผ ์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); console.log("โœ… ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ:", { from: draggedColumnIndex, to: targetIndex }); handleColumnDragEnd(); }, [isColumnDragEnabled, draggedColumnIndex, columnOrder, visibleColumns, handleColumnDragEnd], ); // ๐Ÿ†• ํ–‰ ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ const handleRowDragStart = useCallback( (e: React.DragEvent, index: number) => { if (!isDragEnabled) return; setDraggedRowIndex(index); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(index)); // ๋“œ๋ž˜๊ทธ ์ด๋ฏธ์ง€ ์„ค์ • (๋ฐ˜ํˆฌ๋ช…) const dragImage = e.currentTarget.cloneNode(true) as HTMLElement; dragImage.style.opacity = "0.5"; dragImage.style.position = "absolute"; dragImage.style.top = "-1000px"; document.body.appendChild(dragImage); e.dataTransfer.setDragImage(dragImage, 0, 0); setTimeout(() => document.body.removeChild(dragImage), 0); }, [isDragEnabled], ); // ๐Ÿ†• ํ–‰ ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„ const handleRowDragOver = useCallback( (e: React.DragEvent, index: number) => { if (!isDragEnabled || draggedRowIndex === null) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (index !== draggedRowIndex) { setDropTargetIndex(index); } }, [isDragEnabled, draggedRowIndex], ); // ๐Ÿ†• ํ–‰ ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ const handleRowDragEnd = useCallback(() => { setDraggedRowIndex(null); setDropTargetIndex(null); }, []); // ๐Ÿ†• ํ–‰ ๋“œ๋กญ const handleRowDrop = useCallback( async (e: React.DragEvent, targetIndex: number) => { e.preventDefault(); if (!isDragEnabled || draggedRowIndex === null || draggedRowIndex === targetIndex) { handleRowDragEnd(); return; } try { // ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์žฌ์ •๋ ฌ const newData = [...filteredData]; const [movedRow] = newData.splice(draggedRowIndex, 1); newData.splice(targetIndex, 0, movedRow); // ์„œ๋ฒ„์— ์ˆœ์„œ ์ €์žฅ (order_index ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) const orderField = (tableConfig as any).orderField || "order_index"; const hasOrderField = newData[0] && orderField in newData[0]; if (hasOrderField && tableConfig.selectedTable) { const { apiClient } = await import("@/lib/api/client"); const primaryKeyField = tableConfig.primaryKey || "id"; // ์˜ํ–ฅ๋ฐ›๋Š” ํ–‰๋“ค์˜ ์ˆœ์„œ ์—…๋ฐ์ดํŠธ const updates = newData.map((row, idx) => ({ tableName: tableConfig.selectedTable, keyField: primaryKeyField, keyValue: row[primaryKeyField], updateField: orderField, updateValue: idx + 1, })); // ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ await Promise.all(updates.map((update) => apiClient.put("/dynamic-form/update-field", update))); toast.success("์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); setRefreshTrigger((prev) => prev + 1); } else { // ๋กœ์ปฌ์—์„œ๋งŒ ์ˆœ์„œ ๋ณ€๊ฒฝ (์ €์žฅ ์•ˆํ•จ) toast.info("์ˆœ์„œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (๋กœ์ปฌ๋งŒ)"); } console.log("โœ… ํ–‰ ์ˆœ์„œ ๋ณ€๊ฒฝ:", { from: draggedRowIndex, to: targetIndex }); } catch (error) { console.error("โŒ ํ–‰ ์ˆœ์„œ ๋ณ€๊ฒฝ ์‹คํŒจ:", error); toast.error("์ˆœ์„œ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } handleRowDragEnd(); }, [isDragEnabled, draggedRowIndex, filteredData, tableConfig, handleRowDragEnd], ); // ๐Ÿ†• PDF ๋‚ด๋ณด๋‚ด๊ธฐ (์ธ์‡„์šฉ HTML ์ƒ์„ฑ) const exportToPdf = useCallback( (exportAll: boolean = true) => { try { // ๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ์„ ํƒ let exportData: any[]; if (exportAll) { exportData = filteredData; } else { exportData = filteredData.filter((row, index) => { const rowKey = getRowKey(row, index); return selectedRows.has(rowKey); }); } if (exportData.length === 0) { toast.error(exportAll ? "๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." : "์„ ํƒ๋œ ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค."); return; } // ์ปฌ๋Ÿผ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ) const exportColumns = visibleColumns.filter((col) => col.columnName !== "__checkbox__"); // ์ธ์‡„์šฉ HTML ์ƒ์„ฑ const printContent = ` ${tableLabel || tableConfig.selectedTable || "๋ฐ์ดํ„ฐ"}

${tableLabel || tableConfig.selectedTable || "๋ฐ์ดํ„ฐ ๋ชฉ๋ก"}

์ถœ๋ ฅ์ผ: ${new Date().toLocaleDateString("ko-KR")} | ์ด ${exportData.length}๊ฑด
${exportColumns.map((col) => ``).join("")} ${exportData .map( (row) => ` ${exportColumns .map((col) => { const mappedColumnName = joinColumnMapping[col.columnName] || col.columnName; let value = row[mappedColumnName]; // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ if (categoryMappings[col.columnName] && value !== null && value !== undefined) { const mapping = categoryMappings[col.columnName][String(value)]; if (mapping) value = mapping.label; } const meta = columnMeta[col.columnName]; const inputType = meta?.inputType || (col as any).inputType; const isNumeric = inputType === "number" || inputType === "decimal"; return ``; }) .join("")} `, ) .join("")}
${columnLabels[col.columnName] || col.columnName}
${value ?? ""}
`; // ์ƒˆ ์ฐฝ์—์„œ ์ธ์‡„ const printWindow = window.open("", "_blank"); if (printWindow) { printWindow.document.write(printContent); printWindow.document.close(); printWindow.onload = () => { printWindow.print(); }; toast.success("์ธ์‡„ ์ฐฝ์ด ์—ด๋ ธ์Šต๋‹ˆ๋‹ค."); } else { toast.error("ํŒ์—…์ด ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํŒ์—…์„ ํ—ˆ์šฉํ•ด์ฃผ์„ธ์š”."); } } catch (error) { console.error("โŒ PDF ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํŒจ:", error); toast.error("PDF ๋‚ด๋ณด๋‚ด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } }, [ filteredData, selectedRows, visibleColumns, columnLabels, joinColumnMapping, categoryMappings, columnMeta, tableLabel, tableConfig.selectedTable, getRowKey, ], ); // ๐Ÿ†• ํŽธ์ง‘ ์ค‘ ํ‚ค๋ณด๋“œ ํ•ธ๋“ค๋Ÿฌ (๊ฐ„๋‹จ ๋ฒ„์ „ - Tab ์ด๋™์€ visibleColumns ์ •์˜ ํ›„ ์ฒ˜๋ฆฌ) const handleEditKeyDown = useCallback( (e: React.KeyboardEvent) => { switch (e.key) { case "Enter": e.preventDefault(); saveEditing(); break; case "Escape": e.preventDefault(); cancelEditing(); break; case "Tab": e.preventDefault(); saveEditing(); // Tab ์ด๋™์€ ํŽธ์ง‘ ์ €์žฅ ํ›„ ํ…Œ์ด๋ธ” ํ‚ค๋ณด๋“œ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌ break; } }, [saveEditing, cancelEditing], ); // ๐Ÿ†• ํŽธ์ง‘ ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ๋‚˜ํƒ€๋‚˜๋ฉด ์ž๋™ ํฌ์ปค์Šค useEffect(() => { if (editingCell && editInputRef.current) { editInputRef.current.focus(); // select()๋Š” input ์š”์†Œ์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (select ์š”์†Œ์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€) if (typeof editInputRef.current.select === "function") { editInputRef.current.select(); } } }, [editingCell]); // ๐Ÿ†• ํฌ์ปค์Šค๋œ ์…€๋กœ ์Šคํฌ๋กค useEffect(() => { if (focusedCell && tableContainerRef.current) { const focusedCellElement = tableContainerRef.current.querySelector( `[data-row="${focusedCell.rowIndex}"][data-col="${focusedCell.colIndex}"]`, ) as HTMLElement; if (focusedCellElement) { focusedCellElement.scrollIntoView({ block: "nearest", inline: "nearest" }); } } }, [focusedCell]); // ์ปฌ๋Ÿผ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๊ธฐ๋Šฅ ์ œ๊ฑฐ๋จ (ํ…Œ์ด๋ธ” ์˜ต์…˜ ๋ชจ๋‹ฌ์—์„œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ) const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); onClick?.(); }; // ======================================== // ์ปฌ๋Ÿผ ๊ด€๋ จ (visibleColumns๋Š” ์ƒ๋‹จ์—์„œ ์ •์˜๋จ) // ======================================== // ๐Ÿ†• 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(",")]); // ์˜์กด์„ฑ ๋‹จ์ˆœํ™” // ๐Ÿ†• ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ํ•ธ๋“ค๋Ÿฌ (visibleColumns ์ •์˜ ํ›„์— ๋ฐฐ์น˜) const handleTableKeyDown = useCallback( (e: React.KeyboardEvent) => { // ํŽธ์ง‘ ์ค‘์ผ ๋•Œ๋Š” ํ…Œ์ด๋ธ” ํ‚ค๋ณด๋“œ ํ•ธ๋“ค๋Ÿฌ ๋ฌด์‹œ (ํŽธ์ง‘ ์ž…๋ ฅ์—์„œ ์ฒ˜๋ฆฌ) if (editingCell) return; if (!focusedCell || data.length === 0) return; const { rowIndex, colIndex } = focusedCell; const maxRowIndex = data.length - 1; const maxColIndex = visibleColumns.length - 1; switch (e.key) { case "ArrowUp": e.preventDefault(); if (rowIndex > 0) { setFocusedCell({ rowIndex: rowIndex - 1, colIndex }); } break; case "ArrowDown": e.preventDefault(); if (rowIndex < maxRowIndex) { setFocusedCell({ rowIndex: rowIndex + 1, colIndex }); } break; case "ArrowLeft": e.preventDefault(); if (colIndex > 0) { setFocusedCell({ rowIndex, colIndex: colIndex - 1 }); } break; case "ArrowRight": e.preventDefault(); if (colIndex < maxColIndex) { setFocusedCell({ rowIndex, colIndex: colIndex + 1 }); } break; case "Enter": e.preventDefault(); // ํ˜„์žฌ ํ–‰ ์„ ํƒ/ํ•ด์ œ const enterRow = data[rowIndex]; if (enterRow) { const rowKey = getRowKey(enterRow, rowIndex); const isCurrentlySelected = selectedRows.has(rowKey); handleRowSelection(rowKey, !isCurrentlySelected); } break; case " ": // Space e.preventDefault(); // ์ฒดํฌ๋ฐ•์Šค ํ† ๊ธ€ const spaceRow = data[rowIndex]; if (spaceRow) { const currentRowKey = getRowKey(spaceRow, rowIndex); const isChecked = selectedRows.has(currentRowKey); handleRowSelection(currentRowKey, !isChecked); } break; case "F2": // ๐Ÿ†• F2: ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… e.preventDefault(); { const col = visibleColumns[colIndex]; if (col && col.columnName !== "__checkbox__") { // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ if (col.editable === false) { toast.warning(`'${col.displayName || col.columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); break; } const row = data[rowIndex]; const mappedCol = joinColumnMapping[col.columnName] || col.columnName; const val = row?.[mappedCol]; setEditingCell({ rowIndex, colIndex, columnName: col.columnName, originalValue: val, }); setEditingValue(val !== null && val !== undefined ? String(val) : ""); } } break; case "b": case "B": // ๐Ÿ†• Ctrl+B: ๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ํ† ๊ธ€ if (e.ctrlKey) { e.preventDefault(); setEditMode((prev) => { const newMode = prev === "immediate" ? "batch" : "immediate"; if (newMode === "immediate" && pendingChanges.size > 0) { // ์ฆ‰์‹œ ๋ชจ๋“œ๋กœ ์ „ํ™˜ ์‹œ ์ €์žฅ๋˜์ง€ ์•Š์€ ๋ณ€๊ฒฝ์‚ฌํ•ญ ๊ฒฝ๊ณ  const confirmDiscard = window.confirm( `์ €์žฅ๋˜์ง€ ์•Š์€ ${pendingChanges.size}๊ฐœ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ทจ์†Œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`, ); if (confirmDiscard) { setPendingChanges(new Map()); setLocalEditedData({}); toast.info("๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ์ข…๋ฃŒ"); return "immediate"; } return "batch"; } toast.info(newMode === "batch" ? "๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ ์‹œ์ž‘ (Ctrl+B๋กœ ์ข…๋ฃŒ)" : "์ฆ‰์‹œ ์ €์žฅ ๋ชจ๋“œ"); return newMode; }); } break; case "s": case "S": // ๐Ÿ†• Ctrl+S: ๋ฐฐ์น˜ ์ €์žฅ if (e.ctrlKey && editMode === "batch") { e.preventDefault(); saveBatchChanges(); } break; case "c": case "C": // ๐Ÿ†• Ctrl+C: ์„ ํƒ๋œ ํ–‰/์…€ ๋ณต์‚ฌ if (e.ctrlKey) { e.preventDefault(); handleCopy(); } break; case "v": case "V": // ๐Ÿ†• Ctrl+V: ๋ถ™์—ฌ๋„ฃ๊ธฐ (ํŽธ์ง‘ ์ค‘์ธ ๊ฒฝ์šฐ๋งŒ) if (e.ctrlKey && editingCell) { // ๊ธฐ๋ณธ ๋™์ž‘ ํ—ˆ์šฉ (input์—์„œ ์ฒ˜๋ฆฌ) } break; case "a": case "A": // ๐Ÿ†• Ctrl+A: ์ „์ฒด ์„ ํƒ if (e.ctrlKey) { e.preventDefault(); handleSelectAllRows(); } break; case "f": case "F": // ๐Ÿ†• Ctrl+F: ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ํŒจ๋„ ์—ด๊ธฐ if (e.ctrlKey) { e.preventDefault(); setIsSearchPanelOpen(true); } break; case "F3": // ๐Ÿ†• F3: ๋‹ค์Œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ / Shift+F3: ์ด์ „ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ e.preventDefault(); if (e.shiftKey) { goToPrevSearchResult(); } else { goToNextSearchResult(); } break; case "Home": e.preventDefault(); if (e.ctrlKey) { // Ctrl+Home: ์ฒซ ๋ฒˆ์งธ ์…€๋กœ setFocusedCell({ rowIndex: 0, colIndex: 0 }); } else { // Home: ํ˜„์žฌ ํ–‰์˜ ์ฒซ ๋ฒˆ์งธ ์…€๋กœ setFocusedCell({ rowIndex, colIndex: 0 }); } break; case "End": e.preventDefault(); if (e.ctrlKey) { // Ctrl+End: ๋งˆ์ง€๋ง‰ ์…€๋กœ setFocusedCell({ rowIndex: maxRowIndex, colIndex: maxColIndex }); } else { // End: ํ˜„์žฌ ํ–‰์˜ ๋งˆ์ง€๋ง‰ ์…€๋กœ setFocusedCell({ rowIndex, colIndex: maxColIndex }); } break; case "PageUp": e.preventDefault(); // 10ํ–‰ ์œ„๋กœ setFocusedCell({ rowIndex: Math.max(0, rowIndex - 10), colIndex }); break; case "PageDown": e.preventDefault(); // 10ํ–‰ ์•„๋ž˜๋กœ setFocusedCell({ rowIndex: Math.min(maxRowIndex, rowIndex + 10), colIndex }); break; case "Escape": e.preventDefault(); // ํฌ์ปค์Šค ํ•ด์ œ setFocusedCell(null); break; case "Tab": e.preventDefault(); if (e.shiftKey) { // Shift+Tab: ์ด์ „ ์…€ if (colIndex > 0) { setFocusedCell({ rowIndex, colIndex: colIndex - 1 }); } else if (rowIndex > 0) { setFocusedCell({ rowIndex: rowIndex - 1, colIndex: maxColIndex }); } } else { // Tab: ๋‹ค์Œ ์…€ if (colIndex < maxColIndex) { setFocusedCell({ rowIndex, colIndex: colIndex + 1 }); } else if (rowIndex < maxRowIndex) { setFocusedCell({ rowIndex: rowIndex + 1, colIndex: 0 }); } } break; default: // ๐Ÿ†• ์ง์ ‘ ํƒ€์ดํ•‘์œผ๋กœ ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… (์˜๋ฌธ์ž, ์ˆซ์ž, ํ•œ๊ธ€ ๋“ฑ) if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { const column = visibleColumns[colIndex]; if (column && column.columnName !== "__checkbox__") { // ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ์ฒดํฌ if (column.editable === false) { toast.warning(`'${column.displayName || column.columnName}' ์ปฌ๋Ÿผ์€ ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`); break; } e.preventDefault(); // ํŽธ์ง‘ ์‹œ์ž‘ (ํ˜„์žฌ ํ‚ค๋ฅผ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ) const row = data[rowIndex]; const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; const value = row?.[mappedColumnName]; setEditingCell({ rowIndex, colIndex, columnName: column.columnName, originalValue: value, }); setEditingValue(e.key); // ์ž…๋ ฅํ•œ ํ‚ค๋กœ ์‹œ์ž‘ } } break; } }, [editingCell, focusedCell, data, visibleColumns, joinColumnMapping, selectedRows, getRowKey, handleRowSelection], ); 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) => { // ๐ŸŽฏ ์—”ํ‹ฐํ‹ฐ ์ปฌ๋Ÿผ ํ‘œ์‹œ ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ - value๊ฐ€ null์ด์–ด๋„ rowData์—์„œ ์กฐํ•ฉ ๊ฐ€๋Šฅ // ์ด ์ฒดํฌ๋ฅผ ๊ฐ€์žฅ ๋จผ์ € ์ˆ˜ํ–‰ (null ์ฒดํฌ๋ณด๋‹ค ์•ž์—) if (column.entityDisplayConfig && rowData) { const displayColumns = column.entityDisplayConfig.displayColumns || (column.entityDisplayConfig as any).selectedColumns; const separator = column.entityDisplayConfig.separator; if (displayColumns && displayColumns.length > 0) { // ์„ ํƒ๋œ ์ปฌ๋Ÿผ๋“ค์˜ ๊ฐ’์„ ๊ตฌ๋ถ„์ž๋กœ ์กฐํ•ฉ const values = displayColumns .map((colName: string) => { // ๐ŸŽฏ ๋ฐฑ์—”๋“œ alias ๊ทœ์น™: ${sourceColumn}_${displayColumn} // ์˜ˆ: manager ์ปฌ๋Ÿผ์—์„œ user_name ์„ ํƒ ์‹œ โ†’ manager_user_name const joinedKey = `${column.columnName}_${colName}`; let cellValue = rowData[joinedKey]; // fallback: ์ง์ ‘ ์ปฌ๋Ÿผ๋ช…์œผ๋กœ ์‹œ๋„ (๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ) if (cellValue === null || cellValue === undefined) { cellValue = rowData[colName]; } if (cellValue === null || cellValue === undefined) return ""; return String(cellValue); }) .filter((v: string) => v !== ""); // ๋นˆ ๊ฐ’ ์ œ์™ธ const result = values.join(separator || " - "); if (result) { return result; // ๊ฒฐ๊ณผ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ˜ํ™˜ } // ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด ์•„๋ž˜๋กœ ๊ณ„์† ์ง„ํ–‰ (์›๋ž˜ ๊ฐ’ ์‚ฌ์šฉ) } } // value๊ฐ€ null/undefined๋ฉด "-" ๋ฐ˜ํ™˜ if (value === null || value === undefined) return "-"; // ๐ŸŽฏ writer ์ปฌ๋Ÿผ ์ž๋™ ๋ณ€ํ™˜: user_id -> user_name if (column.columnName === "writer" && rowData && rowData.writer_name) { return rowData.writer_name; } // ๐Ÿ†• ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ฉ”ํƒ€ ๋˜๋Š” ์กฐ์ธ ํ…Œ์ด๋ธ” ๋ฉ”ํƒ€์—์„œ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ const meta = columnMeta[column.columnName] || joinedColumnMeta[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"; }} /> ); } // ๐Ÿ“Ž ์ฒจ๋ถ€ํŒŒ์ผ ํƒ€์ž…: ํŒŒ์ผ ์•„์ด์ฝ˜๊ณผ ๊ฐœ์ˆ˜ ํ‘œ์‹œ // ์ปฌ๋Ÿผ๋ช…์ด 'attachments'๋ฅผ ํฌํ•จํ•˜๊ฑฐ๋‚˜, inputType์ด file/attachment์ธ ๊ฒฝ์šฐ const isAttachmentColumn = inputType === "file" || inputType === "attachment" || column.columnName === "attachments" || column.columnName?.toLowerCase().includes("attachment") || column.columnName?.toLowerCase().includes("file"); if (isAttachmentColumn) { // JSONB ๋ฐฐ์—ด ๋˜๋Š” JSON ๋ฌธ์ž์—ด ํŒŒ์‹ฑ let files: any[] = []; try { if (typeof value === "string" && value.trim()) { const parsed = JSON.parse(value); files = Array.isArray(parsed) ? parsed : []; } else if (Array.isArray(value)) { files = value; } else if (value && typeof value === "object") { // ๋‹จ์ผ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ files = [value]; } } catch (e) { // ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋นˆ ๋ฐฐ์—ด console.warn("๐Ÿ“Ž [TableList] ์ฒจ๋ถ€ํŒŒ์ผ ํŒŒ์‹ฑ ์‹คํŒจ:", { columnName: column.columnName, value, error: e }); } if (!files || files.length === 0) { return -; } // ํŒŒ์ผ ์ด๋ฆ„ ํ‘œ์‹œ (์—ฌ๋Ÿฌ ๊ฐœ๋ฉด ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„) const { Paperclip } = require("lucide-react"); const fileNames = files.map((f: any) => f.realFileName || f.real_file_name || f.name || "ํŒŒ์ผ").join(", "); return (
{fileNames} {files.length > 1 && ({files.length})}
); } // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…: ๋ฐฐ์ง€๋กœ ํ‘œ์‹œ (๋ฐฐ์ง€ ์—†์Œ ์˜ต์…˜ ์ง€์›, ๋‹ค์ค‘ ๊ฐ’ ์ง€์›) if (inputType === "category") { if (!value) return ""; // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ์ปฌ๋Ÿผ์˜ ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ ํ˜•ํƒœ๋กœ ๋งคํ•‘ ์ฐพ๊ธฐ // 1. ์›๋ž˜ ์ปฌ๋Ÿผ๋ช… (item_info.material) // 2. ์ (.) ๋’ค์˜ ์ปฌ๋Ÿผ๋ช…๋งŒ (material) let mapping = categoryMappings[column.columnName]; if (!mapping && column.columnName.includes(".")) { const simpleColumnName = column.columnName.split(".").pop(); if (simpleColumnName) { mapping = categoryMappings[simpleColumnName]; } } const { Badge } = require("@/components/ui/badge"); // ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ: ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๊ฐ’๋“ค์„ ๋ถ„๋ฆฌ const valueStr = String(value); const values = valueStr.includes(",") ? valueStr .split(",") .map((v) => v.trim()) .filter((v) => v) : [valueStr]; // ๋‹จ์ผ ๊ฐ’์ธ ๊ฒฝ์šฐ (๊ธฐ์กด ๋กœ์ง) if (values.length === 1) { const categoryData = mapping?.[values[0]]; const displayLabel = categoryData?.label || values[0]; const displayColor = categoryData?.color; // ๋ฐฐ์ง€ ์—†์Œ ์˜ต์…˜: color๊ฐ€ ์—†๊ฑฐ๋‚˜, "none"์ด๊ฑฐ๋‚˜, ๋งคํ•‘ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ํ…์ŠคํŠธ๋งŒ ํ‘œ์‹œ if (!displayColor || displayColor === "none" || !categoryData) { return {displayLabel}; } return ( {displayLabel} ); } // ๋‹ค์ค‘ ๊ฐ’์ธ ๊ฒฝ์šฐ: ์—ฌ๋Ÿฌ ๋ฐฐ์ง€ ๋ Œ๋”๋ง return (
{values.map((val, idx) => { const categoryData = mapping?.[val]; const displayLabel = categoryData?.label || val; const displayColor = categoryData?.color; // ๋ฐฐ์ง€ ์—†์Œ ์˜ต์…˜: color๊ฐ€ ์—†๊ฑฐ๋‚˜, "none"์ด๊ฑฐ๋‚˜, ๋งคํ•‘ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ํ…์ŠคํŠธ๋งŒ ํ‘œ์‹œ if (!displayColor || displayColor === "none" || !categoryData) { return ( {displayLabel} {idx < values.length - 1 && ", "} ); } 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); } // ๋‚ ์งœ ํƒ€์ž… ํฌ๋งทํŒ… (yyyy-mm-dd) if (inputType === "date" || inputType === "datetime") { if (value) { try { const date = new Date(value); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } catch { return String(value); } } return "-"; } // ์ˆซ์ž ํƒ€์ž… ํฌ๋งทํŒ… (์ฒœ๋‹จ์œ„ ๊ตฌ๋ถ„์ž ์„ค์ • ํ™•์ธ) if (inputType === "number" || inputType === "decimal") { if (value !== null && value !== undefined && value !== "") { const numValue = typeof value === "string" ? parseFloat(value) : value; if (!isNaN(numValue)) { // thousandSeparator๊ฐ€ false๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ(๊ธฐ๋ณธ๊ฐ’ true) ์ฒœ๋‹จ์œ„ ๊ตฌ๋ถ„์ž ์ ์šฉ if (column.thousandSeparator !== false) { return numValue.toLocaleString("ko-KR"); } return String(numValue); } } 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)) { // thousandSeparator๊ฐ€ false๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ(๊ธฐ๋ณธ๊ฐ’ true) ์ฒœ๋‹จ์œ„ ๊ตฌ๋ถ„์ž ์ ์šฉ if (column.thousandSeparator !== false) { return numValue.toLocaleString("ko-KR"); } return String(numValue); } } return String(value); case "date": if (value) { try { const date = new Date(value); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } catch { return value; } } return "-"; case "currency": if (typeof value === "number") { // thousandSeparator๊ฐ€ false๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ(๊ธฐ๋ณธ๊ฐ’ true) ์ฒœ๋‹จ์œ„ ๊ตฌ๋ถ„์ž ์ ์šฉ if (column.thousandSeparator !== false) { return `โ‚ฉ${value.toLocaleString()}`; } return `โ‚ฉ${value}`; } return value; case "boolean": return value ? "์˜ˆ" : "์•„๋‹ˆ์˜ค"; default: return String(value); } }, [columnMeta, joinedColumnMeta, optimizedConvertCode, categoryMappings], ); // ======================================== // useEffect ํ›… // ======================================== // ํ•„ํ„ฐ ์„ค์ • localStorage ํ‚ค ์ƒ์„ฑ (ํ™”๋ฉด๋ณ„๋กœ ๋…๋ฆฝ์ ) const filterSettingKey = useMemo(() => { if (!tableConfig.selectedTable) return null; return screenId ? `tableList_filterSettings_${tableConfig.selectedTable}_screen_${screenId}` : `tableList_filterSettings_${tableConfig.selectedTable}`; }, [tableConfig.selectedTable, screenId]); // ๊ทธ๋ฃน ์„ค์ • localStorage ํ‚ค ์ƒ์„ฑ (ํ™”๋ฉด๋ณ„๋กœ ๋…๋ฆฝ์ ) const groupSettingKey = useMemo(() => { if (!tableConfig.selectedTable) return null; return screenId ? `tableList_groupSettings_${tableConfig.selectedTable}_screen_${screenId}` : `tableList_groupSettings_${tableConfig.selectedTable}`; }, [tableConfig.selectedTable, screenId]); // ์ €์žฅ๋œ ํ•„ํ„ฐ ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ 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]); // ๊ทธ๋ฃน ์„ค์ • ์ž๋™ ์ €์žฅ (localStorage) useEffect(() => { if (!groupSettingKey) return; try { localStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns)); } catch (error) { console.error("๊ทธ๋ฃน ์„ค์ • ์ €์žฅ ์‹คํŒจ:", 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 || filteredData.length === 0) return []; const grouped = new Map(); filteredData.forEach((item) => { // ๊ทธ๋ฃน ํ‚ค ์ƒ์„ฑ: "ํ†ตํ™”:KRW > ๋‹จ์œ„:EA" const keyParts = groupByColumns.map((col) => { // ์นดํ…Œ๊ณ ๋ฆฌ/์—”ํ‹ฐํ‹ฐ ํƒ€์ž…์ธ ๊ฒฝ์šฐ _name ํ•„๋“œ ์‚ฌ์šฉ const inputType = columnMeta?.[col]?.inputType; let displayValue = item[col]; if (inputType === "category" || inputType === "entity" || inputType === "code") { // _name ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ (์˜ˆ: division_name, writer_name) const nameField = `${col}_name`; if (item[nameField] !== undefined && item[nameField] !== null) { displayValue = item[nameField]; } } const label = columnLabels[col] || col; return `${label}:${displayValue !== null && displayValue !== undefined ? displayValue : "-"}`; }); 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]; }); // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ์†Œ๊ณ„ ๊ณ„์‚ฐ const groupSummary: Record = {}; // ์ˆซ์žํ˜• ์ปฌ๋Ÿผ์— ๋Œ€ํ•ด ์†Œ๊ณ„ ๊ณ„์‚ฐ (tableConfig.columns || []).forEach((col: { columnName: string }) => { if (col.columnName === "__checkbox__") return; const colMeta = columnMeta?.[col.columnName]; const inputType = colMeta?.inputType; const isNumeric = inputType === "number" || inputType === "decimal"; if (isNumeric) { const values = items.map((item) => parseFloat(item[col.columnName])).filter((v) => !isNaN(v)); if (values.length > 0) { const sum = values.reduce((a, b) => a + b, 0); groupSummary[col.columnName] = { sum, avg: sum / values.length, count: values.length, }; } } }); return { groupKey, groupValues, items, count: items.length, summary: groupSummary, // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ์†Œ๊ณ„ }; }); }, [data, groupByColumns, columnLabels, columnMeta, tableConfig.columns]); // ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ๋œ ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ (FilterPanel์—์„œ ์„ค์ •ํ•œ ๊ฒฝ์šฐ) const summedData = useMemo(() => { // ๊ทธ๋ฃนํ•‘์ด ๋น„ํ™œ์„ฑํ™”๋˜์—ˆ๊ฑฐ๋‚˜ ๊ทธ๋ฃน ๊ธฐ์ค€ ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ if (!groupSumConfig?.enabled || !groupSumConfig?.groupByColumn) { return filteredData; } console.log("๐Ÿ” [ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ] ๊ทธ๋ฃนํ•ฉ์‚ฐ ์ ์šฉ:", groupSumConfig); const groupByColumn = groupSumConfig.groupByColumn; const groupMap = new Map(); // ์กฐ์ธ ์ปฌ๋Ÿผ์ธ์ง€ ํ™•์ธํ•˜๊ณ  ์‹ค์ œ ํ‚ค ์ถ”๋ก  const getActualKey = (columnName: string, item: any): string => { if (columnName.includes(".")) { const [refTable, fieldName] = columnName.split("."); const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); const exactKey = `${inferredSourceColumn}_${fieldName}`; if (item[exactKey] !== undefined) return exactKey; if (fieldName === "item_name" || fieldName === "name") { const aliasKey = `${inferredSourceColumn}_name`; if (item[aliasKey] !== undefined) return aliasKey; } } return columnName; }; // ์ˆซ์ž ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•˜๋Š” ํ•จ์ˆ˜ const isNumericValue = (value: any): boolean => { if (value === null || value === undefined || value === "") return false; const num = parseFloat(String(value)); return !isNaN(num) && isFinite(num); }; // ๊ทธ๋ฃนํ•‘ ์ˆ˜ํ–‰ filteredData.forEach((item) => { const actualKey = getActualKey(groupByColumn, item); const groupValue = String(item[actualKey] || item[groupByColumn] || ""); if (!groupMap.has(groupValue)) { // ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์„ ๊ธฐ์ค€์œผ๋กœ ์ดˆ๊ธฐํ™” groupMap.set(groupValue, { ...item, _groupCount: 1 }); } else { const existing = groupMap.get(groupValue); existing._groupCount += 1; // ๋ชจ๋“  ํ‚ค์— ๋Œ€ํ•ด ์ˆซ์ž๋ฉด ํ•ฉ์‚ฐ Object.keys(item).forEach((key) => { const value = item[key]; if (isNumericValue(value) && key !== groupByColumn && !key.endsWith("_id") && !key.includes("code")) { const numValue = parseFloat(String(value)); const existingValue = parseFloat(String(existing[key] || 0)); existing[key] = existingValue + numValue; } }); groupMap.set(groupValue, existing); } }); const result = Array.from(groupMap.values()); console.log("๐Ÿ”— [ํ…Œ์ด๋ธ”๋ฆฌ์ŠคํŠธ] ๊ทธ๋ฃน๋ณ„ ํ•ฉ์‚ฐ ๊ฒฐ๊ณผ:", { ์›๋ณธ๊ฐœ์ˆ˜: filteredData.length, ๊ทธ๋ฃน๊ฐœ์ˆ˜: result.length, ๊ทธ๋ฃน๊ธฐ์ค€: groupByColumn, }); return result; }, [filteredData, groupSumConfig]); // ๐Ÿ†• ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ: ํ•ฉ์‚ฐ ๋ชจ๋“œ๋ฉด summedData, ์•„๋‹ˆ๋ฉด filteredData const displayData = useMemo(() => { return groupSumConfig?.enabled ? summedData : filteredData; }, [groupSumConfig?.enabled, summedData, filteredData]); // ๐Ÿ†• Virtual Scrolling: ๋ณด์ด๋Š” ํ–‰ ๋ฒ”์œ„ ๊ณ„์‚ฐ (displayData ์ •์˜ ์ดํ›„์— ์œ„์น˜) const virtualScrollInfo = useMemo(() => { const dataSource = displayData; if (!isVirtualScrollEnabled || dataSource.length === 0) { return { startIndex: 0, endIndex: dataSource.length, visibleData: dataSource, topSpacerHeight: 0, bottomSpacerHeight: 0, totalHeight: dataSource.length * ROW_HEIGHT, }; } const containerHeight = scrollContainerRef.current?.clientHeight || 600; const totalRows = dataSource.length; const totalHeight = totalRows * ROW_HEIGHT; // ํ˜„์žฌ ๋ณด์ด๋Š” ํ–‰ ๋ฒ”์œ„ ๊ณ„์‚ฐ const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN); const visibleRowCount = Math.ceil(containerHeight / ROW_HEIGHT) + OVERSCAN * 2; const endIndex = Math.min(totalRows, startIndex + visibleRowCount); return { startIndex, endIndex, visibleData: dataSource.slice(startIndex, endIndex), topSpacerHeight: startIndex * ROW_HEIGHT, bottomSpacerHeight: (totalRows - endIndex) * ROW_HEIGHT, totalHeight, }; }, [isVirtualScrollEnabled, displayData, scrollTop, ROW_HEIGHT, OVERSCAN]); // ์ €์žฅ๋œ ๊ทธ๋ฃน ์„ค์ • ๋ถˆ๋Ÿฌ์˜ค๊ธฐ 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]); // ๐Ÿ†• ์šฐ์ธก ํ™”๋ฉด์ผ ๋•Œ๋งŒ selectedLeftData ๋ณ€๊ฒฝ์— ๋ฐ˜์‘ํ•˜๋„๋ก ๋ณ€์ˆ˜ ์ƒ์„ฑ const isRightPanel = splitPanelPosition === "right" || currentSplitPosition === "right"; const selectedLeftDataForRightPanel = isRightPanel ? splitPanelContext?.selectedLeftData : null; useEffect(() => { // console.log("๐Ÿ” [TableList] useEffect ์‹คํ–‰ - ๋ฐ์ดํ„ฐ ์กฐํšŒ ํŠธ๋ฆฌ๊ฑฐ", { // isDesignMode, // tableName: tableConfig.selectedTable, // currentPage, // sortColumn, // sortDirection, // }); if (!isDesignMode && tableConfig.selectedTable) { fetchTableDataDebounced(); } }, [ tableConfig.selectedTable, currentPage, localPageSize, sortColumn, sortDirection, searchTerm, searchValues, // ํ•„ํ„ฐ ๊ฐ’ ๋ณ€๊ฒฝ ์‹œ์—๋„ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ refreshKey, refreshTrigger, // ๊ฐ•์ œ ์ƒˆ๋กœ๊ณ ์นจ ํŠธ๋ฆฌ๊ฑฐ isDesignMode, selectedLeftDataForRightPanel, // ๐Ÿ†• ์šฐ์ธก ํ™”๋ฉด์ผ ๋•Œ๋งŒ ์ขŒ์ธก ๋ฐ์ดํ„ฐ ์„ ํƒ ๋ณ€๊ฒฝ ์‹œ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ // fetchTableDataDebounced ์ œ๊ฑฐ: useCallback ์žฌ์ƒ์„ฑ์œผ๋กœ ์ธํ•œ ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ ]); useEffect(() => { if (tableConfig.refreshInterval && !isDesignMode) { const interval = setInterval(() => { fetchTableDataDebounced(); }, tableConfig.refreshInterval * 1000); return () => clearInterval(interval); } }, [tableConfig.refreshInterval, isDesignMode]); // ๐Ÿ†• ์ „์—ญ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ useEffect(() => { const handleRefreshTable = () => { if (tableConfig.selectedTable && !isDesignMode) { console.log("๐Ÿ”„ [TableList] refreshTable ์ด๋ฒคํŠธ ์ˆ˜์‹  - ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ"); setRefreshTrigger((prev) => prev + 1); } }; window.addEventListener("refreshTable", handleRefreshTable); return () => { window.removeEventListener("refreshTable", handleRefreshTable); }; }, [tableConfig.selectedTable, isDesignMode]); // ๐Ÿ†• ํ…Œ์ด๋ธ”๋ช… ๋ณ€๊ฒฝ ์‹œ ์ „์—ญ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ ํ™•์ธ useEffect(() => { if (typeof window !== "undefined" && window.__relatedButtonsTargetTables && tableConfig.selectedTable) { const isTarget = window.__relatedButtonsTargetTables.has(tableConfig.selectedTable); if (isTarget) { console.log("๐Ÿ“ [TableList] ์ „์—ญ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ RelatedDataButtons ๋Œ€์ƒ ํ™•์ธ:", tableConfig.selectedTable); setIsRelatedButtonTarget(true); } } }, [tableConfig.selectedTable]); // ๐Ÿ†• RelatedDataButtons ๋“ฑ๋ก/ํ•ด์ œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ useEffect(() => { const handleRelatedButtonRegister = (event: CustomEvent) => { const { targetTable } = event.detail || {}; if (targetTable === tableConfig.selectedTable) { console.log("๐Ÿ“ [TableList] RelatedDataButtons ๋Œ€์ƒ์œผ๋กœ ๋“ฑ๋ก๋จ:", tableConfig.selectedTable); setIsRelatedButtonTarget(true); } }; const handleRelatedButtonUnregister = (event: CustomEvent) => { const { targetTable } = event.detail || {}; if (targetTable === tableConfig.selectedTable) { console.log("๐Ÿ“ [TableList] RelatedDataButtons ๋Œ€์ƒ์—์„œ ํ•ด์ œ๋จ:", tableConfig.selectedTable); setIsRelatedButtonTarget(false); setRelatedButtonFilter(null); } }; window.addEventListener("related-button-register" as any, handleRelatedButtonRegister); window.addEventListener("related-button-unregister" as any, handleRelatedButtonUnregister); return () => { window.removeEventListener("related-button-register" as any, handleRelatedButtonRegister); window.removeEventListener("related-button-unregister" as any, handleRelatedButtonUnregister); }; }, [tableConfig.selectedTable]); // ๐Ÿ†• RelatedDataButtons ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ (๋ฒ„ํŠผ ์„ ํƒ ์‹œ ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง) useEffect(() => { const handleRelatedButtonSelect = (event: CustomEvent) => { const { targetTable, filterColumn, filterValue } = event.detail || {}; // ์ด ํ…Œ์ด๋ธ”์ด ๋Œ€์ƒ ํ…Œ์ด๋ธ”์ธ์ง€ ํ™•์ธ if (targetTable === tableConfig.selectedTable) { // filterValue๊ฐ€ null์ด๋ฉด ์„ ํƒ ํ•ด์ œ (๋นˆ ์ƒํƒœ) if (filterValue === null || filterValue === undefined) { console.log("๐Ÿ“Œ [TableList] RelatedDataButtons ์„ ํƒ ํ•ด์ œ (๋นˆ ์ƒํƒœ):", tableConfig.selectedTable); setRelatedButtonFilter(null); setIsRelatedButtonTarget(true); // ๋Œ€์ƒ์œผ๋กœ ๋“ฑ๋ก์€ ์œ ์ง€ } else { console.log("๐Ÿ“Œ [TableList] RelatedDataButtons ํ•„ํ„ฐ ์ ์šฉ:", { tableName: tableConfig.selectedTable, filterColumn, filterValue, }); setRelatedButtonFilter({ filterColumn, filterValue }); setIsRelatedButtonTarget(true); } } }; window.addEventListener("related-button-select" as any, handleRelatedButtonSelect); return () => { window.removeEventListener("related-button-select" as any, handleRelatedButtonSelect); }; }, [tableConfig.selectedTable]); // ๐Ÿ†• relatedButtonFilter ๋ณ€๊ฒฝ ์‹œ ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ๋กœ๋“œ useEffect(() => { if (!isDesignMode) { // relatedButtonFilter๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ์ดํ„ฐ ๋กœ๋“œ, null์ด๋ฉด ๋นˆ ์ƒํƒœ (setRefreshTrigger๋กœ ํŠธ๋ฆฌ๊ฑฐ) console.log("๐Ÿ”„ [TableList] RelatedDataButtons ์ƒํƒœ ๋ณ€๊ฒฝ:", { relatedButtonFilter, isRelatedButtonTarget, }); setRefreshTrigger((prev) => prev + 1); } }, [relatedButtonFilter, isDesignMode]); // ๐ŸŽฏ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ž๋™ ๊ณ„์‚ฐ (๋‚ด์šฉ ๊ธฐ๋ฐ˜) const calculateOptimalColumnWidth = useCallback( (columnName: string, displayName: string): number => { // ๊ธฐ๋ณธ ๋„ˆ๋น„ ์„ค์ • const MIN_WIDTH = 100; const MAX_WIDTH = 400; const PADDING = 48; // ์ขŒ์šฐ ํŒจ๋”ฉ + ์—ฌ์œ  ๊ณต๊ฐ„ const HEADER_PADDING = 60; // ํ—ค๋” ์ถ”๊ฐ€ ์—ฌ์œ  (์ •๋ ฌ ์•„์ด์ฝ˜ ๋“ฑ) // ํ—ค๋” ํ…์ŠคํŠธ ๋„ˆ๋น„ ๊ณ„์‚ฐ (๋Œ€๋žต 8px per character) const headerWidth = (displayName?.length || columnName.length) * 10 + HEADER_PADDING; // ๋ฐ์ดํ„ฐ ์…€ ๋„ˆ๋น„ ๊ณ„์‚ฐ (์ƒ์œ„ 50๊ฐœ ์ƒ˜ํ”Œ๋ง) const sampleSize = Math.min(50, data.length); let maxDataWidth = headerWidth; for (let i = 0; i < sampleSize; i++) { const cellValue = data[i]?.[columnName]; if (cellValue !== null && cellValue !== undefined) { const cellText = String(cellValue); // ์ˆซ์ž๋Š” ์ข๊ฒŒ, ํ…์ŠคํŠธ๋Š” ๋„“๊ฒŒ ๊ณ„์‚ฐ const isNumber = !isNaN(Number(cellValue)) && cellValue !== ""; const charWidth = isNumber ? 8 : 9; const cellWidth = cellText.length * charWidth + PADDING; maxDataWidth = Math.max(maxDataWidth, cellWidth); } } // ์ตœ์†Œ/์ตœ๋Œ€ ๋ฒ”์œ„ ๋‚ด๋กœ ์ œํ•œ return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.ceil(maxDataWidth))); }, [data], ); // ๐ŸŽฏ localStorage์—์„œ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๋ฐ ์ดˆ๊ธฐ ๊ณ„์‚ฐ useEffect(() => { if (!hasInitializedWidths.current && visibleColumns.length > 0 && data.length > 0) { const timer = setTimeout(() => { const storageKey = tableConfig.selectedTable && userId ? `table_column_widths_${tableConfig.selectedTable}_${userId}` : null; // 1. localStorage์—์„œ ์ €์žฅ๋œ ๋„ˆ๋น„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ let savedWidths: Record = {}; if (storageKey) { try { const saved = localStorage.getItem(storageKey); if (saved) { savedWidths = JSON.parse(saved); } } catch (error) { console.error("์ปฌ๋Ÿผ ๋„ˆ๋น„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ:", error); } } // 2. ์ž๋™ ๊ณ„์‚ฐ ๋˜๋Š” ์ €์žฅ๋œ ๋„ˆ๋น„ ์ ์šฉ const newWidths: Record = {}; let hasAnyWidth = false; visibleColumns.forEach((column) => { // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ ์ œ์™ธ (๊ณ ์ • 48px) if (column.columnName === "__checkbox__") return; // ์ €์žฅ๋œ ๋„ˆ๋น„๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ if (savedWidths[column.columnName]) { newWidths[column.columnName] = savedWidths[column.columnName]; hasAnyWidth = true; } else { // ์ €์žฅ๋œ ๋„ˆ๋น„๊ฐ€ ์—†์œผ๋ฉด ์ž๋™ ๊ณ„์‚ฐ const optimalWidth = calculateOptimalColumnWidth( column.columnName, columnLabels[column.columnName] || column.displayName, ); newWidths[column.columnName] = optimalWidth; hasAnyWidth = true; } }); if (hasAnyWidth) { setColumnWidths(newWidths); hasInitializedWidths.current = true; } }, 150); // DOM ๋ Œ๋”๋ง ๋Œ€๊ธฐ return () => clearTimeout(timer); } }, [visibleColumns, data, tableConfig.selectedTable, userId, calculateOptimalColumnWidth, columnLabels]); // ======================================== // ํŽ˜์ด์ง€๋„ค์ด์…˜ JSX // ======================================== const paginationJSX = useMemo(() => { if (!tableConfig.pagination?.enabled || isDesignMode) return null; return (
{/* ์ค‘์•™ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ปจํŠธ๋กค */}
{currentPage} / {totalPages || 1}
{/* ์šฐ์ธก ๋ฒ„ํŠผ ๊ทธ๋ฃน */}
{/* ๐Ÿ†• ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ (Excel/PDF) */}
Excel
PDF/์ธ์‡„
{/* ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ (ํ•˜๋‹จ ํŽ˜์ด์ง€๋„ค์ด์…˜) */} {(tableConfig.toolbar?.showPaginationRefresh ?? true) && ( )}
); }, [ tableConfig.pagination, tableConfig.toolbar?.showPaginationRefresh, isDesignMode, currentPage, totalPages, totalItems, loading, selectedRows.size, exportToExcel, exportToPdf, ]); // ======================================== // ๋ Œ๋”๋ง // ======================================== const domProps = { onClick: handleClick, onDragStart: isDesignMode ? onDragStart : undefined, onDragEnd: isDesignMode ? onDragEnd : undefined, draggable: isDesignMode, className: cn("w-full h-full", className, isDesignMode && "cursor-move"), // customer-item-mapping๊ณผ ๋™์ผ style: componentStyle, }; // ์นด๋“œ ๋ชจ๋“œ if (tableConfig.displayMode === "card" && !isDesignMode) { return (
{paginationJSX}
); } // SingleTableWithSticky ๋ชจ๋“œ if (tableConfig.stickyHeader && !isDesignMode) { return (
{/* ํ•„ํ„ฐ ํ—ค๋”๋Š” TableSearchWidget์œผ๋กœ ์ด๋™ */} {/* ๊ทธ๋ฃน ํ‘œ์‹œ ๋ฐฐ์ง€ */} {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 ( <>
{/* ํ•„ํ„ฐ ํ—ค๋”๋Š” TableSearchWidget์œผ๋กœ ์ด๋™ */} {/* ๐Ÿ†• DevExpress ์Šคํƒ€์ผ ๊ธฐ๋Šฅ ํˆด๋ฐ” */}
{/* ํŽธ์ง‘ ๋ชจ๋“œ ํ† ๊ธ€ */} {(tableConfig.toolbar?.showEditMode ?? true) && (
)} {/* ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„ํŠผ๋“ค */} {((tableConfig.toolbar?.showExcel ?? true) || (tableConfig.toolbar?.showPdf ?? true)) && (
{(tableConfig.toolbar?.showExcel ?? true) && ( )} {(tableConfig.toolbar?.showPdf ?? true) && ( )}
)} {/* ๋ณต์‚ฌ ๋ฒ„ํŠผ */} {(tableConfig.toolbar?.showCopy ?? true) && (
)} {/* ์„ ํƒ ์ •๋ณด */} {selectedRows.size > 0 && (
{selectedRows.size}๊ฐœ ์„ ํƒ๋จ
)} {/* ๐Ÿ†• ํ†ตํ•ฉ ๊ฒ€์ƒ‰ ํŒจ๋„ */} {(tableConfig.toolbar?.showSearch ?? true) && (
{isSearchPanelOpen ? (
setGlobalSearchTerm(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { executeGlobalSearch(globalSearchTerm); } else if (e.key === "Escape") { clearGlobalSearch(); } else if (e.key === "F3" || (e.key === "g" && (e.ctrlKey || e.metaKey))) { e.preventDefault(); if (e.shiftKey) { goToPrevSearchResult(); } else { goToNextSearchResult(); } } }} placeholder="๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ... (Enter)" className="border-input bg-background focus:ring-primary h-7 w-32 rounded border px-2 text-xs focus:ring-1 focus:outline-none sm:w-48" autoFocus /> {searchHighlights.size > 0 && ( {searchHighlights.size}๊ฐœ )}
) : ( )}
)} {/* ๐Ÿ†• Filter Builder (๊ณ ๊ธ‰ ํ•„ํ„ฐ) ๋ฒ„ํŠผ */} {(tableConfig.toolbar?.showFilter ?? true) && (
{activeFilterCount > 0 && ( )}
)} {/* ์ƒˆ๋กœ๊ณ ์นจ */} {(tableConfig.toolbar?.showRefresh ?? true) && (
)}
{/* ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘ ํˆด๋ฐ” */} {(editMode === "batch" || pendingChanges.size > 0) && (
๋ฐฐ์น˜ ํŽธ์ง‘ ๋ชจ๋“œ {pendingChanges.size > 0 && ( {pendingChanges.size}๊ฐœ ๋ณ€๊ฒฝ์‚ฌํ•ญ )}
)} {/* ๊ทธ๋ฃน ํ‘œ์‹œ ๋ฐฐ์ง€ */} {groupByColumns.length > 0 && (
๊ทธ๋ฃน:
{groupByColumns.map((col, idx) => ( {idx > 0 && โ†’} {columnLabels[col] || col} ))}
)} {/* ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ - ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ง€์› */}
{/* ์Šคํฌ๋กค ์˜์—ญ */}
{/* ํ…Œ์ด๋ธ” */} {/* ํ—ค๋” (sticky) */} {/* ๐Ÿ†• Multi-Level Headers (Column Bands) */} {columnBandsInfo?.hasBands && ( {visibleColumns.map((column, colIdx) => { // ์ด ์ปฌ๋Ÿผ์ด ์†ํ•œ band ์ฐพ๊ธฐ const band = columnBandsInfo.bands.find( (b) => b.columns.includes(column.columnName) && b.startIndex === colIdx, ); // band์˜ ์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ์—๋งŒ ๋ Œ๋”๋ง if (band) { return ( ); } // band์— ์†ํ•˜์ง€ ์•Š์€ ์ปฌ๋Ÿผ (๊ฐœ๋ณ„ ํ‘œ์‹œ) const isInAnyBand = columnBandsInfo.bands.some((b) => b.columns.includes(column.columnName)); if (!isInAnyBand) { return ( ); } // band์˜ ์ค‘๊ฐ„ ์ปฌ๋Ÿผ์€ ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ return null; })} )} {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]; // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ 48px ๊ณ ์ • const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } // ๐Ÿ†• Column Reordering ์ƒํƒœ const isColumnDragging = draggedColumnIndex === columnIndex; const isColumnDropTarget = dropTargetColumnIndex === columnIndex; 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]; // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ 48px ๊ณ ์ • const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } return ( ); })} ))} {/* ๐Ÿ†• ๊ทธ๋ฃน๋ณ„ ์†Œ๊ณ„ ํ–‰ */} {!isCollapsed && group.summary && Object.keys(group.summary).length > 0 && ( {visibleColumns.map((column, colIndex) => { const summary = group.summary?.[column.columnName]; const meta = columnMeta[column.columnName]; const inputType = meta?.inputType || (column as any).inputType; const isNumeric = inputType === "number" || inputType === "decimal"; if (colIndex === 0 && column.columnName === "__checkbox__") { return ( ); } if (colIndex === 0 && column.columnName !== "__checkbox__") { return ( ); } if (summary) { return ( ); } return )} ); }) ) : ( // ์ผ๋ฐ˜ ๋ Œ๋”๋ง (๊ทธ๋ฃน ์—†์Œ) - ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ง€์› <> {/* ๐Ÿ†• Virtual Scrolling: Top Spacer */} {isVirtualScrollEnabled && virtualScrollInfo.topSpacerHeight > 0 && ( )} {/* ๋ฐ์ดํ„ฐ ํ–‰ ๋ Œ๋”๋ง - ๐Ÿ†• ํ•ฉ์‚ฐ ๋ชจ๋“œ๋ฉด displayData ์‚ฌ์šฉ */} {(isVirtualScrollEnabled ? virtualScrollInfo.visibleData : displayData).map((row, idx) => { // Virtual Scrolling์—์„œ๋Š” ์‹ค์ œ ์ธ๋ฑ์Šค ๊ณ„์‚ฐ const index = isVirtualScrollEnabled ? virtualScrollInfo.startIndex + idx : idx; const rowKey = getRowKey(row, index); const isRowSelected = selectedRows.has(rowKey); const isRowFocused = focusedCell?.rowIndex === index; // ๐Ÿ†• Drag & Drop ์ƒํƒœ const isDragging = draggedRowIndex === index; const isDropTarget = dropTargetIndex === index; return ( handleRowClick(row, index, e)} role="row" aria-selected={isRowSelected} // ๐Ÿ†• Drag & Drop ์ด๋ฒคํŠธ draggable={isDragEnabled} onDragStart={(e) => handleRowDragStart(e, index)} onDragOver={(e) => handleRowDragOver(e, index)} onDragEnd={handleRowDragEnd} onDrop={(e) => handleRowDrop(e, index)} > {visibleColumns.map((column, colIndex) => { const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; // ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘: ๋กœ์ปฌ ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์šฐ์„  ํ‘œ์‹œ const cellValue = editMode === "batch" ? getDisplayValue(row, index, mappedColumnName) : 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); // ์…€ ํฌ์ปค์Šค ์ƒํƒœ const isCellFocused = focusedCell?.rowIndex === index && focusedCell?.colIndex === colIndex; // ๐Ÿ†• ๋ฐฐ์น˜ ํŽธ์ง‘: ์ˆ˜์ •๋œ ์…€ ์—ฌ๋ถ€ const isModified = isCellModified(index, mappedColumnName); // ๐Ÿ†• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ const cellValidationError = getCellValidationError(index, mappedColumnName); // ๐Ÿ†• ๊ฒ€์ƒ‰ ํ•˜์ด๋ผ์ดํŠธ ์—ฌ๋ถ€ const isSearchHighlighted = searchHighlights.has(`${index}-${colIndex}`); // ํ‹€๊ณ ์ •๋œ ์ปฌ๋Ÿผ์˜ left ์œ„์น˜ ๊ณ„์‚ฐ let leftPosition = 0; if (isFrozen && frozenIndex > 0) { for (let i = 0; i < frozenIndex; i++) { const frozenCol = frozenColumns[i]; // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ 48px ๊ณ ์ • const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } return ( ); })} ); })} {/* ๐Ÿ†• Virtual Scrolling: Bottom Spacer */} {isVirtualScrollEnabled && virtualScrollInfo.bottomSpacerHeight > 0 && ( )} )} {/* ๐Ÿ†• ๋ฐ์ดํ„ฐ ์š”์•ฝ (Total Summaries) */} {summaryData && Object.keys(summaryData).length > 0 && ( {visibleColumns.map((column, colIndex) => { const summary = summaryData[column.columnName]; 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]; // ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ์€ 48px ๊ณ ์ • const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } const meta = columnMeta[column.columnName]; const inputType = meta?.inputType || (column as any).inputType; const isNumeric = inputType === "number" || inputType === "decimal"; return ( ); })} )}
{band.caption} {columnLabels[column.columnName] || column.columnName}
(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-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]", // ๐Ÿ†• Column Reordering ์Šคํƒ€์ผ isColumnDragEnabled && column.columnName !== "__checkbox__" && "cursor-grab active:cursor-grabbing", isColumnDragging && "bg-primary/20 opacity-50", isColumnDropTarget && "border-l-primary border-l-4", )} 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` }), }} // ๐Ÿ†• Column Reordering ์ด๋ฒคํŠธ draggable={isColumnDragEnabled && column.columnName !== "__checkbox__"} onDragStart={(e) => handleColumnDragStart(e, columnIndex)} onDragOver={(e) => handleColumnDragOver(e, columnIndex)} onDragEnd={handleColumnDragEnd} onDrop={(e) => handleColumnDrop(e, columnIndex)} onClick={() => { if (isResizing.current) return; if (column.sortable !== false && column.columnName !== "__checkbox__") { handleSort(column.columnName); } }} > {column.columnName === "__checkbox__" ? ( renderCheckboxHeader() ) : (
{/* ๐Ÿ†• ํŽธ์ง‘ ๋ถˆ๊ฐ€ ์ปฌ๋Ÿผ ํ‘œ์‹œ */} {column.editable === false && ( )} {columnLabels[column.columnName] || column.displayName} {column.sortable !== false && sortColumn === column.columnName && ( {sortDirection === "asc" ? "โ†‘" : "โ†“"} )} {/* ๐Ÿ†• ํ—ค๋” ํ•„ํ„ฐ ๋ฒ„ํŠผ */} {tableConfig.headerFilter !== false && columnUniqueValues[column.columnName]?.length > 0 && ( setOpenFilterColumn(open ? column.columnName : null)} > e.stopPropagation()} >
ํ•„ํ„ฐ: {columnLabels[column.columnName] || column.displayName} {headerFilters[column.columnName]?.size > 0 && ( )}
{columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => { const isSelected = headerFilters[column.columnName]?.has(val); return (
toggleHeaderFilter(column.columnName, val)} >
{isSelected && }
{val || "(๋นˆ ๊ฐ’)"}
); })} {(columnUniqueValues[column.columnName]?.length || 0) > 50 && (
...์™ธ {(columnUniqueValues[column.columnName]?.length || 0) - 50}๊ฐœ
)}
)}
)} {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค (์ฒดํฌ๋ฐ•์Šค ์ œ์™ธ) */} {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) => { const newWidths = { ...prev, [column.columnName]: finalWidth }; // ๐ŸŽฏ localStorage์— ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ €์žฅ (์‚ฌ์šฉ์ž๋ณ„) if (tableConfig.selectedTable && userId) { const storageKey = `table_column_widths_${tableConfig.selectedTable}_${userId}`; try { localStorage.setItem(storageKey, JSON.stringify(newWidths)); } catch (error) { console.error("์ปฌ๋Ÿผ ๋„ˆ๋น„ ์ €์žฅ ์‹คํŒจ:", error); } } return newWidths; }); } // ํ…์ŠคํŠธ ์„ ํƒ ๋ณต์› 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)}
์†Œ๊ณ„ ์†Œ๊ณ„ ({group.count}๊ฑด) {summary.sum.toLocaleString()} ; })}
handleCellClick(index, colIndex, e)} onDoubleClick={() => handleCellDoubleClick(index, colIndex, column.columnName, cellValue) } onContextMenu={(e) => handleContextMenu(e, index, colIndex, row)} role="gridcell" tabIndex={isCellFocused ? 0 : -1} > {/* ๐Ÿ†• ์ธ๋ผ์ธ ํŽธ์ง‘ ๋ชจ๋“œ */} {editingCell?.rowIndex === index && editingCell?.colIndex === colIndex ? // ๐Ÿ†• Cascading Lookups: ๋“œ๋กญ๋‹ค์šด ๋˜๋Š” ์ผ๋ฐ˜ ์ž…๋ ฅ (() => { const cascadingConfig = (tableConfig as any).cascadingLookups?.[ column.columnName ]; const options = cascadingConfig ? getCascadingOptions(column.columnName, row) : []; // ๋ถ€๋ชจ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์˜ต์…˜ ๋กœ๋”ฉ if (cascadingConfig && options.length === 0) { const parentValue = row[cascadingConfig.parentColumn]; if (parentValue !== undefined && parentValue !== null) { loadCascadingOptions( column.columnName, cascadingConfig.parentColumn, parentValue, ); } } // ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ํƒ€์ž…์ด๊ฑฐ๋‚˜ Cascading Lookup์ธ ๊ฒฝ์šฐ ๋“œ๋กญ๋‹ค์šด const colMeta = columnMeta[column.columnName]; const isCategoryType = colMeta?.inputType === "category" || colMeta?.inputType === "code"; const hasCategoryOptions = categoryMappings[column.columnName] && Object.keys(categoryMappings[column.columnName]).length > 0; if (cascadingConfig || (isCategoryType && hasCategoryOptions)) { const selectOptions = cascadingConfig ? options : Object.entries(categoryMappings[column.columnName] || {}).map( ([value, info]) => ({ value, label: info.label, }), ); return ( ); } // ์ผ๋ฐ˜ ์ž…๋ ฅ ํ•„๋“œ return ( setEditingValue(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={saveEditing} className="border-primary bg-background h-full w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm" style={{ textAlign: isNumeric ? "right" : column.align || "left", }} /> ); })() : column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) : formatCellValue(cellValue, column, row)}
{summary ? (
{summary.label} {typeof summary.value === "number" ? summary.value.toLocaleString("ko-KR", { maximumFractionDigits: 2, }) : summary.value}
) : colIndex === 0 ? ( ์š”์•ฝ ) : null}
{/* ํŽ˜์ด์ง€๋„ค์ด์…˜ */} {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}๊ฐœ์˜ ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค )}
{/* ๐Ÿ†• Context Menu (์šฐํด๋ฆญ ๋ฉ”๋‰ด) */} {contextMenu && (
e.stopPropagation()} >
{/* ์…€ ๋ณต์‚ฌ */} {/* ํ–‰ ๋ณต์‚ฌ */}
{/* ์…€ ํŽธ์ง‘ */} {(() => { const col = visibleColumns[contextMenu.colIndex]; const isEditable = col?.editable !== false && col?.columnName !== "__checkbox__"; return ( ); })()} {/* ํ–‰ ์„ ํƒ/ํ•ด์ œ */}
{/* ํ–‰ ์‚ญ์ œ */}
)} {/* ๐Ÿ†• Filter Builder ๋ชจ๋‹ฌ */} ๊ณ ๊ธ‰ ํ•„ํ„ฐ ์—ฌ๋Ÿฌ ์กฐ๊ฑด์„ ์กฐํ•ฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค.
{filterGroups.length === 0 ? (
ํ•„ํ„ฐ ์กฐ๊ฑด์ด ์—†์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.
) : ( filterGroups.map((group, groupIndex) => (
์กฐ๊ฑด ๊ทธ๋ฃน {groupIndex + 1}
{group.conditions.map((condition) => (
{/* ์ปฌ๋Ÿผ ์„ ํƒ */} {/* ์—ฐ์‚ฐ์ž ์„ ํƒ */} {/* ๊ฐ’ ์ž…๋ ฅ (isEmpty/isNotEmpty๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ๋งŒ) */} {condition.operator !== "isEmpty" && condition.operator !== "isNotEmpty" && ( updateFilterCondition(group.id, condition.id, "value", e.target.value)} placeholder="๊ฐ’ ์ž…๋ ฅ" className="border-input bg-background h-8 flex-1 rounded border px-2 text-xs" /> )} {/* ์กฐ๊ฑด ์‚ญ์ œ */}
))}
{/* ์กฐ๊ฑด ์ถ”๊ฐ€ ๋ฒ„ํŠผ */}
)) )} {/* ๊ทธ๋ฃน ์ถ”๊ฐ€ ๋ฒ„ํŠผ */}
{/* ํ…Œ์ด๋ธ” ์˜ต์…˜ ๋ชจ๋‹ฌ */} 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 ; };