/** * 문서 뷰어 Alpine.js 컴포넌트 */ window.documentViewer = () => ({ // 상태 loading: true, error: null, document: null, documentId: null, // 하이라이트 및 메모 highlights: [], notes: [], selectedHighlightColor: '#FFFF00', selectedText: '', selectedRange: null, // 책갈피 bookmarks: [], // UI 상태 showNotesPanel: false, showBookmarksPanel: false, activePanel: 'notes', // 검색 searchQuery: '', noteSearchQuery: '', filteredNotes: [], // 언어 전환 isKorean: false, // 모달 showNoteModal: false, showBookmarkModal: false, editingNote: null, editingBookmark: null, noteLoading: false, bookmarkLoading: false, // 폼 데이터 noteForm: { content: '', tags: '' }, bookmarkForm: { title: '', description: '' }, // 초기화 async init() { // 전역 인스턴스 설정 (말풍선에서 함수 호출용) window.documentViewerInstance = this; // URL에서 문서 ID 추출 const urlParams = new URLSearchParams(window.location.search); this.documentId = urlParams.get('id'); if (!this.documentId) { this.error = '문서 ID가 없습니다'; this.loading = false; return; } // 인증 확인 if (!api.token) { window.location.href = '/'; return; } try { await this.loadDocument(); await this.loadDocumentData(); } catch (error) { console.error('Failed to load document:', error); this.error = error.message; } finally { this.loading = false; } // 초기 필터링 this.filterNotes(); }, // 문서 로드 (실제 API 연동) async loadDocument() { try { // 백엔드에서 문서 정보 가져오기 this.document = await api.getDocument(this.documentId); // HTML 파일 경로 구성 (백엔드 서버를 통해 접근) const htmlPath = this.document.html_path; const fileName = htmlPath.split('/').pop(); const response = await fetch(`http://localhost:24102/uploads/documents/${fileName}`); if (!response.ok) { throw new Error('문서 파일을 불러올 수 없습니다'); } const htmlContent = await response.text(); document.getElementById('document-content').innerHTML = htmlContent; // 페이지 제목 업데이트 document.title = `${this.document.title} - Document Server`; // 문서 내 스크립트 오류 방지를 위한 전역 함수들 정의 this.setupDocumentScriptHandlers(); } catch (error) { console.error('Document load error:', error); // 백엔드 연결 실패시 목업 데이터로 폴백 console.warn('Using fallback mock data'); this.document = { id: this.documentId, title: 'Document Server 테스트 문서', description: '하이라이트와 메모 기능을 테스트하기 위한 샘플 문서입니다.', uploader_name: '관리자' }; // 기본 HTML 내용 표시 document.getElementById('document-content').innerHTML = `

테스트 문서

이 문서는 Document Server의 하이라이트 및 메모 기능을 테스트하기 위한 샘플입니다.

텍스트를 선택하면 하이라이트를 추가할 수 있습니다.

주요 기능

테스트 단락

이것은 하이라이트 테스트를 위한 긴 단락입니다. 이 텍스트를 선택하여 하이라이트를 만들어보세요. 하이라이트를 만든 후에는 메모를 추가할 수 있습니다. 메모는 나중에 검색하고 편집할 수 있습니다.

또 다른 단락입니다. 여러 개의 하이라이트를 만들어서 메모 기능을 테스트해보세요. 각 하이라이트는 고유한 색상을 가질 수 있으며, 연결된 메모를 통해 중요한 정보를 기록할 수 있습니다.

`; // 폴백 모드에서도 스크립트 핸들러 설정 this.setupDocumentScriptHandlers(); // 디버깅을 위한 전역 함수 노출 window.testHighlight = () => { console.log('Test highlight function called'); const selection = window.getSelection(); console.log('Current selection:', selection.toString()); this.handleTextSelection(); }; } }, // 문서 내 스크립트 핸들러 설정 setupDocumentScriptHandlers() { // 업로드된 HTML 문서에서 사용할 수 있는 전역 함수들 정의 // 언어 토글 함수 (많은 문서에서 사용) window.toggleLanguage = function() { const koreanContent = document.getElementById('korean-content'); const englishContent = document.getElementById('english-content'); if (koreanContent && englishContent) { // ID 기반 토글 (압력용기 매뉴얼 등) if (koreanContent.style.display === 'none') { koreanContent.style.display = 'block'; englishContent.style.display = 'none'; } else { koreanContent.style.display = 'none'; englishContent.style.display = 'block'; } } else { // 클래스 기반 토글 (다른 문서들) const koreanElements = document.querySelectorAll('.korean, .ko'); const englishElements = document.querySelectorAll('.english, .en'); koreanElements.forEach(el => { el.style.display = el.style.display === 'none' ? 'block' : 'none'; }); englishElements.forEach(el => { el.style.display = el.style.display === 'none' ? 'block' : 'none'; }); } // 토글 버튼 텍스트 업데이트 const toggleButton = document.querySelector('.language-toggle'); if (toggleButton && koreanContent) { const isKoreanVisible = koreanContent.style.display !== 'none'; toggleButton.textContent = isKoreanVisible ? '🌐 English' : '🌐 한국어'; } }; // 기타 공통 함수들 (필요시 추가) window.showSection = function(sectionId) { const section = document.getElementById(sectionId); if (section) { section.scrollIntoView({ behavior: 'smooth' }); } }; // 인쇄 함수 window.printDocument = function() { window.print(); }; // 문서 내 링크 클릭 시 새 창에서 열기 방지 const links = document.querySelectorAll('#document-content a[href^="http"]'); links.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); if (confirm('외부 링크로 이동하시겠습니까?\n' + link.href)) { window.open(link.href, '_blank'); } }); }); }, // 문서 관련 데이터 로드 async loadDocumentData() { try { console.log('Loading document data for:', this.documentId); const [highlights, notes, bookmarks] = await Promise.all([ api.getDocumentHighlights(this.documentId).catch(() => []), api.getDocumentNotes(this.documentId).catch(() => []), api.getDocumentBookmarks(this.documentId).catch(() => []) ]); this.highlights = highlights || []; this.notes = notes || []; this.bookmarks = bookmarks || []; console.log('Loaded data:', { highlights: this.highlights.length, notes: this.notes.length, bookmarks: this.bookmarks.length }); // 하이라이트 렌더링 this.renderHighlights(); } catch (error) { console.warn('Some document data failed to load, continuing with empty data:', error); this.highlights = []; this.notes = []; this.bookmarks = []; } }, // 하이라이트 렌더링 renderHighlights() { const content = document.getElementById('document-content'); // 기존 하이라이트 제거 content.querySelectorAll('.highlight').forEach(el => { const parent = el.parentNode; parent.replaceChild(document.createTextNode(el.textContent), el); parent.normalize(); }); // 새 하이라이트 적용 this.highlights.forEach(highlight => { this.applyHighlight(highlight); }); }, // 개별 하이라이트 적용 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; } }, // 텍스트 선택 처리 handleTextSelection() { console.log('handleTextSelection called'); const selection = window.getSelection(); if (selection.rangeCount === 0 || selection.isCollapsed) { console.log('No selection or collapsed'); return; } const range = selection.getRangeAt(0); const selectedText = selection.toString().trim(); console.log('Selected text:', selectedText); if (selectedText.length < 2) { console.log('Text too short'); return; } // 문서 컨텐츠 내부의 선택인지 확인 const content = document.getElementById('document-content'); if (!content.contains(range.commonAncestorContainer)) { console.log('Selection not in document content'); return; } // 선택된 텍스트와 범위 저장 this.selectedText = selectedText; this.selectedRange = range.cloneRange(); console.log('Showing highlight button'); // 컨텍스트 메뉴 표시 (간단한 버튼) this.showHighlightButton(selection); }, // 하이라이트 버튼 표시 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); }, // 색상 버튼으로 하이라이트 생성 createHighlightWithColor(color) { console.log('createHighlightWithColor called with color:', color); // 현재 선택된 텍스트가 있는지 확인 const selection = window.getSelection(); if (selection.rangeCount === 0 || selection.isCollapsed) { alert('먼저 하이라이트할 텍스트를 선택해주세요.'); return; } // 색상 설정 후 하이라이트 생성 this.selectedHighlightColor = color; this.handleTextSelection(); // 텍스트 선택 처리 // 바로 하이라이트 생성 (버튼 클릭 없이) setTimeout(() => { this.createHighlight(); }, 100); }, // 하이라이트 생성 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' }; const highlight = await api.createHighlight(highlightData); this.highlights.push(highlight); // 하이라이트 렌더링 this.renderHighlights(); // 선택 해제 window.getSelection().removeAllRanges(); this.selectedText = ''; this.selectedRange = null; // 메모 추가 여부 확인 if (confirm('이 하이라이트에 메모를 추가하시겠습니까?')) { this.openNoteModal(highlight); } } catch (error) { console.error('Failed to create highlight:', 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; }, // 메모 저장 async saveNote() { this.noteLoading = true; 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; } }, // 메모 삭제 async deleteNote(noteId) { if (!confirm('이 메모를 삭제하시겠습니까?')) { return; } try { await api.deleteNote(noteId); this.notes = this.notes.filter(n => n.id !== noteId); 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))) ); } }, // 책갈피 추가 async addBookmark() { const scrollPosition = window.scrollY; this.bookmarkForm = { title: `${this.document.title} - ${new Date().toLocaleString()}`, description: '' }; this.currentScrollPosition = scrollPosition; this.showBookmarkModal = true; }, // 책갈피 편집 editBookmark(bookmark) { this.editingBookmark = bookmark; this.bookmarkForm = { title: bookmark.title, description: bookmark.description || '' }; this.showBookmarkModal = true; }, // 책갈피 저장 async saveBookmark() { this.bookmarkLoading = true; 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; } }, // 책갈피 삭제 async deleteBookmark(bookmarkId) { if (!confirm('이 책갈피를 삭제하시겠습니까?')) { return; } try { await api.deleteBookmark(bookmarkId); this.bookmarks = this.bookmarks.filter(b => b.id !== bookmarkId); } catch (error) { console.error('Failed to delete bookmark:', error); alert('책갈피 삭제에 실패했습니다'); } }, // 책갈피로 스크롤 scrollToBookmark(bookmark) { window.scrollTo({ top: bookmark.scroll_position, behavior: 'smooth' }); }, // 책갈피 모달 닫기 closeBookmarkModal() { this.showBookmarkModal = false; this.editingBookmark = null; this.bookmarkForm = { title: '', description: '' }; this.currentScrollPosition = null; }, // 문서 내 검색 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 파라미터에서 from 확인 const urlParams = new URLSearchParams(window.location.search); const fromPage = urlParams.get('from'); // 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' }); }, // 하이라이트 말풍선 표시 showHighlightTooltip(highlight, element) { // 기존 말풍선 제거 this.hideTooltip(); 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-sm'; tooltip.style.minWidth = '300px'; // 하이라이트 정보와 메모 표시 const highlightNotes = this.notes.filter(note => note.highlight_id === highlight.id); tooltip.innerHTML = `
선택된 텍스트
"${highlight.selected_text}"
메모 (${highlightNotes.length})
${highlightNotes.length > 0 ? highlightNotes.map(note => `
${note.content}
${this.formatShortDate(note.created_at)} · Administrator
`).join('') : '
메모가 없습니다
' }
`; // 위치 계산 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); }, // 말풍선 숨기기 hideTooltip() { const tooltip = document.getElementById('highlight-tooltip'); if (tooltip) { tooltip.remove(); } document.removeEventListener('click', this.handleTooltipOutsideClick.bind(this)); }, // 말풍선 외부 클릭 처리 handleTooltipOutsideClick(e) { const tooltip = document.getElementById('highlight-tooltip'); if (tooltip && !tooltip.contains(e.target) && !e.target.classList.contains('highlight')) { 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' }); } }, // 메모 추가 폼 표시 showAddNoteForm(highlightId) { const tooltip = document.getElementById('highlight-tooltip'); if (!tooltip) return; const notesList = tooltip.querySelector('#notes-list'); notesList.innerHTML = `
`; // 텍스트 영역에 포커스 setTimeout(() => { document.getElementById('new-note-content').focus(); }, 100); }, // 메모 추가 취소 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); } } }, // 새 메모 저장 async saveNewNote(highlightId) { const content = document.getElementById('new-note-content').value.trim(); if (!content) { alert('메모 내용을 입력해주세요'); return; } try { const noteData = { highlight_id: highlightId, content: content, is_private: false, tags: [] }; const newNote = await api.createNote(noteData); // 로컬 데이터 업데이트 this.notes.push(newNote); // 말풍선 새로고침 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); } } } catch (error) { console.error('Failed to save note:', error); alert('메모 저장에 실패했습니다'); } }, // 하이라이트 삭제 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.renderHighlights(); } catch (error) { console.error('Failed to delete highlight:', error); alert('하이라이트 삭제에 실패했습니다'); } }, // 언어 전환 함수 toggleLanguage() { this.isKorean = !this.isKorean; // 문서 내 언어별 요소 토글 (더 범용적으로) const primaryLangElements = document.querySelectorAll('[lang="ko"], .korean, .kr, .primary-lang'); const secondaryLangElements = document.querySelectorAll('[lang="en"], .english, .en, [lang="ja"], .japanese, .jp, [lang="zh"], .chinese, .cn, .secondary-lang'); primaryLangElements.forEach(el => { el.style.display = this.isKorean ? 'block' : 'none'; }); secondaryLangElements.forEach(el => { el.style.display = this.isKorean ? 'none' : 'block'; }); 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); } } });