/** * 통합 검색 JavaScript */ // 검색 애플리케이션 Alpine.js 컴포넌트 window.searchApp = function() { return { // 상태 관리 searchQuery: '', searchResults: [], filteredResults: [], loading: false, hasSearched: false, searchTime: 0, // 필터링 typeFilter: '', // '', 'document', 'note', 'memo', 'highlight' sortBy: 'relevance', // 'relevance', 'date_desc', 'date_asc', 'title' // 검색 디바운스 searchTimeout: null, // 미리보기 모달 showPreviewModal: false, previewResult: null, previewLoading: false, pdfError: false, // HTML 뷰어 상태 htmlLoading: false, htmlRawMode: false, htmlSourceCode: '', // 인증 상태 isAuthenticated: false, currentUser: null, // API 클라이언트 api: null, // 초기화 async init() { console.log('🔍 검색 앱 초기화 시작'); try { // API 클라이언트 초기화 this.api = new DocumentServerAPI(); // 헤더 로드 await this.loadHeader(); // 인증 상태 확인 await this.checkAuthStatus(); // URL 파라미터에서 검색어 확인 const urlParams = new URLSearchParams(window.location.search); const query = urlParams.get('q'); if (query) { this.searchQuery = query; await this.performSearch(); } console.log('✅ 검색 앱 초기화 완료'); } catch (error) { console.error('❌ 검색 앱 초기화 실패:', error); } }, // 인증 상태 확인 async checkAuthStatus() { try { const user = await this.api.getCurrentUser(); this.isAuthenticated = true; this.currentUser = user; console.log('✅ 인증됨:', user.username || user.email); } catch (error) { console.log('❌ 인증되지 않음'); this.isAuthenticated = false; this.currentUser = null; // 검색은 로그인 없이도 가능하도록 허용 } }, // 헤더 로드 async loadHeader() { try { if (typeof loadHeaderComponent === 'function') { await loadHeaderComponent(); } else if (typeof window.loadHeaderComponent === 'function') { await window.loadHeaderComponent(); } else { console.warn('헤더 로더 함수를 찾을 수 없습니다.'); } } catch (error) { console.error('헤더 로드 실패:', error); } }, // 검색 디바운스 debounceSearch() { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => { if (this.searchQuery.trim()) { this.performSearch(); } }, 500); }, // 검색 수행 async performSearch() { if (!this.searchQuery.trim()) { this.searchResults = []; this.filteredResults = []; this.hasSearched = false; return; } this.loading = true; const startTime = Date.now(); try { console.log('🔍 검색 시작:', this.searchQuery); // 검색 API 호출 const response = await this.api.search({ q: this.searchQuery, type_filter: this.typeFilter || undefined, limit: 50 }); this.searchResults = response.results || []; this.hasSearched = true; this.searchTime = Date.now() - startTime; // 필터 적용 this.applyFilters(); // URL 업데이트 this.updateURL(); console.log('✅ 검색 완료:', this.searchResults.length, '개 결과'); } catch (error) { console.error('❌ 검색 실패:', error); this.searchResults = []; this.filteredResults = []; this.hasSearched = true; } finally { this.loading = false; } }, // 필터 적용 applyFilters() { let results = [...this.searchResults]; // 타입 필터 if (this.typeFilter) { results = results.filter(result => result.type === this.typeFilter); } // 정렬 results.sort((a, b) => { switch (this.sortBy) { case 'relevance': return (b.relevance_score || 0) - (a.relevance_score || 0); case 'date_desc': return new Date(b.created_at) - new Date(a.created_at); case 'date_asc': return new Date(a.created_at) - new Date(b.created_at); case 'title': return a.title.localeCompare(b.title); default: return 0; } }); this.filteredResults = results; console.log('🔧 필터 적용 완료:', this.filteredResults.length, '개 결과'); }, // URL 업데이트 updateURL() { const url = new URL(window.location); if (this.searchQuery.trim()) { url.searchParams.set('q', this.searchQuery); } else { url.searchParams.delete('q'); } window.history.replaceState({}, '', url); }, // 미리보기 표시 async showPreview(result) { console.log('👁️ 미리보기 표시:', result); this.previewResult = result; this.showPreviewModal = true; this.previewLoading = true; try { // 문서 타입인 경우 상세 정보 먼저 로드 if (result.type === 'document' || result.type === 'document_content') { try { const docInfo = await this.api.get(`/documents/${result.document_id}`); // PDF 정보 업데이트 this.previewResult = { ...result, highlight_info: { ...result.highlight_info, has_pdf: !!docInfo.pdf_path, has_html: !!docInfo.html_path } }; // PDF가 있으면 PDF 미리보기, 없으면 HTML 미리보기 if (docInfo.pdf_path) { // PDF 미리보기는 iframe으로 자동 처리 console.log('PDF 미리보기 준비 완료'); } else if (docInfo.html_path) { // HTML 문서 미리보기 await this.loadHtmlPreview(result.document_id); } } catch (docError) { console.error('문서 정보 로드 실패:', docError); // 기본 내용 로드로 fallback const fullContent = await this.loadFullContent(result); if (fullContent) { this.previewResult = { ...result, content: fullContent }; } } } else { // 기타 타입 - 전체 내용 로드 const fullContent = await this.loadFullContent(result); if (fullContent) { this.previewResult = { ...result, content: fullContent }; } } } catch (error) { console.error('미리보기 로드 실패:', error); } finally { this.previewLoading = false; } }, // 전체 내용 로드 async loadFullContent(result) { try { let content = ''; switch (result.type) { case 'document': case 'document_content': try { // 문서 내용 API 호출 (HTML 응답) const response = await fetch(`/api/documents/${result.document_id}/content`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (response.ok) { const htmlContent = await response.text(); // HTML에서 텍스트만 추출 const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, 'text/html'); content = doc.body.textContent || doc.body.innerText || ''; // 너무 길면 자르기 if (content.length > 2000) { content = content.substring(0, 2000) + '...'; } } else { content = result.content; } } catch (err) { console.warn('문서 내용 로드 실패, 기본 내용 사용:', err); content = result.content; } break; case 'note': try { // 노트 내용 API 호출 const noteContent = await this.api.get(`/note-documents/${result.id}/content`); content = noteContent; } catch (err) { console.warn('노트 내용 로드 실패, 기본 내용 사용:', err); content = result.content; } break; case 'memo': try { // 메모 노드 상세 정보 로드 const memoNode = await this.api.get(`/memo-trees/nodes/${result.id}`); content = memoNode.content || result.content; } catch (err) { console.warn('메모 내용 로드 실패, 기본 내용 사용:', err); content = result.content; } break; default: content = result.content; } return content; } catch (error) { console.error('내용 로드 실패:', error); return result.content; } }, // 미리보기 닫기 closePreview() { this.showPreviewModal = false; this.previewResult = null; this.previewLoading = false; this.pdfError = false; // HTML 리소스 정리 this.htmlLoading = false; this.htmlRawMode = false; this.htmlSourceCode = ''; }, // HTML 미리보기 로드 async loadHtmlPreview(documentId) { this.htmlLoading = true; try { // API를 통해 HTML 내용 가져오기 const htmlContent = await this.api.get(`/documents/${documentId}/content`); if (htmlContent) { this.htmlSourceCode = this.escapeHtml(htmlContent); // iframe에 HTML 로드 const iframe = document.getElementById('htmlPreviewFrame'); if (iframe) { // iframe src를 직접 설정 (인증 헤더 포함) const token = localStorage.getItem('token'); iframe.src = `/api/documents/${documentId}/content?_token=${encodeURIComponent(token)}`; // iframe 로드 완료 후 검색어 하이라이트 iframe.onload = () => { if (this.searchQuery) { setTimeout(() => { this.highlightInIframe(iframe, this.searchQuery); }, 100); } }; } } else { throw new Error('HTML 내용이 비어있습니다'); } } catch (error) { console.error('HTML 미리보기 로드 실패:', error); // 에러 시 기본 내용 표시 this.htmlSourceCode = `

