🚀 배포용: PDF 뷰어 개선 및 서적별 UI 데본씽크 스타일 적용
✨ 주요 개선사항: - PDF API 500 에러 수정 (한글 파일명 UTF-8 인코딩 처리) - PDF 뷰어 기능 완전 구현 (PDF.js 통합, 네비게이션, 확대/축소) - 서적별 문서 그룹화 UI 데본씽크 스타일로 개선 - PDF Manager 페이지 서적별 보기 기능 추가 - Alpine.js 로드 순서 최적화로 JavaScript 에러 해결 🎨 UI/UX 개선: - 확장/축소 가능한 아코디언 스타일 서적 목록 - 간결하고 직관적인 데본씽크 스타일 인터페이스 - PDF 상태 표시 (HTML 연결, 서적 분류) - 반응형 디자인 및 부드러운 애니메이션 🔧 기술적 개선: - PDF.js 워커 설정 및 토큰 인증 처리 - 서적별 PDF 자동 그룹화 로직 - Alpine.js 컴포넌트 초기화 최적화
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;
|
||||
521
frontend/static/js/viewer/utils/cached-api.js
Normal file
521
frontend/static/js/viewer/utils/cached-api.js
Normal file
@@ -0,0 +1,521 @@
|
||||
/**
|
||||
* CachedAPI - 캐싱이 적용된 API 래퍼
|
||||
* 기존 DocumentServerAPI를 확장하여 캐싱 기능을 추가합니다.
|
||||
*/
|
||||
class CachedAPI {
|
||||
constructor(baseAPI) {
|
||||
this.api = baseAPI;
|
||||
this.cache = window.cacheManager;
|
||||
|
||||
console.log('🚀 CachedAPI 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐싱이 적용된 GET 요청
|
||||
*/
|
||||
async get(endpoint, params = {}, options = {}) {
|
||||
const {
|
||||
useCache = true,
|
||||
category = 'api',
|
||||
ttl = null,
|
||||
forceRefresh = false
|
||||
} = options;
|
||||
|
||||
// 캐시 키 생성
|
||||
const cacheKey = this.generateCacheKey(endpoint, params);
|
||||
|
||||
// 강제 새로고침이 아니고 캐시 사용 설정인 경우 캐시 확인
|
||||
if (useCache && !forceRefresh) {
|
||||
const cached = this.cache.get(cacheKey, category);
|
||||
if (cached) {
|
||||
console.log(`🚀 API 캐시 사용: ${endpoint}`);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🌐 API 호출: ${endpoint}`);
|
||||
|
||||
// 실제 백엔드 API 엔드포인트로 매핑
|
||||
let response;
|
||||
if (endpoint === '/highlights' && params.document_id) {
|
||||
// 실제: /highlights/document/{documentId}
|
||||
response = await this.api.get(`/highlights/document/${params.document_id}`);
|
||||
} else if (endpoint === '/notes' && params.document_id) {
|
||||
// 실제: /notes/document/{documentId}
|
||||
response = await this.api.get(`/notes/document/${params.document_id}`);
|
||||
} else if (endpoint === '/bookmarks' && params.document_id) {
|
||||
// 실제: /bookmarks/document/{documentId}
|
||||
response = await this.api.get(`/bookmarks/document/${params.document_id}`);
|
||||
} else if (endpoint === '/document-links' && params.document_id) {
|
||||
// 실제: /documents/{documentId}/links
|
||||
response = await this.api.get(`/documents/${params.document_id}/links`);
|
||||
} else if (endpoint === '/document-links/backlinks' && params.target_document_id) {
|
||||
// 실제: /documents/{documentId}/backlinks
|
||||
response = await this.api.get(`/documents/${params.target_document_id}/backlinks`);
|
||||
} else if (endpoint.startsWith('/documents/') && endpoint.endsWith('/links')) {
|
||||
// /documents/{documentId}/links 패턴
|
||||
const documentId = endpoint.split('/')[2];
|
||||
console.log('🔗 CachedAPI: 링크 API 직접 호출:', documentId);
|
||||
response = await this.api.getDocumentLinks(documentId);
|
||||
} else if (endpoint.startsWith('/documents/') && endpoint.endsWith('/backlinks')) {
|
||||
// /documents/{documentId}/backlinks 패턴
|
||||
const documentId = endpoint.split('/')[2];
|
||||
console.log('🔗 CachedAPI: 백링크 API 직접 호출:', documentId);
|
||||
response = await this.api.getDocumentBacklinks(documentId);
|
||||
} else if (endpoint.startsWith('/documents/') && endpoint.match(/^\/documents\/[^\/]+$/)) {
|
||||
// /documents/{documentId} 패턴만 (추가 경로 없음)
|
||||
const documentId = endpoint.split('/')[2];
|
||||
console.log('📄 CachedAPI: 문서 API 호출:', documentId);
|
||||
response = await this.api.getDocument(documentId);
|
||||
} else {
|
||||
// 기본 API 호출 (기존 방식)
|
||||
response = await this.api.get(endpoint, params);
|
||||
}
|
||||
|
||||
// 성공적인 응답만 캐시에 저장
|
||||
if (useCache && response) {
|
||||
this.cache.set(cacheKey, response, category, ttl);
|
||||
console.log(`💾 API 응답 캐시 저장: ${endpoint}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`❌ API 호출 실패: ${endpoint}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐싱이 적용된 POST 요청 (일반적으로 캐시하지 않음)
|
||||
*/
|
||||
async post(endpoint, data = {}, options = {}) {
|
||||
const {
|
||||
invalidateCache = true,
|
||||
invalidateCategories = []
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const response = await this.api.post(endpoint, data);
|
||||
|
||||
// POST 후 관련 캐시 무효화
|
||||
if (invalidateCache) {
|
||||
this.invalidateRelatedCache(endpoint, invalidateCategories);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`❌ API POST 실패: ${endpoint}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐싱이 적용된 PUT 요청
|
||||
*/
|
||||
async put(endpoint, data = {}, options = {}) {
|
||||
const {
|
||||
invalidateCache = true,
|
||||
invalidateCategories = []
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const response = await this.api.put(endpoint, data);
|
||||
|
||||
// PUT 후 관련 캐시 무효화
|
||||
if (invalidateCache) {
|
||||
this.invalidateRelatedCache(endpoint, invalidateCategories);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`❌ API PUT 실패: ${endpoint}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐싱이 적용된 DELETE 요청
|
||||
*/
|
||||
async delete(endpoint, options = {}) {
|
||||
const {
|
||||
invalidateCache = true,
|
||||
invalidateCategories = []
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const response = await this.api.delete(endpoint);
|
||||
|
||||
// DELETE 후 관련 캐시 무효화
|
||||
if (invalidateCache) {
|
||||
this.invalidateRelatedCache(endpoint, invalidateCategories);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`❌ API DELETE 실패: ${endpoint}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 데이터 조회 (캐싱 최적화)
|
||||
*/
|
||||
async getDocument(documentId, contentType = 'document') {
|
||||
const cacheKey = `document_${documentId}_${contentType}`;
|
||||
|
||||
// 캐시 확인
|
||||
const cached = this.cache.get(cacheKey, 'document');
|
||||
if (cached) {
|
||||
console.log(`🚀 문서 캐시 사용: ${documentId}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 기존 API 메서드 직접 사용
|
||||
try {
|
||||
const result = await this.api.getDocument(documentId);
|
||||
|
||||
// 캐시에 저장
|
||||
this.cache.set(cacheKey, result, 'document', 30 * 60 * 1000);
|
||||
console.log(`💾 문서 캐시 저장: ${documentId}`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('문서 로드 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 하이라이트 조회 (캐싱 최적화)
|
||||
*/
|
||||
async getHighlights(documentId, contentType = 'document') {
|
||||
const cacheKey = `highlights_${documentId}_${contentType}`;
|
||||
|
||||
// 캐시 확인
|
||||
const cached = this.cache.get(cacheKey, 'highlights');
|
||||
if (cached) {
|
||||
console.log(`🚀 하이라이트 캐시 사용: ${documentId}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 기존 API 메서드 직접 사용
|
||||
try {
|
||||
let result;
|
||||
if (contentType === 'note') {
|
||||
result = await this.api.get(`/note/${documentId}/highlights`).catch(() => []);
|
||||
} else {
|
||||
result = await this.api.getDocumentHighlights(documentId).catch(() => []);
|
||||
}
|
||||
|
||||
// 캐시에 저장
|
||||
this.cache.set(cacheKey, result, 'highlights', 10 * 60 * 1000);
|
||||
console.log(`💾 하이라이트 캐시 저장: ${documentId}`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('하이라이트 로드 실패:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 조회 (캐싱 최적화)
|
||||
*/
|
||||
async getNotes(documentId, contentType = 'document') {
|
||||
const cacheKey = `notes_${documentId}_${contentType}`;
|
||||
|
||||
// 캐시 확인
|
||||
const cached = this.cache.get(cacheKey, 'notes');
|
||||
if (cached) {
|
||||
console.log(`🚀 메모 캐시 사용: ${documentId}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 하이라이트 메모 API 사용
|
||||
try {
|
||||
let result;
|
||||
if (contentType === 'note') {
|
||||
// 노트 문서의 하이라이트 메모
|
||||
result = await this.api.get(`/highlight-notes/`, { note_document_id: documentId }).catch(() => []);
|
||||
} else {
|
||||
// 일반 문서의 하이라이트 메모
|
||||
result = await this.api.get(`/highlight-notes/`, { document_id: documentId }).catch(() => []);
|
||||
}
|
||||
|
||||
// 캐시에 저장
|
||||
this.cache.set(cacheKey, result, 'notes', 10 * 60 * 1000);
|
||||
console.log(`💾 메모 캐시 저장: ${documentId} (${result.length}개)`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ 메모 로드 실패:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 조회 (캐싱 최적화)
|
||||
*/
|
||||
async getBookmarks(documentId) {
|
||||
const cacheKey = `bookmarks_${documentId}`;
|
||||
|
||||
// 캐시 확인
|
||||
const cached = this.cache.get(cacheKey, 'bookmarks');
|
||||
if (cached) {
|
||||
console.log(`🚀 북마크 캐시 사용: ${documentId}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 기존 API 메서드 직접 사용
|
||||
try {
|
||||
const result = await this.api.getDocumentBookmarks(documentId).catch(() => []);
|
||||
|
||||
// 캐시에 저장
|
||||
this.cache.set(cacheKey, result, 'bookmarks', 15 * 60 * 1000);
|
||||
console.log(`💾 북마크 캐시 저장: ${documentId}`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('북마크 로드 실패:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 링크 조회 (캐싱 최적화)
|
||||
*/
|
||||
async getDocumentLinks(documentId) {
|
||||
const cacheKey = `links_${documentId}`;
|
||||
|
||||
// 캐시 확인
|
||||
const cached = this.cache.get(cacheKey, 'links');
|
||||
if (cached) {
|
||||
console.log(`🚀 문서 링크 캐시 사용: ${documentId}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 기존 API 메서드 직접 사용
|
||||
try {
|
||||
const result = await this.api.getDocumentLinks(documentId).catch(() => []);
|
||||
|
||||
// 캐시에 저장
|
||||
this.cache.set(cacheKey, result, 'links', 15 * 60 * 1000);
|
||||
console.log(`💾 문서 링크 캐시 저장: ${documentId}`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('문서 링크 로드 실패:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 백링크 조회 (캐싱 최적화)
|
||||
*/
|
||||
async getBacklinks(documentId) {
|
||||
const cacheKey = `backlinks_${documentId}`;
|
||||
|
||||
// 캐시 확인
|
||||
const cached = this.cache.get(cacheKey, 'links');
|
||||
if (cached) {
|
||||
console.log(`🚀 백링크 캐시 사용: ${documentId}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 기존 API 메서드 직접 사용
|
||||
try {
|
||||
const result = await this.api.getDocumentBacklinks(documentId).catch(() => []);
|
||||
|
||||
// 캐시에 저장
|
||||
this.cache.set(cacheKey, result, 'links', 15 * 60 * 1000);
|
||||
console.log(`💾 백링크 캐시 저장: ${documentId}`);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('백링크 로드 실패:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 정보 조회 (캐싱 최적화)
|
||||
*/
|
||||
async getNavigation(documentId, contentType = 'document') {
|
||||
return await this.get('/documents/navigation', { document_id: documentId, content_type: contentType }, {
|
||||
category: 'navigation',
|
||||
ttl: 60 * 60 * 1000 // 1시간
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 하이라이트 생성 (캐시 무효화)
|
||||
*/
|
||||
async createHighlight(data) {
|
||||
return await this.post('/highlights/', data, {
|
||||
invalidateCategories: ['highlights', 'notes']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 생성 (캐시 무효화)
|
||||
*/
|
||||
async createNote(data) {
|
||||
return await this.post('/highlight-notes/', data, {
|
||||
invalidateCategories: ['notes', 'highlights']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 업데이트 (캐시 무효화)
|
||||
*/
|
||||
async updateNote(noteId, data) {
|
||||
return await this.put(`/highlight-notes/${noteId}`, data, {
|
||||
invalidateCategories: ['notes', 'highlights']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 삭제 (캐시 무효화)
|
||||
*/
|
||||
async deleteNote(noteId) {
|
||||
return await this.delete(`/highlight-notes/${noteId}`, {
|
||||
invalidateCategories: ['notes', 'highlights']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 생성 (캐시 무효화)
|
||||
*/
|
||||
async createBookmark(data) {
|
||||
return await this.post('/bookmarks/', data, {
|
||||
invalidateCategories: ['bookmarks']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 링크 생성 (캐시 무효화)
|
||||
*/
|
||||
async createDocumentLink(data) {
|
||||
return await this.post('/document-links/', data, {
|
||||
invalidateCategories: ['links']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 키 생성
|
||||
*/
|
||||
generateCacheKey(endpoint, params) {
|
||||
const sortedParams = Object.keys(params)
|
||||
.sort()
|
||||
.reduce((result, key) => {
|
||||
result[key] = params[key];
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return `${endpoint}_${JSON.stringify(sortedParams)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관련 캐시 무효화
|
||||
*/
|
||||
invalidateRelatedCache(endpoint, categories = []) {
|
||||
console.log(`🗑️ 캐시 무효화: ${endpoint}`);
|
||||
|
||||
// 기본 무효화 규칙
|
||||
const defaultInvalidations = {
|
||||
'/highlights': ['highlights', 'notes'],
|
||||
'/notes': ['notes', 'highlights'],
|
||||
'/bookmarks': ['bookmarks'],
|
||||
'/document-links': ['links']
|
||||
};
|
||||
|
||||
// 엔드포인트별 기본 무효화 적용
|
||||
for (const [pattern, cats] of Object.entries(defaultInvalidations)) {
|
||||
if (endpoint.includes(pattern)) {
|
||||
cats.forEach(cat => this.cache.deleteCategory(cat));
|
||||
}
|
||||
}
|
||||
|
||||
// 추가 무효화 카테고리 적용
|
||||
categories.forEach(category => {
|
||||
this.cache.deleteCategory(category);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 문서의 모든 캐시 무효화
|
||||
*/
|
||||
invalidateDocumentCache(documentId) {
|
||||
console.log(`🗑️ 문서 캐시 무효화: ${documentId}`);
|
||||
|
||||
const categories = ['document', 'highlights', 'notes', 'bookmarks', 'links', 'navigation'];
|
||||
categories.forEach(category => {
|
||||
// 해당 문서 ID가 포함된 캐시만 삭제하는 것이 이상적이지만,
|
||||
// 간단하게 전체 카테고리를 무효화
|
||||
this.cache.deleteCategory(category);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 강제 새로고침
|
||||
*/
|
||||
async refreshCache(endpoint, params = {}, category = 'api') {
|
||||
return await this.get(endpoint, params, {
|
||||
category,
|
||||
forceRefresh: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 통계 조회
|
||||
*/
|
||||
getCacheStats() {
|
||||
return this.cache.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 리포트 조회
|
||||
*/
|
||||
getCacheReport() {
|
||||
return this.cache.getReport();
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 캐시 삭제
|
||||
*/
|
||||
clearAllCache() {
|
||||
this.cache.clear();
|
||||
console.log('🗑️ 모든 API 캐시 삭제 완료');
|
||||
}
|
||||
|
||||
// 기존 API 메서드들을 그대로 위임 (캐싱이 필요 없는 경우)
|
||||
setToken(token) {
|
||||
return this.api.setToken(token);
|
||||
}
|
||||
|
||||
getHeaders() {
|
||||
return this.api.getHeaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 네비게이션 정보 조회 (캐싱 최적화)
|
||||
*/
|
||||
async getDocumentNavigation(documentId) {
|
||||
const cacheKey = `navigation_${documentId}`;
|
||||
return await this.get(`/documents/${documentId}/navigation`, {}, {
|
||||
category: 'navigation',
|
||||
cacheKey,
|
||||
ttl: 30 * 60 * 1000 // 30분 (네비게이션은 자주 변경되지 않음)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 api 인스턴스를 캐싱 API로 래핑
|
||||
if (window.api) {
|
||||
window.cachedApi = new CachedAPI(window.api);
|
||||
console.log('🚀 CachedAPI 래퍼 생성 완료');
|
||||
}
|
||||
|
||||
// 전역으로 내보내기
|
||||
window.CachedAPI = CachedAPI;
|
||||
223
frontend/static/js/viewer/utils/module-loader.js
Normal file
223
frontend/static/js/viewer/utils/module-loader.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* ModuleLoader - 지연 로딩 및 모듈 관리
|
||||
* 필요한 모듈만 동적으로 로드하여 성능을 최적화합니다.
|
||||
*/
|
||||
class ModuleLoader {
|
||||
constructor() {
|
||||
console.log('🔧 ModuleLoader 초기화 시작');
|
||||
|
||||
// 로드된 모듈 캐시
|
||||
this.loadedModules = new Map();
|
||||
|
||||
// 로딩 중인 모듈 Promise 캐시 (중복 로딩 방지)
|
||||
this.loadingPromises = new Map();
|
||||
|
||||
// 모듈 의존성 정의
|
||||
this.moduleDependencies = {
|
||||
'DocumentLoader': [],
|
||||
'HighlightManager': ['DocumentLoader'],
|
||||
'BookmarkManager': ['DocumentLoader'],
|
||||
'LinkManager': ['DocumentLoader'],
|
||||
'UIManager': []
|
||||
};
|
||||
|
||||
// 모듈 경로 정의
|
||||
this.modulePaths = {
|
||||
'DocumentLoader': '/static/js/viewer/core/document-loader.js',
|
||||
'HighlightManager': '/static/js/viewer/features/highlight-manager.js',
|
||||
'BookmarkManager': '/static/js/viewer/features/bookmark-manager.js',
|
||||
'LinkManager': '/static/js/viewer/features/link-manager.js',
|
||||
'UIManager': '/static/js/viewer/features/ui-manager.js'
|
||||
};
|
||||
|
||||
// 캐시 버스팅을 위한 버전
|
||||
this.version = '2025012607';
|
||||
|
||||
console.log('✅ ModuleLoader 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 동적 로드
|
||||
*/
|
||||
async loadModule(moduleName) {
|
||||
// 이미 로드된 모듈인지 확인
|
||||
if (this.loadedModules.has(moduleName)) {
|
||||
console.log(`✅ 모듈 캐시에서 반환: ${moduleName}`);
|
||||
return this.loadedModules.get(moduleName);
|
||||
}
|
||||
|
||||
// 이미 로딩 중인 모듈인지 확인 (중복 로딩 방지)
|
||||
if (this.loadingPromises.has(moduleName)) {
|
||||
console.log(`⏳ 모듈 로딩 대기 중: ${moduleName}`);
|
||||
return await this.loadingPromises.get(moduleName);
|
||||
}
|
||||
|
||||
console.log(`🔄 모듈 로딩 시작: ${moduleName}`);
|
||||
|
||||
// 로딩 Promise 생성 및 캐시
|
||||
const loadingPromise = this._loadModuleScript(moduleName);
|
||||
this.loadingPromises.set(moduleName, loadingPromise);
|
||||
|
||||
try {
|
||||
const moduleClass = await loadingPromise;
|
||||
|
||||
// 로딩 완료 후 캐시에 저장
|
||||
this.loadedModules.set(moduleName, moduleClass);
|
||||
this.loadingPromises.delete(moduleName);
|
||||
|
||||
console.log(`✅ 모듈 로딩 완료: ${moduleName}`);
|
||||
return moduleClass;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ 모듈 로딩 실패: ${moduleName}`, error);
|
||||
this.loadingPromises.delete(moduleName);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 의존성을 포함한 모듈 로드
|
||||
*/
|
||||
async loadModuleWithDependencies(moduleName) {
|
||||
console.log(`🔗 의존성 포함 모듈 로딩: ${moduleName}`);
|
||||
|
||||
// 의존성 먼저 로드
|
||||
const dependencies = this.moduleDependencies[moduleName] || [];
|
||||
if (dependencies.length > 0) {
|
||||
console.log(`📦 의존성 로딩: ${dependencies.join(', ')}`);
|
||||
await Promise.all(dependencies.map(dep => this.loadModule(dep)));
|
||||
}
|
||||
|
||||
// 메인 모듈 로드
|
||||
return await this.loadModule(moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 모듈 병렬 로드
|
||||
*/
|
||||
async loadModules(moduleNames) {
|
||||
console.log(`🚀 병렬 모듈 로딩: ${moduleNames.join(', ')}`);
|
||||
|
||||
const loadPromises = moduleNames.map(name => this.loadModuleWithDependencies(name));
|
||||
const results = await Promise.all(loadPromises);
|
||||
|
||||
console.log(`✅ 병렬 모듈 로딩 완료: ${moduleNames.join(', ')}`);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 스크립트 동적 로딩
|
||||
*/
|
||||
async _loadModuleScript(moduleName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 이미 전역에 클래스가 있는지 확인
|
||||
if (window[moduleName]) {
|
||||
console.log(`✅ 모듈 이미 로드됨: ${moduleName}`);
|
||||
resolve(window[moduleName]);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📥 스크립트 로딩 시작: ${moduleName}`);
|
||||
const script = document.createElement('script');
|
||||
script.src = `${this.modulePaths[moduleName]}?v=${this.version}`;
|
||||
script.async = true;
|
||||
|
||||
script.onload = () => {
|
||||
console.log(`📥 스크립트 로드 완료: ${moduleName}`);
|
||||
|
||||
// 스크립트 로드 후 잠시 대기 (클래스 등록 시간)
|
||||
setTimeout(() => {
|
||||
if (window[moduleName]) {
|
||||
console.log(`✅ 모듈 클래스 확인: ${moduleName}`);
|
||||
resolve(window[moduleName]);
|
||||
} else {
|
||||
console.error(`❌ 모듈 클래스 없음: ${moduleName}`, Object.keys(window).filter(k => k.includes('Manager') || k.includes('Loader')));
|
||||
reject(new Error(`모듈 클래스를 찾을 수 없음: ${moduleName}`));
|
||||
}
|
||||
}, 10); // 10ms 대기
|
||||
};
|
||||
|
||||
script.onerror = (error) => {
|
||||
console.error(`❌ 스크립트 로딩 실패: ${moduleName}`, error);
|
||||
reject(new Error(`스크립트 로딩 실패: ${moduleName}`));
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 인스턴스 생성 (팩토리 패턴)
|
||||
*/
|
||||
async createModuleInstance(moduleName, ...args) {
|
||||
const ModuleClass = await this.loadModuleWithDependencies(moduleName);
|
||||
return new ModuleClass(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 프리로딩 (백그라운드에서 미리 로드)
|
||||
*/
|
||||
async preloadModules(moduleNames) {
|
||||
console.log(`🔮 모듈 프리로딩: ${moduleNames.join(', ')}`);
|
||||
|
||||
// 백그라운드에서 로드 (에러 무시)
|
||||
const preloadPromises = moduleNames.map(async (name) => {
|
||||
try {
|
||||
await this.loadModuleWithDependencies(name);
|
||||
console.log(`✅ 프리로딩 완료: ${name}`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ 프리로딩 실패: ${name}`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// 모든 프리로딩이 완료될 때까지 기다리지 않음
|
||||
Promise.all(preloadPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용하지 않는 모듈 언로드 (메모리 최적화)
|
||||
*/
|
||||
unloadModule(moduleName) {
|
||||
if (this.loadedModules.has(moduleName)) {
|
||||
this.loadedModules.delete(moduleName);
|
||||
console.log(`🗑️ 모듈 언로드: ${moduleName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 모듈 언로드
|
||||
*/
|
||||
unloadAllModules() {
|
||||
this.loadedModules.clear();
|
||||
this.loadingPromises.clear();
|
||||
console.log('🗑️ 모든 모듈 언로드 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 로드된 모듈 상태 확인
|
||||
*/
|
||||
getLoadedModules() {
|
||||
return Array.from(this.loadedModules.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모리 사용량 추정
|
||||
*/
|
||||
getMemoryUsage() {
|
||||
const loadedCount = this.loadedModules.size;
|
||||
const loadingCount = this.loadingPromises.size;
|
||||
|
||||
return {
|
||||
loadedModules: loadedCount,
|
||||
loadingModules: loadingCount,
|
||||
totalModules: Object.keys(this.modulePaths).length,
|
||||
memoryEstimate: `${loadedCount * 50}KB` // 대략적인 추정
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 모듈 로더 인스턴스
|
||||
window.moduleLoader = new ModuleLoader();
|
||||
|
||||
// 전역으로 내보내기
|
||||
window.ModuleLoader = ModuleLoader;
|
||||
Reference in New Issue
Block a user