하이라이트 색상 문제 해결 및 다중 하이라이트 렌더링 개선
주요 수정사항: - 하이라이트 생성 시 color → highlight_color 필드명 수정으로 색상 전달 문제 해결 - 분홍색을 더 연하게 변경하여 글씨 가독성 향상 - 다중 하이라이트 렌더링을 위아래 균등 분할로 개선 - CSS highlight-span 클래스 추가 및 색상 적용 강화 - 하이라이트 생성/렌더링 과정에 상세한 디버깅 로그 추가 UI 개선: - 단일 하이라이트: 선택한 색상으로 정확히 표시 - 다중 하이라이트: 위아래로 균등하게 색상 분할 표시 - 메모 입력 모달에서 선택된 텍스트 표시 개선 버그 수정: - 프론트엔드-백엔드 API 스키마 불일치 해결 - CSS 스타일 우선순위 문제 해결 - 하이라이트 색상이 노랑색으로만 표시되던 문제 해결
This commit is contained in:
396
frontend/static/js/viewer/utils/cache-manager.js
Normal file
396
frontend/static/js/viewer/utils/cache-manager.js
Normal file
@@ -0,0 +1,396 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user