/** * Entity 조인 최적화를 위한 커스텀 훅 * 배치 로딩과 메모이제이션을 통한 성능 최적화 */ import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { codeCache } from "@/lib/caching/codeCache"; interface ColumnMetaInfo { webType?: string; codeCategory?: string; } interface OptimizationConfig { enableBatchLoading?: boolean; preloadCommonCodes?: boolean; cacheTimeout?: number; maxBatchSize?: number; } interface OptimizationMetrics { cacheHitRate: number; totalRequests: number; batchLoadCount: number; averageResponseTime: number; } /** * Entity 조인 최적화 훅 * - 코드 캐시 배치 로딩 * - 성능 메트릭 추적 * - 스마트 프리로딩 */ export function useEntityJoinOptimization(columnMeta: Record, config: OptimizationConfig = {}) { const { enableBatchLoading = true, preloadCommonCodes = true, cacheTimeout = 5 * 60 * 1000, // 5분 maxBatchSize = 10, } = config; // 성능 메트릭 상태 const [metrics, setMetrics] = useState({ cacheHitRate: 0, totalRequests: 0, batchLoadCount: 0, averageResponseTime: 0, }); // 로딩 상태 const [isOptimizing, setIsOptimizing] = useState(false); const [loadingCategories, setLoadingCategories] = useState>(new Set()); // 메트릭 추적용 refs const requestTimes = useRef([]); const totalRequests = useRef(0); const cacheHits = useRef(0); const batchLoadCount = useRef(0); // 변환된 값 캐시 (중복 변환 방지) const convertedCache = useRef(new Map()); // 공통 코드 카테고리 추출 (메모이제이션) const codeCategories = useMemo(() => { return Object.values(columnMeta) .filter((meta) => meta.webType === "code" && meta.codeCategory) .map((meta) => meta.codeCategory!) .filter((category, index, self) => self.indexOf(category) === index); // 중복 제거 }, [columnMeta]); // 일반적으로 자주 사용되는 코드 카테고리들 const commonCodeCategories = useMemo( () => [ "USER_STATUS", // 사용자 상태 "DEPT_TYPE", // 부서 유형 "DOC_STATUS", // 문서 상태 "APPROVAL_STATUS", // 승인 상태 "PRIORITY", // 우선순위 "YES_NO", // 예/아니오 "ACTIVE_INACTIVE", // 활성/비활성 ], [], ); /** * 배치 코드 로딩 */ const batchLoadCodes = useCallback( async (categories: string[]): Promise => { if (!enableBatchLoading || categories.length === 0) return; const startTime = Date.now(); setIsOptimizing(true); try { // 배치 크기별로 분할하여 로딩 const batches: string[][] = []; for (let i = 0; i < categories.length; i += maxBatchSize) { batches.push(categories.slice(i, i + maxBatchSize)); } console.log(`🔄 배치 코드 로딩 시작: ${categories.length}개 카테고리 (${batches.length}개 배치)`); for (const batch of batches) { // 로딩 상태 업데이트 setLoadingCategories((prev) => new Set([...prev, ...batch])); // 배치 로딩 실행 await codeCache.preloadCodes(batch); batchLoadCount.current += 1; // 로딩 완료된 카테고리 제거 setLoadingCategories((prev) => { const newSet = new Set(prev); batch.forEach((category) => newSet.delete(category)); return newSet; }); // 배치 간 짧은 지연 (서버 부하 방지) if (batches.length > 1) { await new Promise((resolve) => setTimeout(resolve, 100)); } } const responseTime = Date.now() - startTime; requestTimes.current.push(responseTime); console.log(`✅ 배치 코드 로딩 완료: ${responseTime}ms`); } catch (error) { console.error("❌ 배치 코드 로딩 실패:", error); } finally { setIsOptimizing(false); setLoadingCategories(new Set()); } }, [enableBatchLoading, maxBatchSize], ); /** * 공통 코드 프리로딩 */ const preloadCommonCodesOnMount = useCallback(async (): Promise => { if (!preloadCommonCodes) return; // console.log("🚀 공통 코드 프리로딩 시작"); // 현재 테이블의 코드 카테고리와 공통 카테고리 합치기 const allCategories = [...new Set([...codeCategories, ...commonCodeCategories])]; if (allCategories.length > 0) { await batchLoadCodes(allCategories); } }, [preloadCommonCodes, codeCategories, commonCodeCategories, batchLoadCodes]); /** * 성능 메트릭 업데이트 */ const updateMetrics = useCallback(() => { const cacheInfo = codeCache.getCacheInfo(); const avgResponseTime = requestTimes.current.length > 0 ? requestTimes.current.reduce((sum, time) => sum + time, 0) / requestTimes.current.length : 0; setMetrics({ cacheHitRate: cacheInfo.hitRate, totalRequests: totalRequests.current, batchLoadCount: batchLoadCount.current, averageResponseTime: Math.round(avgResponseTime), }); }, []); /** * 최적화된 코드 변환 함수 */ const optimizedConvertCode = useCallback( (categoryCode: string, codeValue: string): string => { const startTime = Date.now(); totalRequests.current += 1; // 🎯 중복 호출 방지: 이미 변환된 값인지 확인 const cacheKey = `${categoryCode}:${codeValue}`; if (convertedCache.current.has(cacheKey)) { return convertedCache.current.get(cacheKey)!; } // 🎯 디버깅: 캐시 상태 로깅 (빈도 줄이기) if (totalRequests.current % 10 === 1) { // 10번마다 한 번만 로깅 console.log(`🔍 optimizedConvertCode 호출: categoryCode="${categoryCode}", codeValue="${codeValue}"`); } // 캐시에서 동기적으로 조회 시도 const syncResult = codeCache.getCodeSync(categoryCode); if (totalRequests.current % 10 === 1) { console.log(`🔍 getCodeSync("${categoryCode}") 결과:`, syncResult); } // 🎯 캐시 내용 상세 로깅 (키값들 확인) - 빈도 줄이기 if (syncResult && totalRequests.current % 10 === 1) { console.log(`🔍 캐시 키값들:`, Object.keys(syncResult)); console.log(`🔍 캐시 전체 데이터:`, JSON.stringify(syncResult, null, 2)); } if (syncResult && Array.isArray(syncResult)) { cacheHits.current += 1; if (totalRequests.current % 10 === 1) { console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`); console.log( `🔍 캐시 배열 내용:`, syncResult.map((item) => ({ code_value: item.code_value, code_name: item.code_name, })), ); } // 배열에서 해당 code_value를 가진 항목 찾기 const foundCode = syncResult.find( (item) => String(item.code_value).toUpperCase() === String(codeValue).toUpperCase(), ); const result = foundCode ? foundCode.code_name : codeValue; // 변환 결과를 캐시에 저장 convertedCache.current.set(cacheKey, result); if (totalRequests.current % 10 === 1) { console.log(`🔍 최종 결과: "${codeValue}" → "${result}"`, { foundCode }); } // 응답 시간 추적 (캐시 히트) requestTimes.current.push(Date.now() - startTime); if (requestTimes.current.length > 100) { requestTimes.current = requestTimes.current.slice(-50); // 최근 50개만 유지 } return result; } console.log(`⚠️ 캐시 미스: categoryCode="${categoryCode}" - 비동기 로딩 트리거`); // 캐시 미스인 경우 비동기 로딩 트리거 (백그라운드) codeCache .getCodeAsync(categoryCode) .then(() => { console.log(`✅ 비동기 로딩 완료: categoryCode="${categoryCode}"`); updateMetrics(); }) .catch((err) => { console.warn(`백그라운드 코드 로딩 실패 [${categoryCode}]:`, err); }); return codeValue || ""; }, [updateMetrics], ); /** * 캐시 상태 조회 */ const getCacheStatus = useCallback(() => { const cacheInfo = codeCache.getCacheInfo(); const loadedCategories = codeCategories.filter((category) => { const syncData = codeCache.getCodeSync(category); return syncData !== null; }); return { totalCategories: codeCategories.length, loadedCategories: loadedCategories.length, loadingCategories: Array.from(loadingCategories), cacheInfo, isFullyLoaded: loadedCategories.length === codeCategories.length && !isOptimizing, }; }, [codeCategories, loadingCategories, isOptimizing]); /** * 수동 캐시 새로고침 */ const refreshCache = useCallback( async (categories?: string[]): Promise => { const targetCategories = categories || codeCategories; // 기존 캐시 무효화 targetCategories.forEach((category) => { codeCache.invalidate(category); }); // 다시 로딩 await batchLoadCodes(targetCategories); updateMetrics(); }, [codeCategories, batchLoadCodes, updateMetrics], ); // 초기화 시 공통 코드 프리로딩 useEffect(() => { preloadCommonCodesOnMount(); }, [preloadCommonCodesOnMount]); // 컬럼 메타 변경 시 필요한 코드 추가 로딩 useEffect(() => { if (codeCategories.length > 0) { const unloadedCategories = codeCategories.filter((category) => { return codeCache.getCodeSync(category) === null; }); if (unloadedCategories.length > 0) { console.log(`🔄 새로운 코드 카테고리 감지, 추가 로딩: ${unloadedCategories.join(", ")}`); batchLoadCodes(unloadedCategories); } } }, [codeCategories, batchLoadCodes]); // 주기적으로 메트릭 업데이트 useEffect(() => { const interval = setInterval(updateMetrics, 10000); // 10초마다 return () => clearInterval(interval); }, [updateMetrics]); return { // 상태 isOptimizing, loadingCategories: Array.from(loadingCategories), metrics, // 기능 optimizedConvertCode, batchLoadCodes, refreshCache, getCacheStatus, // 유틸리티 codeCategories, commonCodeCategories, }; }