/** * CacheManager - 데이터 캐싱 및 로컬 스토리지 관리 * API 응답, 문서 데이터, 사용자 설정 등을 효율적으로 캐싱합니다. */ class CacheManager { constructor() { console.log('💾 CacheManager 초기화 시작'); // 캐시 설정 this.config = { // 캐시 만료 시간 (밀리초) ttl: { document: 30 * 60 * 1000, // 문서: 30분 highlights: 10 * 60 * 1000, // 하이라이트: 10분 notes: 10 * 60 * 1000, // 메모: 10분 bookmarks: 15 * 60 * 1000, // 북마크: 15분 links: 15 * 60 * 1000, // 링크: 15분 navigation: 60 * 60 * 1000, // 네비게이션: 1시간 userSettings: 24 * 60 * 60 * 1000 // 사용자 설정: 24시간 }, // 캐시 키 접두사 prefix: 'docviewer_', // 최대 캐시 크기 (항목 수) maxItems: 100, // 로컬 스토리지 사용 여부 useLocalStorage: true }; // 메모리 캐시 (빠른 접근용) this.memoryCache = new Map(); // 캐시 통계 this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 }; // 초기화 시 오래된 캐시 정리 this.cleanupExpiredCache(); console.log('✅ CacheManager 초기화 완료'); } /** * 캐시에서 데이터 가져오기 */ get(key, category = 'default') { const fullKey = this.getFullKey(key, category); // 1. 메모리 캐시에서 먼저 확인 if (this.memoryCache.has(fullKey)) { const cached = this.memoryCache.get(fullKey); if (this.isValid(cached)) { this.stats.hits++; console.log(`💾 메모리 캐시 HIT: ${fullKey}`); return cached.data; } else { // 만료된 캐시 제거 this.memoryCache.delete(fullKey); } } // 2. 로컬 스토리지에서 확인 if (this.config.useLocalStorage) { try { const stored = localStorage.getItem(fullKey); if (stored) { const cached = JSON.parse(stored); if (this.isValid(cached)) { // 메모리 캐시에도 저장 this.memoryCache.set(fullKey, cached); this.stats.hits++; console.log(`💾 로컬 스토리지 캐시 HIT: ${fullKey}`); return cached.data; } else { // 만료된 캐시 제거 localStorage.removeItem(fullKey); } } } catch (error) { console.warn('로컬 스토리지 읽기 오류:', error); } } this.stats.misses++; console.log(`💾 캐시 MISS: ${fullKey}`); return null; } /** * 캐시에 데이터 저장 */ set(key, data, category = 'default', customTtl = null) { const fullKey = this.getFullKey(key, category); const ttl = customTtl || this.config.ttl[category] || this.config.ttl.default || 10 * 60 * 1000; const cached = { data: data, timestamp: Date.now(), ttl: ttl, category: category, size: this.estimateSize(data) }; // 메모리 캐시에 저장 this.memoryCache.set(fullKey, cached); // 로컬 스토리지에 저장 if (this.config.useLocalStorage) { try { localStorage.setItem(fullKey, JSON.stringify(cached)); } catch (error) { console.warn('로컬 스토리지 저장 오류 (용량 부족?):', error); // 용량 부족 시 오래된 캐시 정리 후 재시도 this.cleanupOldCache(); try { localStorage.setItem(fullKey, JSON.stringify(cached)); } catch (retryError) { console.error('로컬 스토리지 저장 재시도 실패:', retryError); } } } this.stats.sets++; console.log(`💾 캐시 저장: ${fullKey} (TTL: ${ttl}ms)`); // 캐시 크기 제한 확인 this.enforceMaxItems(); } /** * 특정 키 또는 카테고리의 캐시 삭제 */ delete(key, category = 'default') { const fullKey = this.getFullKey(key, category); // 메모리 캐시에서 삭제 this.memoryCache.delete(fullKey); // 로컬 스토리지에서 삭제 if (this.config.useLocalStorage) { localStorage.removeItem(fullKey); } console.log(`💾 캐시 삭제: ${fullKey}`); } /** * 카테고리별 캐시 전체 삭제 */ deleteCategory(category) { const prefix = this.getFullKey('', category); // 메모리 캐시에서 삭제 for (const key of this.memoryCache.keys()) { if (key.startsWith(prefix)) { this.memoryCache.delete(key); } } // 로컬 스토리지에서 삭제 if (this.config.useLocalStorage) { for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key && key.startsWith(prefix)) { localStorage.removeItem(key); } } } console.log(`💾 카테고리 캐시 삭제: ${category}`); } /** * 모든 캐시 삭제 */ clear() { // 메모리 캐시 삭제 this.memoryCache.clear(); // 로컬 스토리지에서 관련 캐시만 삭제 if (this.config.useLocalStorage) { for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key && key.startsWith(this.config.prefix)) { localStorage.removeItem(key); } } } // 통계 초기화 this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 }; console.log('💾 모든 캐시 삭제 완료'); } /** * 캐시 유효성 검사 */ isValid(cached) { if (!cached || !cached.timestamp || !cached.ttl) { return false; } const age = Date.now() - cached.timestamp; return age < cached.ttl; } /** * 만료된 캐시 정리 */ cleanupExpiredCache() { console.log('🧹 만료된 캐시 정리 시작'); let cleanedCount = 0; // 메모리 캐시 정리 for (const [key, cached] of this.memoryCache.entries()) { if (!this.isValid(cached)) { this.memoryCache.delete(key); cleanedCount++; } } // 로컬 스토리지 정리 if (this.config.useLocalStorage) { for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key && key.startsWith(this.config.prefix)) { try { const stored = localStorage.getItem(key); if (stored) { const cached = JSON.parse(stored); if (!this.isValid(cached)) { localStorage.removeItem(key); cleanedCount++; } } } catch (error) { // 파싱 오류 시 해당 캐시 삭제 localStorage.removeItem(key); cleanedCount++; } } } } console.log(`🧹 만료된 캐시 ${cleanedCount}개 정리 완료`); } /** * 오래된 캐시 정리 (용량 부족 시) */ cleanupOldCache() { console.log('🧹 오래된 캐시 정리 시작'); const items = []; // 로컬 스토리지의 모든 캐시 항목 수집 for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(this.config.prefix)) { try { const stored = localStorage.getItem(key); if (stored) { const cached = JSON.parse(stored); items.push({ key, cached }); } } catch (error) { // 파싱 오류 시 해당 캐시 삭제 localStorage.removeItem(key); } } } // 타임스탬프 기준으로 정렬 (오래된 것부터) items.sort((a, b) => a.cached.timestamp - b.cached.timestamp); // 오래된 항목의 절반 삭제 const deleteCount = Math.floor(items.length / 2); for (let i = 0; i < deleteCount; i++) { localStorage.removeItem(items[i].key); this.memoryCache.delete(items[i].key); } console.log(`🧹 오래된 캐시 ${deleteCount}개 정리 완료`); } /** * 최대 항목 수 제한 적용 */ enforceMaxItems() { if (this.memoryCache.size > this.config.maxItems) { const excess = this.memoryCache.size - this.config.maxItems; const keys = Array.from(this.memoryCache.keys()); // 오래된 항목부터 삭제 for (let i = 0; i < excess; i++) { this.memoryCache.delete(keys[i]); this.stats.evictions++; } console.log(`💾 캐시 크기 제한으로 ${excess}개 항목 제거`); } } /** * 전체 키 생성 */ getFullKey(key, category) { return `${this.config.prefix}${category}_${key}`; } /** * 데이터 크기 추정 */ estimateSize(data) { try { return JSON.stringify(data).length; } catch { return 0; } } /** * 캐시 통계 조회 */ getStats() { const hitRate = this.stats.hits + this.stats.misses > 0 ? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2) : 0; return { ...this.stats, hitRate: `${hitRate}%`, memoryItems: this.memoryCache.size, localStorageItems: this.getLocalStorageItemCount() }; } /** * 로컬 스토리지 항목 수 조회 */ getLocalStorageItemCount() { let count = 0; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(this.config.prefix)) { count++; } } return count; } /** * 캐시 상태 리포트 */ getReport() { const stats = this.getStats(); const memoryUsage = Array.from(this.memoryCache.values()) .reduce((total, cached) => total + (cached.size || 0), 0); return { stats, memoryUsage: `${(memoryUsage / 1024).toFixed(2)} KB`, categories: this.getCategoryStats(), config: this.config }; } /** * 카테고리별 통계 */ getCategoryStats() { const categories = {}; for (const [key, cached] of this.memoryCache.entries()) { const category = cached.category || 'default'; if (!categories[category]) { categories[category] = { count: 0, size: 0 }; } categories[category].count++; categories[category].size += cached.size || 0; } return categories; } } // 전역 캐시 매니저 인스턴스 window.cacheManager = new CacheManager(); // 전역으로 내보내기 window.CacheManager = CacheManager;