Files
vexplor/frontend/lib/hooks/useEntityJoinOptimization.ts
2025-09-16 16:53:03 +09:00

288 lines
8.4 KiB
TypeScript

/**
* Entity 조인 최적화를 위한 커스텀 훅
* 배치 로딩과 메모이제이션을 통한 성능 최적화
*/
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { codeCache } from "@/lib/cache/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<string, ColumnMetaInfo>, config: OptimizationConfig = {}) {
const {
enableBatchLoading = true,
preloadCommonCodes = true,
cacheTimeout = 5 * 60 * 1000, // 5분
maxBatchSize = 10,
} = config;
// 성능 메트릭 상태
const [metrics, setMetrics] = useState<OptimizationMetrics>({
cacheHitRate: 0,
totalRequests: 0,
batchLoadCount: 0,
averageResponseTime: 0,
});
// 로딩 상태
const [isOptimizing, setIsOptimizing] = useState(false);
const [loadingCategories, setLoadingCategories] = useState<Set<string>>(new Set());
// 메트릭 추적용 refs
const requestTimes = useRef<number[]>([]);
const totalRequests = useRef(0);
const cacheHits = useRef(0);
const batchLoadCount = useRef(0);
// 공통 코드 카테고리 추출 (메모이제이션)
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<void> => {
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<void> => {
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 syncResult = codeCache.getCodeSync(categoryCode);
if (syncResult) {
cacheHits.current += 1;
const result = syncResult[codeValue?.toUpperCase()] || codeValue;
// 응답 시간 추적 (캐시 히트)
requestTimes.current.push(Date.now() - startTime);
if (requestTimes.current.length > 100) {
requestTimes.current = requestTimes.current.slice(-50); // 최근 50개만 유지
}
return result;
}
// 캐시 미스인 경우 비동기 로딩 트리거 (백그라운드)
codeCache
.getCode(categoryCode)
.then(() => {
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<void> => {
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,
};
}