/** * πŸ”₯ μ„±λŠ₯ μ΅œμ ν™”: λ°μ΄ν„°ν”Œλ‘œμš° μ„€μ • 캐싱 μ‹œμŠ€ν…œ * * λ²„νŠΌλ³„ μ œμ–΄κ΄€λ¦¬ 섀정을 λ©”λͺ¨λ¦¬μ— μΊμ‹œν•˜μ—¬ * 1ms μˆ˜μ€€μ˜ μ¦‰μ‹œ 응닡을 μ œκ³΅ν•©λ‹ˆλ‹€. */ import { ButtonDataflowConfig } from "@/types/screen"; import { apiClient } from "@/lib/api/client"; export interface CachedDataflowConfig { config: ButtonDataflowConfig; timestamp: number; hits: number; // μΊμ‹œ 히트 횟수 (ν†΅κ³„μš©) } export interface CacheMetrics { totalRequests: number; cacheHits: number; cacheMisses: number; hitRate: number; // 히트율 (%) averageResponseTime: number; // 평균 응닡 μ‹œκ°„ (ms) } /** * πŸ”₯ L1 λ©”λͺ¨λ¦¬ μΊμ‹œ (1ms 응닡) * * - TTL: 5λΆ„ (300초) * - μžλ™ 만료 처리 * - μ„±λŠ₯ μ§€ν‘œ μˆ˜μ§‘ */ export class DataflowConfigCache { private memoryCache = new Map(); private readonly TTL = 5 * 60 * 1000; // 5λΆ„ TTL private metrics: CacheMetrics = { totalRequests: 0, cacheHits: 0, cacheMisses: 0, hitRate: 0, averageResponseTime: 0, }; /** * πŸ”₯ λ²„νŠΌλ³„ μ œμ–΄κ΄€λ¦¬ μ„€μ • 쑰회 (μΊμ‹œ μš°μ„ ) */ async getConfig(buttonId: string): Promise { const startTime = performance.now(); this.metrics.totalRequests++; const cacheKey = `button_dataflow_${buttonId}`; try { // L1: λ©”λͺ¨λ¦¬ μΊμ‹œ 확인 (1ms) if (this.memoryCache.has(cacheKey)) { const cached = this.memoryCache.get(cacheKey)!; // TTL 확인 if (Date.now() - cached.timestamp < this.TTL) { cached.hits++; this.metrics.cacheHits++; this.updateHitRate(); const responseTime = performance.now() - startTime; this.updateAverageResponseTime(responseTime); console.log(`⚑ Cache hit: ${buttonId} (${responseTime.toFixed(2)}ms)`); return cached.config; } else { // TTL 만료된 μΊμ‹œ 제거 this.memoryCache.delete(cacheKey); } } // L2: μ„œλ²„μ—μ„œ λ‘œλ“œ (100-300ms) console.log(`🌐 Loading from server: ${buttonId}`); const serverConfig = await this.loadFromServer(buttonId); // μΊμ‹œμ— μ €μž₯ if (serverConfig) { this.memoryCache.set(cacheKey, { config: serverConfig, timestamp: Date.now(), hits: 1, }); } this.metrics.cacheMisses++; this.updateHitRate(); const responseTime = performance.now() - startTime; this.updateAverageResponseTime(responseTime); console.log(`πŸ“‘ Server response: ${buttonId} (${responseTime.toFixed(2)}ms)`); return serverConfig; } catch (error) { console.error(`❌ Failed to get config for button ${buttonId}:`, error); const responseTime = performance.now() - startTime; this.updateAverageResponseTime(responseTime); return null; } } /** * πŸ”₯ μ„œλ²„μ—μ„œ μ„€μ • λ‘œλ“œ */ private async loadFromServer(buttonId: string): Promise { try { const response = await apiClient.get(`/api/button-dataflow/config/${buttonId}`); if (response.data.success) { return response.data.data as ButtonDataflowConfig; } return null; } catch (error) { // 404λŠ” 정상 상황 (섀정이 μ—†λŠ” λ²„νŠΌ) if (error.response?.status === 404) { return null; } throw error; } } /** * πŸ”₯ μ„€μ • μ—…λ°μ΄νŠΈ (μΊμ‹œ λ¬΄νš¨ν™”) */ async updateConfig(buttonId: string, config: ButtonDataflowConfig): Promise { const cacheKey = `button_dataflow_${buttonId}`; try { // μ„œλ²„μ— μ €μž₯ await apiClient.put(`/api/button-dataflow/config/${buttonId}`, config); // μΊμ‹œ μ—…λ°μ΄νŠΈ this.memoryCache.set(cacheKey, { config, timestamp: Date.now(), hits: 0, }); console.log(`πŸ’Ύ Config updated: ${buttonId}`); } catch (error) { console.error(`❌ Failed to update config for button ${buttonId}:`, error); throw error; } } /** * πŸ”₯ νŠΉμ • λ²„νŠΌ μΊμ‹œ λ¬΄νš¨ν™” */ invalidateCache(buttonId: string): void { const cacheKey = `button_dataflow_${buttonId}`; this.memoryCache.delete(cacheKey); console.log(`πŸ—‘οΈ Cache invalidated: ${buttonId}`); } /** * πŸ”₯ 전체 μΊμ‹œ λ¬΄νš¨ν™” */ clearAllCache(): void { this.memoryCache.clear(); console.log(`πŸ—‘οΈ All cache cleared`); } /** * πŸ”₯ 관계도별 μΊμ‹œ λ¬΄νš¨ν™” (관계도가 μˆ˜μ •λœ 경우) */ invalidateDiagramCache(diagramId: number): void { let invalidatedCount = 0; for (const [key, cached] of this.memoryCache.entries()) { if (cached.config.selectedDiagramId === diagramId) { this.memoryCache.delete(key); invalidatedCount++; } } if (invalidatedCount > 0) { console.log(`πŸ—‘οΈ Invalidated ${invalidatedCount} caches for diagram ${diagramId}`); } } /** * πŸ”₯ μΊμ‹œ 톡계 쑰회 */ getMetrics(): CacheMetrics { return { ...this.metrics }; } /** * πŸ”₯ 상세 μΊμ‹œ 정보 쑰회 (λ””λ²„κΉ…μš©) */ getCacheInfo(): Array<{ buttonId: string; config: ButtonDataflowConfig; age: number; // μΊμ‹œλœμ§€ λͺ‡ λΆ„ κ²½κ³Ό hits: number; ttlRemaining: number; // 남은 TTL (초) }> { const now = Date.now(); const result: Array = []; for (const [key, cached] of this.memoryCache.entries()) { const buttonId = key.replace("button_dataflow_", ""); const age = Math.floor((now - cached.timestamp) / 1000 / 60); // λΆ„ const ttlRemaining = Math.max(0, Math.floor((this.TTL - (now - cached.timestamp)) / 1000)); // 초 result.push({ buttonId, config: cached.config, age, hits: cached.hits, ttlRemaining, }); } return result.sort((a, b) => b.hits - a.hits); // 히트 수 κΈ°μ€€ λ‚΄λ¦Όμ°¨μˆœ } /** * πŸ”₯ TTL 만료된 μΊμ‹œ 정리 (주기적 호좜) */ cleanupExpiredCache(): number { const now = Date.now(); let cleanedCount = 0; for (const [key, cached] of this.memoryCache.entries()) { if (now - cached.timestamp >= this.TTL) { this.memoryCache.delete(key); cleanedCount++; } } if (cleanedCount > 0) { console.log(`🧹 Cleaned up ${cleanedCount} expired cache entries`); } return cleanedCount; } /** * 히트율 μ—…λ°μ΄νŠΈ */ private updateHitRate(): void { this.metrics.hitRate = this.metrics.totalRequests > 0 ? (this.metrics.cacheHits / this.metrics.totalRequests) * 100 : 0; } /** * 평균 응닡 μ‹œκ°„ μ—…λ°μ΄νŠΈ (이동 평균) */ private updateAverageResponseTime(responseTime: number): void { if (this.metrics.averageResponseTime === 0) { this.metrics.averageResponseTime = responseTime; } else { // 이동 평균 (κΈ°μ‘΄ 90% + μƒˆλ‘œμš΄ κ°’ 10%) this.metrics.averageResponseTime = this.metrics.averageResponseTime * 0.9 + responseTime * 0.1; } } } // πŸ”₯ μ „μ—­ 싱글톀 μΈμŠ€ν„΄μŠ€ export const dataflowConfigCache = new DataflowConfigCache(); // πŸ”₯ 5λΆ„λ§ˆλ‹€ 만료된 μΊμ‹œ 정리 if (typeof window !== "undefined") { setInterval( () => { dataflowConfigCache.cleanupExpiredCache(); }, 5 * 60 * 1000, ); // 5λΆ„ } // πŸ”₯ 개발 λͺ¨λ“œμ—μ„œ μΊμ‹œ 정보λ₯Ό μ „μ—­ 객체에 λ…ΈμΆœ if (typeof window !== "undefined" && process.env.NODE_ENV === "development") { (window as any).dataflowCache = { getMetrics: () => dataflowConfigCache.getMetrics(), getCacheInfo: () => dataflowConfigCache.getCacheInfo(), clearCache: () => dataflowConfigCache.clearAllCache(), }; }