Files
document-server/frontend/static/js/viewer/utils/cached-api.js
Hyungi Ahn 3ba804276c Fix: 노트 링크 관련 모든 기능 완전 수정
주요 수정사항:
- 노트 간 링크 네비게이션 수정 (target_note_id 우선 사용)
- 노트 백링크 네비게이션 수정 (source_note_id 우선 사용)
- 노트 링크 삭제 API 분기 처리 (/note-links vs /document-links)
- 하이라이트 삭제 시 메모 캐시 무효화 추가
- 하이라이트 메모 삭제 API 엔드포인트 추가 (DELETE /highlight-notes/{note_id})
- URL 파싱 개선 (null/undefined ID 감지 및 오류 처리)
- 노트 링크 생성 응답에 source_content_type, target_content_type 추가
- 통합 툴팁에서 노트 링크 제목 표시 수정 (target_note_title 사용)
- 링크 삭제 버튼에서 null 참조 오류 수정

수정된 파일:
- frontend: viewer-core.js, link-manager.js, highlight-manager.js, api.js, cached-api.js
- backend: note_links.py, notes.py
- 브라우저 캐시 무효화: 버전 v=2025012623
2025-09-04 08:42:12 +09:00

522 lines
17 KiB
JavaScript

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