HTML 내용을 로드할 수 없습니다.

${error.message}

`; } finally { this.htmlLoading = false; } }, // HTML 소스/렌더링 모드 토글 toggleHtmlRaw() { this.htmlRawMode = !this.htmlRawMode; }, // iframe 내부 검색어 하이라이트 highlightInIframe(iframe, query) { try { const doc = iframe.contentDocument || iframe.contentWindow.document; const walker = doc.createTreeWalker( doc.body, NodeFilter.SHOW_TEXT, null, false ); const textNodes = []; let node; while (node = walker.nextNode()) { if (node.textContent.toLowerCase().includes(query.toLowerCase())) { textNodes.push(node); } } textNodes.forEach(textNode => { const parent = textNode.parentNode; const text = textNode.textContent; const regex = new RegExp(`(${this.escapeRegExp(query)})`, 'gi'); const highlightedHTML = text.replace(regex, '$1'); const wrapper = doc.createElement('span'); wrapper.innerHTML = highlightedHTML; parent.replaceChild(wrapper, textNode); }); } catch (error) { console.error('iframe 하이라이트 실패:', error); } }, // HTML 이스케이프 escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }, // 노트 편집기에서 열기 toggleNoteEdit() { if (this.previewResult && this.previewResult.type === 'note') { const url = `/note-editor.html?id=${this.previewResult.id}`; window.open(url, '_blank'); } }, // PDF에서 검색 async searchInPdf() { if (!this.previewResult || !this.searchQuery) return; try { const searchResults = await this.api.get( `/documents/${this.previewResult.document_id}/search-in-content?q=${encodeURIComponent(this.searchQuery)}` ); if (searchResults.total_matches > 0) { // 첫 번째 매치로 이동하여 뷰어에서 열기 const firstMatch = searchResults.matches[0]; let url = `/viewer.html?id=${this.previewResult.document_id}`; if (firstMatch.page > 1) { url += `&page=${firstMatch.page}`; } // 검색어 하이라이트를 위한 파라미터 추가 url += `&search=${encodeURIComponent(this.searchQuery)}`; window.open(url, '_blank'); this.closePreview(); } else { alert('PDF에서 검색 결과를 찾을 수 없습니다.'); } } catch (error) { console.error('PDF 검색 실패:', error); alert('PDF 검색 중 오류가 발생했습니다.'); } }, // 검색 결과 열기 openResult(result) { console.log('📂 검색 결과 열기:', result); let url = ''; switch (result.type) { case 'document': case 'document_content': url = `/viewer.html?id=${result.document_id}`; if (result.highlight_info) { // 하이라이트 위치로 이동 const { start_offset, end_offset, selected_text } = result.highlight_info; url += `&highlight_text=${encodeURIComponent(selected_text)}&start_offset=${start_offset}&end_offset=${end_offset}`; } break; case 'note': url = `/viewer.html?id=${result.id}&contentType=note`; break; case 'memo': // 메모 트리에서 해당 노드로 이동 url = `/memo-tree.html?node_id=${result.id}`; break; case 'highlight': case 'highlight_note': url = `/viewer.html?id=${result.document_id}`; if (result.highlight_info) { const { start_offset, end_offset, selected_text } = result.highlight_info; url += `&highlight_text=${encodeURIComponent(selected_text)}&start_offset=${start_offset}&end_offset=${end_offset}`; } break; default: console.warn('알 수 없는 결과 타입:', result.type); return; } // 새 탭에서 열기 window.open(url, '_blank'); }, // 타입별 결과 개수 getResultCount(type) { return this.searchResults.filter(result => result.type === type).length; }, // 타입 라벨 getTypeLabel(type) { const labels = { document: '문서', document_content: '본문', note: '노트', memo: '메모', highlight: '하이라이트', highlight_note: '메모' }; return labels[type] || type; }, // 텍스트 하이라이트 highlightText(text, query) { if (!text || !query) return text; const regex = new RegExp(`(${this.escapeRegExp(query)})`, 'gi'); return text.replace(regex, '$1'); }, // 정규식 이스케이프 escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }, // 텍스트 자르기 truncateText(text, maxLength) { if (!text || text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; }, // 날짜 포맷팅 formatDate(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 if (diffDays <= 30) { return `${Math.ceil(diffDays / 7)}주 전`; } else if (diffDays <= 365) { return `${Math.ceil(diffDays / 30)}개월 전`; } else { return date.toLocaleDateString('ko-KR'); } } }; }; console.log('🔍 검색 JavaScript 로드 완료');