From f49edaf06b8e216ce14ca306b2bedd258ac0aa46 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Thu, 4 Sep 2025 11:21:15 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20UI=20=EA=B0=9C=EC=84=A0:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0,?= =?UTF-8?q?=20=EB=94=94=EB=B2=84=EA=B7=B8=20=EB=A1=9C=EA=B7=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC,=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B0=9C=EC=84=A0,=20=ED=95=A0=EC=9D=BC=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=97=A4=EB=8D=94=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/core/config.py | 1 + backend/src/main.py | 6 +- frontend/static/js/todos.js | 9 +- frontend/static/js/viewer-test.js | 92 - frontend/static/js/viewer.js.backup | 3656 --------------------------- frontend/todos.html | 24 +- 6 files changed, 25 insertions(+), 3763 deletions(-) delete mode 100644 frontend/static/js/viewer-test.js delete mode 100644 frontend/static/js/viewer.js.backup diff --git a/backend/src/core/config.py b/backend/src/core/config.py index 275bfed..2c07451 100644 --- a/backend/src/core/config.py +++ b/backend/src/core/config.py @@ -28,6 +28,7 @@ class Settings(BaseSettings): # CORS 설정 ALLOWED_HOSTS: List[str] = ["http://localhost:24100", "http://127.0.0.1:24100"] + ALLOWED_ORIGINS: List[str] = ["http://localhost:24100", "http://127.0.0.1:24100"] # 파일 업로드 설정 UPLOAD_DIR: str = "uploads" diff --git a/backend/src/main.py b/backend/src/main.py index 1fa1439..9d32862 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -30,12 +30,12 @@ app = FastAPI( lifespan=lifespan, ) -# CORS 설정 (개발용 - 더 관대한 설정) +# CORS 설정 app.add_middleware( CORSMiddleware, - allow_origins=["*"], # 개발용으로 모든 오리진 허용 + allow_origins=settings.ALLOWED_ORIGINS if hasattr(settings, 'ALLOWED_ORIGINS') else ["*"], allow_credentials=True, - allow_methods=["*"], + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], ) diff --git a/frontend/static/js/todos.js b/frontend/static/js/todos.js index a541bb5..e0e5885 100644 --- a/frontend/static/js/todos.js +++ b/frontend/static/js/todos.js @@ -233,11 +233,7 @@ function todosApp() { // 할일 일정 설정 async scheduleTodo() { - console.log('🔧 scheduleTodo 호출됨:', this.currentTodo, this.scheduleForm); - if (!this.currentTodo || !this.scheduleForm.start_date) { - console.log('❌ 필수 데이터 누락:', { currentTodo: this.currentTodo, start_date: this.scheduleForm.start_date }); - return; - } + if (!this.currentTodo || !this.scheduleForm.start_date) return; try { // 선택한 날짜의 총 시간 체크 @@ -273,17 +269,14 @@ function todosApp() { let response; // 이미 일정이 설정된 할일인지 확인 - console.log('📋 할일 상태 확인:', this.currentTodo.status); if (this.currentTodo.status === 'draft') { // 새로 일정 설정 - console.log('📅 새로 일정 설정 API 호출'); response = await window.api.post(`/todos/${this.currentTodo.id}/schedule`, { start_date: startDate.toISOString(), estimated_minutes: newMinutes }); } else { // 기존 일정 지연 (active 상태의 할일) - console.log('🔄 기존 일정 지연 API 호출'); response = await window.api.put(`/todos/${this.currentTodo.id}/delay`, { delayed_until: startDate.toISOString() }); diff --git a/frontend/static/js/viewer-test.js b/frontend/static/js/viewer-test.js deleted file mode 100644 index a00eb61..0000000 --- a/frontend/static/js/viewer-test.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 간단한 테스트용 documentViewer - */ -window.documentViewer = () => ({ - // 기본 상태 - loading: false, - error: null, - - // 네비게이션 - navigation: null, - - // 검색 - searchQuery: '', - - // 데이터 - notes: [], - bookmarks: [], - documentLinks: [], - backlinks: [], - - // UI 상태 - activeFeatureMenu: null, - selectedHighlightColor: '#FFFF00', - - // 모달 상태 - showLinksModal: false, - showLinkModal: false, - showNotesModal: false, - showBookmarksModal: false, - showBacklinksModal: false, - - // 폼 데이터 - linkForm: { - target_document_id: '', - selected_text: '', - book_scope: 'same', - target_book_id: '', - link_type: 'document', - target_text: '', - description: '' - }, - - // 기타 데이터 - availableBooks: [], - filteredDocuments: [], - - // 초기화 - init() { - console.log('🔧 간단한 documentViewer 로드됨'); - this.documentId = new URLSearchParams(window.location.search).get('id'); - console.log('📋 문서 ID:', this.documentId); - }, - - // 뒤로가기 - goBack() { - console.log('🔙 뒤로가기 클릭됨'); - const urlParams = new URLSearchParams(window.location.search); - const fromPage = urlParams.get('from'); - - if (fromPage === 'index') { - window.location.href = '/index.html'; - } else if (fromPage === 'hierarchy') { - window.location.href = '/hierarchy.html'; - } else { - window.location.href = '/index.html'; - } - }, - - // 기본 함수들 - toggleFeatureMenu(feature) { - console.log('🎯 기능 메뉴 토글:', feature); - this.activeFeatureMenu = this.activeFeatureMenu === feature ? null : feature; - }, - - searchInDocument() { - console.log('🔍 문서 검색:', this.searchQuery); - }, - - // 빈 함수들 (오류 방지용) - navigateToDocument() { console.log('네비게이션 함수 호출됨'); }, - goToBookContents() { console.log('목차로 이동 함수 호출됨'); }, - createHighlightWithColor() { console.log('하이라이트 생성 함수 호출됨'); }, - resetTargetSelection() { console.log('타겟 선택 리셋 함수 호출됨'); }, - loadDocumentsFromBook() { console.log('서적 문서 로드 함수 호출됨'); }, - onTargetDocumentChange() { console.log('타겟 문서 변경 함수 호출됨'); }, - openTargetDocumentSelector() { console.log('타겟 문서 선택기 열기 함수 호출됨'); }, - saveDocumentLink() { console.log('문서 링크 저장 함수 호출됨'); }, - closeLinkModal() { console.log('링크 모달 닫기 함수 호출됨'); }, - getSelectedBookTitle() { return '테스트 서적'; } -}); - -console.log('✅ 테스트용 documentViewer 정의됨'); diff --git a/frontend/static/js/viewer.js.backup b/frontend/static/js/viewer.js.backup deleted file mode 100644 index c2e9f43..0000000 --- a/frontend/static/js/viewer.js.backup +++ /dev/null @@ -1,3656 +0,0 @@ -/** - * 문서 뷰어 Alpine.js 컴포넌트 - */ -window.documentViewer = () => ({ - // 상태 - loading: true, - error: null, - document: null, - documentId: null, - contentType: 'document', // 'document' 또는 'note' - navigation: null, // 네비게이션 정보 - - // 하이라이트 및 메모 - highlights: [], - notes: [], - selectedHighlightColor: '#FFFF00', - selectedText: '', - selectedRange: null, - - // 책갈피 - bookmarks: [], - - // 문서 링크 - documentLinks: [], - linkableDocuments: [], - backlinks: [], - - // 텍스트 선택 모드 플래그 - textSelectorUISetup: false, - - // UI 상태 - showNotesPanel: false, - showBookmarksPanel: false, - showBacklinks: false, - activePanel: 'notes', - - // 검색 - searchQuery: '', - noteSearchQuery: '', - filteredNotes: [], - - // 언어 전환 - isKorean: false, - - // 모달 - showNoteModal: false, - showBookmarkModal: false, - showLinkModal: false, - showNotesModal: false, - showBookmarksModal: false, - showLinksModal: false, - showBacklinksModal: false, - - // 기능 메뉴 상태 - activeFeatureMenu: null, - - // 링크 관련 데이터 - availableBooks: [], // 사용 가능한 서적 목록 - filteredDocuments: [], // 필터링된 문서 목록 - - // 모드 및 핸들러 - activeMode: null, // 'link', 'memo', 'bookmark' 등 - textSelectionHandler: null, - editingNote: null, - editingBookmark: null, - editingLink: null, - noteLoading: false, - bookmarkLoading: false, - linkLoading: false, - - // 폼 데이터 - noteForm: { - content: '', - tags: '' - }, - bookmarkForm: { - title: '', - description: '' - }, - linkForm: { - target_document_id: '', - selected_text: '', - start_offset: 0, - end_offset: 0, - link_text: '', - description: '', - // 고급 링크 기능 (무조건 텍스트 선택만 지원) - link_type: 'text_fragment', - target_text: '', - target_start_offset: 0, - target_end_offset: 0, - // 서적 범위 선택 - book_scope: 'same', // 'same' 또는 'other' - target_book_id: '' - }, - - // 초기화 - async init() { - // 중복 초기화 방지 - if (this._initialized) { - console.log('⚠️ 이미 초기화됨, 중복 실행 방지'); - return; - } - this._initialized = true; - - console.log('🚀 DocumentViewer 초기화 시작'); - - // 전역 인스턴스 설정 (말풍선에서 함수 호출용) - window.documentViewerInstance = this; - - // 모듈 초기화 - this.documentLoader = new DocumentLoader(api); - this.highlightManager = new HighlightManager(api); - this.bookmarkManager = new BookmarkManager(api); - this.linkManager = new LinkManager(api); - - // URL에서 문서 ID 추출 - const urlParams = new URLSearchParams(window.location.search); - this.documentId = urlParams.get('id'); - this.contentType = urlParams.get('type') || 'document'; // 'document' 또는 'note' - const mode = urlParams.get('mode'); - const isParentWindow = urlParams.get('parent_window') === 'true'; - - console.log('🔍 URL 파싱 결과:', { - documentId: this.documentId, - mode: mode, - parent_window: urlParams.get('parent_window'), - isParentWindow: isParentWindow, - fullUrl: window.location.href - }); - - // 함수들이 제대로 바인딩되었는지 확인 - console.log('🔧 Alpine.js 컴포넌트 로드됨'); - console.log('🔗 activateLinkMode 함수:', typeof this.activateLinkMode); - console.log('📝 activateNoteMode 함수:', typeof this.activateNoteMode); - console.log('🔖 activateBookmarkMode 함수:', typeof this.activateBookmarkMode); - console.log('🎯 toggleFeatureMenu 함수:', typeof this.toggleFeatureMenu); - - if (!this.documentId) { - this.error = '문서 ID가 없습니다'; - this.loading = false; - return; - } - - // 텍스트 선택 모드인 경우 특별 처리 - console.log('🔍 URL 파라미터 확인:', { mode, isParentWindow, documentId: this.documentId }); - if (mode === 'text_selector') { - console.log('🎯 텍스트 선택 모드로 진입'); - await this.initTextSelectorMode(); - return; - } - - // 인증 확인 - if (!api.token) { - window.location.href = '/'; - return; - } - - try { - if (this.contentType === 'note') { - this.document = await this.documentLoader.loadNote(this.documentId); - } else { - this.document = await this.documentLoader.loadDocument(this.documentId); - this.navigation = await this.documentLoader.loadNavigation(this.documentId); - } - await this.loadDocumentData(); - - // URL 파라미터 확인해서 특정 텍스트로 스크롤 - this.documentLoader.checkForTextHighlight(); - - } catch (error) { - console.error('Failed to load document:', error); - this.error = error.message; - } finally { - this.loading = false; - } - - // 초기 필터링 - this.filterNotes(); - }, - - - - - - - - // 문서 관련 데이터 로드 - async loadDocumentData() { - try { - console.log('Loading document data for:', this.documentId, 'type:', this.contentType); - - if (this.contentType === 'note') { - // 노트의 경우: HighlightManager 사용 - console.log('📝 노트 데이터 로드 중...'); - const [highlights, notes] = await Promise.all([ - this.highlightManager.loadHighlights(this.documentId, this.contentType), - this.highlightManager.loadNotes(this.documentId, this.contentType) - ]); - - this.highlights = highlights; - this.notes = notes; - this.bookmarks = []; // 노트에서는 북마크 미지원 - this.documentLinks = []; // 노트에서는 링크 미지원 (향후 구현 예정) - this.backlinks = []; - - console.log('📝 노트 데이터 로드됨:', { highlights: this.highlights.length, notes: this.notes.length }); - - // HighlightManager에 데이터 동기화 - this.highlightManager.highlights = this.highlights; - this.highlightManager.notes = this.notes; - - // 하이라이트 렌더링 - this.highlightManager.renderHighlights(); - return; - } - - // 문서의 경우: 모듈별 로딩 - const [highlights, notes, bookmarks, documentLinks, backlinks] = await Promise.all([ - 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.highlights = highlights; - this.notes = notes; - this.bookmarks = bookmarks || []; - this.documentLinks = documentLinks || []; - this.backlinks = backlinks || []; - - console.log('Loaded data:', { highlights: this.highlights.length, notes: this.notes.length, bookmarks: this.bookmarks.length }); - - // 모듈에 데이터 동기화 - this.highlightManager.highlights = this.highlights; - this.highlightManager.notes = this.notes; - this.bookmarkManager.bookmarks = this.bookmarks; - this.linkManager.documentLinks = this.documentLinks; - this.linkManager.backlinks = this.backlinks; - - // 하이라이트 렌더링 - this.highlightManager.renderHighlights(); - - // 백링크 렌더링 (먼저 렌더링) - this.linkManager.renderBacklinks(); - - // 문서 링크 렌더링 (백링크 후에 렌더링) - this.linkManager.renderDocumentLinks(); - - // 백링크 배너 숫자 업데이트 - this.updateBacklinkBanner(); - - } catch (error) { - console.warn('Some document data failed to load, continuing with empty data:', error); - this.highlights = []; - this.notes = []; - this.bookmarks = []; - } - }, - - - - // 하이라이트 그룹 적용 (여러 색상 지원) - applyHighlightGroup(highlightGroup) { - if (highlightGroup.length === 0) return; - - // 그룹의 전체 범위 계산 - const minStart = Math.min(...highlightGroup.map(h => h.start_offset)); - const maxEnd = Math.max(...highlightGroup.map(h => h.end_offset)); - - const content = document.getElementById('document-content'); - const walker = document.createTreeWalker( - content, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let currentOffset = 0; - let node; - - while (node = walker.nextNode()) { - const nodeLength = node.textContent.length; - const nodeStart = currentOffset; - const nodeEnd = currentOffset + nodeLength; - - // 그룹 범위와 겹치는지 확인 - if (nodeStart < maxEnd && nodeEnd > minStart) { - const startInNode = Math.max(0, minStart - nodeStart); - const endInNode = Math.min(nodeLength, maxEnd - nodeStart); - - if (startInNode < endInNode) { - // 텍스트 노드를 분할하고 하이라이트 적용 - const beforeText = node.textContent.substring(0, startInNode); - const highlightText = node.textContent.substring(startInNode, endInNode); - const afterText = node.textContent.substring(endInNode); - - const parent = node.parentNode; - - // 하이라이트 요소 생성 - const highlightEl = this.createMultiColorHighlight(highlightGroup, highlightText, minStart + startInNode); - - // 노드 교체 - if (beforeText) { - parent.insertBefore(document.createTextNode(beforeText), node); - } - parent.insertBefore(highlightEl, node); - if (afterText) { - parent.insertBefore(document.createTextNode(afterText), node); - } - parent.removeChild(node); - } - } - - currentOffset = nodeEnd; - } - }, - - // 다중 색상 하이라이트 요소 생성 - createMultiColorHighlight(highlightGroup, text, textOffset) { - const container = document.createElement('span'); - - if (highlightGroup.length === 1) { - // 단일 색상 - const highlight = highlightGroup[0]; - container.className = 'highlight'; - container.style.backgroundColor = highlight.highlight_color; - container.textContent = text; - container.dataset.highlightId = highlight.id; - - // 클릭 이벤트 - container.addEventListener('click', (e) => { - e.stopPropagation(); - this.showHighlightTooltip(highlight, e.target); - }); - } else { - // 다중 색상 - 그라데이션 또는 스트라이프 효과 - container.className = 'multi-highlight'; - container.textContent = text; - - // 색상들 수집 - const colors = highlightGroup.map(h => h.highlight_color); - const uniqueColors = [...new Set(colors)]; - - if (uniqueColors.length === 2) { - // 2색상: 위아래 분할 - container.style.background = `linear-gradient(to bottom, ${uniqueColors[0]} 50%, ${uniqueColors[1]} 50%)`; - } else if (uniqueColors.length === 3) { - // 3색상: 3등분 - container.style.background = `linear-gradient(to bottom, ${uniqueColors[0]} 33%, ${uniqueColors[1]} 33% 66%, ${uniqueColors[2]} 66%)`; - } else { - // 4색상 이상: 스트라이프 패턴 - const stripeSize = 100 / uniqueColors.length; - const gradientStops = uniqueColors.map((color, index) => { - const start = index * stripeSize; - const end = (index + 1) * stripeSize; - return `${color} ${start}% ${end}%`; - }).join(', '); - container.style.background = `linear-gradient(to bottom, ${gradientStops})`; - } - - // 테두리 추가로 더 명확하게 - container.style.border = '1px solid rgba(0,0,0,0.2)'; - container.style.borderRadius = '2px'; - - // 모든 하이라이트 ID 저장 - container.dataset.highlightIds = JSON.stringify(highlightGroup.map(h => h.id)); - - // 클릭 이벤트 - 첫 번째 하이라이트로 툴팁 표시 - container.addEventListener('click', (e) => { - e.stopPropagation(); - this.showHighlightTooltip(highlightGroup[0], e.target); - }); - } - - return container; - }, - - // 개별 하이라이트 적용 - applyHighlight(highlight) { - const content = document.getElementById('document-content'); - const walker = document.createTreeWalker( - content, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let currentOffset = 0; - let node; - - while (node = walker.nextNode()) { - const nodeLength = node.textContent.length; - const nodeStart = currentOffset; - const nodeEnd = currentOffset + nodeLength; - - // 하이라이트 범위와 겹치는지 확인 - if (nodeStart < highlight.end_offset && nodeEnd > highlight.start_offset) { - const startInNode = Math.max(0, highlight.start_offset - nodeStart); - const endInNode = Math.min(nodeLength, highlight.end_offset - nodeStart); - - if (startInNode < endInNode) { - // 텍스트 노드를 분할하고 하이라이트 적용 - const beforeText = node.textContent.substring(0, startInNode); - const highlightText = node.textContent.substring(startInNode, endInNode); - const afterText = node.textContent.substring(endInNode); - - const parent = node.parentNode; - - // 하이라이트 요소 생성 - const highlightEl = document.createElement('span'); - highlightEl.className = 'highlight'; - highlightEl.style.backgroundColor = highlight.highlight_color; - highlightEl.textContent = highlightText; - highlightEl.dataset.highlightId = highlight.id; - - // 클릭 이벤트 추가 - 말풍선 표시 - highlightEl.addEventListener('click', (e) => { - e.stopPropagation(); - this.showHighlightTooltip(highlight, e.target); - }); - - // 노드 교체 - if (beforeText) { - parent.insertBefore(document.createTextNode(beforeText), node); - } - parent.insertBefore(highlightEl, node); - if (afterText) { - parent.insertBefore(document.createTextNode(afterText), node); - } - parent.removeChild(node); - } - } - - currentOffset = nodeEnd; - } - }, - - // 텍스트 선택 처리 (HighlightManager로 위임) - handleTextSelection() { - this.highlightManager.handleTextSelection(); - // 상태 동기화 - this.selectedText = this.highlightManager.selectedText; - this.selectedRange = this.highlightManager.selectedRange; - }, - - // 하이라이트 버튼 표시 - showHighlightButton(selection) { - // 기존 버튼 제거 - const existingButton = document.querySelector('.highlight-button'); - if (existingButton) { - existingButton.remove(); - } - - const range = selection.getRangeAt(0); - const rect = range.getBoundingClientRect(); - - const button = document.createElement('button'); - button.className = 'highlight-button fixed z-50 bg-blue-600 text-white px-4 py-2 rounded shadow-lg text-sm font-medium border-2 border-blue-700'; - button.style.left = `${rect.left + window.scrollX}px`; - button.style.top = `${rect.bottom + window.scrollY + 10}px`; - button.innerHTML = '🖍️ 하이라이트'; - - console.log('Highlight button created at:', button.style.left, button.style.top); - - button.addEventListener('click', () => { - this.createHighlight(); - button.remove(); - }); - - document.body.appendChild(button); - - // 3초 후 자동 제거 - setTimeout(() => { - if (button.parentNode) { - button.remove(); - } - }, 3000); - }, - - // 색상 버튼으로 하이라이트 생성 - // HighlightManager로 위임 - createHighlightWithColor(color) { - this.highlightManager.selectedHighlightColor = color; - this.selectedHighlightColor = color; - this.highlightManager.createHighlightWithColor(color); - // 상태 동기화 - this.highlights = this.highlightManager.highlights; - }, - - // 하이라이트 생성 - async createHighlight() { - console.log('createHighlight called'); - console.log('selectedText:', this.selectedText); - console.log('selectedRange:', this.selectedRange); - - if (!this.selectedText || !this.selectedRange) { - console.log('No selected text or range'); - return; - } - - try { - console.log('Starting highlight creation...'); - // 텍스트 오프셋 계산 - const content = document.getElementById('document-content'); - const { startOffset, endOffset } = this.calculateTextOffsets(this.selectedRange, content); - console.log('Text offsets:', startOffset, endOffset); - - const highlightData = { - document_id: this.documentId, - start_offset: startOffset, - end_offset: endOffset, - selected_text: this.selectedText, - highlight_color: this.selectedHighlightColor, - highlight_type: 'highlight' - }; - - // 노트와 문서에 따라 다른 API 호출 - let highlight; - if (this.contentType === 'note') { - // 노트용 하이라이트 API 호출 (document_id를 note_id로 변경) - const noteHighlightData = { - ...highlightData, - note_id: highlightData.document_id - }; - delete noteHighlightData.document_id; - console.log('📝 노트 하이라이트 데이터:', noteHighlightData); - highlight = await api.post('/note-highlights/', noteHighlightData); - } else { - highlight = await api.createHighlight(highlightData); - } - this.highlights.push(highlight); - - // 하이라이트 렌더링 - this.highlightManager.renderHighlights(); - - // 선택 해제 - window.getSelection().removeAllRanges(); - this.selectedText = ''; - this.selectedRange = null; - - // 메모 추가 여부 확인 - if (confirm('이 하이라이트에 메모를 추가하시겠습니까?')) { - // 노트와 문서 모두 동일한 방식으로 처리 - this.createMemoForHighlight(highlight); - } - - } catch (error) { - console.error('Failed to create highlight:', error); - alert('하이라이트 생성에 실패했습니다'); - } - }, - - // 하이라이트 메모 생성 (노트/문서 통합) - async createMemoForHighlight(highlight) { - try { - // 메모 내용 입력받기 - const content = prompt('메모 내용을 입력하세요:', ''); - if (content === null || content.trim() === '') { - return; // 취소하거나 빈 내용인 경우 - } - - // 메모 생성 데이터 - const noteData = { - highlight_id: highlight.id, - content: content.trim() - }; - - let note; - if (this.contentType === 'note') { - // 노트용 메모 API - noteData.note_id = this.documentId; - console.log('📝 노트 메모 생성 데이터:', noteData); - note = await api.post('/note-notes/', noteData); - } else { - // 문서용 메모 API - noteData.is_private = false; - noteData.tags = []; - console.log('📝 문서 메모 생성 데이터:', noteData); - note = await api.createNote(noteData); - } - - // 메모 목록에 추가 - this.notes.push(note); - - console.log('✅ 메모 생성 완료:', note); - - } catch (error) { - console.error('❌ 메모 생성 실패:', error); - alert('메모 생성에 실패했습니다.'); - } - }, - - // 텍스트 오프셋 계산 - calculateTextOffsets(range, container) { - const walker = document.createTreeWalker( - container, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let currentOffset = 0; - let startOffset = -1; - let endOffset = -1; - let node; - - while (node = walker.nextNode()) { - const nodeLength = node.textContent.length; - - if (range.startContainer === node) { - startOffset = currentOffset + range.startOffset; - } - - if (range.endContainer === node) { - endOffset = currentOffset + range.endOffset; - break; - } - - currentOffset += nodeLength; - } - - return { startOffset, endOffset }; - }, - - // 하이라이트 선택 - selectHighlight(highlightId) { - // 모든 하이라이트에서 selected 클래스 제거 - document.querySelectorAll('.highlight').forEach(el => { - el.classList.remove('selected'); - }); - - // 선택된 하이라이트에 selected 클래스 추가 - const highlightEl = document.querySelector(`[data-highlight-id="${highlightId}"]`); - if (highlightEl) { - highlightEl.classList.add('selected'); - } - - // 해당 하이라이트의 메모 찾기 - const note = this.notes.find(n => n.highlight.id === highlightId); - if (note) { - this.editNote(note); - } else { - // 메모가 없으면 새로 생성 - const highlight = this.highlights.find(h => h.id === highlightId); - if (highlight) { - this.openNoteModal(highlight); - } - } - }, - - // 하이라이트로 스크롤 - scrollToHighlight(highlightId) { - const highlightEl = document.querySelector(`[data-highlight-id="${highlightId}"]`); - if (highlightEl) { - highlightEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); - highlightEl.classList.add('selected'); - - // 2초 후 선택 해제 - setTimeout(() => { - highlightEl.classList.remove('selected'); - }, 2000); - } - }, - - // 메모 모달 열기 - openNoteModal(highlight = null) { - this.editingNote = null; - this.noteForm = { - content: '', - tags: '' - }; - - if (highlight) { - this.selectedHighlight = highlight; - this.selectedText = highlight.selected_text; - } - - this.showNoteModal = true; - }, - - // 메모 편집 - editNote(note) { - this.editingNote = note; - this.noteForm = { - content: note.content, - tags: note.tags ? note.tags.join(', ') : '' - }; - this.selectedText = note.highlight.selected_text; - this.showNoteModal = true; - }, - - // 메모 저장 (HighlightManager로 위임) - async saveNote() { - await this.highlightManager.saveNote(); - // 상태 동기화 - this.notes = this.highlightManager.notes; - this.noteLoading = false; - - try { - const noteData = { - content: this.noteForm.content, - tags: this.noteForm.tags ? this.noteForm.tags.split(',').map(t => t.trim()).filter(t => t) : [] - }; - - if (this.editingNote) { - // 메모 수정 - const updatedNote = await api.updateNote(this.editingNote.id, noteData); - const index = this.notes.findIndex(n => n.id === this.editingNote.id); - if (index !== -1) { - this.notes[index] = updatedNote; - } - } else { - // 새 메모 생성 - noteData.highlight_id = this.selectedHighlight.id; - const newNote = await api.createNote(noteData); - this.notes.push(newNote); - } - - this.filterNotes(); - this.closeNoteModal(); - - } catch (error) { - console.error('Failed to save note:', error); - alert('메모 저장에 실패했습니다'); - } finally { - this.noteLoading = false; - } - }, - - // 메모 삭제 (HighlightManager로 위임) - async deleteNote(noteId) { - try { - await this.highlightManager.deleteNote(noteId); - // 상태 동기화 - this.notes = this.highlightManager.notes; - this.filterNotes(); - } catch (error) { - console.error('Failed to delete note:', error); - alert('메모 삭제에 실패했습니다'); - } - }, - - // 메모 모달 닫기 - closeNoteModal() { - this.showNoteModal = false; - this.editingNote = null; - this.selectedHighlight = null; - this.selectedText = ''; - this.noteForm = { content: '', tags: '' }; - }, - - // 메모 필터링 - filterNotes() { - if (!this.noteSearchQuery.trim()) { - this.filteredNotes = [...this.notes]; - } else { - const query = this.noteSearchQuery.toLowerCase(); - this.filteredNotes = this.notes.filter(note => - note.content.toLowerCase().includes(query) || - note.highlight.selected_text.toLowerCase().includes(query) || - (note.tags && note.tags.some(tag => tag.toLowerCase().includes(query))) - ); - } - }, - - // 책갈피 추가 (BookmarkManager로 위임) - async addBookmark() { - await this.bookmarkManager.addBookmark(this.document); - // 상태 동기화 - this.bookmarkForm = this.bookmarkManager.bookmarkForm; - this.currentScrollPosition = this.bookmarkManager.currentScrollPosition; - }, - - // 책갈피 편집 (BookmarkManager로 위임) - editBookmark(bookmark) { - this.bookmarkManager.editBookmark(bookmark); - // 상태 동기화 - this.editingBookmark = this.bookmarkManager.editingBookmark; - this.bookmarkForm = this.bookmarkManager.bookmarkForm; - }, - - // 책갈피 저장 (BookmarkManager로 위임) - async saveBookmark() { - // BookmarkManager의 폼 데이터 동기화 - this.bookmarkManager.bookmarkForm = this.bookmarkForm; - this.bookmarkManager.editingBookmark = this.editingBookmark; - this.bookmarkManager.currentScrollPosition = this.currentScrollPosition; - - await this.bookmarkManager.saveBookmark(this.documentId); - - // 상태 동기화 - this.bookmarks = this.bookmarkManager.bookmarks; - this.editingBookmark = this.bookmarkManager.editingBookmark; - this.bookmarkForm = this.bookmarkManager.bookmarkForm; - this.currentScrollPosition = this.bookmarkManager.currentScrollPosition; - - try { - const bookmarkData = { - title: this.bookmarkForm.title, - description: this.bookmarkForm.description, - scroll_position: this.currentScrollPosition || 0 - }; - - if (this.editingBookmark) { - // 책갈피 수정 - const updatedBookmark = await api.updateBookmark(this.editingBookmark.id, bookmarkData); - const index = this.bookmarks.findIndex(b => b.id === this.editingBookmark.id); - if (index !== -1) { - this.bookmarks[index] = updatedBookmark; - } - } else { - // 새 책갈피 생성 - bookmarkData.document_id = this.documentId; - const newBookmark = await api.createBookmark(bookmarkData); - this.bookmarks.push(newBookmark); - } - - this.closeBookmarkModal(); - - } catch (error) { - console.error('Failed to save bookmark:', error); - alert('책갈피 저장에 실패했습니다'); - } finally { - this.bookmarkLoading = false; - } - }, - - // 책갈피 삭제 (BookmarkManager로 위임) - async deleteBookmark(bookmarkId) { - await this.bookmarkManager.deleteBookmark(bookmarkId); - // 상태 동기화 - this.bookmarks = this.bookmarkManager.bookmarks; - }, - - // 책갈피로 스크롤 (BookmarkManager로 위임) - scrollToBookmark(bookmark) { - this.bookmarkManager.scrollToBookmark(bookmark); - }, - - // 책갈피 모달 닫기 (BookmarkManager로 위임) - closeBookmarkModal() { - this.bookmarkManager.closeBookmarkModal(); - // 상태 동기화 - this.editingBookmark = this.bookmarkManager.editingBookmark; - this.bookmarkForm = this.bookmarkManager.bookmarkForm; - this.currentScrollPosition = this.bookmarkManager.currentScrollPosition; - }, - - // 문서 내 검색 - searchInDocument() { - // 기존 검색 하이라이트 제거 - document.querySelectorAll('.search-highlight').forEach(el => { - const parent = el.parentNode; - parent.replaceChild(document.createTextNode(el.textContent), el); - parent.normalize(); - }); - - if (!this.searchQuery.trim()) { - return; - } - - // 새 검색 하이라이트 적용 - const content = document.getElementById('document-content'); - this.highlightSearchResults(content, this.searchQuery); - }, - - // 검색 결과 하이라이트 - highlightSearchResults(element, searchText) { - const walker = document.createTreeWalker( - element, - NodeFilter.SHOW_TEXT, - null, - false - ); - - const textNodes = []; - let node; - while (node = walker.nextNode()) { - textNodes.push(node); - } - - textNodes.forEach(textNode => { - const text = textNode.textContent; - const regex = new RegExp(`(${searchText})`, 'gi'); - - if (regex.test(text)) { - const parent = textNode.parentNode; - const highlightedHTML = text.replace(regex, '$1'); - - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = highlightedHTML; - - while (tempDiv.firstChild) { - parent.insertBefore(tempDiv.firstChild, textNode); - } - parent.removeChild(textNode); - } - }); - }, - - // 문서 클릭 처리 - handleDocumentClick(event) { - // 하이라이트 버튼 제거 - const button = document.querySelector('.highlight-button'); - if (button && !button.contains(event.target)) { - button.remove(); - } - - // 하이라이트 선택 해제 - document.querySelectorAll('.highlight.selected').forEach(el => { - el.classList.remove('selected'); - }); - }, - - // 뒤로가기 - 문서 관리 페이지로 이동 - goBack() { - // 1. URL 파라미터에서 type과 from 확인 - const urlParams = new URLSearchParams(window.location.search); - const fromPage = urlParams.get('from'); - - // 노트인 경우 노트 목록으로 이동 - if (this.contentType === 'note') { - window.location.href = 'notes.html'; - return; - } - - // 2. 세션 스토리지에서 이전 페이지 확인 - const previousPage = sessionStorage.getItem('previousPage'); - - // 3. referrer 확인 - const referrer = document.referrer; - - let targetPage = 'index.html'; // 기본값: 그리드 뷰 - - // 우선순위: URL 파라미터 > 세션 스토리지 > referrer - if (fromPage === 'hierarchy') { - targetPage = 'hierarchy.html'; - } else if (previousPage === 'hierarchy.html') { - targetPage = 'hierarchy.html'; - } else if (referrer && referrer.includes('hierarchy.html')) { - targetPage = 'hierarchy.html'; - } - - console.log(`🔙 뒤로가기: ${targetPage}로 이동`); - window.location.href = targetPage; - }, - - // 날짜 포맷팅 - formatDate(dateString) { - const date = new Date(dateString); - return date.toLocaleDateString('ko-KR', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }, - - // 동일한 텍스트 범위의 모든 하이라이트 찾기 - findOverlappingHighlights(clickedHighlight) { - const overlapping = []; - - this.highlights.forEach(highlight => { - // 텍스트 범위가 겹치는지 확인 - const isOverlapping = ( - (highlight.start_offset <= clickedHighlight.end_offset && - highlight.end_offset >= clickedHighlight.start_offset) || - (clickedHighlight.start_offset <= highlight.end_offset && - clickedHighlight.end_offset >= highlight.start_offset) - ); - - if (isOverlapping) { - overlapping.push(highlight); - } - }); - - // 시작 위치 순으로 정렬 - return overlapping.sort((a, b) => a.start_offset - b.start_offset); - }, - - // 색상별로 하이라이트 그룹화 - groupHighlightsByColor(highlights) { - const colorGroups = {}; - - highlights.forEach(highlight => { - const color = highlight.highlight_color || highlight.color || '#FFB6C1'; - if (!colorGroups[color]) { - colorGroups[color] = []; - } - colorGroups[color].push(highlight); - }); - - return colorGroups; - }, - - // 색상 이름 매핑 (더 많은 색상 지원) - getColorName(color) { - const colorNames = { - // 기본 색상들 - '#FFB6C1': '핑크', - '#FFFF99': '노랑', - '#FFFF00': '노랑', - '#YELLOW': '노랑', - '#98FB98': '연두', - '#90EE90': '연두', - '#LIGHTGREEN': '연두', - '#87CEEB': '하늘', - '#ADD8E6': '하늘', - '#LIGHTBLUE': '하늘', - '#DDA0DD': '보라', - '#DA70D6': '보라', - '#ORCHID': '보라', - '#FFA500': '주황', - '#ORANGE': '주황', - // RGB 형식 - 'rgb(255, 255, 0)': '노랑', - 'rgb(255, 255, 153)': '노랑', - 'rgb(152, 251, 152)': '연두', - 'rgb(144, 238, 144)': '연두', - 'rgb(135, 206, 235)': '하늘', - 'rgb(173, 216, 230)': '하늘', - 'rgb(255, 182, 193)': '핑크', - 'rgb(255, 165, 0)': '주황' - }; - - // 대소문자 구분 없이 매칭 - const normalizedColor = color?.toUpperCase(); - const exactMatch = colorNames[color] || colorNames[normalizedColor]; - - if (exactMatch) { - return exactMatch; - } - - // RGB 값으로 색상 추정 - if (color?.includes('rgb')) { - const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); - if (rgbMatch) { - const [, r, g, b] = rgbMatch.map(Number); - - // 색상 범위로 판단 - if (r > 200 && g > 200 && b < 100) return '노랑'; - if (r < 200 && g > 200 && b < 200) return '연두'; - if (r < 200 && g < 200 && b > 200) return '하늘'; - if (r > 200 && g < 200 && b > 200) return '핑크'; - if (r > 200 && g > 100 && b < 100) return '주황'; - if (r > 150 && g < 150 && b > 150) return '보라'; - } - } - - console.log('🎨 알 수 없는 색상:', color); - return '기타'; - }, - - // 하이라이트 말풍선 표시 (HighlightManager로 위임) - showHighlightTooltip(clickedHighlight, element) { - if (this.highlightManager) { - this.highlightManager.showHighlightTooltip(clickedHighlight, element); - } - }, - - // 말풍선 외부 클릭 처리 - handleTooltipOutsideClick(e) { - const highlightTooltip = document.getElementById('highlight-tooltip'); - const linkTooltip = document.getElementById('link-tooltip'); - const backlinkTooltip = document.getElementById('backlink-tooltip'); - - const isOutsideHighlightTooltip = highlightTooltip && !highlightTooltip.contains(e.target) && !e.target.classList.contains('highlight'); - const isOutsideLinkTooltip = linkTooltip && !linkTooltip.contains(e.target) && !e.target.classList.contains('document-link'); - const isOutsideBacklinkTooltip = backlinkTooltip && !backlinkTooltip.contains(e.target) && !e.target.classList.contains('backlink-highlight'); - - if (isOutsideHighlightTooltip || isOutsideLinkTooltip || isOutsideBacklinkTooltip) { - this.hideTooltip(); - } - }, - - // 짧은 날짜 형식 - formatShortDate(dateString) { - 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', { - year: '2-digit', - month: '2-digit', - day: '2-digit' - }); - } - }, - - // 메모 추가 폼 표시 (HighlightManager로 위임) - showAddNoteForm(highlightId) { - if (this.highlightManager) { - this.highlightManager.showAddNoteForm(highlightId); - } - }, - - // 동일한 범위의 모든 하이라이트 찾기 - const overlappingHighlights = this.findOverlappingHighlights(clickedHighlight); - const colorGroups = this.groupHighlightsByColor(overlappingHighlights); - - console.log('🎨 겹치는 하이라이트:', overlappingHighlights.length, '개'); - console.log('🎨 하이라이트 상세:', overlappingHighlights.map(h => ({ - id: h.id, - color: h.highlight_color, - colorName: this.getColorName(h.highlight_color), - text: h.selected_text - }))); - console.log('🎨 색상 그룹:', Object.keys(colorGroups)); - - // 각 색상별 메모 개수 디버깅 - Object.entries(colorGroups).forEach(([color, highlights]) => { - const noteCount = highlights.flatMap(h => - this.notes.filter(note => note.highlight_id === h.id) - ).length; - console.log(`🎨 ${this.getColorName(color)} (${color}): ${highlights.length}개 하이라이트, ${noteCount}개 메모`); - }); - - const tooltip = document.createElement('div'); - tooltip.id = 'highlight-tooltip'; - tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50 max-w-lg'; - tooltip.style.minWidth = '350px'; - - // 선택된 텍스트 표시 (가장 긴 텍스트 사용) - const longestText = overlappingHighlights.reduce((longest, current) => - current.selected_text.length > longest.length ? current.selected_text : longest, '' - ); - - let tooltipHTML = ` -
-
선택된 텍스트
-
- "${longestText}" -
-
- `; - - // 하이라이트가 여러 개인 경우 색상별로 표시 - if (overlappingHighlights.length > 1) { - tooltipHTML += ` -
-
- 하이라이트 색상 (${overlappingHighlights.length}개) -
-
- ${Object.keys(colorGroups).map(color => ` -
-
- ${this.getColorName(color)} (${colorGroups[color].length}) -
- `).join('')} -
-
- `; - } - - // 색상별로 메모 표시 - tooltipHTML += '
'; - - 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) - ); - - tooltipHTML += ` -
-
-
-
- ${colorName} 메모 (${allNotes.length}) -
- -
- -
- ${allNotes.length > 0 ? - allNotes.map(note => ` -
-
${note.content}
-
- ${this.formatShortDate(note.created_at)} · Administrator - -
-
- `).join('') : - '
메모가 없습니다
' - } -
-
- `; - }); - - tooltipHTML += '
'; - - // 하이라이트 삭제 버튼들 - if (overlappingHighlights.length > 1) { - tooltipHTML += ` -
-
하이라이트 삭제
-
- ${Object.entries(colorGroups).map(([color, highlights]) => ` - - `).join('')} - -
-
- `; - } else { - tooltipHTML += ` -
- -
- `; - } - - tooltipHTML += ` -
- -
- `; - - tooltip.innerHTML = tooltipHTML; - - // 위치 계산 - const rect = element.getBoundingClientRect(); - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - - document.body.appendChild(tooltip); - - // 말풍선 위치 조정 - const tooltipRect = tooltip.getBoundingClientRect(); - let top = rect.bottom + scrollTop + 5; - let left = rect.left + scrollLeft; - - // 화면 경계 체크 - if (left + tooltipRect.width > window.innerWidth) { - left = window.innerWidth - tooltipRect.width - 10; - } - if (top + tooltipRect.height > window.innerHeight + scrollTop) { - top = rect.top + scrollTop - tooltipRect.height - 5; - } - - tooltip.style.top = top + 'px'; - tooltip.style.left = left + 'px'; - - // 외부 클릭 시 닫기 - setTimeout(() => { - document.addEventListener('click', this.handleTooltipOutsideClick.bind(this)); - }, 100); - }, - - // 말풍선 숨기기 (HighlightManager로 위임) - hideTooltip() { - if (this.highlightManager) { - this.highlightManager.hideTooltip(); - } - }, - - - - // 말풍선 외부 클릭 처리 - handleTooltipOutsideClick(e) { - const highlightTooltip = document.getElementById('highlight-tooltip'); - const linkTooltip = document.getElementById('link-tooltip'); - const backlinkTooltip = document.getElementById('backlink-tooltip'); - - const isOutsideHighlightTooltip = highlightTooltip && !highlightTooltip.contains(e.target) && !e.target.classList.contains('highlight'); - const isOutsideLinkTooltip = linkTooltip && !linkTooltip.contains(e.target) && !e.target.classList.contains('document-link'); - const isOutsideBacklinkTooltip = backlinkTooltip && !backlinkTooltip.contains(e.target) && !e.target.classList.contains('backlink-highlight'); - - if (isOutsideHighlightTooltip || isOutsideLinkTooltip || isOutsideBacklinkTooltip) { - this.hideTooltip(); - } - }, - - // 짧은 날짜 형식 - formatShortDate(dateString) { - 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', { - year: '2-digit', - month: '2-digit', - day: '2-digit' - }); - } - }, - - // 메모 추가 폼 표시 (HighlightManager로 위임) - showAddNoteForm(highlightId) { - if (this.highlightManager) { - this.highlightManager.showAddNoteForm(highlightId); - } - }, - - - - // 메모 추가 취소 - cancelAddNote(highlightId) { - // 말풍선 다시 표시 - const highlight = this.highlights.find(h => h.id === highlightId); - if (highlight) { - const element = document.querySelector(`[data-highlight-id="${highlightId}"]`); - if (element) { - this.showHighlightTooltip(highlight, element); - } - } - }, - - // 새 메모 저장 (HighlightManager로 위임) - async saveNewNote(highlightId) { - if (this.highlightManager) { - await this.highlightManager.saveNewNote(highlightId); - // 상태 동기화 - this.notes = this.highlightManager.notes; - } - }, - - - - // 하이라이트 삭제 - async deleteHighlight(highlightId) { - if (!confirm('이 하이라이트를 삭제하시겠습니까? 연결된 메모도 함께 삭제됩니다.')) { - return; - } - - try { - await api.deleteHighlight(highlightId); - - // 로컬 데이터에서 제거 - this.highlights = this.highlights.filter(h => h.id !== highlightId); - this.notes = this.notes.filter(n => n.highlight_id !== highlightId); - - // UI 업데이트 - this.hideTooltip(); - this.highlightManager.renderHighlights(); - - } catch (error) { - console.error('Failed to delete highlight:', error); - alert('하이라이트 삭제에 실패했습니다'); - } - }, - - // 특정 색상의 하이라이트들 삭제 - async deleteHighlightsByColor(color, highlightIds) { - const colorName = this.getColorName(color); - if (!confirm(`${colorName} 색상의 하이라이트를 모두 삭제하시겠습니까? 연결된 메모도 함께 삭제됩니다.`)) { - return; - } - - try { - // 각 하이라이트를 개별적으로 삭제 - for (const highlightId of highlightIds) { - await api.deleteHighlight(highlightId); - } - - // 로컬 데이터에서 제거 - this.highlights = this.highlights.filter(h => !highlightIds.includes(h.id)); - this.notes = this.notes.filter(n => !highlightIds.includes(n.highlight_id)); - - // UI 업데이트 - this.hideTooltip(); - this.highlightManager.renderHighlights(); - - } catch (error) { - console.error('Failed to delete highlights:', error); - alert('하이라이트 삭제에 실패했습니다'); - } - }, - - // 겹치는 모든 하이라이트 삭제 - async deleteAllOverlappingHighlights(highlightIds) { - if (!confirm(`겹치는 모든 하이라이트를 삭제하시겠습니까? (${highlightIds.length}개) 연결된 메모도 함께 삭제됩니다.`)) { - return; - } - - try { - // 각 하이라이트를 개별적으로 삭제 - for (const highlightId of highlightIds) { - await api.deleteHighlight(highlightId); - } - - // 로컬 데이터에서 제거 - this.highlights = this.highlights.filter(h => !highlightIds.includes(h.id)); - this.notes = this.notes.filter(n => !highlightIds.includes(n.highlight_id)); - - // UI 업데이트 - this.hideTooltip(); - this.highlightManager.renderHighlights(); - - } catch (error) { - console.error('Failed to delete highlights:', error); - alert('하이라이트 삭제에 실패했습니다'); - } - }, - - // 언어 전환 함수 - toggleLanguage() { - this.isKorean = !this.isKorean; - - console.log(`🌐 언어 전환 시작 (isKorean: ${this.isKorean})`); - - // 문서 내 언어별 요소 토글 (HTML, HEAD, BODY 태그 제외) - const primaryLangElements = document.querySelectorAll('[lang="ko"]:not(html):not(head):not(body), .korean, .kr, .primary-lang'); - const secondaryLangElements = document.querySelectorAll('[lang="en"]:not(html):not(head):not(body), .english, .en, [lang="ja"]:not(html):not(head):not(body), .japanese, .jp, [lang="zh"]:not(html):not(head):not(body), .chinese, .cn, .secondary-lang'); - - // 디버깅: 찾은 요소 수 출력 - console.log(`🔍 Primary 요소 수: ${primaryLangElements.length}`); - console.log(`🔍 Secondary 요소 수: ${secondaryLangElements.length}`); - - // 언어별 요소가 있는 경우에만 토글 적용 - if (primaryLangElements.length > 0 || secondaryLangElements.length > 0) { - console.log('✅ 언어별 요소 발견, 토글 적용 중...'); - - primaryLangElements.forEach((el, index) => { - const oldDisplay = el.style.display || getComputedStyle(el).display; - const newDisplay = this.isKorean ? 'block' : 'none'; - console.log(`Primary 요소 ${index + 1}: ${el.tagName}#${el.id || 'no-id'}.${el.className || 'no-class'}`); - console.log(` - 이전 display: ${oldDisplay}`); - console.log(` - 새로운 display: ${newDisplay}`); - console.log(` - 요소 내용 미리보기: "${el.textContent?.substring(0, 50) || 'no-text'}..."`); - console.log(` - 요소 위치:`, el.getBoundingClientRect()); - el.style.display = newDisplay; - console.log(` - 적용 후 display: ${el.style.display}`); - }); - - secondaryLangElements.forEach((el, index) => { - const oldDisplay = el.style.display || getComputedStyle(el).display; - const newDisplay = this.isKorean ? 'none' : 'block'; - console.log(`Secondary 요소 ${index + 1}: ${el.tagName}#${el.id || 'no-id'}.${el.className || 'no-class'}`); - console.log(` - 이전 display: ${oldDisplay}`); - console.log(` - 새로운 display: ${newDisplay}`); - console.log(` - 요소 내용 미리보기: "${el.textContent?.substring(0, 50) || 'no-text'}..."`); - console.log(` - 요소 위치:`, el.getBoundingClientRect()); - el.style.display = newDisplay; - console.log(` - 적용 후 display: ${el.style.display}`); - }); - } else { - // 문서 내 콘텐츠에서 언어별 요소를 더 광범위하게 찾기 - console.log('⚠️ 기본 언어별 요소를 찾을 수 없습니다. 문서 내 콘텐츠를 분석합니다.'); - - // 문서 콘텐츠 영역에서 언어별 요소 찾기 - const contentArea = document.querySelector('#document-content, .document-content, main, .content, #content'); - - if (contentArea) { - console.log('📄 문서 콘텐츠 영역 발견:', contentArea.tagName, contentArea.id || contentArea.className); - - // 콘텐츠 영역의 구조 분석 - console.log('📋 콘텐츠 영역 내 모든 자식 요소들:'); - const allChildren = contentArea.querySelectorAll('*'); - const childrenInfo = Array.from(allChildren).slice(0, 10).map(el => { - return `${el.tagName}${el.id ? '#' + el.id : ''}${el.className ? '.' + el.className.split(' ').join('.') : ''} [lang="${el.lang || 'none'}"]`; - }); - console.log(childrenInfo); - - // 콘텐츠 영역 내에서 언어별 요소 재검색 - const contentPrimary = contentArea.querySelectorAll('[lang="ko"], .korean, .kr, .primary-lang'); - const contentSecondary = contentArea.querySelectorAll('[lang="en"], .english, .en, [lang="ja"], .japanese, .jp, [lang="zh"], .chinese, .cn, .secondary-lang'); - - console.log(`📄 콘텐츠 내 Primary 요소: ${contentPrimary.length}개`); - console.log(`📄 콘텐츠 내 Secondary 요소: ${contentSecondary.length}개`); - - if (contentPrimary.length > 0 || contentSecondary.length > 0) { - // 콘텐츠 영역 내 요소들에 토글 적용 - contentPrimary.forEach(el => { - el.style.display = this.isKorean ? 'block' : 'none'; - }); - contentSecondary.forEach(el => { - el.style.display = this.isKorean ? 'none' : 'block'; - }); - console.log('✅ 콘텐츠 영역 내 언어 토글 적용됨'); - return; - } else { - // 실제 문서 내용에서 언어 패턴 찾기 - console.log('🔍 문서 내용에서 언어 패턴을 찾습니다...'); - - // 문서의 실제 텍스트 내용 확인 - const textContent = contentArea.textContent || ''; - const hasKorean = /[가-힣]/.test(textContent); - const hasEnglish = /[a-zA-Z]/.test(textContent); - - console.log(`📝 문서 언어 분석: 한국어=${hasKorean}, 영어=${hasEnglish}`); - console.log(`📝 문서 내용 미리보기: "${textContent.substring(0, 100)}..."`); - - if (!hasKorean && !hasEnglish) { - console.log('❌ 텍스트 콘텐츠를 찾을 수 없습니다.'); - } - } - } - - // ID 기반 토글 시도 - console.log('🔍 ID 기반 토글을 시도합니다.'); - const koreanContent = document.getElementById('korean-content'); - const englishContent = document.getElementById('english-content'); - - if (koreanContent && englishContent) { - koreanContent.style.display = this.isKorean ? 'block' : 'none'; - englishContent.style.display = this.isKorean ? 'none' : 'block'; - console.log('✅ ID 기반 토글 적용됨'); - } else { - console.log('❌ 언어 전환 가능한 요소를 찾을 수 없습니다.'); - console.log('📋 이 문서는 단일 언어 문서이거나 언어 구분이 없습니다.'); - - // 단일 언어 문서의 경우 아무것도 하지 않음 (흰색 페이지 방지) - console.log('🔄 언어 전환을 되돌립니다.'); - this.isKorean = !this.isKorean; // 상태를 원래대로 되돌림 - return; - } - } - - console.log(`🌐 언어 전환 완료 (Primary: ${this.isKorean ? '표시' : '숨김'})`); - }, - - // 매칭된 PDF 다운로드 - async downloadMatchedPDF() { - if (!this.document.matched_pdf_id) { - console.warn('매칭된 PDF가 없습니다'); - return; - } - - try { - console.log('📕 PDF 다운로드 시작:', this.document.matched_pdf_id); - - // PDF 문서 정보 가져오기 - const pdfDocument = await window.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 다운로드 완료'); - - } catch (error) { - console.error('❌ PDF 다운로드 실패:', error); - alert('PDF 다운로드에 실패했습니다: ' + error.message); - } - }, - - // 원본 파일 다운로드 (연결된 PDF 파일) - 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 window.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 다운로드 완료'); - - } catch (error) { - console.error('❌ 연결된 PDF 다운로드 실패:', error); - alert('연결된 PDF 다운로드에 실패했습니다: ' + error.message); - } - }, - - - - // 다른 문서로 네비게이션 - navigateToDocument(documentId) { - if (!documentId) return; - - const currentUrl = new URL(window.location); - currentUrl.searchParams.set('id', documentId); - window.location.href = currentUrl.toString(); - }, - - // 서적 목차로 이동 - goToBookContents() { - if (!this.navigation?.book_info) return; - - window.location.href = `/book-documents.html?bookId=${this.navigation.book_info.id}`; - }, - - // === 문서 링크 관련 함수들 === - - // 문서 링크 생성 - async createDocumentLink() { - console.log('🔗 createDocumentLink 함수 실행'); - - // 이미 설정된 selectedText와 selectedRange 사용 - let selectedText = this.selectedText; - let range = this.selectedRange; - - // 설정되지 않은 경우 현재 선택 확인 - if (!selectedText || !range) { - console.log('📝 기존 선택 없음, 현재 선택 확인'); - const selection = window.getSelection(); - if (!selection.rangeCount || selection.isCollapsed) { - alert('텍스트를 선택한 후 링크를 생성해주세요.'); - return; - } - range = selection.getRangeAt(0); - selectedText = selection.toString().trim(); - } - - console.log('✅ 선택된 텍스트:', selectedText); - - if (selectedText.length === 0) { - alert('텍스트를 선택한 후 링크를 생성해주세요.'); - return; - } - - // 선택된 텍스트의 위치 계산 - const documentContent = document.getElementById('document-content'); - const startOffset = this.getTextOffset(documentContent, range.startContainer, range.startOffset); - const endOffset = startOffset + selectedText.length; - - console.log('📍 텍스트 위치:', { startOffset, endOffset }); - - // 폼 데이터 설정 - this.linkForm = { - target_document_id: '', - selected_text: selectedText, - start_offset: startOffset, - end_offset: endOffset, - link_text: '', - description: '', - link_type: 'document', // 기본값: 문서 전체 링크 - target_text: '', - target_start_offset: null, - target_end_offset: null, - book_scope: 'same', // 기본값: 같은 서적 - target_book_id: '' - }; - - console.log('📋 링크 폼 데이터:', this.linkForm); - - // 링크 가능한 문서 목록 로드 - await this.loadLinkableDocuments(); - - // 모달 열기 - console.log('🔗 링크 모달 열기'); - console.log('🔗 showLinksModal 설정 전:', this.showLinksModal); - this.showLinksModal = true; - this.showLinkModal = true; // 기존 호환성 - console.log('🔗 showLinksModal 설정 후:', this.showLinksModal); - this.editingLink = null; - }, - - // 링크 가능한 문서 목록 로드 - async loadLinkableDocuments() { - try { - if (this.contentType === 'note') { - // 노트의 경우: 다른 노트들과 문서들 모두 로드 - console.log('📝 노트 링크 대상 로드 중...'); - - // 임시: 빈 배열로 설정 (나중에 노트-문서 간 링크 API 구현 시 수정) - this.linkableDocuments = []; - this.availableBooks = []; - this.filteredDocuments = []; - - console.warn('📝 노트 간 링크는 아직 지원되지 않습니다.'); - return; - } else { - // 문서의 경우: 기존 로직 - this.linkableDocuments = await api.getLinkableDocuments(this.documentId); - console.log('🔗 링크 가능한 문서들:', this.linkableDocuments); - - // 서적 목록도 함께 로드 - await this.loadAvailableBooks(); - - // 기본적으로 같은 서적 문서들 로드 - await this.loadSameBookDocuments(); - } - } catch (error) { - console.error('❌ 링크 가능한 문서 로드 실패:', error); - this.linkableDocuments = []; - } - }, - - // 문서 링크 저장 (고급 기능 포함) - async saveDocumentLink() { - if (!this.linkForm.target_document_id || !this.linkForm.selected_text) { - alert('필수 정보가 누락되었습니다.'); - return; - } - - // 대상 텍스트가 선택되지 않았으면 경고 - if (!this.linkForm.target_text) { - alert('대상 문서에서 텍스트를 선택해주세요. "대상 문서에서 텍스트 선택" 버튼을 클릭하여 연결할 텍스트를 드래그해주세요.'); - return; - } - - this.linkLoading = true; - - try { - const linkData = { - target_document_id: this.linkForm.target_document_id, - selected_text: this.linkForm.selected_text, - start_offset: this.linkForm.start_offset, - end_offset: this.linkForm.end_offset, - link_text: this.linkForm.link_text || null, - description: this.linkForm.description || null, - 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 - }; - - if (this.editingLink) { - await window.api.updateDocumentLink(this.editingLink.id, linkData); - console.log('✅ 링크 수정됨'); - } else { - await window.api.createDocumentLink(this.documentId, linkData); - console.log('✅ 링크 생성됨'); - } - - // 데이터 새로고침 - await this.loadDocumentData(); - // 링크 렌더링은 loadDocumentData에서 처리됨 - this.closeLinkModal(); - } catch (error) { - console.error('❌ 링크 저장 실패:', error); - alert('링크 저장에 실패했습니다: ' + error.message); - } finally { - this.linkLoading = false; - } - }, - - // 링크 모달 닫기 (고급 기능 포함) - closeLinkModal() { - this.showLinksModal = false; - this.showLinkModal = false; - this.editingLink = null; - this.linkForm = { - target_document_id: '', - selected_text: '', - start_offset: 0, - end_offset: 0, - link_text: '', - description: '', - link_type: 'document', - target_text: '', - target_start_offset: 0, - target_end_offset: 0, - book_scope: 'same', - target_book_id: '' - }; - - // 필터링된 문서 목록 초기화 - this.filteredDocuments = []; - }, - - // 문서 링크 렌더링 (LinkManager로 위임) - renderDocumentLinks() { - if (this.linkManager) { - this.linkManager.renderDocumentLinks(); - } - }, - - // 백링크 하이라이트 렌더링 (LinkManager로 위임) - async renderBacklinkHighlights() { - if (this.linkManager) { - this.linkManager.renderBacklinks(); - } - }, - - // 백링크 배너 업데이트 (LinkManager 데이터 사용) - updateBacklinkBanner() { - const backlinkCount = this.backlinks ? this.backlinks.length : 0; - const backlinkBanner = document.getElementById('backlink-banner'); - if (backlinkBanner) { - const countElement = backlinkBanner.querySelector('.backlink-count'); - if (countElement) { - countElement.textContent = backlinkCount; - } - } - }, - existingLinks.forEach(link => { - // 백링크는 제거하지 않음 - if (!link.classList.contains('backlink-highlight')) { - const parent = link.parentNode; - parent.replaceChild(document.createTextNode(link.textContent), link); - parent.normalize(); - } - }); - - // 백링크도 보호 (별도 클래스) - const existingBacklinks = documentContent.querySelectorAll('.backlink-highlight'); - console.log(`🔒 백링크 보호: ${existingBacklinks.length}개 백링크 발견`); - // 백링크는 건드리지 않음 (보호만 함) - - // 새 링크 적용 - console.log(`🔗 링크 렌더링 시작 - 총 ${this.documentLinks.length}개`); - this.documentLinks.forEach((link, index) => { - console.log(`🔗 링크 ${index + 1}:`, link); - console.log(` - selected_text: "${link.selected_text}"`); - console.log(` - start_offset: ${link.start_offset}`); - console.log(` - end_offset: ${link.end_offset}`); - - const span = this.highlightTextRange( - documentContent, - link.start_offset, - link.end_offset, - 'document-link', - { - 'data-link-id': link.id, - 'data-target-document': link.target_document_id, - 'data-target-title': link.target_document_title, - 'title': `링크: ${link.target_document_title}${link.description ? '\n' + link.description : ''}`, - 'style': 'color: #7C3AED; text-decoration: underline; cursor: pointer; background-color: rgba(124, 58, 237, 0.1);' - } - ); - - if (span) { - console.log(`✅ 링크 렌더링 성공: "${link.selected_text}"`); - } else { - console.log(`❌ 링크 렌더링 실패: "${link.selected_text}"`); - } - }); - - // 링크 클릭 이벤트 추가 - const linkElements = documentContent.querySelectorAll('.document-link'); - linkElements.forEach(linkEl => { - linkEl.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - - // 클릭된 위치의 모든 링크 찾기 - const clickedText = linkEl.textContent; - const overlappingLinks = this.getOverlappingLinks(linkEl); - - // 링크 툴팁 표시 - this.showLinkTooltip(overlappingLinks, linkEl, clickedText); - }); - }); - }, - - // 겹치는 링크들 찾기 - getOverlappingLinks(clickedElement) { - const clickedLinkId = clickedElement.getAttribute('data-link-id'); - const clickedText = clickedElement.textContent; - - // 동일한 텍스트 범위에 있는 모든 링크 찾기 - const overlappingLinks = this.documentLinks.filter(link => { - // 클릭된 링크와 텍스트가 겹치는지 확인 - const linkElement = document.querySelector(`[data-link-id="${link.id}"]`); - if (!linkElement) return false; - - // 텍스트가 겹치는지 확인 (간단한 방법: 텍스트 내용 비교) - return linkElement.textContent === clickedText; - }); - - return overlappingLinks; - }, - - // 링크 툴팁 표시 - showLinkTooltip(links, element, selectedText) { - // 기존 툴팁 제거 - 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 = '350px'; - - let tooltipHTML = ` -
-
선택된 텍스트
-
- "${selectedText}" -
-
- `; - - if (links.length > 1) { - tooltipHTML += ` -
-
- 연결된 링크 (${links.length}개) -
-
- `; - } - - tooltipHTML += '
'; - - links.forEach(link => { - tooltipHTML += ` -
-
-
-
${link.target_document_title}
- ${link.description ? `
${link.description}
` : ''} -
- ${link.link_type === 'text_fragment' ? '텍스트 조각 링크' : '문서 링크'} -
-
-
- - - -
-
-
- `; - }); - - 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 - 175) + 'px'; - - document.body.appendChild(tooltip); - - // 외부 클릭 시 툴팁 숨기기 - setTimeout(() => { - document.addEventListener('click', this.handleTooltipOutsideClick.bind(this)); - }, 100); - }, - - // 백링크 하이라이트 렌더링 (LinkManager로 위임) - async renderBacklinkHighlights() { - if (this.linkManager) { - this.linkManager.renderBacklinks(); - } - }, - - // 텍스트 범위 하이라이트 (하이라이트와 동일한 로직, 클래스명만 다름) - highlightTextRange(container, startOffset, endOffset, className, attributes = {}) { - console.log(`🎯 highlightTextRange 호출: ${startOffset}-${endOffset}, 클래스: ${className}`); - const walker = document.createTreeWalker( - container, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let currentOffset = 0; - let startNode = null; - let startNodeOffset = 0; - let endNode = null; - let endNodeOffset = 0; - - let node; - while (node = walker.nextNode()) { - const nodeLength = node.textContent.length; - const nodeStart = currentOffset; - const nodeEnd = currentOffset + nodeLength; - - // 시작 노드 찾기 - if (!startNode && nodeEnd > startOffset) { - startNode = node; - startNodeOffset = startOffset - nodeStart; - } - - // 끝 노드 찾기 - if (!endNode && nodeEnd >= endOffset) { - endNode = node; - endNodeOffset = endOffset - nodeStart; - break; - } - - currentOffset += nodeLength; - } - - if (!startNode || !endNode) return; - - try { - // DOM 변경 전에 Range 유효성 검사 - if (!startNode.parentNode || !endNode.parentNode || - startNodeOffset < 0 || endNodeOffset < 0) { - console.warn(`❌ 유효하지 않은 노드 또는 오프셋 (${className})`); - return null; - } - - const range = document.createRange(); - - // Range 설정 시 예외 처리 - try { - range.setStart(startNode, startNodeOffset); - range.setEnd(endNode, endNodeOffset); - } catch (rangeError) { - console.warn(`❌ Range 설정 실패 (${className}):`, rangeError); - return null; - } - - // 빈 범위 체크 - if (range.collapsed) { - console.warn(`❌ 빈 범위 (${className})`); - range.detach(); - return null; - } - - const span = document.createElement('span'); - span.className = className; - - // 속성 추가 - Object.entries(attributes).forEach(([key, value]) => { - span.setAttribute(key, value); - }); - - // 더 안전한 하이라이트 적용 방식 - try { - // 범위가 단일 텍스트 노드인지 확인 - if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) { - // 단일 텍스트 노드인 경우 직접 분할 - const text = startNode.textContent; - const beforeText = text.substring(0, startNodeOffset); - const highlightText = text.substring(startNodeOffset, endNodeOffset); - const afterText = text.substring(endNodeOffset); - - // 새로운 노드들 생성 - const parent = startNode.parentNode; - const fragment = document.createDocumentFragment(); - - if (beforeText) { - fragment.appendChild(document.createTextNode(beforeText)); - } - - span.textContent = highlightText; - fragment.appendChild(span); - - if (afterText) { - fragment.appendChild(document.createTextNode(afterText)); - } - - // 원본 노드를 새로운 fragment로 교체 - parent.replaceChild(fragment, startNode); - console.log(`✅ 안전한 하이라이트 적용: "${highlightText}" (${className})`); - return span; - } else { - // 복잡한 경우 surroundContents 시도 - range.surroundContents(span); - console.log(`✅ surroundContents 성공: "${span.textContent}" (${className})`); - return span; - } - } catch (error) { - console.warn(`❌ 하이라이트 적용 실패 (${className}):`, error); - // 실패 시 범위만 표시하고 실제 DOM은 건드리지 않음 - return null; - } - } catch (error) { - console.warn(`❌ highlightTextRange 실패 (${className}):`, error); - return null; - } - }, - - try { - // 백링크 정보 가져오기 - console.log('🔍 백링크 API 호출 시작 - 문서 ID:', this.documentId); - const backlinks = await api.getDocumentBacklinks(this.documentId); - console.log('🔗 백링크 API 응답:', backlinks); - console.log('🔗 백링크 개수:', backlinks.length); - - if (backlinks.length === 0) { - console.log('⚠️ 백링크가 없습니다. 이 문서를 참조하는 링크가 없거나 권한이 없을 수 있습니다.'); - } - - const documentContent = document.getElementById('document-content'); - if (!documentContent) return; - - // 기존 백링크 하이라이트 제거 - const existingBacklinks = documentContent.querySelectorAll('.backlink-highlight'); - existingBacklinks.forEach(el => { - const parent = el.parentNode; - parent.replaceChild(document.createTextNode(el.textContent), el); - parent.normalize(); - }); - - // 백링크 하이라이트 적용 (간단한 방법) - console.log(`🔗 백링크 렌더링 시작 - 총 ${backlinks.length}개`); - console.log(`📄 현재 문서 내용 (처음 200자):`, documentContent.textContent.substring(0, 200)); - console.log(`📄 현재 문서 전체 길이:`, documentContent.textContent.length); - - // 백링크 렌더링 전략 결정 - console.log(`🎯 백링크 렌더링 전략:`); - backlinks.forEach((backlink, index) => { - console.log(`🔍 백링크 ${index + 1}:`); - console.log(` - 타입: ${backlink.link_type}`); - console.log(` - target_text: "${backlink.target_text || 'null'}"`); - console.log(` - selected_text: "${backlink.selected_text}"`); - - if (backlink.link_type === 'document') { - // 문서 레벨 백링크: 문서 제목이나 첫 번째 헤딩 찾기 - const titleElement = documentContent.querySelector('h1, h2, .title, title'); - if (titleElement) { - console.log(`✅ 문서 레벨 백링크 - 제목 요소 발견: "${titleElement.textContent.trim()}"`); - } else { - console.log(`⚠️ 문서 레벨 백링크 - 제목 요소 없음`); - } - } else if (backlink.link_type === 'text_fragment') { - // 텍스트 프래그먼트 백링크: selected_text가 현재 문서에 있는지 확인 - const searchText = backlink.selected_text; - const found = documentContent.textContent.includes(searchText); - console.log(`${found ? '✅' : '❌'} 텍스트 프래그먼트 백링크 - "${searchText}" 존재: ${found}`); - } - }); - - if (backlinks.length === 0) { - console.log(`⚠️ 백링크가 없어서 렌더링하지 않음`); - - // 테스트용: 강제로 백링크 표시 (디버깅용) - console.log(`🧪 테스트용 백링크 강제 생성...`); - const testText = "pressure vessel"; - if (documentContent.textContent.includes(testText)) { - console.log(`🎯 테스트 텍스트 발견: "${testText}"`); - - // 간단한 텍스트 하이라이트 - const walker = document.createTreeWalker( - documentContent, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let node; - while (node = walker.nextNode()) { - const text = node.textContent; - const index = text.indexOf(testText); - - if (index !== -1) { - console.log(`🎯 테스트 텍스트 노드에서 발견!`); - - try { - const beforeText = text.substring(0, index); - const matchText = text.substring(index, index + testText.length); - const afterText = text.substring(index + testText.length); - - const parent = node.parentNode; - const fragment = document.createDocumentFragment(); - - if (beforeText) { - fragment.appendChild(document.createTextNode(beforeText)); - } - - const span = document.createElement('span'); - span.className = 'backlink-highlight'; - span.textContent = matchText; - span.style.cssText = 'color: #EA580C !important; text-decoration: underline !important; cursor: pointer !important; background-color: rgba(234, 88, 12, 0.2) !important; border: 2px solid #EA580C !important; border-radius: 4px !important; padding: 4px 6px !important; font-weight: bold !important; box-shadow: 0 2px 4px rgba(234, 88, 12, 0.3) !important;'; - span.setAttribute('title', '테스트 백링크'); - - fragment.appendChild(span); - - if (afterText) { - fragment.appendChild(document.createTextNode(afterText)); - } - - parent.replaceChild(fragment, node); - - console.log(`✅ 테스트 백링크 렌더링 성공: "${matchText}"`); - break; - - } catch (error) { - console.error(`❌ 테스트 백링크 렌더링 실패:`, error); - } - } - } - } - - return; - } - - backlinks.forEach((backlink, index) => { - console.log(`🔗 백링크 ${index + 1}:`, backlink); - - let searchText = null; - let renderStrategy = null; - - if (backlink.link_type === 'document') { - // 문서 레벨 백링크: 제목 요소 찾기 - const titleElement = documentContent.querySelector('h1, h2, .title, title'); - if (titleElement) { - searchText = titleElement.textContent.trim(); - renderStrategy = 'title'; - console.log(`📋 문서 레벨 백링크 - 제목으로 렌더링: "${searchText}"`); - } - } else if (backlink.link_type === 'text_fragment') { - // 텍스트 프래그먼트 백링크: selected_text 사용 - searchText = backlink.selected_text; - renderStrategy = 'text'; - console.log(`📋 텍스트 프래그먼트 백링크 - 텍스트로 렌더링: "${searchText}"`); - } - if (!searchText) { - console.log(`❌ selected_text가 없음`); - return; - } - - console.log(`🔍 텍스트 검색: "${searchText}"`); - console.log(`📊 문서에 해당 텍스트 포함 여부:`, documentContent.textContent.includes(searchText)); - - // 기존 링크 요소들 확인 - const existingLinks = documentContent.querySelectorAll('.document-link'); - console.log(`🔗 기존 링크 요소 개수:`, existingLinks.length); - existingLinks.forEach((link, i) => { - console.log(` 링크 ${i + 1}: "${link.textContent}"`); - if (link.textContent.includes(searchText)) { - console.log(` ⚠️ 이 링크가 백링크 텍스트를 포함하고 있음!`); - } - }); - - // DOM에서 직접 텍스트 찾기 (링크 요소 포함) - let found = false; - - // 1. 일반 텍스트 노드에서 찾기 - const walker = document.createTreeWalker( - documentContent, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let node; - while (node = walker.nextNode()) { - const text = node.textContent; - const index = text.indexOf(searchText); - - if (index !== -1) { - console.log(`🎯 일반 텍스트에서 발견! 노드: "${text.substring(0, 50)}..."`); - - try { - // 텍스트를 3부분으로 나누기 - const beforeText = text.substring(0, index); - const matchText = text.substring(index, index + searchText.length); - const afterText = text.substring(index + searchText.length); - - // 새로운 요소들 생성 - const parent = node.parentNode; - const fragment = document.createDocumentFragment(); - - if (beforeText) { - fragment.appendChild(document.createTextNode(beforeText)); - } - - // 백링크 스팬 생성 - const span = document.createElement('span'); - span.className = 'backlink-highlight'; - span.textContent = matchText; - span.style.cssText = 'color: #EA580C !important; text-decoration: underline !important; cursor: pointer !important; background-color: rgba(234, 88, 12, 0.2) !important; border: 2px solid #EA580C !important; border-radius: 4px !important; padding: 4px 6px !important; font-weight: bold !important; box-shadow: 0 2px 4px rgba(234, 88, 12, 0.3) !important;'; - span.setAttribute('data-backlink-id', backlink.id); - span.setAttribute('data-source-document', backlink.source_document_id); - span.setAttribute('data-source-title', backlink.source_document_title); - span.setAttribute('title', `백링크: ${backlink.source_document_title}에서 참조`); - - // 백링크 클릭 이벤트 추가 - span.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - console.log(`🔗 백링크 클릭됨: ${backlink.source_document_title}`); - this.showBacklinkTooltip(e, [backlink]); - }); - - fragment.appendChild(span); - - if (afterText) { - fragment.appendChild(document.createTextNode(afterText)); - } - - // DOM에 교체 - parent.replaceChild(fragment, node); - - console.log(`✅ 백링크 렌더링 성공: "${matchText}"`); - - // 강제로 스타일 확인 - setTimeout(() => { - const renderedBacklink = documentContent.querySelector('.backlink-highlight'); - if (renderedBacklink) { - console.log(`🎯 백링크 DOM 확인됨:`, renderedBacklink); - console.log(`🎯 백링크 스타일:`, renderedBacklink.style.cssText); - console.log(`🎯 백링크 클래스:`, renderedBacklink.className); - } else { - console.error(`❌ 백링크 DOM에서 사라짐!`); - } - }, 100); - - found = true; - break; - - } catch (error) { - console.error(`❌ 백링크 렌더링 실패:`, error); - } - } - } - - // 2. 링크 요소 내부에서 찾기 (일반 텍스트에서 못 찾은 경우) - if (!found) { - console.log(`🔍 링크 요소 내부에서 검색 시도...`); - - existingLinks.forEach((linkEl, i) => { - if (linkEl.textContent.includes(searchText) && !found) { - console.log(`🎯 링크 ${i + 1} 내부에서 발견: "${linkEl.textContent}"`); - - // 이미 백링크인지 확인 - if (linkEl.classList.contains('backlink-highlight')) { - console.log(`⚠️ 이미 백링크로 렌더링됨: "${searchText}"`); - found = true; - return; - } - - // 링크 요소를 백링크로 변경 - linkEl.className = 'backlink-highlight document-link'; // 두 클래스 모두 유지 - linkEl.style.cssText = 'color: #EA580C; text-decoration: underline; cursor: pointer; background-color: rgba(234, 88, 12, 0.1); border-left: 3px solid #EA580C; padding-left: 2px;'; - linkEl.setAttribute('data-backlink-id', backlink.id); - linkEl.setAttribute('data-source-document', backlink.source_document_id); - linkEl.setAttribute('data-source-title', backlink.source_document_title); - linkEl.setAttribute('title', `백링크: ${backlink.source_document_title}에서 참조`); - - console.log(`✅ 링크를 백링크로 변경 성공: "${searchText}"`); - found = true; - } - }); - } - - if (!found) { - console.log(`❌ 텍스트를 찾을 수 없음: "${searchText}"`); - } - }); - - // 백링크 클릭 이벤트 추가 - const backlinkElements = documentContent.querySelectorAll('.backlink-highlight'); - backlinkElements.forEach(backlinkEl => { - backlinkEl.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - - // 클릭된 위치의 모든 백링크 찾기 - const clickedText = backlinkEl.textContent; - const overlappingBacklinks = this.getOverlappingBacklinks(backlinkEl); - - // 백링크 툴팁 표시 - this.showBacklinkTooltip(overlappingBacklinks, backlinkEl, clickedText); - }); - }); - - } catch (error) { - console.warn('백링크 하이라이트 렌더링 실패:', error); - } - }, - - // 문서에서 특정 텍스트 찾기 - findTextInDocument(container, searchText) { - const textNodes = []; - const walker = document.createTreeWalker( - container, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let node; - while (node = walker.nextNode()) { - if (node.textContent.includes(searchText)) { - textNodes.push(node); - } - } - - return textNodes; - }, - - // 텍스트 노드를 스타일로 감싸기 (간단 버전) - wrapTextNode(textNode, className, attributes = {}) { - try { - const parent = textNode.parentNode; - const span = document.createElement('span'); - - // span 설정 - span.className = className; - span.textContent = textNode.textContent; - - // 속성 설정 - Object.keys(attributes).forEach(key => { - if (key === 'style') { - span.style.cssText = attributes[key]; - } else { - span.setAttribute(key, attributes[key]); - } - }); - - // DOM에 교체 - parent.replaceChild(span, textNode); - - return span; - } catch (error) { - console.error('텍스트 노드 감싸기 실패:', error); - return null; - } - }, - - // 겹치는 백링크들 찾기 - getOverlappingBacklinks(clickedElement) { - const clickedBacklinkId = clickedElement.getAttribute('data-backlink-id'); - const clickedText = clickedElement.textContent; - - // 동일한 텍스트 범위에 있는 모든 백링크 찾기 - const overlappingBacklinks = this.backlinks.filter(backlink => { - // 클릭된 백링크와 텍스트가 겹치는지 확인 - const backlinkElement = document.querySelector(`[data-backlink-id="${backlink.id}"]`); - if (!backlinkElement) return false; - - // 텍스트가 겹치는지 확인 (간단한 방법: 텍스트 내용 비교) - return backlinkElement.textContent === clickedText; - }); - - return overlappingBacklinks; - }, - - // 백링크 툴팁 표시 - showBacklinkTooltip(backlinks, element, selectedText) { - // 기존 툴팁 제거 - this.hideTooltip(); - - const tooltip = document.createElement('div'); - tooltip.id = 'backlink-tooltip'; - tooltip.className = 'absolute bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-50 max-w-lg'; - tooltip.style.minWidth = '350px'; - - let tooltipHTML = ` -
-
참조된 텍스트
-
- "${selectedText}" -
-
- `; - - if (backlinks.length > 1) { - tooltipHTML += ` -
-
- 이 텍스트를 참조하는 문서 (${backlinks.length}개) -
-
- `; - } else { - tooltipHTML += ` -
-
- 이 텍스트를 참조하는 문서 -
-
- `; - } - - tooltipHTML += '
'; - - backlinks.forEach(backlink => { - tooltipHTML += ` -
-
-
-
${backlink.source_document_title}
- ${backlink.description ? `
${backlink.description}
` : ''} -
- 원본 텍스트: "${backlink.selected_text}" -
-
-
- - - -
-
-
- `; - }); - - 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 - 175) + 'px'; - - document.body.appendChild(tooltip); - - // 외부 클릭 시 툴팁 숨기기 - setTimeout(() => { - document.addEventListener('click', this.handleTooltipOutsideClick.bind(this)); - }, 100); - }, - - // 백링크 문서로 이동 - navigateToBacklinkDocument(sourceDocumentId, backlinkInfo) { - console.log('🔗 백링크로 이동:', sourceDocumentId, backlinkInfo); - - // 툴팁 숨기기 - this.hideTooltip(); - - // 백링크 문서로 이동 - window.location.href = `/viewer.html?id=${sourceDocumentId}`; - }, - - // 링크된 문서로 이동 (특정 텍스트 위치 포함) - navigateToLinkedDocument(targetDocumentId, linkInfo) { - let targetUrl = `/viewer.html?id=${targetDocumentId}`; - - // 특정 텍스트 위치가 있는 경우 URL에 추가 - if (linkInfo && linkInfo.link_type === 'text_fragment' && linkInfo.target_text) { - const params = new URLSearchParams({ - highlight_text: linkInfo.target_text, - start_offset: linkInfo.target_start_offset, - end_offset: linkInfo.target_end_offset - }); - targetUrl += `&${params.toString()}`; - } - - console.log('🔗 링크된 문서로 이동:', targetUrl); - window.location.href = targetUrl; - }, - - // 기존 navigateToDocument 함수 (백워드 호환성) - navigateToDocument(documentId) { - window.location.href = `/viewer.html?id=${documentId}`; - }, - - - - // 특정 텍스트를 하이라이트하고 스크롤 - highlightAndScrollToText(targetText, startOffset, endOffset) { - console.log('🎯 highlightAndScrollToText 호출됨:', { targetText, startOffset, endOffset }); - - const documentContent = document.getElementById('document-content'); - if (!documentContent) { - console.error('❌ document-content 요소를 찾을 수 없습니다'); - return; - } - - // 백링크는 LinkManager가 관리하므로 별도 처리 불필요 - console.log('🔗 LinkManager가 백링크를 관리 중'); - - console.log('📄 문서 내용 길이:', documentContent.textContent.length); - - try { - // 임시 하이라이트 적용 - console.log('🎨 하이라이트 적용 시작...'); - const highlightElement = this.highlightTextRange( - documentContent, - startOffset, - endOffset, - 'linked-text-highlight', - { - 'style': 'background-color: #FEF3C7 !important; border: 2px solid #F59E0B; border-radius: 4px; padding: 2px;' - } - ); - - console.log('🔍 하이라이트 요소 결과:', highlightElement); - - if (highlightElement) { - console.log('📐 하이라이트 요소 위치:', highlightElement.getBoundingClientRect()); - - // 해당 요소로 스크롤 - highlightElement.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); - - console.log('✅ 링크된 텍스트로 스크롤 완료'); - } else { - console.warn('⚠️ 링크된 텍스트를 찾을 수 없습니다'); - console.log('🔍 전체 텍스트 미리보기:', documentContent.textContent.substring(startOffset - 50, endOffset + 50)); - } - - // 5초 후 하이라이트 제거 및 백링크 복원 (하이라이트 성공 여부와 관계없이) - const self = this; - setTimeout(() => { - const tempHighlight = document.querySelector('.linked-text-highlight'); - if (tempHighlight) { - const parent = tempHighlight.parentNode; - parent.replaceChild(document.createTextNode(tempHighlight.textContent), tempHighlight); - parent.normalize(); - console.log('🗑️ 임시 하이라이트 제거됨'); - } - - // 백링크는 LinkManager가 관리하므로 별도 재렌더링 불필요 - console.log('✅ 임시 하이라이트 제거 완료 - 백링크는 LinkManager가 유지 관리'); - }, 5000); - - } catch (error) { - console.error('❌ 텍스트 하이라이트 실패:', error); - } - }, - - // 텍스트 오프셋 계산 (하이라이트와 동일한 로직) - getTextOffset(container, node, offset) { - let textOffset = 0; - const walker = document.createTreeWalker( - container, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let currentNode; - while (currentNode = walker.nextNode()) { - if (currentNode === node) { - return textOffset + offset; - } - textOffset += currentNode.textContent.length; - } - return textOffset; - }, - - // 텍스트 범위 하이라이트 (하이라이트와 동일한 로직, 클래스명만 다름) - highlightTextRange(container, startOffset, endOffset, className, attributes = {}) { - console.log(`🎯 highlightTextRange 호출: ${startOffset}-${endOffset}, 클래스: ${className}`); - const walker = document.createTreeWalker( - container, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let currentOffset = 0; - let startNode = null; - let startNodeOffset = 0; - let endNode = null; - let endNodeOffset = 0; - - // 시작과 끝 노드 찾기 - let node; - while (node = walker.nextNode()) { - const nodeLength = node.textContent.length; - - if (!startNode && currentOffset + nodeLength > startOffset) { - startNode = node; - startNodeOffset = startOffset - currentOffset; - } - - if (currentOffset + nodeLength >= endOffset) { - endNode = node; - endNodeOffset = endOffset - currentOffset; - break; - } - - currentOffset += nodeLength; - } - - if (!startNode || !endNode) return; - - try { - // DOM 변경 전에 Range 유효성 검사 - if (!startNode.parentNode || !endNode.parentNode || - startNodeOffset < 0 || endNodeOffset < 0) { - console.warn(`❌ 유효하지 않은 노드 또는 오프셋 (${className})`); - return null; - } - - const range = document.createRange(); - - // Range 설정 시 예외 처리 - try { - range.setStart(startNode, startNodeOffset); - range.setEnd(endNode, endNodeOffset); - } catch (rangeError) { - console.warn(`❌ Range 설정 실패 (${className}):`, rangeError); - return null; - } - - // 빈 범위 체크 - if (range.collapsed) { - console.warn(`❌ 빈 범위 (${className})`); - range.detach(); - return null; - } - - const span = document.createElement('span'); - span.className = className; - - // 속성 추가 - Object.entries(attributes).forEach(([key, value]) => { - span.setAttribute(key, value); - }); - - // 더 안전한 하이라이트 적용 방식 - try { - // 범위가 단일 텍스트 노드인지 확인 - if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) { - // 단일 텍스트 노드인 경우 직접 분할 - const text = startNode.textContent; - const beforeText = text.substring(0, startNodeOffset); - const highlightText = text.substring(startNodeOffset, endNodeOffset); - const afterText = text.substring(endNodeOffset); - - // 새로운 노드들 생성 - const parent = startNode.parentNode; - const fragment = document.createDocumentFragment(); - - if (beforeText) { - fragment.appendChild(document.createTextNode(beforeText)); - } - - span.textContent = highlightText; - fragment.appendChild(span); - - if (afterText) { - fragment.appendChild(document.createTextNode(afterText)); - } - - // 원본 노드를 새로운 fragment로 교체 - parent.replaceChild(fragment, startNode); - console.log(`✅ 안전한 하이라이트 적용: "${highlightText}" (${className})`); - return span; - } else { - // 복잡한 경우 surroundContents 시도 - range.surroundContents(span); - console.log(`✅ surroundContents 성공: "${span.textContent}" (${className})`); - return span; - } - } catch (error) { - console.warn(`❌ 하이라이트 적용 실패 (${className}):`, error); - // 실패 시 범위만 표시하고 실제 DOM은 건드리지 않음 - return null; - } - } catch (error) { - console.warn(`❌ highlightTextRange 실패 (${className}):`, error); - return null; - } - }, - - // 백링크 배너 업데이트 (LinkManager 데이터 사용) - updateBacklinkBanner() { - const backlinkCount = this.backlinks ? this.backlinks.length : 0; - const backlinkBanner = document.getElementById('backlink-banner'); - if (backlinkBanner) { - const countElement = backlinkBanner.querySelector('.backlink-count'); - if (countElement) { - countElement.textContent = backlinkCount; - } - } - }, - - // 특정 텍스트를 하이라이트하고 스크롤 - highlightAndScrollToText(targetText, startOffset, endOffset) { - console.log('🎯 highlightAndScrollToText 호출됨:', { targetText, startOffset, endOffset }); - - const documentContent = document.getElementById('document-content'); - if (!documentContent) { - console.error('❌ document-content 요소를 찾을 수 없습니다'); - return; - } - - // 백링크는 LinkManager가 관리하므로 별도 처리 불필요 - console.log('🔗 LinkManager가 백링크를 관리 중'); - - console.log('📄 문서 내용 길이:', documentContent.textContent.length); - - try { - // 임시 하이라이트 적용 - console.log('🎨 하이라이트 적용 시작...'); - - const highlightElement = this.highlightTextRange( - documentContent, - startOffset, - endOffset, - 'linked-text-highlight', - { - 'style': 'background-color: #FEF3C7 !important; border: 2px solid #F59E0B; border-radius: 4px; padding: 2px;' - } - ); - - console.log('🔍 하이라이트 요소 결과:', highlightElement); - - if (highlightElement) { - // 요소 위치 가져오기 - const rect = highlightElement.getBoundingClientRect(); - console.log('📀 하이라이트 요소 위치:', rect); - - // 스크롤 - const scrollTop = window.pageYOffset + rect.top - window.innerHeight / 2; - window.scrollTo({ top: scrollTop, behavior: 'smooth' }); - console.log('✅ 링크된 텍스트로 스크롤 완료'); - } else { - console.warn('⚠️ 링크된 텍스트를 찾을 수 없습니다'); - console.log('🔍 전체 텍스트 미리보기:', documentContent.textContent.substring(startOffset - 50, endOffset + 50)); - } - - // 5초 후 하이라이트 제거 및 백링크 복원 (하이라이트 성공 여부와 관계없이) - const self = this; - setTimeout(() => { - const tempHighlight = document.querySelector('.linked-text-highlight'); - if (tempHighlight) { - const parent = tempHighlight.parentNode; - parent.replaceChild(document.createTextNode(tempHighlight.textContent), tempHighlight); - parent.normalize(); - console.log('🗑️ 임시 하이라이트 제거됨'); - } - - // 백링크는 LinkManager가 관리하므로 별도 재렌더링 불필요 - console.log('✅ 임시 하이라이트 제거 완료 - 백링크는 LinkManager가 유지 관리'); - }, 5000); - - } catch (error) { - console.error('❌ 텍스트 하이라이트 실패:', error); - } - }, - - try { - console.log('🔗 백링크 로드 중...'); - console.log('📋 현재 문서 ID:', this.documentId); - console.log('📋 현재 문서 제목:', this.documentTitle); - - this.backlinks = await window.api.getDocumentBacklinks(this.documentId); - console.log(`✅ 백링크 ${this.backlinks.length}개 로드됨:`, this.backlinks); - - // 각 백링크의 상세 정보 출력 - this.backlinks.forEach((backlink, index) => { - console.log(`🔗 백링크 ${index + 1}:`); - console.log(` - 소스 문서: ${backlink.source_document_title}`); - console.log(` - 타겟 문서: ${backlink.target_document_title} (현재 문서와 일치해야 함)`); - console.log(` - 선택된 텍스트: "${backlink.selected_text}"`); - console.log(` - 링크 타입: ${backlink.link_type}`); - - // 현재 문서와 일치하는지 확인 - if (backlink.target_document_id !== this.documentId) { - console.warn(`⚠️ 백링크 타겟 문서 ID 불일치!`); - console.warn(` - 백링크 타겟: ${backlink.target_document_id}`); - console.warn(` - 현재 문서: ${this.documentId}`); - } - }); - - // Alpine.js 상태 업데이트 강제 - if (window.Alpine && window.Alpine.store) { - console.log('🔄 Alpine.js 상태 업데이트 시도...'); - } - - } catch (error) { - console.error('백링크 로드 실패:', error); - this.backlinks = []; - } - }, - - navigateToBacklink(backlink) { - console.log('🔗 배너에서 백링크로 이동:', backlink); - - // 백링크의 출발 문서로 이동 - const url = `/viewer.html?id=${backlink.source_document_id}`; - console.log('🔗 이동할 URL:', url); - - window.location.href = url; - }, - - // 링크 배너에서 링크로 이동 - navigateToLink(link) { - console.log('🔗 배너에서 링크로 이동:', link); - - // 링크의 대상 문서로 이동 - const url = `/viewer.html?id=${link.target_document_id}`; - console.log('🔗 이동할 URL:', url); - - // 텍스트 조각 링크인 경우 해당 위치로 스크롤 - if (link.link_type === 'text_fragment' && link.target_text) { - const urlWithFragment = `${url}&highlight=${link.target_start_offset}-${link.target_end_offset}&text=${encodeURIComponent(link.target_text)}`; - window.location.href = urlWithFragment; - } else { - window.location.href = url; - } - }, - - // 고급 링크 기능 메서드들 - onTargetDocumentChange() { - // 대상 문서가 변경되면 target_text 초기화 - this.linkForm.target_text = ''; - this.linkForm.target_start_offset = 0; - this.linkForm.target_end_offset = 0; - }, - - openTargetDocumentSelector() { - console.log('🎯 openTargetDocumentSelector 함수 호출됨!'); - console.log('📋 현재 linkForm.target_document_id:', this.linkForm.target_document_id); - - 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 }); - }, - - // 텍스트 선택 모드 초기화 - async initTextSelectorMode() { - console.log('🎯 텍스트 선택 모드로 초기화 중...'); - - // Alpine.js 완전 차단 - window.Alpine = { - start: () => console.log('Alpine.js 초기화 차단됨'), - data: () => ({}), - directive: () => {}, - magic: () => {}, - store: () => ({}), - version: '3.0.0' - }; - - // 기존 Alpine 인스턴스 제거 - if (window.Alpine && window.Alpine.stop) { - window.Alpine.stop(); - } - - // 인증 확인 - if (!api.token) { - window.location.href = '/'; - return; - } - - try { - // 문서만 로드 (다른 데이터는 불필요) - await this.loadDocument(); - - // UI 설정 - console.log('🔧 텍스트 선택 모드 UI 설정 시작'); - this.setupTextSelectorUI(); - console.log('✅ 텍스트 선택 모드 UI 설정 완료'); - - } catch (error) { - console.error('텍스트 선택 모드 초기화 실패:', error); - this.error = '문서를 불러올 수 없습니다: ' + error.message; - } finally { - this.loading = false; - } - }, - - // 텍스트 선택 모드 UI 설정 - setupTextSelectorUI() { - console.log('🔧 setupTextSelectorUI 함수 실행됨'); - - // 이미 설정되었는지 확인 - if (this.textSelectorUISetup) { - console.log('⚠️ 텍스트 선택 모드 UI가 이미 설정됨 - 중복 실행 방지'); - return; - } - - // 헤더를 텍스트 선택 모드용으로 변경 - const header = document.querySelector('header'); - console.log('📋 헤더 요소 찾기:', header); - - if (header) { - console.log('🎨 헤더 HTML 교체 중...'); - - // 기존 Alpine 속성 제거 - header.removeAttribute('x-data'); - header.removeAttribute('x-init'); - - header.innerHTML = ` -
-
-
- -
-

텍스트 선택 모드

-

연결하고 싶은 텍스트를 선택하세요

-
-
-
- - -
-
-
- `; - - // 헤더가 다시 변경되지 않도록 보호 - header.setAttribute('data-text-selector-mode', 'true'); - console.log('🔒 헤더 보호 설정 완료'); - - // 실제 헤더 내용 확인 - console.log('📄 헤더 HTML 확인:', header.innerHTML.substring(0, 200) + '...'); - - // 언어전환 버튼 확인 - const langBtn = header.querySelector('#language-toggle-selector'); - console.log('🌐 언어전환 버튼 찾기:', langBtn); - - // 취소 버튼 확인 - const closeBtn = header.querySelector('button[onclick*="window.close"]'); - console.log('❌ 취소 버튼 찾기:', closeBtn); - - // 헤더 변경 감지 - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'childList' || mutation.type === 'attributes') { - console.log('⚠️ 헤더가 변경되었습니다!', mutation); - console.log('🔍 변경 후 헤더 내용:', header.innerHTML.substring(0, 100) + '...'); - } - }); - }); - - observer.observe(header, { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true - }); - } - - // 사이드 패널 숨기기 - const aside = document.querySelector('aside'); - if (aside) { - aside.style.display = 'none'; - } - - // 메인 컨텐츠 영역 조정 - const main = document.querySelector('main'); - if (main) { - main.style.marginRight = '0'; - main.classList.add('text-selector-mode'); - } - - // 문서 콘텐츠에 텍스트 선택 이벤트 추가 - const documentContent = document.getElementById('document-content'); - if (documentContent) { - documentContent.addEventListener('mouseup', this.handleTextSelectionForLinking.bind(this)); - - // 선택 가능한 영역임을 시각적으로 표시 - documentContent.style.cursor = 'crosshair'; - documentContent.style.userSelect = 'text'; - - // 안내 메시지 추가 - const guideDiv = document.createElement('div'); - guideDiv.className = 'bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6'; - guideDiv.innerHTML = ` -
- -
-

텍스트 선택 방법

-

- 마우스로 연결하고 싶은 텍스트를 드래그하여 선택하세요. - 선택이 완료되면 자동으로 부모 창으로 전달됩니다. -

-
-
- `; - documentContent.parentNode.insertBefore(guideDiv, documentContent); - } - - // Alpine.js 컴포넌트 비활성화 (텍스트 선택 모드에서는 불필요) - const alpineElements = document.querySelectorAll('[x-data]'); - alpineElements.forEach(el => { - el.removeAttribute('x-data'); - }); - - // 설정 완료 플래그 - this.textSelectorUISetup = true; - console.log('✅ 텍스트 선택 모드 UI 설정 완료'); - }, - - // 텍스트 선택 모드에서의 텍스트 선택 처리 - handleTextSelectionForLinking() { - const selection = window.getSelection(); - if (!selection.rangeCount || selection.isCollapsed) return; - - const range = selection.getRangeAt(0); - const selectedText = selection.toString().trim(); - - if (selectedText.length < 3) { - alert('최소 3글자 이상 선택해주세요.'); - return; - } - - if (selectedText.length > 500) { - alert('선택된 텍스트가 너무 깁니다. 500자 이하로 선택해주세요.'); - return; - } - - // 텍스트 오프셋 계산 - const documentContent = document.getElementById('document-content'); - const { startOffset, endOffset } = this.getTextOffset(documentContent, range); - - console.log('🎯 텍스트 선택됨:', { - selectedText, - startOffset, - endOffset - }); - - // 선택 확인 UI 표시 - this.showTextSelectionConfirm(selectedText, startOffset, endOffset); - }, - - // 텍스트 선택 확인 UI - showTextSelectionConfirm(selectedText, startOffset, endOffset) { - // 기존 확인 UI 제거 - const existingConfirm = document.querySelector('.text-selection-confirm'); - if (existingConfirm) { - existingConfirm.remove(); - } - - // 확인 UI 생성 - const confirmDiv = document.createElement('div'); - confirmDiv.className = 'text-selection-confirm fixed bottom-6 left-1/2 transform -translate-x-1/2 bg-white rounded-lg shadow-2xl border p-6 max-w-md z-50'; - - // 텍스트 미리보기 (안전하게 처리) - const previewText = selectedText.length > 100 ? selectedText.substring(0, 100) + '...' : selectedText; - - confirmDiv.innerHTML = ` -
-
- -
-

텍스트가 선택되었습니다

-
-

-
-
- - -
-
- `; - - // 텍스트 안전하게 설정 - const previewElement = confirmDiv.querySelector('#selected-text-preview'); - previewElement.textContent = `"${previewText}"`; - - // 이벤트 리스너 추가 - const reselectBtn = confirmDiv.querySelector('#reselect-btn'); - const confirmBtn = confirmDiv.querySelector('#confirm-selection-btn'); - - reselectBtn.addEventListener('click', () => { - confirmDiv.remove(); - }); - - confirmBtn.addEventListener('click', () => { - this.confirmTextSelection(selectedText, startOffset, endOffset); - }); - - document.body.appendChild(confirmDiv); - - // 10초 후 자동 제거 (사용자가 선택하지 않은 경우) - setTimeout(() => { - if (document.contains(confirmDiv)) { - confirmDiv.remove(); - } - }, 10000); - }, - - // 텍스트 선택 확정 - confirmTextSelection(selectedText, startOffset, endOffset) { - // 부모 창에 선택된 텍스트 정보 전달 - if (window.opener) { - window.opener.postMessage({ - type: 'TEXT_SELECTED', - selectedText: selectedText, - startOffset: startOffset, - endOffset: endOffset - }, '*'); - - console.log('✅ 부모 창에 텍스트 선택 정보 전달됨'); - - // 성공 메시지 표시 후 창 닫기 - const confirmDiv = document.querySelector('.text-selection-confirm'); - if (confirmDiv) { - confirmDiv.innerHTML = ` -
-
- -
-

선택 완료!

-

창이 자동으로 닫힙니다...

-
- `; - - setTimeout(() => { - window.close(); - }, 1500); - } - } else { - alert('부모 창을 찾을 수 없습니다.'); - } - }, - - // 기능 메뉴 토글 - toggleFeatureMenu(feature) { - if (this.activeFeatureMenu === feature) { - this.activeFeatureMenu = null; - } else { - this.activeFeatureMenu = feature; - - // 해당 기능의 모달 표시 - switch(feature) { - case 'link': - this.showLinksModal = true; - break; - case 'memo': - this.showNotesModal = true; - break; - case 'bookmark': - this.showBookmarksModal = true; - break; - case 'backlink': - this.showBacklinksModal = true; - break; - } - } - }, - - // 링크 모드 활성화 - activateLinkMode() { - if (this.contentType === 'note') { - alert('📝 노트에서는 링크 기능이 향후 지원 예정입니다.'); - return; - } - - console.log('🔗 링크 모드 활성화 - activateLinkMode 함수 실행됨'); - - // 이미 선택된 텍스트가 있는지 확인 - const selection = window.getSelection(); - console.log('📝 현재 선택 상태:', { - rangeCount: selection.rangeCount, - isCollapsed: selection.isCollapsed, - selectedText: selection.toString() - }); - - if (selection.rangeCount > 0 && !selection.isCollapsed) { - const selectedText = selection.toString().trim(); - if (selectedText.length > 0) { - console.log('✅ 선택된 텍스트 발견:', selectedText); - this.selectedText = selectedText; - this.selectedRange = selection.getRangeAt(0); - console.log('🔗 LinkManager로 링크 생성 위임'); - this.linkManager.createLinkFromSelection(this.documentId, selectedText, selection.getRangeAt(0)); - return; - } - } - - // 선택된 텍스트가 없으면 선택 모드 활성화 - console.log('📝 텍스트 선택 모드 활성화'); - this.activeMode = 'link'; - this.showSelectionMessage('텍스트를 선택하세요.'); - - // 기존 리스너 제거 후 새로 추가 - this.removeTextSelectionListener(); - this.textSelectionHandler = this.handleTextSelection.bind(this); - document.addEventListener('mouseup', this.textSelectionHandler); - }, - - // 메모 모드 활성화 - activateNoteMode() { - console.log('📝 메모 모드 활성화'); - - // 이미 선택된 텍스트가 있는지 확인 - const selection = window.getSelection(); - if (selection.rangeCount > 0 && !selection.isCollapsed) { - const selectedText = selection.toString().trim(); - if (selectedText.length > 0) { - console.log('✅ 선택된 텍스트 발견:', selectedText); - this.selectedText = selectedText; - this.selectedRange = selection.getRangeAt(0); - this.createNoteFromSelection(); - return; - } - } - - // 선택된 텍스트가 없으면 선택 모드 활성화 - console.log('📝 텍스트 선택 모드 활성화'); - this.activeMode = 'memo'; - this.showSelectionMessage('텍스트를 선택하세요.'); - - // 기존 리스너 제거 후 새로 추가 - this.removeTextSelectionListener(); - this.textSelectionHandler = this.handleTextSelection.bind(this); - document.addEventListener('mouseup', this.textSelectionHandler); - }, - - // 책갈피 모드 활성화 - activateBookmarkMode() { - console.log('🔖 책갈피 모드 활성화'); - - // 이미 선택된 텍스트가 있는지 확인 - const selection = window.getSelection(); - if (selection.rangeCount > 0 && !selection.isCollapsed) { - const selectedText = selection.toString().trim(); - if (selectedText.length > 0) { - console.log('✅ 선택된 텍스트 발견:', selectedText); - this.selectedText = selectedText; - this.selectedRange = selection.getRangeAt(0); - this.createBookmarkFromSelection(); - return; - } - } - - // 선택된 텍스트가 없으면 선택 모드 활성화 - console.log('📝 텍스트 선택 모드 활성화'); - this.activeMode = 'bookmark'; - this.showSelectionMessage('텍스트를 선택하세요.'); - - // 기존 리스너 제거 후 새로 추가 - this.removeTextSelectionListener(); - this.textSelectionHandler = this.handleTextSelection.bind(this); - document.addEventListener('mouseup', this.textSelectionHandler); - }, - - // 텍스트 선택 리스너 제거 - removeTextSelectionListener() { - if (this.textSelectionHandler) { - document.removeEventListener('mouseup', this.textSelectionHandler); - this.textSelectionHandler = null; - } - }, - - // 텍스트 선택 처리 - handleTextSelection(event) { - console.log('🎯 텍스트 선택 이벤트 발생'); - - const selection = window.getSelection(); - if (selection.rangeCount > 0 && !selection.isCollapsed) { - const range = selection.getRangeAt(0); - const selectedText = selection.toString().trim(); - - console.log('📝 선택된 텍스트:', selectedText); - - if (selectedText.length > 0) { - this.selectedText = selectedText; - this.selectedRange = range; - - // 선택 모드에 따라 다른 동작 - console.log('🎯 현재 모드:', this.activeMode); - - if (this.activeMode === 'link') { - console.log('🔗 링크 생성 실행'); - this.createDocumentLink(); - } else if (this.activeMode === 'memo') { - console.log('📝 메모 생성 실행'); - this.createNoteFromSelection(); - } else if (this.activeMode === 'bookmark') { - console.log('🔖 책갈피 생성 실행'); - this.createBookmarkFromSelection(); - } - - // 모드 해제 - this.activeMode = null; - this.hideSelectionMessage(); - this.removeTextSelectionListener(); - } - } - }, - - // 선택 메시지 표시 - showSelectionMessage(message) { - // 기존 메시지 제거 - const existingMessage = document.querySelector('.selection-message'); - if (existingMessage) { - existingMessage.remove(); - } - - // 새 메시지 생성 - const messageDiv = document.createElement('div'); - messageDiv.className = 'selection-message fixed top-20 left-1/2 transform -translate-x-1/2 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg z-50'; - messageDiv.textContent = message; - document.body.appendChild(messageDiv); - }, - - // 선택 메시지 숨기기 - hideSelectionMessage() { - const existingMessage = document.querySelector('.selection-message'); - if (existingMessage) { - existingMessage.remove(); - } - }, - - // 링크 생성 UI 표시 - showLinkCreationUI() { - this.createDocumentLink(); - }, - - // 선택된 텍스트로 메모 생성 (HighlightManager로 위임) - async createNoteFromSelection() { - if (!this.selectedText || !this.selectedRange) return; - - try { - // HighlightManager의 상태 설정 - this.highlightManager.selectedText = this.selectedText; - this.highlightManager.selectedRange = this.selectedRange; - this.highlightManager.selectedHighlightColor = '#FFFF00'; - - // HighlightManager의 createNoteFromSelection 호출 - await this.highlightManager.createNoteFromSelection(this.documentId, this.contentType); - - // 상태 동기화 - this.highlights = this.highlightManager.highlights; - this.notes = this.highlightManager.notes; - - return; - - } catch (error) { - console.error('메모 생성 실패:', error); - alert('메모 생성에 실패했습니다.'); - } - }, - - // 선택된 텍스트로 책갈피 생성 - async createBookmarkFromSelection() { - if (!this.selectedText || !this.selectedRange) return; - - try { - // 하이라이트 생성 (책갈피는 주황색) - const highlightData = await this.createHighlight(this.selectedText, this.selectedRange, '#FFA500'); - - // 책갈피 생성 - const bookmarkData = { - highlight_id: highlightData.id, - title: this.selectedText.substring(0, 50) + (this.selectedText.length > 50 ? '...' : '') - }; - - const bookmark = await api.createBookmark(this.documentId, bookmarkData); - - // 데이터 새로고침 - await this.loadBookmarks(); - - alert('책갈피가 생성되었습니다.'); - - } catch (error) { - console.error('책갈피 생성 실패:', error); - alert('책갈피 생성에 실패했습니다.'); - } - }, - - // 대상 선택 초기화 - resetTargetSelection() { - this.linkForm.target_book_id = ''; - this.linkForm.target_document_id = ''; - this.linkForm.target_text = ''; - this.linkForm.target_start_offset = null; - this.linkForm.target_end_offset = null; - this.filteredDocuments = []; - - // 같은 서적인 경우 현재 서적의 문서들 로드 - if (this.linkForm.book_scope === 'same') { - this.loadSameBookDocuments(); - } - }, - - // 같은 서적의 문서들 로드 - async loadSameBookDocuments() { - try { - if (this.navigation?.book_info?.id) { - // 현재 서적의 문서들만 가져오기 - const allDocuments = await api.getLinkableDocuments(this.documentId); - this.filteredDocuments = allDocuments.filter(doc => - doc.book_id === this.navigation.book_info.id && doc.id !== this.documentId - ); - console.log('📚 같은 서적 문서들:', this.filteredDocuments); - } else { - // 서적 정보가 없으면 모든 문서 - this.filteredDocuments = await api.getLinkableDocuments(this.documentId); - } - } catch (error) { - console.error('같은 서적 문서 로드 실패:', error); - this.filteredDocuments = []; - } - }, - - // 서적별 문서 로드 - async loadDocumentsFromBook() { - try { - if (this.linkForm.target_book_id) { - // 선택된 서적의 문서들만 가져오기 - const allDocuments = await 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 = []; - } - }, - - // 사용 가능한 서적 목록 로드 - async loadAvailableBooks() { - try { - console.log('📚 서적 목록 로딩 시작...'); - - // 문서 목록에서 서적 정보 추출 - const allDocuments = await api.getLinkableDocuments(this.documentId); - console.log('📄 모든 문서들:', allDocuments); - - // 서적별로 그룹화 - const bookMap = new Map(); - allDocuments.forEach(doc => { - if (doc.book_id && doc.book_title) { - bookMap.set(doc.book_id, { - id: doc.book_id, - title: doc.book_title - }); - } - }); - - // 현재 서적 제외 - const currentBookId = this.navigation?.book_info?.id; - if (currentBookId) { - bookMap.delete(currentBookId); - } - - this.availableBooks = Array.from(bookMap.values()); - console.log('📚 사용 가능한 서적들:', this.availableBooks); - console.log('🔍 현재 서적 ID:', currentBookId); - } catch (error) { - console.error('서적 목록 로드 실패:', error); - this.availableBooks = []; - } - }, - - // 선택된 서적 제목 가져오기 - getSelectedBookTitle() { - const selectedBook = this.availableBooks.find(book => book.id === this.linkForm.target_book_id); - return selectedBook ? selectedBook.title : ''; - } -}); diff --git a/frontend/todos.html b/frontend/todos.html index fb1697b..95bc096 100644 --- a/frontend/todos.html +++ b/frontend/todos.html @@ -226,12 +226,28 @@
-
-

- +
+

+ 할일관리

-

간편한 할일 관리

+

효율적인 일정 관리와 생산성 향상

+ + +
+
+ + 검토필요 +
+
+ + 진행중 +
+
+ + 완료 +
+