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
This commit is contained in:
@@ -253,6 +253,20 @@ def create_note_link(
|
||||
"updated_at": note_link.updated_at.isoformat() if note_link.updated_at else None,
|
||||
}
|
||||
|
||||
# 소스 및 타겟 타입 설정
|
||||
response_data["source_content_type"] = "note" # 노트에서 출발하는 링크
|
||||
|
||||
if note_link.target_note_id:
|
||||
target_note = db.query(NoteDocument).filter(NoteDocument.id == note_link.target_note_id).first()
|
||||
if target_note:
|
||||
response_data["target_note_title"] = target_note.title
|
||||
response_data["target_content_type"] = "note"
|
||||
elif note_link.target_document_id:
|
||||
target_doc = db.query(Document).filter(Document.id == note_link.target_document_id).first()
|
||||
if target_doc:
|
||||
response_data["target_document_title"] = target_doc.title
|
||||
response_data["target_content_type"] = "document"
|
||||
|
||||
return NoteLinkResponse(**response_data)
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,29 @@ def update_note(
|
||||
|
||||
return note
|
||||
|
||||
@router.delete("/{note_id}")
|
||||
def delete_highlight_note(
|
||||
note_id: str,
|
||||
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="메모를 찾을 수 없습니다")
|
||||
|
||||
db.delete(note)
|
||||
db.commit()
|
||||
|
||||
return {"message": "메모가 삭제되었습니다"}
|
||||
|
||||
@router.get("/document/{document_id}")
|
||||
async def get_document_notes(
|
||||
document_id: str,
|
||||
|
||||
@@ -277,6 +277,10 @@ class DocumentServerAPI {
|
||||
return await this.put(`/highlight-notes/${noteId}`, updateData);
|
||||
}
|
||||
|
||||
async deleteNote(noteId) {
|
||||
return await this.delete(`/highlight-notes/${noteId}`);
|
||||
}
|
||||
|
||||
// === 문서 메모 조회 ===
|
||||
// 용어 정의: 특정 문서의 모든 하이라이트 메모 조회
|
||||
async getDocumentNotes(documentId) {
|
||||
|
||||
@@ -730,7 +730,21 @@ class HighlightManager {
|
||||
async deleteHighlight(highlightId) {
|
||||
try {
|
||||
await this.api.delete(`/highlights/${highlightId}`);
|
||||
|
||||
// 하이라이트 배열에서 제거
|
||||
this.highlights = this.highlights.filter(h => h.id !== highlightId);
|
||||
|
||||
// 메모 배열에서도 해당 하이라이트의 메모들 제거
|
||||
this.notes = this.notes.filter(note => note.highlight_id !== highlightId);
|
||||
|
||||
// 캐시 무효화 (하이라이트와 메모 모두)
|
||||
if (window.documentViewerInstance && window.documentViewerInstance.cacheManager) {
|
||||
window.documentViewerInstance.cacheManager.invalidateCategory('highlights');
|
||||
window.documentViewerInstance.cacheManager.invalidateCategory('notes');
|
||||
console.log('🗑️ 하이라이트 삭제 후 캐시 무효화 완료');
|
||||
}
|
||||
|
||||
// 화면 다시 렌더링
|
||||
this.renderHighlights();
|
||||
console.log('하이라이트 삭제 완료:', highlightId);
|
||||
} catch (error) {
|
||||
@@ -1071,7 +1085,7 @@ class HighlightManager {
|
||||
</svg>
|
||||
복사
|
||||
</button>
|
||||
<button onclick="window.documentViewerInstance.highlightManager.deleteHighlightWithConfirm('${clickedHighlight.id}')"
|
||||
<button onclick="window.documentViewerInstance.deleteHighlightWithConfirm('${clickedHighlight.id}')"
|
||||
class="text-xs bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600 transition-colors flex items-center"
|
||||
title="하이라이트 삭제">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -781,7 +781,7 @@ class LinkManager {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3 mb-4">
|
||||
<button onclick="window.documentViewerInstance.linkManager.navigateToLinkedDocument('${link.target_document_id}', ${JSON.stringify(link).replace(/"/g, '"')})"
|
||||
<button onclick="window.documentViewerInstance.linkManager.navigateToLinkedDocument('${link.target_note_id || link.target_document_id}', ${JSON.stringify(link).replace(/"/g, '"')})"
|
||||
class="flex-1 bg-purple-500 text-white px-4 py-2 rounded-lg hover:bg-purple-600 transition-colors flex items-center justify-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
@@ -1033,10 +1033,22 @@ class LinkManager {
|
||||
linkInfo: linkInfo
|
||||
});
|
||||
|
||||
if (!targetDocumentId) {
|
||||
console.error('❌ targetDocumentId가 없습니다!');
|
||||
alert('대상 문서 ID가 없습니다.');
|
||||
return;
|
||||
// targetDocumentId가 null이거나 'null' 문자열인 경우 처리
|
||||
if (!targetDocumentId || targetDocumentId === 'null' || targetDocumentId === null) {
|
||||
console.error('❌ targetDocumentId가 유효하지 않습니다:', targetDocumentId);
|
||||
console.log('🔍 linkInfo에서 대체 ID 찾기:', linkInfo);
|
||||
|
||||
// linkInfo에서 대체 ID 찾기 (노트 링크의 경우 target_note_id 우선)
|
||||
if (linkInfo && linkInfo.target_note_id && linkInfo.target_note_id !== 'null') {
|
||||
targetDocumentId = linkInfo.target_note_id;
|
||||
console.log('✅ linkInfo에서 target_note_id 발견:', targetDocumentId);
|
||||
} else if (linkInfo && linkInfo.target_document_id && linkInfo.target_document_id !== 'null') {
|
||||
targetDocumentId = linkInfo.target_document_id;
|
||||
console.log('✅ linkInfo에서 target_document_id 발견:', targetDocumentId);
|
||||
} else {
|
||||
alert('대상 문서 ID가 유효하지 않습니다.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// contentType에 따라 적절한 URL 생성
|
||||
@@ -1083,7 +1095,18 @@ class LinkManager {
|
||||
// source_content_type에 따라 적절한 URL 생성
|
||||
let targetUrl;
|
||||
|
||||
if (backlinkInfo.source_content_type === 'note') {
|
||||
// source_content_type이 없으면 ID로 추론
|
||||
let sourceContentType = backlinkInfo.source_content_type;
|
||||
if (!sourceContentType) {
|
||||
if (backlinkInfo.source_note_id) {
|
||||
sourceContentType = 'note';
|
||||
} else if (backlinkInfo.source_document_id) {
|
||||
sourceContentType = 'document';
|
||||
}
|
||||
console.log('🔍 백링크에서 source_content_type 추론됨:', sourceContentType);
|
||||
}
|
||||
|
||||
if (sourceContentType === 'note') {
|
||||
// 노트 문서로 이동
|
||||
targetUrl = `/viewer.html?id=${sourceDocumentId}&contentType=note`;
|
||||
console.log('📝 노트 문서로 이동 (백링크):', sourceDocumentId);
|
||||
@@ -1114,7 +1137,19 @@ class LinkManager {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.delete(`/document-links/${linkId}`);
|
||||
// 링크 타입 확인 (노트 링크인지 문서 링크인지)
|
||||
const link = this.documentLinks.find(l => l.id === linkId);
|
||||
|
||||
if (link && link.source_note_id) {
|
||||
// 노트 링크 삭제
|
||||
console.log('📝 노트 링크 삭제 API 호출');
|
||||
await this.api.delete(`/note-links/${linkId}`);
|
||||
} else {
|
||||
// 문서 링크 삭제
|
||||
console.log('📄 문서 링크 삭제 API 호출');
|
||||
await this.api.delete(`/document-links/${linkId}`);
|
||||
}
|
||||
|
||||
this.documentLinks = this.documentLinks.filter(l => l.id !== linkId);
|
||||
|
||||
this.hideTooltip();
|
||||
|
||||
@@ -375,6 +375,15 @@ class CachedAPI {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 삭제 (캐시 무효화)
|
||||
*/
|
||||
async deleteNote(noteId) {
|
||||
return await this.delete(`/highlight-notes/${noteId}`, {
|
||||
invalidateCategories: ['notes', 'highlights']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 생성 (캐시 무효화)
|
||||
*/
|
||||
|
||||
@@ -246,19 +246,24 @@ window.documentViewer = () => ({
|
||||
// ==================== URL 파라미터 처리 ====================
|
||||
parseUrlParameters() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.documentId = urlParams.get('id');
|
||||
const rawId = urlParams.get('id');
|
||||
|
||||
// null 문자열이나 빈 값 처리
|
||||
if (!rawId || rawId === 'null' || rawId === 'undefined' || rawId.trim() === '') {
|
||||
console.error('❌ 유효하지 않은 문서 ID:', rawId);
|
||||
throw new Error('유효한 문서 ID가 필요합니다. URL을 확인해주세요.');
|
||||
}
|
||||
|
||||
this.documentId = rawId;
|
||||
// contentType 파라미터를 올바르게 가져오기 (type과 contentType 둘 다 지원)
|
||||
this.contentType = urlParams.get('contentType') || urlParams.get('type') || 'document';
|
||||
|
||||
console.log('🔍 URL 파싱 결과:', {
|
||||
rawId: rawId,
|
||||
documentId: this.documentId,
|
||||
contentType: this.contentType,
|
||||
fullURL: window.location.href
|
||||
});
|
||||
|
||||
if (!this.documentId) {
|
||||
throw new Error('문서 ID가 필요합니다.');
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 문서 로드 ====================
|
||||
@@ -1162,7 +1167,8 @@ window.documentViewer = () => ({
|
||||
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);
|
||||
const linkTitle = link.target_note_title || link.target_document_title || 'Unknown';
|
||||
console.log('✅ 겹치는 링크 발견:', linkTitle);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1362,7 +1368,7 @@ window.documentViewer = () => ({
|
||||
'<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>'
|
||||
}
|
||||
</svg>
|
||||
<span class="font-medium ${iconClass}">${link.target_document_title}</span>
|
||||
<span class="font-medium ${iconClass}">${link.target_note_title || link.target_document_title}</span>
|
||||
</div>
|
||||
|
||||
${link.target_text ? `
|
||||
@@ -1390,7 +1396,7 @@ window.documentViewer = () => ({
|
||||
</div>
|
||||
|
||||
<!-- 삭제 버튼 -->
|
||||
<button onclick="event.stopPropagation(); window.documentViewerInstance.deleteLinkWithConfirm('${link.id}', '${link.target_document_title.replace(/'/g, "\\'")}');"
|
||||
<button onclick="event.stopPropagation(); window.documentViewerInstance.deleteLinkWithConfirm('${link.id}', '${(link.target_note_title || link.target_document_title).replace(/'/g, "\\'")}');"
|
||||
class="ml-3 p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors duration-200 opacity-0 group-hover:opacity-100"
|
||||
title="링크 삭제">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -1650,17 +1656,21 @@ window.documentViewer = () => ({
|
||||
console.log('🔙 백링크 클릭:', backlink);
|
||||
console.log('📋 백링크 상세 정보:', {
|
||||
source_document_id: backlink.source_document_id,
|
||||
source_note_id: backlink.source_note_id,
|
||||
source_content_type: backlink.source_content_type,
|
||||
source_document_title: backlink.source_document_title
|
||||
});
|
||||
|
||||
if (!backlink.source_document_id) {
|
||||
console.error('❌ 소스 문서 ID가 없습니다!', backlink);
|
||||
// 소스 ID 찾기 (노트 백링크의 경우 source_note_id 우선)
|
||||
const sourceId = backlink.source_note_id || backlink.source_document_id;
|
||||
if (!sourceId) {
|
||||
console.error('❌ 소스 문서/노트 ID가 없습니다!', backlink);
|
||||
alert('백링크 소스를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
return this.linkManager.navigateToSourceDocument(backlink.source_document_id, backlink);
|
||||
console.log('✅ 백링크 소스 ID 발견:', sourceId);
|
||||
return this.linkManager.navigateToSourceDocument(sourceId, backlink);
|
||||
},
|
||||
|
||||
// 링크 삭제 (확인 후)
|
||||
@@ -1676,8 +1686,16 @@ window.documentViewer = () => ({
|
||||
try {
|
||||
console.log('🗑️ 링크 삭제 시작:', linkId);
|
||||
|
||||
// API 호출
|
||||
await this.api.deleteDocumentLink(linkId);
|
||||
// 출발지 타입에 따라 다른 API 사용
|
||||
if (this.contentType === 'note') {
|
||||
// 노트에서 출발하는 링크: NoteLink API 사용
|
||||
console.log('📝 노트 링크 삭제 API 호출');
|
||||
await this.api.delete(`/note-links/${linkId}`);
|
||||
} else {
|
||||
// 문서에서 출발하는 링크: DocumentLink API 사용
|
||||
console.log('📄 문서 링크 삭제 API 호출');
|
||||
await this.api.deleteDocumentLink(linkId);
|
||||
}
|
||||
console.log('✅ 링크 삭제 성공');
|
||||
|
||||
// 툴팁 숨기기
|
||||
|
||||
@@ -773,22 +773,22 @@
|
||||
</div>
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<script src="/static/js/api.js?v=2025012615"></script>
|
||||
<script src="/static/js/api.js?v=2025012618"></script>
|
||||
|
||||
<!-- 캐시 및 성능 최적화 시스템 -->
|
||||
<script src="/static/js/viewer/utils/cache-manager.js?v=2025012607"></script>
|
||||
<script src="/static/js/viewer/utils/cached-api.js?v=2025012615"></script>
|
||||
<script src="/static/js/viewer/utils/cached-api.js?v=2025012618"></script>
|
||||
<script src="/static/js/viewer/utils/module-loader.js?v=2025012607"></script>
|
||||
|
||||
<!-- 모든 모듈들 직접 로드 -->
|
||||
<script src="/static/js/viewer/core/document-loader.js?v=2025012607"></script>
|
||||
<script src="/static/js/viewer/features/ui-manager.js?v=2025012607"></script>
|
||||
<script src="/static/js/viewer/features/highlight-manager.js?v=2025012617"></script>
|
||||
<script src="/static/js/viewer/features/link-manager.js?v=2025012607"></script>
|
||||
<script src="/static/js/viewer/features/highlight-manager.js?v=2025012619"></script>
|
||||
<script src="/static/js/viewer/features/link-manager.js?v=2025012622"></script>
|
||||
<script src="/static/js/viewer/features/bookmark-manager.js?v=2025012607"></script>
|
||||
|
||||
<!-- ViewerCore (Alpine.js 컴포넌트) -->
|
||||
<script src="/static/js/viewer/viewer-core.js?v=2025012617"></script>
|
||||
<script src="/static/js/viewer/viewer-core.js?v=2025012623"></script>
|
||||
|
||||
<!-- Alpine.js 프레임워크 -->
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user