/** * 문서 뷰어 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, showBacklinksModal: false, showLinksModal: false, activeFeatureMenu: null, activeMode: null, // 'link', 'memo', 'bookmark' 등 textSelectionHandler: null, availableBooks: [], // 사용 가능한 서적 목록 filteredDocuments: [], // 필터링된 문서 목록 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: 'document', target_text: '', target_start_offset: 0, target_end_offset: 0 }, // 초기화 async init() { // 전역 인스턴스 설정 (말풍선에서 함수 호출용) window.documentViewerInstance = this; // 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') { await this.loadNote(); } else { await this.loadDocument(); await this.loadNavigation(); } await this.loadDocumentData(); // URL 파라미터 확인해서 특정 텍스트로 스크롤 this.checkForTextHighlight(); } catch (error) { console.error('Failed to load document:', error); this.error = error.message; } finally { this.loading = false; } // 초기 필터링 this.filterNotes(); }, // 노트 로드 async loadNote() { try { console.log('📝 노트 로드 시작:', this.documentId); // 백엔드에서 노트 정보 가져오기 this.document = await api.get(`/note-documents/${this.documentId}`); // 노트 제목 설정 document.title = `${this.document.title} - Document Server`; // 노트 내용을 HTML로 설정 const contentElement = document.getElementById('document-content'); if (contentElement && this.document.content) { contentElement.innerHTML = this.document.content; } console.log('📝 노트 로드 완료:', this.document.title); } catch (error) { console.error('노트 로드 실패:', error); throw new Error('노트를 불러올 수 없습니다'); } }, // 문서 로드 (실제 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, 'type:', this.contentType); if (this.contentType === 'note') { // 노트의 경우: 노트용 API 호출 console.log('📝 노트 데이터 로드 중...'); const [highlights, notes] = await Promise.all([ api.get(`/note/${this.documentId}/highlights`).catch(() => []), api.get(`/note/${this.documentId}/notes`).catch(() => []) ]); this.highlights = highlights || []; this.notes = notes || []; this.bookmarks = []; // 노트에서는 북마크 미지원 this.documentLinks = []; // 노트에서는 링크 미지원 (향후 구현 예정) this.backlinks = []; console.log('📝 노트 데이터 로드됨:', { highlights: this.highlights.length, notes: this.notes.length }); // 하이라이트 렌더링 this.renderHighlights(); return; } // 문서의 경우: 기존 로직 const [highlights, notes, bookmarks, documentLinks] = await Promise.all([ api.getDocumentHighlights(this.documentId).catch(() => []), api.getDocumentNotes(this.documentId).catch(() => []), api.getDocumentBookmarks(this.documentId).catch(() => []), api.getDocumentLinks(this.documentId).catch(() => []) ]); this.highlights = highlights || []; this.notes = notes || []; this.bookmarks = bookmarks || []; this.documentLinks = documentLinks || []; console.log('Loaded data:', { highlights: this.highlights.length, notes: this.notes.length, bookmarks: this.bookmarks.length }); // 하이라이트 렌더링 this.renderHighlights(); // 백링크 렌더링 (이 문서를 참조하는 링크들) - 먼저 렌더링 this.renderBacklinkHighlights(); // 문서 링크 렌더링 - 백링크 후에 렌더링 (백링크 보호) this.renderDocumentLinks(); // 백링크 데이터 로딩 (배너 숫자 표시용) this.loadBacklinks(); } 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, .multi-highlight').forEach(el => { const parent = el.parentNode; parent.replaceChild(document.createTextNode(el.textContent), el); parent.normalize(); }); // 텍스트 위치별로 하이라이트 그룹화 const positionGroups = this.groupHighlightsByPosition(); // 각 위치 그룹에 대해 하이라이트 적용 Object.values(positionGroups).forEach(group => { this.applyHighlightGroup(group); }); }, // 위치별로 하이라이트 그룹화 groupHighlightsByPosition() { const groups = {}; this.highlights.forEach(highlight => { // 겹치는 하이라이트들을 찾아서 그룹화 let foundGroup = null; for (const [key, group] of Object.entries(groups)) { const hasOverlap = group.some(h => (highlight.start_offset < h.end_offset && highlight.end_offset > h.start_offset) ); if (hasOverlap) { foundGroup = key; break; } } if (foundGroup) { groups[foundGroup].push(highlight); } else { // 새 그룹 생성 const groupKey = `${highlight.start_offset}-${highlight.end_offset}`; groups[groupKey] = [highlight]; } }); return groups; }, // 하이라이트 그룹 적용 (여러 색상 지원) 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; } }, // 텍스트 선택 처리 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' }; // 노트와 문서에 따라 다른 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.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; }, // 메모 저장 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 파라미터에서 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 '기타'; }, // 하이라이트 말풍선 표시 showHighlightTooltip(clickedHighlight, element) { // 기존 말풍선 제거 this.hideTooltip(); // 동일한 범위의 모든 하이라이트 찾기 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); }, // 말풍선 숨기기 hideTooltip() { const highlightTooltip = document.getElementById('highlight-tooltip'); if (highlightTooltip) { highlightTooltip.remove(); } const linkTooltip = document.getElementById('link-tooltip'); if (linkTooltip) { linkTooltip.remove(); } const backlinkTooltip = document.getElementById('backlink-tooltip'); if (backlinkTooltip) { backlinkTooltip.remove(); } document.removeEventListener('click', this.handleTooltipOutsideClick.bind(this)); }, // 말풍선 외부 클릭 처리 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' }); } }, // 메모 추가 폼 표시 showAddNoteForm(highlightId) { console.log('🔍 showAddNoteForm 호출됨, highlightId:', highlightId); const tooltip = document.getElementById('highlight-tooltip'); if (!tooltip) { console.error('❌ 툴팁을 찾을 수 없습니다'); return; } console.log('✅ 툴팁 찾음:', tooltip); console.log('🔍 찾고 있는 ID:', `#notes-list-${highlightId}`); // 툴팁 내의 모든 ID 요소 확인 const allIds = tooltip.querySelectorAll('[id]'); console.log('📋 툴팁 내 모든 ID 요소들:', Array.from(allIds).map(el => el.id)); // 툴팁 HTML 전체 구조 확인 console.log('🔍 툴팁 HTML 구조:', tooltip.innerHTML.substring(0, 500) + '...'); // 해당 하이라이트의 메모 섹션을 찾기 const notesList = tooltip.querySelector(`#notes-list-${highlightId}`); if (!notesList) { console.error('❌ 메모 리스트 요소를 찾을 수 없습니다:', highlightId); console.log('🔍 툴팁 HTML:', tooltip.innerHTML); return; } console.log('✅ 메모 리스트 찾음:', notesList); 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 }; // 노트와 문서에 따라 다른 API 호출 let newNote; if (this.contentType === 'note') { noteData.note_id = this.documentId; // 노트 메모는 note_id 필요 newNote = await api.post('/note-notes/', noteData); } else { noteData.is_private = false; noteData.tags = []; 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('하이라이트 삭제에 실패했습니다'); } }, // 특정 색상의 하이라이트들 삭제 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.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.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); } }, // 네비게이션 정보 로드 async loadNavigation() { try { this.navigation = await window.api.getDocumentNavigation(this.documentId); console.log('📍 네비게이션 정보 로드됨:', this.navigation); } catch (error) { console.error('❌ 네비게이션 정보 로드 실패:', error); // 네비게이션 정보는 필수가 아니므로 에러를 던지지 않음 } }, // 다른 문서로 네비게이션 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; } // text_fragment 타입인데 target_text가 없으면 경고 if (this.linkForm.link_type === 'text_fragment' && !this.linkForm.target_text) { const confirm = window.confirm('특정 부분 링크를 선택했지만 대상 텍스트가 선택되지 않았습니다. 전체 문서 링크로 생성하시겠습니까?'); if (confirm) { this.linkForm.link_type = 'document'; } else { 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(); // 백링크 먼저 렌더링 this.renderBacklinkHighlights(); // 일반 링크 렌더링 (백링크 보호) this.renderDocumentLinks(); 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 = []; }, // 문서 링크 렌더링 renderDocumentLinks() { const documentContent = document.getElementById('document-content'); if (!documentContent) return; // 기존 링크 스타일 제거 (백링크는 보호) const existingLinks = documentContent.querySelectorAll('.document-link'); 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); }, // 백링크 하이라이트 렌더링 (이 문서를 참조하는 다른 문서의 링크들) async renderBacklinkHighlights() { if (!this.documentId) return; 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}`; }, // URL 파라미터에서 특정 텍스트 하이라이트 확인 checkForTextHighlight() { const urlParams = new URLSearchParams(window.location.search); const highlightText = urlParams.get('highlight_text'); const startOffset = parseInt(urlParams.get('start_offset')); const endOffset = parseInt(urlParams.get('end_offset')); if (highlightText && !isNaN(startOffset) && !isNaN(endOffset)) { console.log('🎯 링크된 텍스트로 이동:', { highlightText, startOffset, endOffset }); // 약간의 지연 후 하이라이트 및 스크롤 (DOM이 완전히 로드된 후) setTimeout(() => { this.highlightAndScrollToText(highlightText, startOffset, endOffset); }, 500); } }, // 특정 텍스트를 하이라이트하고 스크롤 highlightAndScrollToText(targetText, startOffset, endOffset) { console.log('🎯 highlightAndScrollToText 호출됨:', { targetText, startOffset, endOffset }); const documentContent = document.getElementById('document-content'); if (!documentContent) { console.error('❌ document-content 요소를 찾을 수 없습니다'); return; } // 백링크 보호: 기존 백링크 저장 const existingBacklinks = Array.from(documentContent.querySelectorAll('.backlink-highlight')); console.log(`🔒 백링크 보호: ${existingBacklinks.length}개 백링크 저장`); const backlinkData = existingBacklinks.map(bl => ({ element: bl, outerHTML: bl.outerHTML, parentNode: bl.parentNode, nextSibling: bl.nextSibling })); 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('🗑️ 임시 하이라이트 제거됨'); } // 백링크 복원 console.log(`🔄 백링크 복원 시작: ${backlinkData ? backlinkData.length : 0}개`); if (backlinkData && backlinkData.length > 0) { backlinkData.forEach((data, index) => { try { if (data.parentNode && data.parentNode.isConnected) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = data.outerHTML; const restoredBacklink = tempDiv.firstChild; // 클릭 이벤트 다시 추가 restoredBacklink.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); console.log(`🔗 복원된 백링크 클릭됨`); }); if (data.nextSibling && data.nextSibling.parentNode) { data.parentNode.insertBefore(restoredBacklink, data.nextSibling); } else { data.parentNode.appendChild(restoredBacklink); } console.log(`✅ 백링크 ${index + 1} 복원됨`); } else { console.warn(`⚠️ 백링크 ${index + 1} 부모 노드 없음`); } } catch (error) { console.error(`❌ 백링크 ${index + 1} 복원 실패:`, error); } }); } else { console.warn('⚠️ 복원할 백링크 데이터가 없음'); // 백링크 재렌더링 console.log('🔄 백링크 재렌더링 시도...'); self.renderBacklinkHighlights(); } }, 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 { const range = document.createRange(); range.setStart(startNode, startNodeOffset); range.setEnd(endNode, endNodeOffset); const span = document.createElement('span'); span.className = className; // 속성 추가 Object.entries(attributes).forEach(([key, value]) => { span.setAttribute(key, value); }); range.surroundContents(span); console.log(`✅ highlightTextRange 성공: "${span.textContent}" (${className})`); return span; // 생성된 요소 반환 } catch (error) { console.warn(`❌ highlightTextRange 실패 (${className}):`, error); return null; } }, // 백링크 관련 메서드들 async loadBacklinks() { if (!this.documentId) return; 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; } }, // 링크 모드 활성화 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('🔗 createDocumentLink 함수 호출 예정'); this.createDocumentLink(); 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(); }, // 선택된 텍스트로 메모 생성 async createNoteFromSelection() { if (!this.selectedText || !this.selectedRange) return; try { // 하이라이트 생성 const highlightData = await this.createHighlight(this.selectedText, this.selectedRange, '#FFFF00'); // 메모 내용 입력받기 const content = prompt('메모 내용을 입력하세요:', ''); if (content === null) { // 취소한 경우 하이라이트 제거 const highlightElement = document.querySelector(`[data-highlight-id="${highlightData.id}"]`); if (highlightElement) { const parent = highlightElement.parentNode; parent.replaceChild(document.createTextNode(highlightElement.textContent), highlightElement); parent.normalize(); } return; } // 메모 생성 const noteData = { highlight_id: highlightData.id, content: content }; // 노트와 문서에 따라 다른 API 호출 let note; if (this.contentType === 'note') { noteData.note_id = this.documentId; // 노트 메모는 note_id 필요 note = await api.post('/note-notes/', noteData); } else { note = await api.createNote(this.documentId, noteData); } // 데이터 새로고침 await this.loadNotes(); alert('메모가 생성되었습니다.'); } 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 : ''; } });