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 += `
+
+
+
+
+
+
${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 @@
-
+
-
+
-
+
-
+