From 6e8f529cd310dc9e0c77e6eee8c5b3f01c2e0c33 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Mon, 29 Sep 2025 17:29:58 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EA=B0=90=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table-list/TableListComponent.tsx | 106 +++++++++++++++--- 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index ac2f49b8..6d0067c1 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect, useMemo } from "react"; +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"; @@ -35,6 +35,51 @@ const cleanupTableCache = () => { if (typeof window !== "undefined") { setInterval(cleanupTableCache, 10 * 60 * 1000); } + +// 요청 디바운싱을 위한 전역 타이머 +const debounceTimers = new Map(); + +// 진행 중인 요청 추적 (중복 요청 방지) +const activeRequests = new Map>(); + +// 디바운싱된 API 호출 함수 (중복 요청 방지 포함) +const debouncedApiCall = (key: string, fn: (...args: T) => Promise, delay: number = 300) => { + return (...args: T): Promise => { + // 이미 진행 중인 동일한 요청이 있으면 그 결과를 반환 + const activeRequest = activeRequests.get(key); + if (activeRequest) { + console.log(`🔄 진행 중인 요청 재사용: ${key}`); + 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); + }); + }; +}; import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -148,6 +193,15 @@ export const TableListComponent: React.FC = ({ const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리) + // 컬럼 정보 메모이제이션 + const memoizedColumnInfo = useMemo(() => { + return { + labels: columnLabels, + meta: columnMeta, + visibleColumns: (tableConfig.columns || []).filter((col) => col.visible !== false), + }; + }, [columnLabels, columnMeta, tableConfig.columns]); + // 고급 필터 관련 state const [searchValues, setSearchValues] = useState>({}); @@ -369,8 +423,20 @@ export const TableListComponent: React.FC = ({ } }; - // 테이블 데이터 가져오기 - const fetchTableData = async () => { + // 디바운싱된 테이블 데이터 가져오기 + const fetchTableDataDebounced = useCallback( + debouncedApiCall( + `fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`, + async () => { + return fetchTableDataInternal(); + }, + 200, // 200ms 디바운스 + ), + [tableConfig.selectedTable, currentPage, localPageSize, searchTerm, sortColumn, sortDirection, searchValues], + ); + + // 실제 테이블 데이터 가져오기 함수 + const fetchTableDataInternal = async () => { if (!tableConfig.selectedTable) { setData([]); return; @@ -558,14 +624,24 @@ export const TableListComponent: React.FC = ({ codeColumns.map(([col, meta]) => `${col}(${meta.codeCategory})`), ); - // 필요한 코드 카테고리들을 추출하여 배치 로드 - const categoryList = codeColumns.map(([, meta]) => meta.codeCategory).filter(Boolean) as string[]; + // 필요한 코드 카테고리들을 추출하여 배치 로드 (중복 제거) + const categoryList = [ + ...new Set(codeColumns.map(([, meta]) => meta.codeCategory).filter(Boolean)), + ] as string[]; - try { - await codeCache.preloadCodes(categoryList); - console.log("📋 모든 코드 캐시 로드 완료 (전역 캐시)"); - } catch (error) { - console.error("❌ 코드 캐시 로드 중 오류:", error); + // 이미 캐시된 카테고리는 제외 + const uncachedCategories = categoryList.filter((category) => !codeCache.getCodeSync(category)); + + if (uncachedCategories.length > 0) { + try { + console.log(`📋 새로운 코드 카테고리 로딩: ${uncachedCategories.join(", ")}`); + await codeCache.preloadCodes(uncachedCategories); + console.log("📋 모든 코드 캐시 로드 완료 (전역 캐시)"); + } catch (error) { + console.error("❌ 코드 캐시 로드 중 오류:", error); + } + } else { + console.log("📋 모든 코드 카테고리가 이미 캐시됨"); } } @@ -759,18 +835,18 @@ export const TableListComponent: React.FC = ({ const handleAdvancedSearch = () => { setCurrentPage(1); - fetchTableData(); + fetchTableDataDebounced(); }; const handleClearAdvancedFilters = () => { setSearchValues({}); setCurrentPage(1); - fetchTableData(); + fetchTableDataDebounced(); }; // 새로고침 const handleRefresh = () => { - fetchTableData(); + fetchTableDataDebounced(); }; // 체크박스 핸들러들 @@ -872,7 +948,7 @@ export const TableListComponent: React.FC = ({ useEffect(() => { if (tableConfig.autoLoad && !isDesignMode) { - fetchTableData(); + fetchTableDataDebounced(); } }, [ tableConfig.selectedTable, @@ -896,7 +972,7 @@ export const TableListComponent: React.FC = ({ console.log("🔄 선택 상태 초기화 - 빈 배열 전달"); onSelectedRowsChange?.([], []); // 테이블 데이터 새로고침 - fetchTableData(); + fetchTableDataDebounced(); } }, [refreshKey]);