2025-09-16 16:53:03 +09:00
|
|
|
/**
|
|
|
|
|
* Entity 조인 최적화를 위한 커스텀 훅
|
|
|
|
|
* 배치 로딩과 메모이제이션을 통한 성능 최적화
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
2025-09-18 19:15:13 +09:00
|
|
|
import { codeCache } from "@/lib/caching/codeCache";
|
2025-09-16 16:53:03 +09:00
|
|
|
|
|
|
|
|
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);
|
2025-09-24 18:07:36 +09:00
|
|
|
|
|
|
|
|
// 변환된 값 캐시 (중복 변환 방지)
|
|
|
|
|
const convertedCache = useRef(new Map<string, string>());
|
2025-09-16 16:53:03 +09:00
|
|
|
|
|
|
|
|
// 공통 코드 카테고리 추출 (메모이제이션)
|
|
|
|
|
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;
|
|
|
|
|
|
2025-11-04 18:31:26 +09:00
|
|
|
// console.log("🚀 공통 코드 프리로딩 시작");
|
2025-09-16 16:53:03 +09:00
|
|
|
|
|
|
|
|
// 현재 테이블의 코드 카테고리와 공통 카테고리 합치기
|
|
|
|
|
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;
|
|
|
|
|
|
2025-09-24 18:07:36 +09:00
|
|
|
// 🎯 중복 호출 방지: 이미 변환된 값인지 확인
|
|
|
|
|
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}"`);
|
|
|
|
|
}
|
2025-09-19 02:15:21 +09:00
|
|
|
|
2025-09-16 16:53:03 +09:00
|
|
|
// 캐시에서 동기적으로 조회 시도
|
|
|
|
|
const syncResult = codeCache.getCodeSync(categoryCode);
|
2025-09-24 18:07:36 +09:00
|
|
|
if (totalRequests.current % 10 === 1) {
|
|
|
|
|
console.log(`🔍 getCodeSync("${categoryCode}") 결과:`, syncResult);
|
|
|
|
|
}
|
2025-09-19 02:15:21 +09:00
|
|
|
|
2025-09-24 18:07:36 +09:00
|
|
|
// 🎯 캐시 내용 상세 로깅 (키값들 확인) - 빈도 줄이기
|
|
|
|
|
if (syncResult && totalRequests.current % 10 === 1) {
|
2025-09-19 02:15:21 +09:00
|
|
|
console.log(`🔍 캐시 키값들:`, Object.keys(syncResult));
|
|
|
|
|
console.log(`🔍 캐시 전체 데이터:`, JSON.stringify(syncResult, null, 2));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (syncResult && Array.isArray(syncResult)) {
|
2025-09-16 16:53:03 +09:00
|
|
|
cacheHits.current += 1;
|
2025-09-24 18:07:36 +09:00
|
|
|
if (totalRequests.current % 10 === 1) {
|
|
|
|
|
console.log(`🔍 배열에서 코드 검색: codeValue="${codeValue}"`);
|
|
|
|
|
console.log(
|
|
|
|
|
`🔍 캐시 배열 내용:`,
|
|
|
|
|
syncResult.map((item) => ({
|
|
|
|
|
code_value: item.code_value,
|
|
|
|
|
code_name: item.code_name,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-19 02:15:21 +09:00
|
|
|
|
|
|
|
|
// 배열에서 해당 code_value를 가진 항목 찾기
|
|
|
|
|
const foundCode = syncResult.find(
|
|
|
|
|
(item) => String(item.code_value).toUpperCase() === String(codeValue).toUpperCase(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = foundCode ? foundCode.code_name : codeValue;
|
2025-09-24 18:07:36 +09:00
|
|
|
|
|
|
|
|
// 변환 결과를 캐시에 저장
|
|
|
|
|
convertedCache.current.set(cacheKey, result);
|
|
|
|
|
|
|
|
|
|
if (totalRequests.current % 10 === 1) {
|
|
|
|
|
console.log(`🔍 최종 결과: "${codeValue}" → "${result}"`, { foundCode });
|
|
|
|
|
}
|
2025-09-16 16:53:03 +09:00
|
|
|
|
|
|
|
|
// 응답 시간 추적 (캐시 히트)
|
|
|
|
|
requestTimes.current.push(Date.now() - startTime);
|
|
|
|
|
if (requestTimes.current.length > 100) {
|
|
|
|
|
requestTimes.current = requestTimes.current.slice(-50); // 최근 50개만 유지
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
console.log(`⚠️ 캐시 미스: categoryCode="${categoryCode}" - 비동기 로딩 트리거`);
|
|
|
|
|
|
2025-09-16 16:53:03 +09:00
|
|
|
// 캐시 미스인 경우 비동기 로딩 트리거 (백그라운드)
|
|
|
|
|
codeCache
|
2025-09-19 02:15:21 +09:00
|
|
|
.getCodeAsync(categoryCode)
|
2025-09-16 16:53:03 +09:00
|
|
|
.then(() => {
|
2025-09-19 02:15:21 +09:00
|
|
|
console.log(`✅ 비동기 로딩 완료: categoryCode="${categoryCode}"`);
|
2025-09-16 16:53:03 +09:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|