diff --git a/frontend/static/js/viewer/features/link-manager.js b/frontend/static/js/viewer/features/link-manager.js index 7c6e685..fbe204f 100644 --- a/frontend/static/js/viewer/features/link-manager.js +++ b/frontend/static/js/viewer/features/link-manager.js @@ -10,6 +10,9 @@ class LinkManager { this.cachedApi = window.cachedApi || api; this.documentLinks = []; this.backlinks = []; + + // 안전한 초기화 확인 + console.log('🔧 LinkManager 초기화 - backlinks 타입:', typeof this.backlinks, Array.isArray(this.backlinks)); this.selectedText = ''; this.selectedRange = null; this.availableBooks = []; @@ -23,10 +26,15 @@ class LinkManager { */ async loadDocumentLinks(documentId) { try { - this.documentLinks = await this.cachedApi.get('/document-links', { document_id: documentId }, { category: 'links' }).catch(() => []); - return this.documentLinks || []; + console.log('📡 링크 API 호출:', `/documents/${documentId}/links`); + const response = await this.cachedApi.get(`/documents/${documentId}/links`, {}, { category: 'links' }).catch(() => []); + this.documentLinks = Array.isArray(response) ? response : []; + console.log('📡 API 응답 링크 개수:', this.documentLinks.length); + console.log('📡 API 응답 타입:', typeof response, response); + return this.documentLinks; } catch (error) { console.error('문서 링크 로드 실패:', error); + this.documentLinks = []; return []; } } @@ -36,10 +44,15 @@ class LinkManager { */ async loadBacklinks(documentId) { try { - this.backlinks = await this.cachedApi.get('/document-links/backlinks', { target_document_id: documentId }, { category: 'links' }).catch(() => []); - return this.backlinks || []; + console.log('📡 백링크 API 호출:', `/documents/${documentId}/backlinks`); + const response = await this.cachedApi.get(`/documents/${documentId}/backlinks`, {}, { category: 'links' }).catch(() => []); + this.backlinks = Array.isArray(response) ? response : []; + console.log('📡 API 응답 백링크 개수:', this.backlinks.length); + console.log('📡 API 응답 타입:', typeof response, response); + return this.backlinks; } catch (error) { console.error('백링크 로드 실패:', error); + this.backlinks = []; return []; } } @@ -51,7 +64,20 @@ class LinkManager { const documentContent = document.getElementById('document-content'); if (!documentContent) return; + // 안전한 링크 초기화 + if (!Array.isArray(this.documentLinks)) { + console.warn('⚠️ this.documentLinks가 배열이 아닙니다. 빈 배열로 초기화합니다.'); + console.log('🔍 기존 this.documentLinks:', typeof this.documentLinks, this.documentLinks); + this.documentLinks = []; + } + console.log('🔗 링크 렌더링 시작 - 총', this.documentLinks.length, '개'); + + // 링크 데이터가 없으면 렌더링하지 않음 (기존 링크 유지) + if (this.documentLinks.length === 0) { + console.log('📝 링크 데이터가 없어서 기존 링크를 유지합니다.'); + return; + } // 기존 링크 제거 const existingLinks = documentContent.querySelectorAll('.document-link'); @@ -62,9 +88,13 @@ class LinkManager { }); // 각 링크 렌더링 - this.documentLinks.forEach(link => { - this.renderSingleLink(link); - }); + if (Array.isArray(this.documentLinks)) { + this.documentLinks.forEach(link => { + this.renderSingleLink(link); + }); + } else { + console.warn('⚠️ this.documentLinks가 배열이 아닙니다:', typeof this.documentLinks, this.documentLinks); + } console.log('✅ 링크 렌더링 완료'); } @@ -166,26 +196,43 @@ class LinkManager { const documentContent = document.getElementById('document-content'); if (!documentContent) return; + // 안전한 백링크 초기화 + if (!Array.isArray(this.backlinks)) { + console.warn('⚠️ this.backlinks가 배열이 아닙니다. 빈 배열로 초기화합니다.'); + console.log('🔍 기존 this.backlinks:', typeof this.backlinks, this.backlinks); + this.backlinks = []; + } + console.log('🔗 백링크 렌더링 시작 - 총', this.backlinks.length, '개'); + + // 백링크 데이터가 없으면 렌더링하지 않음 (기존 백링크 유지) + if (this.backlinks.length === 0) { + console.log('📝 백링크 데이터가 없어서 기존 백링크를 유지합니다.'); + return; + } // 기존 백링크는 제거하지 않고 중복 체크만 함 const existingBacklinks = documentContent.querySelectorAll('.backlink-highlight'); console.log(`🔍 기존 백링크 ${existingBacklinks.length}개 발견 (유지)`); // 각 백링크 렌더링 (중복되지 않는 것만) - this.backlinks.forEach(backlink => { - // 이미 렌더링된 백링크인지 확인 - const existingBacklink = Array.from(existingBacklinks).find(el => - el.dataset.backlinkId === backlink.id.toString() - ); - - if (!existingBacklink) { - console.log(`🆕 새로운 백링크 렌더링: ${backlink.id}`); - this.renderSingleBacklink(backlink); - } else { - console.log(`✅ 백링크 이미 존재: ${backlink.id}`); - } - }); + if (Array.isArray(this.backlinks)) { + this.backlinks.forEach(backlink => { + // 이미 렌더링된 백링크인지 확인 + const existingBacklink = Array.from(existingBacklinks).find(el => + el.dataset.backlinkId === backlink.id.toString() + ); + + if (!existingBacklink) { + console.log(`🆕 새로운 백링크 렌더링: ${backlink.id}`); + this.renderSingleBacklink(backlink); + } else { + console.log(`✅ 백링크 이미 존재: ${backlink.id}`); + } + }); + } else { + console.warn('⚠️ this.backlinks가 배열이 아닙니다:', typeof this.backlinks, this.backlinks); + } console.log('✅ 백링크 렌더링 완료'); } @@ -534,8 +581,23 @@ class LinkManager { /** * 선택된 텍스트로 링크 생성 */ - async createLinkFromSelection(documentId, selectedText, selectedRange) { - if (!selectedText || !selectedRange) return; + async createLinkFromSelection(documentId = null, selectedText = null, selectedRange = null) { + // 매개변수가 없으면 현재 선택된 텍스트 사용 + if (!selectedText || !selectedRange) { + selectedText = window.getSelection().toString().trim(); + const selection = window.getSelection(); + + if (!selectedText || selection.rangeCount === 0) { + alert('텍스트를 먼저 선택해주세요.'); + return; + } + + selectedRange = selection.getRangeAt(0); + } + + if (!documentId && window.documentViewerInstance) { + documentId = window.documentViewerInstance.documentId; + } try { console.log('🔗 링크 생성 시작:', selectedText); @@ -547,6 +609,12 @@ class LinkManager { window.documentViewerInstance.showLinkModal = true; window.documentViewerInstance.linkForm.selected_text = selectedText; + // 서적 목록 로드 + await window.documentViewerInstance.loadAvailableBooks(); + + // 기본적으로 같은 서적 문서들 로드 + await window.documentViewerInstance.loadSameBookDocuments(); + // 텍스트 오프셋 계산 const documentContent = document.getElementById('document-content'); const fullText = documentContent.textContent; @@ -563,6 +631,8 @@ class LinkManager { } } + + /** * 텍스트 오프셋 계산 */ diff --git a/frontend/static/js/viewer/viewer-core.js b/frontend/static/js/viewer/viewer-core.js index d0762b9..7e4d99f 100644 --- a/frontend/static/js/viewer/viewer-core.js +++ b/frontend/static/js/viewer/viewer-core.js @@ -44,7 +44,7 @@ window.documentViewer = () => ({ target_text: '', target_start_offset: 0, target_end_offset: 0, - book_scope: 'same', // 'same' 또는 'other' + target_book_id: '' }, @@ -422,15 +422,254 @@ window.documentViewer = () => ({ activateLinkMode() { console.log('🔗 링크 모드 활성화'); this.activeMode = 'link'; - this.linkManager.createLinkFromSelection(); + + // 선택된 텍스트 확인 + const selectedText = window.getSelection().toString().trim(); + const selection = window.getSelection(); + + if (!selectedText || selection.rangeCount === 0) { + alert('텍스트를 먼저 선택해주세요.'); + return; + } + + const selectedRange = selection.getRangeAt(0); + this.linkManager.createLinkFromSelection(this.documentId, selectedText, selectedRange); }, + + activateNoteMode() { console.log('📝 메모 모드 활성화'); this.activeMode = 'memo'; this.highlightManager.activateNoteMode(); }, + async loadBacklinks() { + console.log('🔗 백링크 로드 시작'); + if (this.linkManager) { + await this.linkManager.loadBacklinks(this.documentId); + // UI 상태 동기화 + this.backlinks = this.linkManager.backlinks || []; + } + }, + + async loadAvailableBooks() { + try { + console.log('📚 서적 목록 로딩 시작...'); + + // 문서 목록에서 서적 정보 추출 + const allDocuments = await this.api.getLinkableDocuments(this.documentId); + console.log('📄 모든 문서들 (총 개수):', allDocuments.length); + + // 소스 문서의 서적 정보 찾기 + const sourceBookInfo = this.getSourceBookInfo(allDocuments); + console.log('📖 소스 문서 서적 정보:', sourceBookInfo); + + // 서적별로 그룹화 + 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 + }); + } + }); + + console.log('📚 그룹화된 모든 서적들:', Array.from(bookMap.values())); + + // 소스 문서 기준으로 서적 분류 + if (sourceBookInfo.id) { + // 소스 서적 제외 (다른 서적만 남김) + const wasDeleted = bookMap.delete(sourceBookInfo.id); + console.log('✅ 소스 서적 제외 결과:', { + sourceBookId: sourceBookInfo.id, + sourceBookTitle: sourceBookInfo.title, + wasDeleted: wasDeleted, + remainingBooks: bookMap.size + }); + } else { + console.warn('⚠️ 소스 문서의 서적 정보를 찾을 수 없습니다!'); + } + + this.availableBooks = Array.from(bookMap.values()); + console.log('📚 최종 사용 가능한 서적들 (다른 서적):', this.availableBooks); + console.log('🔍 제외된 소스 서적 ID:', sourceBookInfo.id); + } catch (error) { + console.error('서적 목록 로드 실패:', error); + this.availableBooks = []; + } + }, + + getSourceBookInfo(allDocuments = null) { + // 여러 소스에서 현재 문서의 서적 정보 찾기 + let sourceBookId = this.navigation?.book_info?.id || + this.document?.book_id || + this.document?.book_info?.id; + + let sourceBookTitle = this.navigation?.book_info?.title || + this.document?.book_title || + this.document?.book_info?.title; + + // allDocuments에서도 확인 (가장 확실한 방법) + if (allDocuments) { + const currentDoc = allDocuments.find(doc => doc.id === this.documentId); + if (currentDoc) { + sourceBookId = currentDoc.book_id; + sourceBookTitle = currentDoc.book_title; + } + } + + return { + id: sourceBookId, + title: sourceBookTitle + }; + }, + + async loadSameBookDocuments() { + try { + const allDocuments = await this.api.getLinkableDocuments(this.documentId); + + // 소스 문서의 서적 정보 가져오기 + const sourceBookInfo = this.getSourceBookInfo(allDocuments); + + console.log('📚 같은 서적 문서 로드 시작:', { + sourceBookId: sourceBookInfo.id, + sourceBookTitle: sourceBookInfo.title, + totalDocs: allDocuments.length + }); + + if (sourceBookInfo.id) { + // 소스 문서와 같은 서적의 문서들만 필터링 (현재 문서 제외) + this.filteredDocuments = allDocuments.filter(doc => + doc.book_id === sourceBookInfo.id && doc.id !== this.documentId + ); + console.log('📚 같은 서적 문서들:', { + count: this.filteredDocuments.length, + bookTitle: sourceBookInfo.title, + documents: this.filteredDocuments.map(doc => ({ id: doc.id, title: doc.title })) + }); + } else { + console.warn('⚠️ 소스 문서의 서적 정보를 찾을 수 없습니다!'); + this.filteredDocuments = []; + } + } catch (error) { + console.error('같은 서적 문서 로드 실패:', error); + this.filteredDocuments = []; + } + }, + + async loadSameBookDocumentsForSelected() { + try { + console.log('📚 선택한 문서 기준으로 같은 서적 문서 로드 시작'); + + const allDocuments = await this.api.getLinkableDocuments(this.documentId); + + // 선택한 대상 문서 찾기 + const selectedDoc = allDocuments.find(doc => doc.id === this.linkForm.target_document_id); + + if (!selectedDoc) { + console.error('❌ 선택한 문서를 찾을 수 없습니다:', this.linkForm.target_document_id); + return; + } + + console.log('🎯 선택한 문서 정보:', { + id: selectedDoc.id, + title: selectedDoc.title, + bookId: selectedDoc.book_id, + bookTitle: selectedDoc.book_title + }); + + // 선택한 문서와 같은 서적의 모든 문서들 (소스 문서 제외) + this.filteredDocuments = allDocuments.filter(doc => + doc.book_id === selectedDoc.book_id && doc.id !== this.documentId + ); + + console.log('📚 선택한 문서와 같은 서적 문서들:', { + selectedBookTitle: selectedDoc.book_title, + count: this.filteredDocuments.length, + documents: this.filteredDocuments.map(doc => ({ id: doc.id, title: doc.title })) + }); + + } catch (error) { + console.error('선택한 문서 기준 같은 서적 로드 실패:', error); + this.filteredDocuments = []; + } + }, + + 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); + } else { + this.filteredDocuments = []; + } + + // 문서 선택 초기화 + this.linkForm.target_document_id = ''; + } catch (error) { + console.error('서적별 문서 로드 실패:', error); + this.filteredDocuments = []; + } + }, + + resetTargetSelection() { + console.log('🔄 대상 선택 초기화'); + this.linkForm.target_book_id = ''; + this.linkForm.target_document_id = ''; + this.filteredDocuments = []; + + // 초기화 후 아무것도 하지 않음 (서적 선택 후 문서 로드) + }, + + async onTargetDocumentChange() { + console.log('📄 대상 문서 변경:', this.linkForm.target_document_id); + + // 대상 문서 변경 시 특별한 처리 없음 + }, + + selectTextFromDocument() { + console.log('🎯 대상 문서에서 텍스트 선택 시작'); + + if (!this.linkForm.target_document_id) { + alert('대상 문서를 먼저 선택해주세요.'); + return; + } + + // 새 창에서 대상 문서 열기 (텍스트 선택 모드 전용 페이지) + const targetUrl = `/text-selector.html?id=${this.linkForm.target_document_id}`; + console.log('🚀 텍스트 선택 창 열기:', targetUrl); + const popup = window.open(targetUrl, 'targetDocumentSelector', 'width=1200,height=800,scrollbars=yes,resizable=yes'); + + if (!popup) { + console.error('❌ 팝업 창이 차단되었습니다!'); + alert('팝업 창이 차단되었습니다. 브라우저 설정에서 팝업을 허용해주세요.'); + } else { + console.log('✅ 팝업 창이 성공적으로 열렸습니다'); + } + + // 팝업에서 텍스트 선택 완료 시 메시지 수신 + window.addEventListener('message', (event) => { + if (event.data.type === 'TEXT_SELECTED') { + this.linkForm.target_text = event.data.selectedText; + this.linkForm.target_start_offset = event.data.startOffset; + this.linkForm.target_end_offset = event.data.endOffset; + console.log('🎯 대상 텍스트 선택됨:', event.data); + popup.close(); + } + }, { once: true }); + }, + activateBookmarkMode() { console.log('🔖 북마크 모드 활성화'); this.activeMode = 'bookmark'; @@ -550,6 +789,66 @@ window.documentViewer = () => ({ // 언어 전환 로직 구현 필요 }, + async downloadOriginalFile() { + if (!this.document || !this.document.id) { + console.warn('문서 정보가 없습니다'); + return; + } + + // 연결된 PDF가 있는지 확인 + if (!this.document.matched_pdf_id) { + alert('연결된 원본 PDF 파일이 없습니다.\n\n서적 편집 페이지에서 PDF 파일을 연결해주세요.'); + return; + } + + try { + console.log('📕 연결된 PDF 다운로드 시작:', this.document.matched_pdf_id); + + // 연결된 PDF 문서 정보 가져오기 + const pdfDocument = await this.api.getDocument(this.document.matched_pdf_id); + + if (!pdfDocument) { + throw new Error('연결된 PDF 문서를 찾을 수 없습니다'); + } + + // PDF 파일 다운로드 URL 생성 + const downloadUrl = `/api/documents/${this.document.matched_pdf_id}/download`; + + // 인증 헤더 추가를 위해 fetch 사용 + const response = await fetch(downloadUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}` + } + }); + + if (!response.ok) { + throw new Error('연결된 PDF 다운로드에 실패했습니다'); + } + + // Blob으로 변환하여 다운로드 + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + // 다운로드 링크 생성 및 클릭 + const link = document.createElement('a'); + link.href = url; + link.download = pdfDocument.original_filename || `${pdfDocument.title}.pdf`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // URL 정리 + window.URL.revokeObjectURL(url); + + console.log('📕 PDF 다운로드 완료:', pdfDocument.original_filename); + + } catch (error) { + console.error('PDF 다운로드 오류:', error); + alert('PDF 다운로드 중 오류가 발생했습니다: ' + error.message); + } + }, + // ==================== 유틸리티 메서드 ==================== formatDate(dateString) { return new Date(dateString).toLocaleString('ko-KR'); @@ -574,12 +873,8 @@ window.documentViewer = () => ({ }, getSelectedBookTitle() { - if (this.linkForm.book_scope === 'same') { - return this.document?.book_title || '현재 서적'; - } else { - const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id); - return selectedBook ? selectedBook.title : '서적을 선택하세요'; - } + const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id); + return selectedBook ? selectedBook.title : '서적을 선택하세요'; }, // ==================== 모듈 메서드 위임 ==================== @@ -629,6 +924,94 @@ window.documentViewer = () => ({ deleteBookmark(bookmarkId) { return this.bookmarkManager.deleteBookmark(bookmarkId); + }, + + // ==================== 링크 생성 ==================== + async createDocumentLink() { + console.log('🔗 createDocumentLink 함수 실행'); + console.log('📋 현재 linkForm 상태:', JSON.stringify(this.linkForm, null, 2)); + + try { + // 링크 데이터 검증 + if (!this.linkForm.target_document_id) { + alert('대상 문서를 선택해주세요.'); + return; + } + + if (this.linkForm.link_type === 'text' && !this.linkForm.target_text) { + alert('대상 문서에서 텍스트를 선택해주세요. "대상 문서에서 텍스트 선택" 버튼을 클릭하여 연결할 텍스트를 드래그해주세요.'); + return; + } + + // API 호출용 데이터 준비 (백엔드 필드명에 맞춤) + const linkData = { + target_document_id: this.linkForm.target_document_id, + selected_text: this.linkForm.selected_text, // 백엔드: selected_text + start_offset: this.linkForm.start_offset, // 백엔드: start_offset + end_offset: this.linkForm.end_offset, // 백엔드: end_offset + link_text: this.linkForm.link_text || this.linkForm.selected_text, + description: this.linkForm.description, + link_type: this.linkForm.link_type, + target_text: this.linkForm.target_text || null, + target_start_offset: this.linkForm.target_start_offset || null, + target_end_offset: this.linkForm.target_end_offset || null + }; + + console.log('📤 링크 생성 데이터:', linkData); + console.log('📤 링크 생성 데이터 (JSON):', JSON.stringify(linkData, null, 2)); + + // 필수 필드 검증 + const requiredFields = ['target_document_id', 'selected_text', 'start_offset', 'end_offset']; + const missingFields = requiredFields.filter(field => + linkData[field] === undefined || linkData[field] === null || linkData[field] === '' + ); + + if (missingFields.length > 0) { + console.error('❌ 필수 필드 누락:', missingFields); + alert('필수 필드가 누락되었습니다: ' + missingFields.join(', ')); + return; + } + + console.log('✅ 모든 필수 필드 확인됨'); + + // API 호출 + await this.api.createDocumentLink(this.documentId, linkData); + console.log('✅ 링크 생성됨'); + + // 성공 알림 + alert('링크가 성공적으로 생성되었습니다!'); + + // 모달 닫기 + this.showLinkModal = false; + + // 링크 목록 새로고침 + console.log('🔄 링크 목록 새로고침 시작...'); + await this.linkManager.loadDocumentLinks(this.documentId); + this.documentLinks = this.linkManager.documentLinks || []; + console.log('📊 로드된 링크 개수:', this.documentLinks.length); + console.log('📊 링크 데이터:', this.documentLinks); + + // 링크 렌더링 + console.log('🎨 링크 렌더링 시작...'); + await this.linkManager.renderDocumentLinks(); + console.log('✅ 링크 렌더링 완료'); + + } catch (error) { + console.error('링크 생성 실패:', error); + console.error('에러 상세:', { + message: error.message, + stack: error.stack, + response: error.response + }); + + // 422 에러인 경우 상세 정보 표시 + if (error.response && error.response.status === 422) { + console.error('422 Validation Error Details:', error.response.data); + alert('데이터 검증 실패: ' + JSON.stringify(error.response.data, null, 2)); + } else { + alert('링크 생성에 실패했습니다: ' + error.message); + } + } } }); @@ -649,3 +1032,44 @@ document.addEventListener('alpine:init', () => { } }; }); + +// Alpine.js Store 등록 +document.addEventListener('alpine:init', () => { + Alpine.store('documentViewer', { + instance: null, + + init() { + // DocumentViewer 인스턴스가 생성되면 저장 + setTimeout(() => { + this.instance = window.documentViewerInstance; + }, 500); + }, + + downloadOriginalFile() { + console.log('🏪 Store downloadOriginalFile 호출'); + if (this.instance) { + return this.instance.downloadOriginalFile(); + } else { + console.warn('DocumentViewer 인스턴스가 없습니다'); + } + }, + + toggleLanguage() { + console.log('🏪 Store toggleLanguage 호출'); + if (this.instance) { + return this.instance.toggleLanguage(); + } else { + console.warn('DocumentViewer 인스턴스가 없습니다'); + } + }, + + loadBacklinks() { + console.log('🏪 Store loadBacklinks 호출'); + if (this.instance) { + return this.instance.loadBacklinks(); + } else { + console.warn('DocumentViewer 인스턴스가 없습니다'); + } + } + }); +}); diff --git a/frontend/viewer.html b/frontend/viewer.html index 8d9a073..8c506ec 100644 --- a/frontend/viewer.html +++ b/frontend/viewer.html @@ -26,7 +26,7 @@ })(); -
+
@@ -134,67 +134,26 @@
- -
- - -
- - -
-
+ + - -
- - -
- -
+ +
+ + 백링크 +
@@ -268,14 +227,14 @@
- -
- +
- -
- - -
-
- - -
- -