// 계층구조 뷰 전용 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 = `
선택된 텍스트
"${highlight.selected_text}"
메모 (${highlightNotes.length})
${highlightNotes.length > 0 ? highlightNotes.map(note => `
${note.content}
${new Date(note.created_at).toLocaleDateString()}
`).join('') : '
메모가 없습니다
' }
`; document.body.appendChild(tooltip); // 위치 계산 const rect = element.getBoundingClientRect(); const tooltipRect = tooltip.getBoundingClientRect(); let top = rect.bottom + window.scrollY + 10; let left = rect.left + window.scrollX; // 화면 경계 체크 if (left + tooltipRect.width > window.innerWidth) { left = window.innerWidth - tooltipRect.width - 10; } if (top + tooltipRect.height > window.innerHeight + window.scrollY) { top = rect.top + window.scrollY - tooltipRect.height - 10; } tooltip.style.top = top + 'px'; tooltip.style.left = left + 'px'; // 외부 클릭 시 닫기 setTimeout(() => { document.addEventListener('click', this.handleTooltipOutsideClick.bind(this)); }, 100); }, // 툴팁 숨기기 hideTooltip() { const tooltip = document.getElementById('highlight-tooltip'); if (tooltip) { tooltip.remove(); } document.removeEventListener('click', this.handleTooltipOutsideClick.bind(this)); }, // 툴팁 외부 클릭 처리 handleTooltipOutsideClick(e) { if (!e.target.closest('#highlight-tooltip')) { this.hideTooltip(); } }, // 메모 추가 폼 표시 showAddNoteForm(highlightId) { const tooltip = document.getElementById('highlight-tooltip'); if (!tooltip) return; const notesList = tooltip.querySelector('#notes-list'); notesList.innerHTML = `
`; }, // 새 메모 저장 async saveNewNote(highlightId) { const content = document.getElementById('new-note-content').value.trim(); if (!content) { alert('메모 내용을 입력해주세요'); return; } try { const noteData = { highlight_id: highlightId, content: content, is_private: false, tags: [] }; const newNote = await window.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.showHighlightDetails(highlight, element); } } } catch (error) { console.error('Failed to save note:', error); alert('메모 저장에 실패했습니다'); } }, // 메모 추가 취소 cancelAddNote(highlightId) { const highlight = this.highlights.find(h => h.id === highlightId); if (highlight) { const element = document.querySelector(`[data-highlight-id="${highlightId}"]`); if (element) { this.showHighlightDetails(highlight, element); } } }, // 메모 수정 editNote(noteId, currentContent) { const newContent = prompt('메모를 수정하세요:', currentContent); if (newContent !== null && newContent.trim() !== currentContent) { this.updateNote(noteId, newContent.trim()); } }, // 메모 업데이트 async updateNote(noteId, content) { try { await window.api.updateNote(noteId, { content }); // 로컬 데이터 업데이트 const note = this.notes.find(n => n.id === noteId); if (note) { note.content = content; } // 툴팁 새로고침 const highlight = this.highlights.find(h => this.notes.some(n => n.id === noteId && n.highlight_id === h.id) ); if (highlight) { const element = document.querySelector(`[data-highlight-id="${highlight.id}"]`); if (element) { this.showHighlightDetails(highlight, element); } } } catch (error) { console.error('Failed to update note:', error); alert('메모 수정에 실패했습니다'); } }, // 메모 삭제 async deleteNote(noteId) { if (!confirm('이 메모를 삭제하시겠습니까?')) { return; } try { await window.api.deleteNote(noteId); // 로컬 데이터에서 제거 this.notes = this.notes.filter(n => n.id !== noteId); // 툴팁 새로고침 const highlight = this.highlights.find(h => this.notes.some(n => n.highlight_id === h.id) || document.querySelector(`[data-highlight-id="${h.id}"]`) ); if (highlight) { const element = document.querySelector(`[data-highlight-id="${highlight.id}"]`); if (element) { this.showHighlightDetails(highlight, element); } } } catch (error) { console.error('Failed to delete note:', error); alert('메모 삭제에 실패했습니다'); } }, // 하이라이트 삭제 async deleteHighlight(highlightId) { if (!confirm('이 하이라이트를 삭제하시겠습니까? 연결된 메모도 함께 삭제됩니다.')) { return; } try { await window.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.applyHighlights(); } catch (error) { console.error('Failed to delete highlight:', error); alert('하이라이트 삭제에 실패했습니다'); } }, // 하이라이트 메모 업데이트 async updateHighlightNote(highlightId, note) { try { await window.api.updateHighlight(highlightId, { note }); const highlight = this.highlights.find(h => h.id === highlightId); if (highlight) { highlight.note = note; } console.log('✅ 하이라이트 메모 업데이트 완료'); } catch (error) { console.error('❌ 하이라이트 메모 업데이트 실패:', error); } }, // 하이라이트 모드 토글 toggleHighlightMode() { this.highlightMode = !this.highlightMode; if (!this.highlightMode) { this.showHighlightMenu = false; } console.log('🎨 하이라이트 모드:', this.highlightMode ? '활성화' : '비활성화'); }, // 문서 편집 editDocument(document) { console.log('✏️ 문서 편집:', document.title); // TODO: 편집 모달 구현 alert('문서 편집 기능은 곧 구현될 예정입니다.'); }, // 문서 삭제 async deleteDocument(documentId) { if (!confirm('정말로 이 문서를 삭제하시겠습니까?')) { return; } try { await window.api.deleteDocument(documentId); console.log('✅ 문서 삭제 완료'); // 선택된 문서가 삭제된 경우 선택 해제 if (this.selectedDocument && this.selectedDocument.id === documentId) { this.selectedDocument = null; this.documentContent = null; } // 계층구조 새로고침 await this.loadHierarchy(); } catch (error) { console.error('❌ 문서 삭제 실패:', error); alert('문서 삭제 중 오류가 발생했습니다.'); } }, // 검색 async searchDocuments() { // TODO: 검색 기능 구현 console.log('🔍 검색:', this.searchQuery); }, // 드래그 앤 드롭 처리 handleDragStart(event, document) { this.draggedDocument = document; event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/html', document.id); // 드래그 중인 요소 스타일 event.target.style.opacity = '0.5'; }, handleDrop(event, targetCategoryId) { event.preventDefault(); if (!this.draggedDocument) return; console.log('📦 드롭:', this.draggedDocument.title, '→', targetCategoryId); // TODO: 문서 순서 변경 API 호출 // await window.api.updateDocumentOrder({ // document_id: this.draggedDocument.id, // category_id: targetCategoryId, // new_order: targetOrder // }); // 드래그 상태 초기화 this.draggedDocument = null; // UI 새로고침 this.loadHierarchy(); }, // 로그인 모달 열기 openLoginModal() { this.showLoginModal = true; }, // 로그아웃 async logout() { try { await window.api.logout(); this.currentUser = null; this.hierarchy = { books: {}, uncategorized: [] }; this.selectedDocument = null; this.documentContent = null; console.log('✅ 로그아웃 완료'); } catch (error) { console.error('❌ 로그아웃 실패:', error); } } }; }; // 업로드 모달 (기존 로직 재사용) window.uploadModal = function() { return { // 폼 데이터 uploadForm: { html_file: null, pdf_file: null, title: '', description: '', document_date: '', tags: '', is_public: false }, // 서적 관련 bookSelectionMode: 'none', bookSearchQuery: '', searchedBooks: [], selectedBook: null, newBook: { title: '', author: '' }, suggestions: [], searchTimeout: null, // 상태 uploading: false, uploadError: null, // 파일 선택 처리 handleFileSelect(event, fileType) { const file = event.target.files[0]; if (file) { this.uploadForm[fileType] = file; console.log(`📎 ${fileType} 선택:`, file.name); } }, // 서적 검색 async searchBooks() { if (!this.bookSearchQuery.trim()) { this.searchedBooks = []; return; } try { const books = await window.api.searchBooks(this.bookSearchQuery); this.searchedBooks = books; } catch (error) { console.error('❌ 서적 검색 실패:', error); } }, // 서적 선택 selectBook(book) { this.selectedBook = book; this.searchedBooks = []; this.bookSearchQuery = book.title; }, // 추천 서적 가져오기 async getSuggestions() { if (!this.newBook.title.trim()) { this.suggestions = []; return; } // 디바운스 if (this.searchTimeout) { clearTimeout(this.searchTimeout); } this.searchTimeout = setTimeout(async () => { try { const suggestions = await window.api.getBookSuggestions(this.newBook.title); this.suggestions = suggestions; } catch (error) { console.error('❌ 추천 가져오기 실패:', error); } }, 300); }, // 추천에서 기존 서적 선택 selectExistingFromSuggestion(suggestion) { this.bookSelectionMode = 'existing'; this.selectedBook = suggestion; this.suggestions = []; this.newBook = { title: '', author: '' }; }, // 업로드 실행 async upload() { this.uploading = true; this.uploadError = null; try { const formData = new FormData(); // 파일 추가 formData.append('html_file', this.uploadForm.html_file); if (this.uploadForm.pdf_file) { formData.append('pdf_file', this.uploadForm.pdf_file); } // 문서 정보 추가 formData.append('title', this.uploadForm.title); formData.append('description', this.uploadForm.description || ''); formData.append('document_date', this.uploadForm.document_date || ''); formData.append('tags', this.uploadForm.tags || ''); formData.append('is_public', this.uploadForm.is_public); // 서적 정보 처리 let bookId = null; if (this.bookSelectionMode === 'existing' && this.selectedBook) { bookId = this.selectedBook.id; } else if (this.bookSelectionMode === 'new' && this.newBook.title) { // 새 서적 생성 const newBook = await window.api.createBook({ title: this.newBook.title, author: this.newBook.author || null }); bookId = newBook.id; } if (bookId) { formData.append('book_id', bookId); } // 업로드 실행 await window.api.uploadDocument(formData); console.log('✅ 문서 업로드 완료'); // 성공 후 처리 this.closeUploadModal(); window.dispatchEvent(new CustomEvent('upload-complete')); } catch (error) { console.error('❌ 업로드 실패:', error); this.uploadError = error.message || '업로드 중 오류가 발생했습니다.'; } finally { this.uploading = false; } }, // 모달 닫기 closeUploadModal() { this.showUploadModal = false; this.resetForm(); }, // 폼 초기화 resetForm() { this.uploadForm = { html_file: null, pdf_file: null, title: '', description: '', document_date: '', tags: '', is_public: false }; this.bookSelectionMode = 'none'; this.bookSearchQuery = ''; this.searchedBooks = []; this.selectedBook = null; this.newBook = { title: '', author: '' }; this.suggestions = []; this.uploadError = null; } }; }; // 로그인 모달 (기존 로직 재사용) window.authModal = function() { return { loginForm: { email: '', password: '' }, loggingIn: false, loginError: null, async login() { this.loggingIn = true; this.loginError = null; try { const response = await window.api.login(this.loginForm.email, this.loginForm.password); // 토큰 저장 localStorage.setItem('access_token', response.access_token); if (response.refresh_token) { localStorage.setItem('refresh_token', response.refresh_token); } // 사용자 정보 가져오기 const user = await window.api.getCurrentUser(); console.log('✅ 로그인 성공:', user.email); // 전역 이벤트 발생 window.dispatchEvent(new CustomEvent('auth-changed', { detail: { user } })); // 모달 닫기 this.showLoginModal = false; this.resetLoginForm(); } catch (error) { console.error('❌ 로그인 실패:', error); this.loginError = error.message || '로그인에 실패했습니다.'; } finally { this.loggingIn = false; } }, resetLoginForm() { this.loginForm = { email: '', password: '' }; this.loginError = null; } }; }; console.log('📚 계층구조 JavaScript 로드 완료');