// 계층구조 뷰 전용 JavaScript // 메인 계층구조 앱 window.hierarchyApp = function() { return { // 상태 관리 currentUser: null, hierarchy: { books: {}, uncategorized: [] }, loading: false, searchQuery: '', // UI 상태 expandedBooks: [], expandedCategories: [], expandedUncategorizedInBooks: [], showUncategorized: false, showUploadModal: false, showLoginModal: false, // 선택된 문서 selectedDocument: null, documentContent: null, loadingDocument: false, // 하이라이트 & 메모 highlights: [], notes: [], selectedText: '', showHighlightMenu: false, highlightMenuPosition: { x: 0, y: 0 }, highlightMode: false, // 하이라이트 모드 토글 // 드래그 앤 드롭 draggedDocument: null, // 초기화 async init() { console.log('🚀 계층구조 앱 초기화 중...'); // 전역 인스턴스 등록 window.hierarchyInstance = this; await this.checkAuthStatus(); if (this.currentUser) { await this.loadHierarchy(); } this.setupEventListeners(); }, // 이벤트 리스너 설정 setupEventListeners() { // 인증 상태 변경 이벤트 window.addEventListener('auth-changed', (event) => { this.currentUser = event.detail.user; if (this.currentUser) { this.loadHierarchy(); } }); // 업로드 완료 이벤트 window.addEventListener('upload-complete', () => { this.loadHierarchy(); }); }, // 인증 상태 확인 async checkAuthStatus() { try { const user = await window.api.getCurrentUser(); this.currentUser = user; console.log('✅ 사용자 인증됨:', user.email); } catch (error) { console.log('❌ 인증되지 않음'); this.currentUser = null; } }, // 계층구조 데이터 로드 async loadHierarchy() { if (!this.currentUser) return; this.loading = true; try { console.log('📊 계층구조 데이터 로딩...'); const data = await window.api.getDocumentsHierarchy(); this.hierarchy = data; console.log('✅ 계층구조 로드 완료:', data); // 첫 번째 서적과 미분류를 기본으로 펼치기 if (Object.keys(data.books).length > 0) { const firstBookId = Object.keys(data.books)[0]; this.expandedBooks = [firstBookId]; } if (data.uncategorized.length > 0) { this.showUncategorized = true; } } catch (error) { console.error('❌ 계층구조 로드 실패:', error); } finally { this.loading = false; } }, // 계층구조 새로고침 async refreshHierarchy() { await this.loadHierarchy(); }, // 서적 토글 toggleBook(bookId) { const index = this.expandedBooks.indexOf(bookId); if (index > -1) { this.expandedBooks.splice(index, 1); } else { this.expandedBooks.push(bookId); } }, // 소분류 토글 toggleCategory(categoryId) { const index = this.expandedCategories.indexOf(categoryId); if (index > -1) { this.expandedCategories.splice(index, 1); } else { this.expandedCategories.push(categoryId); } }, // 서적 내 미분류 토글 toggleUncategorizedInBook(bookId) { const index = this.expandedUncategorizedInBooks.indexOf(bookId); if (index > -1) { this.expandedUncategorizedInBooks.splice(index, 1); } else { this.expandedUncategorizedInBooks.push(bookId); } }, // 전체 미분류 토글 toggleUncategorized() { this.showUncategorized = !this.showUncategorized; }, // 문서 열기 async openDocument(documentId) { console.log('📄 문서 열기:', documentId); this.loadingDocument = true; this.documentContent = null; this.highlights = []; this.notes = []; try { // 문서 정보 가져오기 const docInfo = await window.api.getDocument(documentId); this.selectedDocument = docInfo; // HTML 콘텐츠 가져오기 (인증된 API 사용) const content = await window.api.getDocumentContent(documentId); this.documentContent = content; // 하이라이트 및 메모 로드 await this.loadHighlightsAndNotes(documentId); // 문서 로드 후 하이라이트 적용 (DOM이 준비될 때까지 대기) setTimeout(async () => { console.log(`🎨 하이라이트 적용 시도: ${this.highlights.length}개`); this.applyHighlights(); this.setupTextSelection(); }, 200); console.log('✅ 문서 로드 완료'); } catch (error) { console.error('❌ 문서 로드 실패:', error); alert('문서를 불러오는 중 오류가 발생했습니다.'); } finally { this.loadingDocument = false; } }, // 하이라이트 및 메모 로드 async loadHighlightsAndNotes(documentId) { try { console.log(`📊 하이라이트/메모 로드 시작: ${documentId}`); // 하이라이트 로드 console.log('🔍 하이라이트 API 호출 중...'); const highlights = await window.api.getDocumentHighlights(documentId); console.log('📥 하이라이트 API 응답:', highlights); // 메모 로드 console.log('🔍 메모 API 호출 중...'); const notes = await window.api.getDocumentNotes(documentId); console.log('📥 메모 API 응답:', notes); this.highlights = highlights || []; this.notes = notes || []; console.log(`📝 최종 저장: 하이라이트 ${this.highlights.length}개, 메모 ${this.notes.length}개`); if (this.highlights.length > 0) { console.log('📝 하이라이트 상세:', this.highlights.map(h => ({ id: h.id, text: h.selected_text, color: h.color || h.highlight_color }))); } } catch (error) { console.error('❌ 하이라이트/메모 로드 실패:', error); console.error('❌ 오류 상세:', error.message, error.stack); this.highlights = []; this.notes = []; } }, // 하이라이트 적용 (viewer.js와 동일한 로직) applyHighlights() { if (!this.selectedDocument || !this.highlights.length) { console.log('📝 하이라이트 적용 건너뜀: 문서 없음 또는 하이라이트 없음'); return; } const contentElement = document.querySelector('.prose'); if (!contentElement) { console.log('📝 하이라이트 적용 건너뜀: .prose 요소 없음'); return; } console.log(`📝 하이라이트 적용 시작: ${this.highlights.length}개`); // 기존 하이라이트 제거 contentElement.querySelectorAll('.highlight').forEach(el => { const parent = el.parentNode; parent.replaceChild(document.createTextNode(el.textContent), el); parent.normalize(); }); // 새 하이라이트 적용 this.highlights.forEach(highlight => { this.applyHighlight(highlight); }); }, // 개별 하이라이트 적용 (viewer.js와 동일한 로직) applyHighlight(highlight) { const content = document.querySelector('.prose'); if (!content) return; const walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT, null, false ); let currentOffset = 0; let node; while (node = walker.nextNode()) { const nodeLength = node.textContent.length; if (currentOffset + nodeLength > highlight.start_offset) { const startInNode = Math.max(0, highlight.start_offset - currentOffset); const endInNode = Math.min(nodeLength, highlight.end_offset - currentOffset); if (startInNode < endInNode) { try { const range = document.createRange(); range.setStart(node, startInNode); range.setEnd(node, endInNode); const span = document.createElement('span'); span.className = `highlight cursor-pointer`; span.style.backgroundColor = highlight.color || '#FFFF00'; span.dataset.highlightId = highlight.id; span.title = highlight.note || '하이라이트'; // 하이라이트 클릭 이벤트 span.addEventListener('click', (e) => { e.stopPropagation(); this.showHighlightDetails(highlight, e.target); }); range.surroundContents(span); console.log(`✅ 하이라이트 적용됨: ${highlight.selected_text}`); break; } catch (error) { console.warn('하이라이트 적용 실패:', error); } } } currentOffset += nodeLength; if (currentOffset >= highlight.end_offset) { break; } } }, // 텍스트 선택 설정 setupTextSelection() { console.log('🎯 텍스트 선택 이벤트 설정 중...'); const contentElement = document.querySelector('.prose'); if (!contentElement) { console.warn('❌ .prose 요소를 찾을 수 없습니다'); return; } contentElement.addEventListener('mouseup', (e) => { console.log('🖱️ 마우스업 이벤트 발생'); const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const text = selection.toString().trim(); console.log('📝 선택된 텍스트:', text); if (text.length > 0 && this.highlightMode) { this.selectedText = text; // 직접 DOM 조작으로 메뉴 표시 const menu = document.getElementById('highlight-menu'); if (menu) { menu.style.display = 'block'; console.log('✅ 하이라이트 메뉴 표시됨'); } else { console.error('❌ 하이라이트 메뉴 DOM 요소를 찾을 수 없음'); } } } }); // 다른 곳 클릭 시 메뉴 숨기기 (메뉴 자체 클릭은 제외) document.addEventListener('click', (e) => { if (!e.target.closest('#highlight-menu') && !e.target.closest('.prose')) { const menu = document.getElementById('highlight-menu'); if (menu) { menu.style.display = 'none'; } } }); }, // 하이라이트 생성 (viewer.js와 동일한 로직) async createHighlight(color = 'yellow') { if (!this.selectedText || !this.selectedDocument) return; const selection = window.getSelection(); if (selection.rangeCount === 0) return; try { const range = selection.getRangeAt(0); const content = document.querySelector('.prose'); // 텍스트 오프셋 계산 const offsets = this.calculateTextOffsets(range, content); // 색상 매핑 const colorMap = { 'yellow': '#FFFF00', 'blue': '#87CEEB', 'green': '#90EE90', 'red': '#FFB6C1' }; const highlightData = { document_id: this.selectedDocument.id, start_offset: offsets.start, end_offset: offsets.end, selected_text: this.selectedText, color: colorMap[color] || '#FFFF00', note: null }; console.log('📝 하이라이트 생성 데이터:', highlightData); const newHighlight = await window.api.createHighlight(highlightData); this.highlights.push(newHighlight); // 하이라이트 다시 렌더링 this.applyHighlights(); // 선택 해제 selection.removeAllRanges(); const menu = document.getElementById('highlight-menu'); if (menu) { menu.style.display = 'none'; } this.selectedText = ''; console.log('✅ 하이라이트 생성 완료'); // 성공 메시지 alert(`하이라이트가 생성되었습니다: "${this.selectedText}"`); // 메모 추가는 하이라이트 클릭으로만 가능하도록 변경 } catch (error) { console.error('❌ 하이라이트 생성 실패:', error); alert('하이라이트 생성 중 오류가 발생했습니다.'); } }, // 텍스트 오프셋 계산 (viewer.js와 동일한 로직) 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 (startOffset === -1 && node === range.startContainer) { startOffset = currentOffset + range.startOffset; } if (endOffset === -1 && node === range.endContainer) { endOffset = currentOffset + range.endOffset; break; } currentOffset += nodeLength; } return { start: startOffset, end: endOffset }; }, // 텍스트 오프셋 계산 (간단한 버전) getTextOffset(container, offset) { const contentElement = document.querySelector('.prose'); if (!contentElement) return 0; const walker = document.createTreeWalker( contentElement, NodeFilter.SHOW_TEXT, null, false ); let textOffset = 0; let node; while (node = walker.nextNode()) { if (node === container) { return textOffset + offset; } textOffset += node.textContent.length; } return textOffset; }, // 하이라이트에서 Range 생성 (간단한 버전) createRangeFromHighlight(highlight) { const contentElement = document.querySelector('.prose'); if (!contentElement) return null; const walker = document.createTreeWalker( contentElement, NodeFilter.SHOW_TEXT, null, false ); let currentOffset = 0; let startNode = null, startOffset = 0; let endNode = null, endOffset = 0; let node; while (node = walker.nextNode()) { const nodeLength = node.textContent.length; if (!startNode && currentOffset + nodeLength > highlight.start_offset) { startNode = node; startOffset = highlight.start_offset - currentOffset; } if (!endNode && currentOffset + nodeLength >= highlight.end_offset) { endNode = node; endOffset = highlight.end_offset - currentOffset; break; } currentOffset += nodeLength; } if (startNode && endNode) { const range = document.createRange(); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); return range; } return null; }, // 하이라이트 상세 정보 표시 (viewer.js와 동일한 툴팁 방식) showHighlightDetails(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 = `