Files
Hyungi Ahn 5d4465b15c 하이라이트 색상 문제 해결 및 다중 하이라이트 렌더링 개선
주요 수정사항:
- 하이라이트 생성 시 color → highlight_color 필드명 수정으로 색상 전달 문제 해결
- 분홍색을 더 연하게 변경하여 글씨 가독성 향상
- 다중 하이라이트 렌더링을 위아래 균등 분할로 개선
- CSS highlight-span 클래스 추가 및 색상 적용 강화
- 하이라이트 생성/렌더링 과정에 상세한 디버깅 로그 추가

UI 개선:
- 단일 하이라이트: 선택한 색상으로 정확히 표시
- 다중 하이라이트: 위아래로 균등하게 색상 분할 표시
- 메모 입력 모달에서 선택된 텍스트 표시 개선

버그 수정:
- 프론트엔드-백엔드 API 스키마 불일치 해결
- CSS 스타일 우선순위 문제 해결
- 하이라이트 색상이 노랑색으로만 표시되던 문제 해결
2025-08-28 07:13:00 +09:00

397 lines
12 KiB
JavaScript

/**
* 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;