feat: 노트북-서적 간 양방향 링크/백링크 시스템 완성
✨ 주요 기능 - 노트 ↔ 서적 문서 간 양방향 링크 생성 및 이동 - 링크 대상 타입 선택 UI (서적 문서/노트북 노트) - 통합 백링크 시스템 (일반 문서에서 노트 백링크도 표시) - 링크 목록 UI 개선 (상세 정보 표시, 타입 구분) 🔧 백엔드 개선 - NoteLink 모델 및 API 추가 (/note-documents/{id}/links, /note-documents/{id}/backlinks) - 일반 문서 백링크 API에서 노트 링크도 함께 조회 - target_content_type, source_content_type 필드 추가 - 노트 문서 콘텐츠 API 추가 (/note-documents/{id}/content) 🎨 프론트엔드 개선 - text-selector.html에서 노트 문서 지원 - 링크 이동 시 contentType에 따른 올바른 URL 생성 - URL 파라미터 파싱 수정 (contentType 지원) - 링크 타입 자동 추론 로직 - 링크 목록 UI 대폭 개선 (출발점/도착점 텍스트, 타입 배지 등) 🐛 버그 수정 - 서적 목록 로드 실패 문제 해결 - 노트에서 링크 생성 시 대상 문서 열기 문제 해결 - 더미 문서로 이동하는 문제 해결 - 캐시 관련 문제 해결
This commit is contained in:
@@ -34,6 +34,7 @@ window.documentViewer = () => ({
|
||||
description: ''
|
||||
},
|
||||
linkForm: {
|
||||
target_type: 'document', // 'document' 또는 'note'
|
||||
target_document_id: '',
|
||||
selected_text: '',
|
||||
start_offset: 0,
|
||||
@@ -246,11 +247,13 @@ window.documentViewer = () => ({
|
||||
parseUrlParameters() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.documentId = urlParams.get('id');
|
||||
this.contentType = urlParams.get('type') || 'document';
|
||||
// contentType 파라미터를 올바르게 가져오기 (type과 contentType 둘 다 지원)
|
||||
this.contentType = urlParams.get('contentType') || urlParams.get('type') || 'document';
|
||||
|
||||
console.log('🔍 URL 파싱 결과:', {
|
||||
documentId: this.documentId,
|
||||
contentType: this.contentType
|
||||
contentType: this.contentType,
|
||||
fullURL: window.location.href
|
||||
});
|
||||
|
||||
if (!this.documentId) {
|
||||
@@ -304,8 +307,8 @@ window.documentViewer = () => ({
|
||||
this.highlightManager.loadHighlights(this.documentId, this.contentType),
|
||||
this.highlightManager.loadNotes(this.documentId, this.contentType),
|
||||
this.bookmarkManager.loadBookmarks(this.documentId),
|
||||
this.linkManager.loadDocumentLinks(this.documentId),
|
||||
this.linkManager.loadBacklinks(this.documentId)
|
||||
this.linkManager.loadDocumentLinks(this.documentId, this.contentType),
|
||||
this.linkManager.loadBacklinks(this.documentId, this.contentType)
|
||||
]);
|
||||
|
||||
// 데이터 저장 및 모듈 동기화
|
||||
@@ -475,33 +478,72 @@ window.documentViewer = () => ({
|
||||
async loadBacklinks() {
|
||||
console.log('🔗 백링크 로드 시작');
|
||||
if (this.linkManager) {
|
||||
await this.linkManager.loadBacklinks(this.documentId);
|
||||
await this.linkManager.loadBacklinks(this.documentId, this.contentType);
|
||||
// UI 상태 동기화
|
||||
this.backlinks = this.linkManager.backlinks || [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadAvailableBooks() {
|
||||
// 링크 대상 타입 변경 시 호출
|
||||
async onTargetTypeChange() {
|
||||
console.log('🔄 링크 대상 타입 변경:', this.linkForm.target_type);
|
||||
|
||||
// 기존 선택 초기화
|
||||
this.linkForm.target_book_id = '';
|
||||
this.linkForm.target_document_id = '';
|
||||
this.availableBooks = [];
|
||||
this.filteredDocuments = [];
|
||||
|
||||
// 선택된 타입에 따라 데이터 로드
|
||||
if (this.linkForm.target_type === 'note') {
|
||||
await this.loadNotebooks();
|
||||
} else {
|
||||
await this.loadBooks();
|
||||
}
|
||||
},
|
||||
|
||||
// 노트북 목록 로드
|
||||
async loadNotebooks() {
|
||||
try {
|
||||
console.log('📚 노트북 목록 로딩 시작...');
|
||||
|
||||
const notebooks = await this.api.get('/notebooks/', { active_only: true });
|
||||
this.availableBooks = notebooks.map(notebook => ({
|
||||
id: notebook.id,
|
||||
title: notebook.title
|
||||
})) || [];
|
||||
|
||||
console.log('📚 로드된 노트북 목록:', this.availableBooks.length, '개');
|
||||
} catch (error) {
|
||||
console.error('노트북 목록 로드 실패:', error);
|
||||
this.availableBooks = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 서적 목록 로드
|
||||
async loadBooks() {
|
||||
try {
|
||||
console.log('📚 서적 목록 로딩 시작...');
|
||||
|
||||
// 문서 목록에서 서적 정보 추출
|
||||
const allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||||
console.log('📄 모든 문서들 (총 개수):', allDocuments.length);
|
||||
let allDocuments;
|
||||
|
||||
// 소스 문서의 서적 정보 찾기
|
||||
const sourceBookInfo = this.getSourceBookInfo(allDocuments);
|
||||
console.log('📖 소스 문서 서적 정보:', sourceBookInfo);
|
||||
// contentType에 따라 다른 API 사용
|
||||
if (this.contentType === 'note') {
|
||||
// 노트의 경우 전체 문서 목록에서 서적 정보 추출
|
||||
console.log('📝 노트 모드: 전체 문서 목록에서 서적 정보 추출');
|
||||
allDocuments = await this.api.getDocuments();
|
||||
console.log('📄 전체 문서들 (총 개수):', allDocuments.length);
|
||||
} else {
|
||||
// 일반 문서의 경우 linkable-documents API 사용
|
||||
console.log('📄 문서 모드: linkable-documents API 사용');
|
||||
allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||||
console.log('📄 링크 가능한 문서들 (총 개수):', allDocuments.length);
|
||||
}
|
||||
|
||||
// 서적별로 그룹화
|
||||
const bookMap = new Map();
|
||||
allDocuments.forEach(doc => {
|
||||
if (doc.book_id && doc.book_title) {
|
||||
console.log('📖 문서 서적 정보:', {
|
||||
docId: doc.id,
|
||||
bookId: doc.book_id,
|
||||
bookTitle: doc.book_title
|
||||
});
|
||||
bookMap.set(doc.book_id, {
|
||||
id: doc.book_id,
|
||||
title: doc.book_title
|
||||
@@ -509,18 +551,28 @@ window.documentViewer = () => ({
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📚 그룹화된 모든 서적들:', Array.from(bookMap.values()));
|
||||
|
||||
// 모든 서적 표시 (소스 서적 포함)
|
||||
this.availableBooks = Array.from(bookMap.values());
|
||||
console.log('📚 최종 사용 가능한 서적들 (모든 서적):', this.availableBooks);
|
||||
console.log('📖 소스 서적 정보 (포함됨):', sourceBookInfo);
|
||||
console.log('📚 로드된 서적 목록:', this.availableBooks.length, '개');
|
||||
} catch (error) {
|
||||
console.error('서적 목록 로드 실패:', error);
|
||||
this.availableBooks = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadAvailableBooks() {
|
||||
try {
|
||||
// 기본값으로 문서 타입 설정 (기존 호환성)
|
||||
if (this.linkForm.target_type === 'note') {
|
||||
await this.loadNotebooks();
|
||||
} else {
|
||||
await this.loadBooks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('목록 로드 실패:', error);
|
||||
this.availableBooks = [];
|
||||
}
|
||||
},
|
||||
|
||||
getSourceBookInfo(allDocuments = null) {
|
||||
// 여러 소스에서 현재 문서의 서적 정보 찾기
|
||||
let sourceBookId = this.navigation?.book_info?.id ||
|
||||
@@ -548,6 +600,30 @@ window.documentViewer = () => ({
|
||||
|
||||
async loadSameBookDocuments() {
|
||||
try {
|
||||
if (this.contentType === 'note') {
|
||||
console.log('📚 같은 노트북의 노트들 로드 시작...');
|
||||
|
||||
// 현재 노트의 노트북 정보 가져오기
|
||||
const currentNote = this.document;
|
||||
const notebookId = currentNote?.notebook_id;
|
||||
|
||||
if (notebookId) {
|
||||
// 같은 노트북의 노트들 로드 (현재 노트 제외)
|
||||
const notes = await this.api.getNotesInNotebook(notebookId);
|
||||
|
||||
this.filteredDocuments = notes.filter(note => note.id !== this.documentId);
|
||||
console.log('📚 같은 노트북 노트들:', {
|
||||
count: this.filteredDocuments.length,
|
||||
notebookId: notebookId,
|
||||
notes: this.filteredDocuments.map(note => ({ id: note.id, title: note.title }))
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ 현재 노트의 노트북 정보를 찾을 수 없습니다');
|
||||
this.filteredDocuments = [];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||||
|
||||
// 소스 문서의 서적 정보 가져오기
|
||||
@@ -574,7 +650,7 @@ window.documentViewer = () => ({
|
||||
this.filteredDocuments = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('같은 서적 문서 로드 실패:', error);
|
||||
console.error('같은 서적/노트북 문서 로드 실패:', error);
|
||||
this.filteredDocuments = [];
|
||||
}
|
||||
},
|
||||
@@ -620,12 +696,30 @@ window.documentViewer = () => ({
|
||||
async loadDocumentsFromBook() {
|
||||
try {
|
||||
if (this.linkForm.target_book_id) {
|
||||
// 선택된 서적의 문서들만 가져오기
|
||||
const allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||||
this.filteredDocuments = allDocuments.filter(doc =>
|
||||
doc.book_id === this.linkForm.target_book_id
|
||||
);
|
||||
console.log('📚 선택된 서적 문서들:', this.filteredDocuments);
|
||||
if (this.linkForm.target_type === 'note') {
|
||||
// 노트북 선택: 선택된 노트북의 노트들 가져오기
|
||||
const notes = await this.api.getNotesInNotebook(this.linkForm.target_book_id);
|
||||
this.filteredDocuments = notes.filter(note => note.id !== this.documentId);
|
||||
console.log('📚 선택된 노트북 노트들:', this.filteredDocuments);
|
||||
} else {
|
||||
// 서적 선택: 선택된 서적의 문서들만 가져오기
|
||||
let allDocuments;
|
||||
|
||||
if (this.contentType === 'note') {
|
||||
// 노트에서 서적 문서를 선택하는 경우: 전체 문서 목록에서 필터링
|
||||
console.log('📝 노트에서 서적 문서 선택: 전체 문서 목록 사용');
|
||||
allDocuments = await this.api.getDocuments();
|
||||
} else {
|
||||
// 일반 문서에서 서적 문서를 선택하는 경우: linkable-documents API 사용
|
||||
console.log('📄 문서에서 서적 문서 선택: linkable-documents API 사용');
|
||||
allDocuments = await this.api.getLinkableDocuments(this.documentId);
|
||||
}
|
||||
|
||||
this.filteredDocuments = allDocuments.filter(doc =>
|
||||
doc.book_id === this.linkForm.target_book_id
|
||||
);
|
||||
console.log('📚 선택된 서적 문서들:', this.filteredDocuments);
|
||||
}
|
||||
} else {
|
||||
this.filteredDocuments = [];
|
||||
}
|
||||
@@ -633,7 +727,7 @@ window.documentViewer = () => ({
|
||||
// 문서 선택 초기화
|
||||
this.linkForm.target_document_id = '';
|
||||
} catch (error) {
|
||||
console.error('서적별 문서 로드 실패:', error);
|
||||
console.error('서적/노트북별 문서 로드 실패:', error);
|
||||
this.filteredDocuments = [];
|
||||
}
|
||||
},
|
||||
@@ -662,8 +756,9 @@ window.documentViewer = () => ({
|
||||
}
|
||||
|
||||
// 새 창에서 대상 문서 열기 (텍스트 선택 모드 전용 페이지)
|
||||
const targetUrl = `/text-selector.html?id=${this.linkForm.target_document_id}`;
|
||||
console.log('🚀 텍스트 선택 창 열기:', targetUrl);
|
||||
const targetContentType = this.linkForm.target_type === 'note' ? 'note' : 'document';
|
||||
const targetUrl = `/text-selector.html?id=${this.linkForm.target_document_id}&contentType=${targetContentType}`;
|
||||
console.log('🚀 텍스트 선택 창 열기:', targetUrl, 'contentType:', targetContentType);
|
||||
const popup = window.open(targetUrl, 'targetDocumentSelector', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||
|
||||
if (!popup) {
|
||||
@@ -1087,6 +1182,62 @@ window.documentViewer = () => ({
|
||||
return this.linkManager.navigateToBacklinkDocument(documentId, backlinkData);
|
||||
},
|
||||
|
||||
// HTML에서 사용되는 링크 이동 함수들
|
||||
navigateToLink(link) {
|
||||
console.log('🔗 링크 클릭:', link);
|
||||
console.log('📋 링크 상세 정보:', {
|
||||
target_document_id: link.target_document_id,
|
||||
target_note_id: link.target_note_id,
|
||||
target_content_type: link.target_content_type,
|
||||
target_document_title: link.target_document_title,
|
||||
target_note_title: link.target_note_title
|
||||
});
|
||||
|
||||
// target_content_type이 없으면 ID로 추론
|
||||
let targetContentType = link.target_content_type;
|
||||
if (!targetContentType) {
|
||||
if (link.target_note_id) {
|
||||
targetContentType = 'note';
|
||||
} else if (link.target_document_id) {
|
||||
targetContentType = 'document';
|
||||
}
|
||||
console.log('🔍 target_content_type 추론됨:', targetContentType);
|
||||
}
|
||||
|
||||
const targetId = link.target_document_id || link.target_note_id;
|
||||
if (!targetId) {
|
||||
console.error('❌ 대상 문서/노트 ID가 없습니다!', link);
|
||||
alert('링크 대상을 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 링크 객체에 추론된 타입 추가
|
||||
const linkWithType = {
|
||||
...link,
|
||||
target_content_type: targetContentType
|
||||
};
|
||||
|
||||
console.log('🚀 최종 링크 데이터:', linkWithType);
|
||||
return this.linkManager.navigateToLinkedDocument(targetId, linkWithType);
|
||||
},
|
||||
|
||||
navigateToBacklink(backlink) {
|
||||
console.log('🔙 백링크 클릭:', backlink);
|
||||
console.log('📋 백링크 상세 정보:', {
|
||||
source_document_id: backlink.source_document_id,
|
||||
source_content_type: backlink.source_content_type,
|
||||
source_document_title: backlink.source_document_title
|
||||
});
|
||||
|
||||
if (!backlink.source_document_id) {
|
||||
console.error('❌ 소스 문서 ID가 없습니다!', backlink);
|
||||
alert('백링크 소스를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
return this.linkManager.navigateToSourceDocument(backlink.source_document_id, backlink);
|
||||
},
|
||||
|
||||
// 북마크 관련
|
||||
scrollToBookmark(bookmark) {
|
||||
return this.bookmarkManager.scrollToBookmark(bookmark);
|
||||
@@ -1144,8 +1295,29 @@ window.documentViewer = () => ({
|
||||
|
||||
console.log('✅ 모든 필수 필드 확인됨');
|
||||
|
||||
// API 호출
|
||||
await this.api.createDocumentLink(this.documentId, linkData);
|
||||
// API 호출 (출발지와 대상에 따라 다른 API 사용)
|
||||
if (this.contentType === 'note') {
|
||||
// 노트에서 출발하는 링크
|
||||
if (this.linkForm.target_type === 'note') {
|
||||
// 노트 → 노트: 노트 링크 API 사용
|
||||
linkData.target_note_id = linkData.target_document_id;
|
||||
delete linkData.target_document_id;
|
||||
await this.api.post(`/note-documents/${this.documentId}/links`, linkData);
|
||||
} else {
|
||||
// 노트 → 문서: 노트 링크 API 사용 (target_document_id 유지)
|
||||
await this.api.post(`/note-documents/${this.documentId}/links`, linkData);
|
||||
}
|
||||
} else {
|
||||
// 문서에서 출발하는 링크
|
||||
if (this.linkForm.target_type === 'note') {
|
||||
// 문서 → 노트: 문서 링크 API에 노트 대상 지원 필요 (향후 구현)
|
||||
// 현재는 기존 API 사용
|
||||
await this.api.createDocumentLink(this.documentId, linkData);
|
||||
} else {
|
||||
// 문서 → 문서: 기존 문서 링크 API 사용
|
||||
await this.api.createDocumentLink(this.documentId, linkData);
|
||||
}
|
||||
}
|
||||
console.log('✅ 링크 생성됨');
|
||||
|
||||
// 성공 알림
|
||||
@@ -1157,13 +1329,17 @@ window.documentViewer = () => ({
|
||||
// 캐시 무효화 (새 링크가 반영되도록)
|
||||
console.log('🗑️ 링크 캐시 무효화 시작...');
|
||||
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||||
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/links`, ['links']);
|
||||
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);
|
||||
await this.linkManager.loadDocumentLinks(this.documentId, this.contentType);
|
||||
this.documentLinks = this.linkManager.documentLinks || [];
|
||||
console.log('📊 로드된 링크 개수:', this.documentLinks.length);
|
||||
console.log('📊 링크 데이터:', this.documentLinks);
|
||||
@@ -1177,10 +1353,14 @@ window.documentViewer = () => ({
|
||||
console.log('🔄 백링크 새로고침 시작...');
|
||||
// 백링크 캐시도 무효화
|
||||
if (window.cachedApi && window.cachedApi.invalidateRelatedCache) {
|
||||
window.cachedApi.invalidateRelatedCache(`/documents/${this.documentId}/backlinks`, ['links']);
|
||||
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);
|
||||
await this.linkManager.loadBacklinks(this.documentId, this.contentType);
|
||||
this.backlinks = this.linkManager.backlinks || [];
|
||||
this.linkManager.renderBacklinks();
|
||||
console.log('✅ 백링크 새로고침 완료');
|
||||
|
||||
Reference in New Issue
Block a user