Merge branch 'dev' into feature/screen-management
This commit is contained in:
198
frontend/lib/caching/codeCache.ts
Normal file
198
frontend/lib/caching/codeCache.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 공통 코드 캐시 시스템
|
||||
* 자주 사용되는 공통 코드들을 메모리에 캐싱하여 성능을 향상시킵니다.
|
||||
*/
|
||||
|
||||
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||
|
||||
interface CacheEntry {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
expiry: number;
|
||||
}
|
||||
|
||||
class CodeCache {
|
||||
private cache = new Map<string, CacheEntry>();
|
||||
private defaultTTL = 5 * 60 * 1000; // 5분
|
||||
|
||||
/**
|
||||
* 캐시에 데이터 저장
|
||||
*/
|
||||
set(key: string, data: any, ttl?: number): void {
|
||||
const expiry = ttl || this.defaultTTL;
|
||||
const entry: CacheEntry = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
expiry,
|
||||
};
|
||||
this.cache.set(key, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시에서 데이터 조회
|
||||
*/
|
||||
get(key: string): any | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TTL 체크
|
||||
if (Date.now() - entry.timestamp > entry.expiry) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시에서 데이터 삭제
|
||||
*/
|
||||
delete(key: string): boolean {
|
||||
return this.cache.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 캐시 삭제
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 만료된 캐시 정리
|
||||
*/
|
||||
cleanup(): void {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if (now - entry.timestamp > entry.expiry) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 상태 조회
|
||||
*/
|
||||
getStats(): { size: number; keys: string[] } {
|
||||
return {
|
||||
size: this.cache.size,
|
||||
keys: Array.from(this.cache.keys()),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 코드 캐시 키 생성
|
||||
*/
|
||||
createCodeKey(category: string, companyCode?: string): string {
|
||||
return `code:${category}:${companyCode || "*"}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 코드 카테고리를 배치로 미리 로딩
|
||||
*/
|
||||
async preloadCodes(categories: string[]): Promise<void> {
|
||||
console.log(`🔄 코드 배치 로딩 시작: ${categories.join(", ")}`);
|
||||
|
||||
const promises = categories.map(async (category) => {
|
||||
try {
|
||||
const response = await commonCodeApi.codes.getList(category, { isActive: true });
|
||||
if (response.success && response.data) {
|
||||
const cacheKey = this.createCodeKey(category);
|
||||
this.set(cacheKey, response.data, this.defaultTTL);
|
||||
console.log(`✅ 코드 로딩 완료: ${category} (${response.data.length}개)`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ 코드 로딩 실패: ${category}`, error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
console.log(`✅ 코드 배치 로딩 완료: ${categories.length}개 카테고리`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드를 동기적으로 조회 (캐시에서만)
|
||||
*/
|
||||
getCodeSync(category: string, companyCode?: string): any[] | null {
|
||||
const cacheKey = this.createCodeKey(category, companyCode);
|
||||
return this.get(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드를 비동기적으로 조회 (캐시 미스 시 API 호출)
|
||||
*/
|
||||
async getCodeAsync(category: string, companyCode?: string): Promise<any[]> {
|
||||
const cached = this.getCodeSync(category, companyCode);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await commonCodeApi.codes.getList(category, { isActive: true });
|
||||
if (response.success && response.data) {
|
||||
const cacheKey = this.createCodeKey(category, companyCode);
|
||||
this.set(cacheKey, response.data, this.defaultTTL);
|
||||
return response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ 코드 조회 실패: ${category}`, error);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 정보 조회 (성능 메트릭용)
|
||||
*/
|
||||
getCacheInfo(): {
|
||||
size: number;
|
||||
keys: string[];
|
||||
totalMemoryUsage: number;
|
||||
hitRate?: number;
|
||||
} {
|
||||
const stats = this.getStats();
|
||||
|
||||
// 메모리 사용량 추정 (대략적)
|
||||
let totalMemoryUsage = 0;
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
// 키 크기 + 데이터 크기 추정
|
||||
totalMemoryUsage += key.length * 2; // 문자열은 UTF-16이므로 2바이트
|
||||
if (Array.isArray(entry.data)) {
|
||||
totalMemoryUsage += entry.data.length * 100; // 각 항목당 대략 100바이트로 추정
|
||||
} else {
|
||||
totalMemoryUsage += JSON.stringify(entry.data).length * 2;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...stats,
|
||||
totalMemoryUsage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 카테고리의 캐시 무효화
|
||||
*/
|
||||
invalidate(category: string, companyCode?: string): boolean {
|
||||
const cacheKey = this.createCodeKey(category, companyCode);
|
||||
return this.delete(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 싱글톤 인스턴스 생성
|
||||
const codeCache = new CodeCache();
|
||||
|
||||
// 주기적으로 만료된 캐시 정리 (10분마다)
|
||||
if (typeof window !== "undefined") {
|
||||
setInterval(
|
||||
() => {
|
||||
codeCache.cleanup();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
export default codeCache;
|
||||
export { CodeCache, codeCache };
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { codeCache } from "@/lib/cache/codeCache";
|
||||
import { codeCache } from "@/lib/caching/codeCache";
|
||||
|
||||
interface ColumnMetaInfo {
|
||||
webType?: string;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { WebTypeRegistry } from "./WebTypeRegistry";
|
||||
import { DynamicComponentProps } from "./types";
|
||||
import { getWidgetComponentByWebType, getWidgetComponentByName } from "@/components/screen/widgets/types";
|
||||
// import { getWidgetComponentByWebType, getWidgetComponentByName } from "@/components/screen/widgets/types"; // 임시 비활성화
|
||||
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
||||
|
||||
/**
|
||||
@@ -53,9 +53,11 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||
console.log(`웹타입 "${webType}" → DB 지정 컴포넌트 "${dbWebType.component_name}" 사용`);
|
||||
console.log(`DB 웹타입 정보:`, dbWebType);
|
||||
console.log(`웹타입 데이터 배열:`, webTypes);
|
||||
const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
|
||||
console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
|
||||
return <ComponentByName {...props} {...finalProps} />;
|
||||
// const ComponentByName = getWidgetComponentByName(dbWebType.component_name);
|
||||
// console.log(`컴포넌트 "${dbWebType.component_name}" 성공적으로 로드됨:`, ComponentByName);
|
||||
// return <ComponentByName {...props} {...finalProps} />;
|
||||
console.warn(`DB 지정 컴포넌트 "${dbWebType.component_name}" 기능 임시 비활성화`);
|
||||
return <div>컴포넌트 로딩 중...</div>;
|
||||
} catch (error) {
|
||||
console.error(`DB 지정 컴포넌트 "${dbWebType.component_name}" 렌더링 실패:`, error);
|
||||
}
|
||||
@@ -89,8 +91,10 @@ export const DynamicWebTypeRenderer: React.FC<DynamicComponentProps> = ({
|
||||
// 3순위: 웹타입명으로 자동 매핑 (폴백)
|
||||
try {
|
||||
console.warn(`웹타입 "${webType}" → 자동 매핑 폴백 사용`);
|
||||
const FallbackComponent = getWidgetComponentByWebType(webType);
|
||||
return <FallbackComponent {...props} />;
|
||||
// const FallbackComponent = getWidgetComponentByWebType(webType);
|
||||
// return <FallbackComponent {...props} />;
|
||||
console.warn(`웹타입 "${webType}" 폴백 기능 임시 비활성화`);
|
||||
return <div>웹타입 로딩 중...</div>;
|
||||
} catch (error) {
|
||||
console.error(`웹타입 "${webType}" 폴백 컴포넌트 렌더링 실패:`, error);
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useMemo } from "react";
|
||||
import { TableListConfig, ColumnConfig } from "./types";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
import { codeCache } from "@/lib/cache/codeCache";
|
||||
import { codeCache } from "@/lib/caching/codeCache";
|
||||
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
@@ -36,8 +36,10 @@ const nextConfig = {
|
||||
|
||||
// 환경 변수 (런타임에 읽기)
|
||||
env: {
|
||||
// 개발 환경에서는 Next.js rewrites를 통해 /api로 프록시
|
||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || "/api",
|
||||
// 프로덕션에서는 직접 백엔드 URL 사용, 개발환경에서는 프록시 사용
|
||||
NEXT_PUBLIC_API_URL:
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
(process.env.NODE_ENV === "production" ? "http://39.117.244.52:8080/api" : "/api"),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user