조인기능 최적화

This commit is contained in:
kjs
2025-09-16 16:53:03 +09:00
parent 6a3a7b915d
commit 1d05965a55
8 changed files with 1082 additions and 201 deletions

View File

@@ -1,11 +1,11 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import { ComponentRendererProps } from "@/types/component";
import { TableListConfig, ColumnConfig, TableDataResponse } from "./types";
import { TableListConfig, ColumnConfig } from "./types";
import { tableTypeApi } from "@/lib/api/screen";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { commonCodeApi } from "@/lib/api/commonCode";
import { codeCache } from "@/lib/cache/codeCache";
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -101,7 +101,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [selectedSearchColumn, setSelectedSearchColumn] = useState<string>(""); // 선택된 검색 컬럼
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨)
const [columnMeta, setColumnMeta] = useState<Record<string, { webType?: string; codeCategory?: string }>>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리)
const [codeCache, setCodeCache] = useState<Record<string, Record<string, string>>>({}); // 🎯 코드명 캐시 (categoryCode: {codeValue: codeName})
// 🎯 Entity 조인 최적화 훅 사용
const { isOptimizing, metrics, optimizedConvertCode, getCacheStatus } = useEntityJoinOptimization(columnMeta, {
enableBatchLoading: true,
preloadCommonCodes: true,
maxBatchSize: 5,
});
// 높이 계산 함수
const calculateOptimalHeight = () => {
@@ -145,7 +150,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
try {
const response = await tableTypeApi.getColumns(tableConfig.selectedTable);
// API 응답 구조 확인 및 컬럼 배열 추출
const columns = response.columns || response;
const columns = Array.isArray(response) ? response : response.columns || [];
const labels: Record<string, string> = {};
const meta: Record<string, { webType?: string; codeCategory?: string }> = {};
@@ -167,45 +172,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
};
// 🎯 코드 캐시 로드 함수
const loadCodeCache = async (categoryCode: string): Promise<void> => {
if (codeCache[categoryCode]) {
return; // 이미 캐시됨
}
try {
const response = await commonCodeApi.options.getOptions(categoryCode);
const codeMap: Record<string, string> = {};
if (response.success && response.data) {
response.data.forEach((option: any) => {
// 🎯 대소문자 구분 없이 저장 (모두 대문자로 키 저장)
codeMap[option.value?.toUpperCase()] = option.label;
});
}
setCodeCache((prev) => ({
...prev,
[categoryCode]: codeMap,
}));
console.log(`📋 코드 캐시 로드 완료 [${categoryCode}]:`, codeMap);
} catch (error) {
console.error(`❌ 코드 캐시 로드 실패 [${categoryCode}]:`, error);
}
};
// 🎯 코드값을 코드명으로 변환하는 함수 (대소문자 구분 없음)
const convertCodeToName = (categoryCode: string, codeValue: string): string => {
if (!categoryCode || !codeValue) return codeValue;
const codes = codeCache[categoryCode];
if (!codes) return codeValue;
// 🎯 대소문자 구분 없이 검색
const upperCodeValue = codeValue.toUpperCase();
return codes[upperCodeValue] || codeValue;
};
// 🎯 전역 코드 캐시 사용으로 함수 제거 (codeCache.convertCodeToName 사용)
// 테이블 라벨명 가져오기
const fetchTableLabel = async () => {
@@ -313,7 +280,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
console.log("🔗 Entity 조인 없음");
}
// 🎯 코드 컬럼들의 캐시 미리 로드
// 🎯 코드 컬럼들의 캐시 미리 로드 (전역 캐시 사용)
const codeColumns = Object.entries(columnMeta).filter(
([_, meta]) => meta.webType === "code" && meta.codeCategory,
);
@@ -324,14 +291,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
codeColumns.map(([col, meta]) => `${col}(${meta.codeCategory})`),
);
// 필요한 코드 캐시들을 병렬로 로드
const loadPromises = codeColumns.map(([_, meta]) =>
meta.codeCategory ? loadCodeCache(meta.codeCategory) : Promise.resolve(),
);
// 필요한 코드 카테고리들을 추출하여 배치 로드
const categoryList = codeColumns.map(([, meta]) => meta.codeCategory).filter(Boolean) as string[];
try {
await Promise.all(loadPromises);
console.log("📋 모든 코드 캐시 로드 완료");
await codeCache.preloadCodes(categoryList);
console.log("📋 모든 코드 캐시 로드 완료 (전역 캐시)");
} catch (error) {
console.error("❌ 코드 캐시 로드 중 오류:", error);
}
@@ -475,35 +440,37 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return displayColumns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
}, [displayColumns, tableConfig.columns]);
// 🎯 값 포맷팅 (코드 변환 포함)
const formatCellValue = (value: any, format?: string, columnName?: string) => {
if (value === null || value === undefined) return "";
// 🎯 값 포맷팅 (전역 코드 캐시 사용)
const formatCellValue = useMemo(() => {
return (value: any, format?: string, columnName?: string) => {
if (value === null || value === undefined) return "";
// 🎯 코드 컬럼인 경우 코드명으로 변환
if (columnName && columnMeta[columnName]?.webType === "code" && columnMeta[columnName]?.codeCategory) {
const categoryCode = columnMeta[columnName].codeCategory!;
const convertedValue = convertCodeToName(categoryCode, String(value));
// 🎯 코드 컬럼인 경우 최적화된 코드명 변환 사용
if (columnName && columnMeta[columnName]?.webType === "code" && columnMeta[columnName]?.codeCategory) {
const categoryCode = columnMeta[columnName].codeCategory!;
const convertedValue = optimizedConvertCode(categoryCode, String(value));
if (convertedValue !== String(value)) {
console.log(`🔄 코드 변환: ${columnName}[${categoryCode}] ${value}${convertedValue}`);
if (convertedValue !== String(value)) {
console.log(`🔄 코드 변환: ${columnName}[${categoryCode}] ${value}${convertedValue}`);
}
value = convertedValue;
}
value = convertedValue;
}
switch (format) {
case "number":
return typeof value === "number" ? value.toLocaleString() : value;
case "currency":
return typeof value === "number" ? `${value.toLocaleString()}` : value;
case "date":
return value instanceof Date ? value.toLocaleDateString() : value;
case "boolean":
return value ? "예" : "아니오";
default:
return String(value);
}
};
switch (format) {
case "number":
return typeof value === "number" ? value.toLocaleString() : value;
case "currency":
return typeof value === "number" ? `${value.toLocaleString()}` : value;
case "date":
return value instanceof Date ? value.toLocaleDateString() : value;
case "boolean":
return value ? "예" : "아니오";
default:
return String(value);
}
};
}, [columnMeta, optimizedConvertCode]); // 최적화된 변환 함수 의존성 추가
// 이벤트 핸들러
const handleClick = (e: React.MouseEvent) => {
@@ -582,6 +549,39 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</div>
)}
{/* 성능 상태 표시 (개발 모드에서만) */}
{process.env.NODE_ENV === "development" && (
<div className="flex items-center space-x-2 text-xs text-gray-500">
{isOptimizing && (
<div className="flex items-center space-x-1">
<RefreshCw className="h-3 w-3 animate-spin" />
<span> </span>
</div>
)}
<div className="flex items-center space-x-1">
<span>:</span>
<span
className={cn(
"rounded px-1 py-0.5 font-mono text-xs",
metrics.cacheHitRate > 0.8
? "bg-green-100 text-green-700"
: metrics.cacheHitRate > 0.5
? "bg-yellow-100 text-yellow-700"
: "bg-red-100 text-red-700",
)}
>
{(metrics.cacheHitRate * 100).toFixed(1)}%
</span>
</div>
{metrics.averageResponseTime > 0 && (
<div className="flex items-center space-x-1">
<span>:</span>
<span className="font-mono text-xs">{metrics.averageResponseTime}ms</span>
</div>
)}
</div>
)}
{/* 새로고침 */}
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={loading}>
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />