From 0786bdc86dc5e2266cbd5dc6dc0cfb60e030bf94 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Thu, 4 Sep 2025 07:48:43 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=ED=95=98=EC=9D=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EB=AA=A8=20=ED=91=9C=EC=8B=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - highlight-manager.js에서 showHighlightTooltip 함수 호출 시 배열 대신 단일 객체 전달하도록 수정 - 하이라이트 클릭 시 메모가 0개로 표시되던 문제 해결 - getOverlappingElements 함수에 디버깅 로그 추가 - 하이라이트 매니저 상태 확인 로그 추가 - 브라우저 캐시 무효화를 위한 버전 업데이트 (v=2025012617) --- backend/src/api/routes/document_links.py | 50 +- backend/src/api/routes/notes.py | 63 +- frontend/static/js/api.js | 28 +- .../js/viewer/features/highlight-manager.js | 368 ++++++++-- .../static/js/viewer/features/link-manager.js | 293 +++++++- frontend/static/js/viewer/utils/cached-api.js | 23 +- frontend/static/js/viewer/viewer-core.js | 655 +++++++++++++++++- frontend/viewer.html | 8 +- 8 files changed, 1355 insertions(+), 133 deletions(-) diff --git a/backend/src/api/routes/document_links.py b/backend/src/api/routes/document_links.py index 921e613..bec63ed 100644 --- a/backend/src/api/routes/document_links.py +++ b/backend/src/api/routes/document_links.py @@ -64,6 +64,7 @@ class DocumentLinkResponse(BaseModel): # 대상 문서 정보 target_document_title: str target_document_book_id: Optional[str] + target_content_type: Optional[str] = "document" # "document" 또는 "note" class Config: from_attributes = True @@ -241,19 +242,51 @@ async def get_document_links( detail="Access denied" ) - # 링크 조회 (JOIN으로 대상 문서 정보도 함께) + # 모든 링크 조회 (문서→문서 + 문서→노트) result = await db.execute( - select(DocumentLink, Document) - .join(Document, DocumentLink.target_document_id == Document.id) + select(DocumentLink) .where(DocumentLink.source_document_id == document_id) .order_by(DocumentLink.start_offset.asc()) ) - links_with_targets = result.all() + all_links = result.scalars().all() + print(f"🔍 문서 링크 조회 완료: {len(all_links)}개 발견") # 응답 데이터 구성 response_links = [] - for link, target_doc in links_with_targets: + for link in all_links: + print(f"🔗 링크 처리 중: {link.id} -> {link.target_document_id}") + + # 대상이 문서인지 노트인지 확인 + target_doc = None + target_note = None + + # 먼저 Document 테이블에서 찾기 + doc_result = await db.execute(select(Document).where(Document.id == link.target_document_id)) + target_doc = doc_result.scalar_one_or_none() + + if target_doc: + print(f"✅ 대상 문서 찾음: {target_doc.title}") + target_title = target_doc.title + target_book_id = str(target_doc.book_id) if target_doc.book_id else None + target_content_type = "document" + else: + # Document에서 찾지 못하면 NoteDocument에서 찾기 + from ...models.note_document import NoteDocument + note_result = await db.execute(select(NoteDocument).where(NoteDocument.id == link.target_document_id)) + target_note = note_result.scalar_one_or_none() + + if target_note: + print(f"✅ 대상 노트 찾음: {target_note.title}") + target_title = f"📝 {target_note.title}" # 노트임을 표시 + target_book_id = str(target_note.notebook_id) if target_note.notebook_id else None + target_content_type = "note" + else: + print(f"❌ 대상을 찾을 수 없음: {link.target_document_id}") + target_title = "Unknown Target" + target_book_id = None + target_content_type = "document" # 기본값 + response_links.append(DocumentLinkResponse( id=str(link.id), source_document_id=str(link.source_document_id), @@ -270,9 +303,10 @@ async def get_document_links( target_start_offset=getattr(link, 'target_start_offset', None), target_end_offset=getattr(link, 'target_end_offset', None), link_type=getattr(link, 'link_type', 'document'), - # 대상 문서 정보 - target_document_title=target_doc.title, - target_document_book_id=str(target_doc.book_id) if target_doc.book_id else None + # 대상 문서/노트 정보 추가 + target_document_title=target_title, + target_document_book_id=target_book_id, + target_content_type=target_content_type )) return response_links diff --git a/backend/src/api/routes/notes.py b/backend/src/api/routes/notes.py index f92243d..cfd697d 100644 --- a/backend/src/api/routes/notes.py +++ b/backend/src/api/routes/notes.py @@ -33,7 +33,7 @@ router = APIRouter() # 용어 정의: 하이라이트에 달리는 짧은 코멘트 @router.post("/") -async def create_note( +def create_note( note_data: dict, db: Session = Depends(get_sync_db), current_user: User = Depends(get_current_user) @@ -65,6 +65,39 @@ async def create_note( return note +@router.put("/{note_id}") +def update_note( + note_id: str, + note_data: dict, + db: Session = Depends(get_sync_db), + current_user: User = Depends(get_current_user) +): + """하이라이트 메모 업데이트""" + from ...models.note import Note + from ...models.highlight import Highlight + + # 메모 존재 및 소유권 확인 + note = db.query(Note).join(Highlight).filter( + Note.id == note_id, + Highlight.user_id == current_user.id + ).first() + + if not note: + raise HTTPException(status_code=404, detail="메모를 찾을 수 없습니다") + + # 메모 업데이트 + if 'content' in note_data: + note.content = note_data['content'] + if 'tags' in note_data: + note.tags = note_data['tags'] + + note.updated_at = datetime.utcnow() + + db.commit() + db.refresh(note) + + return note + @router.get("/document/{document_id}") async def get_document_notes( document_id: str, @@ -118,7 +151,7 @@ def calculate_word_count(content: str) -> int: return korean_chars + english_words -@router.get("/", response_model=List[NoteDocumentListItem]) +@router.get("/") def get_notes( skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), @@ -128,10 +161,34 @@ def get_notes( published_only: bool = Query(False), parent_id: Optional[str] = Query(None), notebook_id: Optional[str] = Query(None), # 노트북 필터 + document_id: Optional[str] = Query(None), # 하이라이트 메모 조회용 + note_document_id: Optional[str] = Query(None), # 노트 문서의 하이라이트 메모 조회용 db: Session = Depends(get_sync_db), current_user: User = Depends(get_current_user) ): - """노트 목록 조회""" + """노트 목록 조회 또는 하이라이트 메모 조회""" + + # 하이라이트 메모 조회 요청인 경우 + if document_id or note_document_id: + from ...models.note import Note + from ...models.highlight import Highlight + + if document_id: + # 일반 문서의 하이라이트 메모 조회 + notes = db.query(Note).join(Highlight).filter( + Highlight.document_id == document_id, + Highlight.user_id == current_user.id + ).options( + selectinload(Note.highlight) + ).all() + else: + # 노트 문서의 하이라이트 메모 조회 (note_document_id) + # 노트 하이라이트 모델이 있다면 사용, 없다면 빈 리스트 반환 + notes = [] + + return notes + + # 일반 노트 문서 목록 조회 # 동기 SQLAlchemy 스타일 query = db.query(NoteDocument) diff --git a/frontend/static/js/api.js b/frontend/static/js/api.js index 23cc083..e35e256 100644 --- a/frontend/static/js/api.js +++ b/frontend/static/js/api.js @@ -201,7 +201,7 @@ class DocumentServerAPI { } async getCurrentUser() { - return await this.get('/users/me'); + return await this.get('/auth/me'); } async refreshToken(refreshToken) { @@ -255,8 +255,8 @@ class DocumentServerAPI { return await this.get(`/highlights/document/${documentId}`); } - async updateHighlight(highlightId, data) { - return await this.put(`/highlights/${highlightId}`, data); + async updateHighlight(highlightId, updateData) { + return await this.put(`/highlights/${highlightId}`, updateData); } async deleteHighlight(highlightId) { @@ -266,22 +266,24 @@ class DocumentServerAPI { // === 하이라이트 메모 (Highlight Memo) 관련 API === // 용어 정의: 하이라이트에 달리는 짧은 코멘트 async createNote(noteData) { - return await this.post('/notes/', noteData); + return await this.post('/highlight-notes/', noteData); } async getNotes(params = {}) { - return await this.get('/notes/', params); + return await this.get('/highlight-notes/', params); + } + + async updateNote(noteId, updateData) { + return await this.put(`/highlight-notes/${noteId}`, updateData); } // === 문서 메모 조회 === // 용어 정의: 특정 문서의 모든 하이라이트 메모 조회 async getDocumentNotes(documentId) { - return await this.get(`/notes/document/${documentId}`); + return await this.get(`/highlight-notes/`, { document_id: documentId }); } - async updateNote(noteId, data) { - return await this.put(`/notes/${noteId}`, data); - } + async deleteNote(noteId) { return await this.delete(`/notes/${noteId}`); @@ -375,10 +377,6 @@ class DocumentServerAPI { return await this.post('/notes/', noteData); } - async updateNote(noteId, noteData) { - return await this.put(`/notes/${noteId}`, noteData); - } - async deleteNote(noteId) { return await this.delete(`/notes/${noteId}`); } @@ -500,10 +498,6 @@ class DocumentServerAPI { return await this.post('/notes/', noteData); } - async updateNote(noteId, noteData) { - return await this.put(`/notes/${noteId}`, noteData); - } - async deleteNote(noteId) { return await this.delete(`/notes/${noteId}`); } diff --git a/frontend/static/js/viewer/features/highlight-manager.js b/frontend/static/js/viewer/features/highlight-manager.js index a223beb..8d04152 100644 --- a/frontend/static/js/viewer/features/highlight-manager.js +++ b/frontend/static/js/viewer/features/highlight-manager.js @@ -42,14 +42,20 @@ class HighlightManager { */ async loadNotes(documentId, contentType) { try { + console.log('📝 메모 로드 시작:', { documentId, contentType }); + if (contentType === 'note') { - this.notes = await this.api.get(`/note/${documentId}/notes`).catch(() => []); + // 노트 문서의 하이라이트 메모 + this.notes = await this.api.get(`/highlight-notes/`, { note_document_id: documentId }).catch(() => []); } else { - this.notes = await this.cachedApi.get('/notes', { document_id: documentId, content_type: contentType }, { category: 'notes' }).catch(() => []); + // 일반 문서의 하이라이트 메모 + this.notes = await this.api.get(`/highlight-notes/`, { document_id: documentId }).catch(() => []); } + + console.log('📝 메모 로드 완료:', this.notes.length, '개'); return this.notes || []; } catch (error) { - console.error('메모 로드 실패:', error); + console.error('❌ 메모 로드 실패:', error); return []; } } @@ -235,22 +241,46 @@ class HighlightManager { span.style.cursor = 'help'; } - // 하이라이트 클릭 이벤트 추가 (겹치는 요소 감지) + // 하이라이트 클릭 이벤트 추가 (통합 툴팁 사용) span.style.cursor = 'pointer'; - span.addEventListener('click', (e) => { + span.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); - // 겹치는 링크나 백링크가 있는지 확인 - if (window.documentViewerInstance && window.documentViewerInstance.linkManager) { - const overlappingElements = window.documentViewerInstance.linkManager.findOverlappingElements(span); - if (overlappingElements.length > 0) { - window.documentViewerInstance.linkManager.showOverlapMenu(e, span, overlappingElements, 'highlight'); - return; + console.log('🎨 하이라이트 클릭됨:', { + text: span.textContent, + highlightId: span.dataset.highlightId, + classList: Array.from(span.classList) + }); + + // 링크, 백링크, 하이라이트 모두 찾기 + const overlappingElements = window.documentViewerInstance.getOverlappingElements(span); + const totalElements = overlappingElements.links.length + overlappingElements.backlinks.length + overlappingElements.highlights.length; + + console.log('🎨 하이라이트 클릭 분석:', { + links: overlappingElements.links.length, + backlinks: overlappingElements.backlinks.length, + highlights: overlappingElements.highlights.length, + total: totalElements, + selectedText: overlappingElements.selectedText + }); + + if (totalElements > 1) { + // 통합 툴팁 표시 (링크 + 백링크 + 하이라이트) + console.log('🎯 통합 툴팁 표시 시작 (하이라이트에서)'); + await window.documentViewerInstance.showUnifiedTooltip(overlappingElements, span); + } else { + // 단일 하이라이트 툴팁 + console.log('🎨 단일 하이라이트 툴팁 표시'); + // 클릭된 하이라이트 찾기 + const clickedHighlightId = span.dataset.highlightId; + const clickedHighlight = this.highlights.find(h => h.id === clickedHighlightId); + if (clickedHighlight) { + await this.showHighlightTooltip(clickedHighlight, span); + } else { + console.error('❌ 클릭된 하이라이트를 찾을 수 없음:', clickedHighlightId); } } - - this.showHighlightModal(highlights); }); // DOM 교체 @@ -498,6 +528,109 @@ class HighlightManager { } } + /** + * 하이라이트 색상 변경 + */ + async updateHighlightColor(highlightId, newColor) { + try { + console.log('🎨 하이라이트 색상 업데이트:', highlightId, newColor); + + // API 호출 (구현 필요) + await this.api.updateHighlight(highlightId, { highlight_color: newColor }); + + // 로컬 데이터 업데이트 + const highlight = this.highlights.find(h => h.id === highlightId); + if (highlight) { + highlight.highlight_color = newColor; + } + + // 하이라이트 다시 렌더링 + this.renderHighlights(); + this.hideTooltip(); + + console.log('✅ 하이라이트 색상 변경 완료'); + + } catch (error) { + console.error('❌ 하이라이트 색상 변경 실패:', error); + throw error; + } + } + + /** + * 하이라이트 복사 + */ + async duplicateHighlight(highlightId) { + try { + console.log('📋 하이라이트 복사:', highlightId); + + const originalHighlight = this.highlights.find(h => h.id === highlightId); + if (!originalHighlight) { + throw new Error('원본 하이라이트를 찾을 수 없습니다.'); + } + + // 새 하이라이트 데이터 생성 (약간 다른 위치에) + const duplicateData = { + document_id: originalHighlight.document_id, + start_offset: originalHighlight.start_offset, + end_offset: originalHighlight.end_offset, + selected_text: originalHighlight.selected_text, + highlight_color: originalHighlight.highlight_color, + highlight_type: originalHighlight.highlight_type + }; + + // API 호출 + const newHighlight = await this.api.createHighlight(duplicateData); + + // 로컬 데이터에 추가 + this.highlights.push(newHighlight); + + // 하이라이트 다시 렌더링 + this.renderHighlights(); + this.hideTooltip(); + + console.log('✅ 하이라이트 복사 완료:', newHighlight); + + } catch (error) { + console.error('❌ 하이라이트 복사 실패:', error); + throw error; + } + } + + /** + * 메모 업데이트 + */ + async updateNote(noteId, newContent) { + try { + console.log('✏️ 메모 업데이트:', noteId, newContent); + + // API 호출 + const apiToUse = this.cachedApi || this.api; + await apiToUse.updateNote(noteId, { content: newContent }); + + // 로컬 데이터 업데이트 + const note = this.notes.find(n => n.id === noteId); + if (note) { + note.content = newContent; + } + + // 툴팁 새로고침 (현재 표시 중인 경우) + const tooltip = document.getElementById('highlight-tooltip'); + if (tooltip) { + // 간단히 툴팁을 다시 로드하는 대신 텍스트만 업데이트 + const noteElement = document.querySelector(`[data-note-id="${noteId}"] .text-gray-800`); + if (noteElement) { + noteElement.textContent = newContent; + } + } + + console.log('✅ 메모 업데이트 완료'); + + } catch (error) { + console.error('❌ 메모 업데이트 실패:', error); + throw error; + } + } + /** * 텍스트 오프셋 계산 */ @@ -729,13 +862,70 @@ class HighlightManager { return colorNames[color] || '기타'; } + /** + * 날짜 포맷팅 (상세) + */ + formatDate(dateString) { + if (!dateString) return '알 수 없음'; + const date = new Date(dateString); + return date.toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + /** + * 날짜 포맷팅 (간단) + */ + formatShortDate(dateString) { + if (!dateString) return ''; + const date = new Date(dateString); + const now = new Date(); + const diffTime = Math.abs(now - date); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 1) { + return '오늘'; + } else if (diffDays === 2) { + return '어제'; + } else if (diffDays <= 7) { + return `${diffDays - 1}일 전`; + } else { + return date.toLocaleDateString('ko-KR', { + month: 'short', + day: 'numeric' + }); + } + } + /** * 하이라이트 툴팁 표시 */ - showHighlightTooltip(clickedHighlight, element) { + async showHighlightTooltip(clickedHighlight, element) { // 기존 말풍선 제거 this.hideTooltip(); + // 메모 데이터 다시 로드 (최신 상태 보장) + console.log('📝 하이라이트 툴팁용 메모 로드 시작...'); + const documentId = window.documentViewerInstance.documentId; + const contentType = window.documentViewerInstance.contentType; + + console.log('📝 메모 로드 파라미터:', { documentId, contentType }); + console.log('📝 기존 메모 개수:', this.notes ? this.notes.length : 'undefined'); + + await this.loadNotes(documentId, contentType); + + console.log('📝 메모 로드 완료:', this.notes.length, '개'); + console.log('📝 로드된 메모 상세:', this.notes.map(n => ({ + id: n.id, + highlight_id: n.highlight_id, + content: n.content, + created_at: n.created_at + }))); + // 동일한 범위의 모든 하이라이트 찾기 const overlappingHighlights = this.findOverlappingHighlights(clickedHighlight); const colorGroups = this.groupHighlightsByColor(overlappingHighlights); @@ -754,9 +944,19 @@ class HighlightManager { let tooltipHTML = `
-
선택된 텍스트
-
- "${longestText}" +
+
+ + + + 하이라이트 정보 +
+
${overlappingHighlights.length}개 하이라이트
+
+ +
+
선택된 텍스트
+
"${longestText}"
`; @@ -766,39 +966,84 @@ class HighlightManager { Object.entries(colorGroups).forEach(([color, highlights]) => { const colorName = this.getColorName(color); - const allNotes = highlights.flatMap(h => - this.notes.filter(note => note.highlight_id === h.id) - ); + + // 각 하이라이트에 대한 메모 찾기 (디버깅 로그 추가) + const allNotes = highlights.flatMap(h => { + const notesForHighlight = this.notes.filter(note => note.highlight_id === h.id); + console.log(`📝 하이라이트 ${h.id}에 대한 메모:`, notesForHighlight.length, '개'); + if (notesForHighlight.length > 0) { + console.log('📝 메모 내용:', notesForHighlight.map(n => n.content)); + } + return notesForHighlight; + }); + + console.log(`🎨 ${colorName} 하이라이트의 총 메모:`, allNotes.length, '개'); + + const createdDate = highlights[0].created_at ? this.formatDate(highlights[0].created_at) : '알 수 없음'; tooltipHTML += ` -
-
-
-
- ${colorName} 메모 (${allNotes.length}) +
+
+
+
+
+ ${colorName} 하이라이트 +
${createdDate} 생성
+
+
+
+ +
-
-
- ${allNotes.length > 0 ? - allNotes.map(note => ` -
-
${note.content}
-
- ${this.formatShortDate(note.created_at)} · Administrator - + +
+
+ + + + + 메모 (${allNotes.length}개) +
+ +
+ ${allNotes.length > 0 ? + allNotes.map(note => ` +
+
${note.content}
+
+ + + + + ${this.formatShortDate(note.created_at)} + +
+ + +
+
-
- `).join('') : - '
메모가 없습니다
' - } + `).join('') : + '
메모가 없습니다. 위의 "📝 메모 추가" 버튼을 클릭해보세요!
' + } +
`; @@ -806,13 +1051,36 @@ class HighlightManager { tooltipHTML += '
'; - // 하이라이트 삭제 버튼 + // 하이라이트 관리 버튼들 tooltipHTML += ` -
- +
+
+
+ + + + 하이라이트 관리 +
+
+ + +
+
`; diff --git a/frontend/static/js/viewer/features/link-manager.js b/frontend/static/js/viewer/features/link-manager.js index 33a38bc..8b6bf08 100644 --- a/frontend/static/js/viewer/features/link-manager.js +++ b/frontend/static/js/viewer/features/link-manager.js @@ -295,16 +295,36 @@ class LinkManager { box-sizing: border-box !important; `; - // 클릭 이벤트 추가 (겹치는 요소 감지) - span.addEventListener('click', (e) => { + // 클릭 이벤트 추가 (통합 툴팁 사용) + span.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); - // 겹치는 하이라이트나 백링크가 있는지 확인 - const overlappingElements = this.findOverlappingElements(span); - if (overlappingElements.length > 0) { - this.showOverlapMenu(e, span, overlappingElements, 'link'); + console.log('🔗 링크 클릭됨:', { + text: span.textContent, + linkId: link.id, + classList: Array.from(span.classList) + }); + + // 링크, 백링크, 하이라이트 모두 찾기 + const overlappingElements = window.documentViewerInstance.getOverlappingElements(span); + const totalElements = overlappingElements.links.length + overlappingElements.backlinks.length + overlappingElements.highlights.length; + + console.log('🎯 링크 클릭 분석:', { + links: overlappingElements.links.length, + backlinks: overlappingElements.backlinks.length, + highlights: overlappingElements.highlights.length, + total: totalElements, + selectedText: overlappingElements.selectedText + }); + + if (totalElements > 1) { + // 통합 툴팁 표시 (링크 + 백링크 + 하이라이트) + console.log('🎨 통합 툴팁 표시 시작 (링크에서)'); + await window.documentViewerInstance.showUnifiedTooltip(overlappingElements, span); } else { + // 단일 링크 툴팁 + console.log('🔗 단일 링크 툴팁 표시'); this.showLinkTooltip(link, span); } }); @@ -503,16 +523,36 @@ class LinkManager { box-sizing: border-box !important; `; - // 클릭 이벤트 추가 (겹치는 요소 감지) - span.addEventListener('click', (e) => { + // 클릭 이벤트 추가 (통합 툴팁 사용) + span.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); - // 겹치는 하이라이트나 링크가 있는지 확인 - const overlappingElements = this.findOverlappingElements(span); - if (overlappingElements.length > 0) { - this.showOverlapMenu(e, span, overlappingElements, 'backlink'); + console.log('🔙 백링크 클릭됨:', { + text: span.textContent, + backlinkId: backlink.id, + classList: Array.from(span.classList) + }); + + // 링크, 백링크, 하이라이트 모두 찾기 + const overlappingElements = window.documentViewerInstance.getOverlappingElements(span); + const totalElements = overlappingElements.links.length + overlappingElements.backlinks.length + overlappingElements.highlights.length; + + console.log('🔙 백링크 클릭 분석:', { + links: overlappingElements.links.length, + backlinks: overlappingElements.backlinks.length, + highlights: overlappingElements.highlights.length, + total: totalElements, + selectedText: overlappingElements.selectedText + }); + + if (totalElements > 1) { + // 통합 툴팁 표시 (링크 + 백링크 + 하이라이트) + console.log('🎯 통합 툴팁 표시 시작 (백링크에서)'); + await window.documentViewerInstance.showUnifiedTooltip(overlappingElements, span); } else { + // 단일 백링크 툴팁 + console.log('🔙 단일 백링크 툴팁 표시'); this.showBacklinkTooltip(backlink, span); } }); @@ -530,7 +570,160 @@ class LinkManager { } /** - * 링크 툴팁 표시 + * 겹치는 링크들 찾기 + */ + getOverlappingLinks(clickedElement) { + const clickedLinkId = clickedElement.getAttribute('data-link-id'); + const clickedText = clickedElement.textContent; + + console.log('🔍 겹치는 링크 찾기:', { + clickedLinkId: clickedLinkId, + clickedText: clickedText, + totalLinks: this.documentLinks.length + }); + + // 동일한 텍스트 범위에 있는 모든 링크 찾기 + const overlappingLinks = this.documentLinks.filter(link => { + // 클릭된 링크와 텍스트가 겹치는지 확인 + const linkElement = document.querySelector(`[data-link-id="${link.id}"]`); + if (!linkElement) return false; + + // 텍스트가 겹치는지 확인 (간단한 방법: 텍스트 내용 비교) + const isOverlapping = linkElement.textContent === clickedText; + + if (isOverlapping) { + console.log('✅ 겹치는 링크 발견:', { + id: link.id, + text: linkElement.textContent, + target: link.target_document_title + }); + } + + return isOverlapping; + }); + + console.log(`🔍 총 ${overlappingLinks.length}개의 겹치는 링크 발견`); + return overlappingLinks; + } + + /** + * 다중 링크 툴팁 표시 + */ + showMultiLinkTooltip(links, element, selectedText) { + console.log('🔗 다중 링크 툴팁 표시:', links.length, '개'); + + // 기존 툴팁 제거 + this.hideTooltip(); + + const tooltip = document.createElement('div'); + tooltip.id = 'link-tooltip'; + tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50 max-w-lg'; + tooltip.style.minWidth = '400px'; + tooltip.style.maxHeight = '80vh'; + tooltip.style.overflowY = 'auto'; + + let tooltipHTML = ` +
+
선택된 텍스트
+
+ "${selectedText}" +
+
+ `; + + if (links.length > 1) { + tooltipHTML += ` +
+
+ + + + 연결된 링크 (${links.length}개) +
+
+ `; + } + + tooltipHTML += '
'; + + links.forEach((link, index) => { + const createdDate = link.created_at ? this.formatDate(link.created_at) : '알 수 없음'; + const isNote = link.target_content_type === 'note'; + const iconClass = isNote ? 'text-green-600' : 'text-purple-600'; + const bgClass = isNote ? 'hover:bg-green-50' : 'hover:bg-purple-50'; + + tooltipHTML += ` +
+
+
+
+ + ${isNote ? + '' : + '' + } + + ${link.target_document_title} +
+ + + ${link.target_text ? ` +
+
연결된 텍스트
+
"${link.target_text}"
+
+ ` : ''} + + ${link.description ? ` +
+
링크 설명
+
${link.description}
+
+ ` : ''} + +
+ + + + + ${link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'} + + ${createdDate} +
+
+ + + +
+
+ `; + }); + + tooltipHTML += '
'; + + tooltip.innerHTML = tooltipHTML; + + // 위치 계산 및 표시 + const rect = element.getBoundingClientRect(); + tooltip.style.top = (rect.bottom + window.scrollY + 10) + 'px'; + tooltip.style.left = Math.max(10, rect.left + window.scrollX - 200) + 'px'; + + document.body.appendChild(tooltip); + + // 외부 클릭 시 툴팁 숨기기 + setTimeout(() => { + document.addEventListener('click', this.handleTooltipOutsideClick.bind(this)); + }, 100); + } + + /** + * 링크 툴팁 표시 (단일 링크용) */ showLinkTooltip(link, element) { this.hideTooltip(); @@ -645,9 +838,28 @@ class LinkManager {
${createdDate}
-
-
현재 문서의 텍스트
-
"${backlink.target_text || backlink.selected_text}"
+ +
+
+ 💡 백링크란?
+ 다른 문서에서 현재 문서의 이 텍스트를 참조하는 연결입니다.
+ 클릭하면 참조하는 문서로 이동할 수 있습니다. +
+
+ + +
+
+
현재 문서의 참조된 텍스트
+
"${backlink.target_text || backlink.selected_text}"
+
+ + ${backlink.target_text && backlink.target_text !== backlink.selected_text ? ` +
+
원본 링크에서 선택한 텍스트
+
"${backlink.selected_text}"
+
+ ` : ''}
@@ -656,28 +868,47 @@ class LinkManager { - 참조하는 문서 + 이 텍스트를 참조하는 문서
-
${backlink.source_document_title}
-
-
원본 텍스트
-
"${backlink.selected_text}"
-
- ${backlink.description ? ` -
-
설명
-
${backlink.description}
+
+
+ + + + ${backlink.source_document_title}
- ` : ''} + + +
+
+
원본 문서에서 링크로 설정한 텍스트
+
"${backlink.selected_text}"
+
+ + ${backlink.target_text ? ` +
+
현재 문서에서 연결된 구체적인 텍스트
+
"${backlink.target_text}"
+
+ ` : ''} + + ${backlink.description ? ` +
+
링크 설명
+
${backlink.description}
+
+ ` : ''} +
+
-
+
diff --git a/frontend/static/js/viewer/utils/cached-api.js b/frontend/static/js/viewer/utils/cached-api.js index 8c2ba1c..a4f52e3 100644 --- a/frontend/static/js/viewer/utils/cached-api.js +++ b/frontend/static/js/viewer/utils/cached-api.js @@ -232,22 +232,24 @@ class CachedAPI { return cached; } - // 기존 API 메서드 직접 사용 + // 하이라이트 메모 API 사용 try { let result; if (contentType === 'note') { - result = await this.api.get(`/note/${documentId}/notes`).catch(() => []); + // 노트 문서의 하이라이트 메모 + result = await this.api.get(`/highlight-notes/`, { note_document_id: documentId }).catch(() => []); } else { - result = await this.api.getDocumentNotes(documentId).catch(() => []); + // 일반 문서의 하이라이트 메모 + result = await this.api.get(`/highlight-notes/`, { document_id: documentId }).catch(() => []); } // 캐시에 저장 this.cache.set(cacheKey, result, 'notes', 10 * 60 * 1000); - console.log(`💾 메모 캐시 저장: ${documentId}`); + console.log(`💾 메모 캐시 저장: ${documentId} (${result.length}개)`); return result; } catch (error) { - console.error('메모 로드 실패:', error); + console.error('❌ 메모 로드 실패:', error); return []; } } @@ -359,7 +361,16 @@ class CachedAPI { * 메모 생성 (캐시 무효화) */ async createNote(data) { - return await this.post('/notes/', 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'] }); } diff --git a/frontend/static/js/viewer/viewer-core.js b/frontend/static/js/viewer/viewer-core.js index 75088b3..b4ef265 100644 --- a/frontend/static/js/viewer/viewer-core.js +++ b/frontend/static/js/viewer/viewer-core.js @@ -1136,6 +1136,423 @@ window.documentViewer = () => ({ } }, + // ==================== 통합 툴팁 처리 ==================== + /** + * 클릭된 요소에서 링크, 백링크, 하이라이트 모두 찾기 (완전 개선 버전) + */ + getOverlappingElements(clickedElement) { + const selectedText = clickedElement.textContent.trim(); + console.log('🔍 통합 요소 찾기 시작:', selectedText); + console.log('🔍 하이라이트 매니저 상태:', { + highlightManager: !!this.highlightManager, + highlightsCount: this.highlightManager?.highlights?.length || 0, + highlights: this.highlightManager?.highlights || [] + }); + + // 결과 배열들 + const overlappingLinks = []; + const overlappingBacklinks = []; + const overlappingHighlights = []; + + // 1. 모든 링크 요소 찾기 (같은 텍스트) + const allLinkElements = document.querySelectorAll('.document-link'); + allLinkElements.forEach(linkEl => { + if (linkEl.textContent.trim() === selectedText) { + const linkId = linkEl.dataset.linkId; + const link = this.linkManager.documentLinks.find(l => l.id === linkId); + if (link && !overlappingLinks.find(l => l.id === link.id)) { + overlappingLinks.push(link); + console.log('✅ 겹치는 링크 발견:', link.target_document_title); + } + } + }); + + // 2. 모든 백링크 요소 찾기 (같은 텍스트) + const allBacklinkElements = document.querySelectorAll('.backlink-highlight'); + allBacklinkElements.forEach(backlinkEl => { + if (backlinkEl.textContent.trim() === selectedText) { + const backlinkId = backlinkEl.dataset.backlinkId; + const backlink = this.linkManager.backlinks.find(b => b.id === backlinkId); + if (backlink && !overlappingBacklinks.find(b => b.id === backlink.id)) { + overlappingBacklinks.push(backlink); + console.log('✅ 겹치는 백링크 발견:', backlink.source_document_title); + } + } + }); + + // 3. 모든 하이라이트 요소 찾기 (같은 텍스트) + const allHighlightElements = document.querySelectorAll('.highlight-span'); + console.log('🔍 페이지의 모든 하이라이트 요소:', allHighlightElements.length, '개'); + allHighlightElements.forEach(highlightEl => { + const highlightText = highlightEl.textContent.trim(); + + // 텍스트가 정확히 일치하거나 포함 관계인 경우 + if (highlightText === selectedText || + highlightText.includes(selectedText) || + selectedText.includes(highlightText)) { + + const highlightId = highlightEl.dataset.highlightId; + console.log('🔍 하이라이트 요소 확인:', { + element: highlightEl, + highlightId: highlightId, + text: highlightText, + selectedText: selectedText + }); + + const highlight = this.highlightManager.highlights.find(h => h.id === highlightId); + if (highlight && !overlappingHighlights.find(h => h.id === highlight.id)) { + overlappingHighlights.push(highlight); + console.log('✅ 겹치는 하이라이트 발견:', { + id: highlight.id, + text: highlightText, + color: highlight.highlight_color + }); + } else if (!highlight) { + console.log('❌ 하이라이트 데이터를 찾을 수 없음:', highlightId); + } + } + }); + + console.log('📊 최종 발견된 요소들:', { + links: overlappingLinks.length, + backlinks: overlappingBacklinks.length, + highlights: overlappingHighlights.length, + selectedText: selectedText + }); + + return { + links: overlappingLinks, + backlinks: overlappingBacklinks, + highlights: overlappingHighlights, + selectedText: selectedText + }; + }, + + /** + * 통합 툴팁 표시 (링크 + 하이라이트 + 백링크) + */ + async showUnifiedTooltip(overlappingElements, element) { + const { links = [], highlights = [], backlinks = [], selectedText } = overlappingElements; + + console.log('🎯 통합 툴팁 표시:', { + links: links.length, + highlights: highlights.length, + backlinks: backlinks.length + }); + + // 하이라이트가 있으면 메모 데이터 로드 + if (highlights.length > 0) { + console.log('📝 통합 툴팁용 메모 로드 시작...'); + const documentId = this.documentId; + const contentType = this.contentType; + await this.highlightManager.loadNotes(documentId, contentType); + console.log('📝 통합 툴팁용 메모 로드 완료:', this.highlightManager.notes.length, '개'); + } + + // 기존 툴팁들 숨기기 + this.linkManager.hideTooltip(); + this.highlightManager.hideTooltip(); + + // 툴팁 컨테이너 생성 + const tooltip = document.createElement('div'); + tooltip.id = 'unified-tooltip'; + tooltip.className = 'fixed z-50 bg-white rounded-xl shadow-2xl border border-gray-200'; + tooltip.style.cssText = ` + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + backdrop-filter: blur(10px); + border: 1px solid rgba(148, 163, 184, 0.2); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + max-width: 90vw; + max-height: 80vh; + overflow-y: auto; + z-index: 9999; + `; + + const totalElements = links.length + highlights.length + backlinks.length; + + let tooltipHTML = ` +
+
+
+
+ + + 겹치는 요소들 +
+
${totalElements}개 요소
+
+ +
+
선택된 텍스트
+
"${selectedText}"
+
+
+ `; + + // 하이라이트 섹션 + if (highlights.length > 0) { + tooltipHTML += ` +
+
+ + + 하이라이트 (${highlights.length}개) +
+
+ `; + + highlights.forEach(highlight => { + const colorName = this.highlightManager.getColorName(highlight.highlight_color); + const createdDate = this.formatDate(highlight.created_at); + const notes = this.highlightManager.notes.filter(note => note.highlight_id === highlight.id); + + console.log(`📝 통합 툴팁 - 하이라이트 ${highlight.id}의 메모:`, notes.length, '개'); + if (notes.length > 0) { + console.log('📝 메모 내용:', notes.map(n => n.content)); + } + + tooltipHTML += ` +
+
+
+
+ ${colorName} + ${createdDate} +
+
${notes.length}개 메모
+
+
+ `; + }); + + tooltipHTML += ` +
+
+ `; + } + + // 링크 섹션 + if (links.length > 0) { + tooltipHTML += ` +
+
+ + + + 링크 (${links.length}개) +
+
+ `; + + links.forEach(link => { + const isNote = link.target_content_type === 'note'; + const bgClass = isNote ? 'from-green-50 to-emerald-50' : 'from-purple-50 to-indigo-50'; + const iconClass = isNote ? 'text-green-600' : 'text-purple-600'; + const createdDate = this.formatDate(link.created_at); + + tooltipHTML += ` +
+
+
+
+ + ${isNote ? + '' : + '' + } + + ${link.target_document_title} +
+ + ${link.target_text ? ` +
+
연결된 텍스트
+
"${link.target_text}"
+
+ ` : ''} + + ${link.description ? ` +
+
링크 설명
+
${link.description}
+
+ ` : ''} + +
+ + + + ${link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'} + + ${createdDate} +
+
+ + + +
+
+ `; + }); + + tooltipHTML += ` +
+
+ `; + } + + // 백링크 섹션 + if (backlinks.length > 0) { + tooltipHTML += ` +
+
+ + + + 백링크 (${backlinks.length}개) +
+
+ `; + + backlinks.forEach(backlink => { + const createdDate = this.formatDate(backlink.created_at); + + tooltipHTML += ` +
+
+
+ + + ${backlink.source_document_title} +
+ +
+
원본 문서에서 링크로 설정한 텍스트
+
"${backlink.selected_text}"
+
+ + ${backlink.target_text ? ` +
+
현재 문서에서 연결된 구체적인 텍스트
+
"${backlink.target_text}"
+
+ ` : ''} + + ${backlink.description ? ` +
+
링크 설명
+
${backlink.description}
+
+ ` : ''} + +
+ + + + 백링크 + + ${createdDate} +
+
+
+ `; + }); + + tooltipHTML += ` +
+
+ `; + } + + tooltipHTML += ` +
+ +
+
+ `; + + tooltip.innerHTML = tooltipHTML; + document.body.appendChild(tooltip); + + // 위치 조정 + this.positionTooltip(tooltip, element); + }, + + /** + * 통합 툴팁 숨기기 + */ + hideUnifiedTooltip() { + const tooltip = document.getElementById('unified-tooltip'); + if (tooltip) { + tooltip.remove(); + } + }, + + /** + * 툴팁 위치 조정 (화면 밖으로 나가지 않도록 개선) + */ + positionTooltip(tooltip, element) { + const rect = element.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const scrollX = window.scrollX; + const scrollY = window.scrollY; + + console.log('🎯 툴팁 위치 계산:', { + elementRect: rect, + tooltipSize: { width: tooltipRect.width, height: tooltipRect.height }, + viewport: { width: viewportWidth, height: viewportHeight } + }); + + // 기본 위치: 요소 아래 중앙 + let left = rect.left + scrollX + (rect.width / 2) - (tooltipRect.width / 2); + let top = rect.bottom + scrollY + 10; + + // 좌우 경계 체크 및 조정 + const margin = 20; + if (left < margin) { + left = margin; + console.log('🔧 좌측 경계 조정:', left); + } else if (left + tooltipRect.width > viewportWidth - margin) { + left = viewportWidth - tooltipRect.width - margin; + console.log('🔧 우측 경계 조정:', left); + } + + // 상하 경계 체크 및 조정 + if (top + tooltipRect.height > viewportHeight - margin) { + // 요소 위쪽에 표시 + top = rect.top + scrollY - tooltipRect.height - 10; + console.log('🔧 상단으로 이동:', top); + + // 위쪽에도 공간이 부족하면 뷰포트 내에 강제로 맞춤 + if (top < margin) { + top = margin; + console.log('🔧 상단 경계 조정:', top); + } + } + + // 최종 위치 설정 + tooltip.style.position = 'fixed'; + tooltip.style.left = `${left - scrollX}px`; + tooltip.style.top = `${top - scrollY}px`; + + console.log('✅ 최종 툴팁 위치:', { + left: left - scrollX, + top: top - scrollY + }); + }, + // ==================== 유틸리티 메서드 ==================== formatDate(dateString) { return new Date(dateString).toLocaleString('ko-KR'); @@ -1145,20 +1562,6 @@ window.documentViewer = () => ({ return new Date(dateString).toLocaleDateString('ko-KR'); }, - getColorName(color) { - const colorNames = { - '#FFFF00': '노란색', - '#00FF00': '초록색', - '#FF0000': '빨간색', - '#0000FF': '파란색', - '#FF00FF': '보라색', - '#00FFFF': '청록색', - '#FFA500': '주황색', - '#FFC0CB': '분홍색' - }; - return colorNames[color] || '기타'; - }, - getSelectedBookTitle() { const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id); return selectedBook ? selectedBook.title : '서적을 선택하세요'; @@ -1260,6 +1663,230 @@ window.documentViewer = () => ({ return this.linkManager.navigateToSourceDocument(backlink.source_document_id, backlink); }, + // 링크 삭제 (확인 후) + async deleteLinkWithConfirm(linkId, targetTitle) { + console.log('🗑️ 링크 삭제 요청:', { linkId, targetTitle }); + + const confirmed = confirm(`"${targetTitle}"로의 링크를 삭제하시겠습니까?`); + if (!confirmed) { + console.log('❌ 링크 삭제 취소됨'); + return; + } + + try { + console.log('🗑️ 링크 삭제 시작:', linkId); + + // API 호출 + await this.api.deleteDocumentLink(linkId); + console.log('✅ 링크 삭제 성공'); + + // 툴팁 숨기기 + this.linkManager.hideTooltip(); + + // 캐시 무효화 + console.log('🗑️ 링크 캐시 무효화 시작...'); + if (window.cachedApi && window.cachedApi.invalidateRelatedCache) { + if (this.contentType === 'note') { + window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/links`, ['links']); + } else { + window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/links`, ['links']); + } + console.log('✅ 링크 캐시 무효화 완료'); + } + + // 링크 목록 새로고침 + console.log('🔄 링크 목록 새로고침 시작...'); + await this.linkManager.loadDocumentLinks(this.documentId, this.contentType); + this.documentLinks = this.linkManager.documentLinks || []; + console.log('📊 새로고침된 링크 개수:', this.documentLinks.length); + + // 링크 렌더링 + console.log('🎨 링크 렌더링 시작...'); + this.linkManager.renderDocumentLinks(); + console.log('✅ 링크 렌더링 완료'); + + // 백링크도 다시 로드 (삭제된 링크가 다른 문서의 백링크였을 수 있음) + console.log('🔄 백링크 새로고침 시작...'); + if (window.cachedApi && window.cachedApi.invalidateRelatedCache) { + if (this.contentType === 'note') { + window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/backlinks`, ['links']); + } else { + window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']); + } + console.log('✅ 백링크 캐시도 무효화 완료'); + } + await this.linkManager.loadBacklinks(this.documentId, this.contentType); + this.backlinks = this.linkManager.backlinks || []; + this.linkManager.renderBacklinks(); + console.log('✅ 백링크 새로고침 완료'); + + // 성공 메시지 + this.showSuccessMessage('링크가 삭제되었습니다.'); + + } catch (error) { + console.error('❌ 링크 삭제 실패:', error); + alert('링크 삭제에 실패했습니다: ' + error.message); + } + }, + + // 성공 메시지 표시 + showSuccessMessage(message) { + const toast = document.createElement('div'); + toast.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 transition-opacity duration-300'; + toast.textContent = message; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => { + if (toast.parentNode) { + toast.parentNode.removeChild(toast); + } + }, 300); + }, 2000); + }, + + // 하이라이트 관련 추가 기능들 + async changeHighlightColor(highlightId) { + console.log('🎨 하이라이트 색상 변경:', highlightId); + + const colors = [ + { name: '노란색', value: '#FFFF00' }, + { name: '초록색', value: '#00FF00' }, + { name: '파란색', value: '#00BFFF' }, + { name: '분홍색', value: '#FFB6C1' }, + { name: '주황색', value: '#FFA500' }, + { name: '보라색', value: '#DDA0DD' } + ]; + + const colorOptions = colors.map(c => `${c.name} (${c.value})`).join('\n'); + const selectedColor = prompt(`새로운 색상을 선택하세요:\n\n${colorOptions}\n\n색상 코드를 입력하세요 (예: #FFFF00):`); + + if (selectedColor && selectedColor.match(/^#[0-9A-Fa-f]{6}$/)) { + try { + await this.highlightManager.updateHighlightColor(highlightId, selectedColor); + this.showSuccessMessage('하이라이트 색상이 변경되었습니다.'); + } catch (error) { + console.error('❌ 색상 변경 실패:', error); + alert('색상 변경에 실패했습니다: ' + error.message); + } + } else if (selectedColor !== null) { + alert('올바른 색상 코드를 입력해주세요 (예: #FFFF00)'); + } + }, + + async duplicateHighlight(highlightId) { + console.log('📋 하이라이트 복사:', highlightId); + + try { + await this.highlightManager.duplicateHighlight(highlightId); + this.showSuccessMessage('하이라이트가 복사되었습니다.'); + } catch (error) { + console.error('❌ 하이라이트 복사 실패:', error); + alert('하이라이트 복사에 실패했습니다: ' + error.message); + } + }, + + async deleteHighlightWithConfirm(highlightId) { + console.log('🗑️ 하이라이트 삭제 확인:', highlightId); + + const confirmed = confirm('이 하이라이트를 삭제하시겠습니까?\n\n⚠️ 주의: 연결된 모든 메모도 함께 삭제됩니다.'); + if (!confirmed) { + console.log('❌ 하이라이트 삭제 취소됨'); + return; + } + + try { + await this.highlightManager.deleteHighlight(highlightId); + this.highlightManager.hideTooltip(); + this.showSuccessMessage('하이라이트가 삭제되었습니다.'); + } catch (error) { + console.error('❌ 하이라이트 삭제 실패:', error); + alert('하이라이트 삭제에 실패했습니다: ' + error.message); + } + }, + + async editNote(noteId, currentContent) { + console.log('✏️ 메모 편집:', noteId); + console.log('🔍 HighlightManager 상태:', this.highlightManager); + console.log('🔍 updateNote 함수 존재:', typeof this.highlightManager?.updateNote); + + if (!this.highlightManager) { + console.error('❌ HighlightManager가 초기화되지 않음'); + alert('하이라이트 매니저가 초기화되지 않았습니다.'); + return; + } + + if (typeof this.highlightManager.updateNote !== 'function') { + console.error('❌ updateNote 함수가 존재하지 않음'); + alert('메모 업데이트 함수가 존재하지 않습니다.'); + return; + } + + const newContent = prompt('메모 내용을 수정하세요:', currentContent); + if (newContent !== null && newContent.trim() !== currentContent) { + try { + await this.highlightManager.updateNote(noteId, newContent.trim()); + this.showSuccessMessage('메모가 수정되었습니다.'); + } catch (error) { + console.error('❌ 메모 수정 실패:', error); + alert('메모 수정에 실패했습니다: ' + error.message); + } + } + }, + + // 백링크 삭제 (확인 후) + async deleteBacklinkWithConfirm(backlinkId, sourceTitle) { + console.log('🗑️ 백링크 삭제 요청:', { backlinkId, sourceTitle }); + + const confirmed = confirm(`"${sourceTitle}"에서 오는 백링크를 삭제하시겠습니까?\n\n⚠️ 주의: 이는 원본 문서의 링크를 삭제합니다.`); + if (!confirmed) { + console.log('❌ 백링크 삭제 취소됨'); + return; + } + + try { + console.log('🗑️ 백링크 삭제 시작:', backlinkId); + + // 백링크 삭제는 실제로는 원본 링크를 삭제하는 것 + await this.api.deleteDocumentLink(backlinkId); + console.log('✅ 백링크 삭제 성공'); + + // 툴팁 숨기기 + this.linkManager.hideTooltip(); + + // 캐시 무효화 (현재 문서의 백링크 캐시) + console.log('🗑️ 백링크 캐시 무효화 시작...'); + if (window.cachedApi && window.cachedApi.invalidateRelatedCache) { + if (this.contentType === 'note') { + window.cachedApi.invalidateRelatedCache(`/note-documents/${this.documentId}/backlinks`, ['links']); + } else { + window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']); + } + console.log('✅ 백링크 캐시 무효화 완료'); + } + + // 백링크 목록 새로고침 + console.log('🔄 백링크 목록 새로고침 시작...'); + await this.linkManager.loadBacklinks(this.documentId, this.contentType); + this.backlinks = this.linkManager.backlinks || []; + console.log('📊 새로고침된 백링크 개수:', this.backlinks.length); + + // 백링크 렌더링 + console.log('🎨 백링크 렌더링 시작...'); + this.linkManager.renderBacklinks(); + console.log('✅ 백링크 렌더링 완료'); + + // 성공 메시지 + this.showSuccessMessage('백링크가 삭제되었습니다.'); + + } catch (error) { + console.error('❌ 백링크 삭제 실패:', error); + alert('백링크 삭제에 실패했습니다: ' + error.message); + } + }, + // 북마크 관련 scrollToBookmark(bookmark) { return this.bookmarkManager.scrollToBookmark(bookmark); diff --git a/frontend/viewer.html b/frontend/viewer.html index f7f528e..2f52108 100644 --- a/frontend/viewer.html +++ b/frontend/viewer.html @@ -773,22 +773,22 @@
- + - + - + - +