/** * 통합 검색 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, pdfLoading: false, pdfLoaded: false, pdfSrc: '', // 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 미리보기 준비 await this.loadPdfPreview(result.document_id); } 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; // PDF 리소스 정리 this.pdfLoading = false; this.pdfLoaded = false; this.pdfSrc = ''; // HTML 리소스 정리 this.htmlLoading = false; this.htmlRawMode = false; this.htmlSourceCode = ''; }, // PDF 미리보기 로드 async loadPdfPreview(documentId) { this.pdfLoading = true; this.pdfError = false; this.pdfLoaded = false; try { // PDF 파일 존재 여부 먼저 확인 const response = await fetch(`/api/documents/${documentId}/pdf`, { method: 'HEAD', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (response.ok) { // PDF 파일이 존재하면 src 설정 const token = localStorage.getItem('token'); this.pdfSrc = `/api/documents/${documentId}/pdf?_token=${encodeURIComponent(token)}`; console.log('PDF 미리보기 준비 완료:', this.pdfSrc); } else { throw new Error(`PDF 파일을 찾을 수 없습니다 (${response.status})`); } } catch (error) { console.error('PDF 미리보기 로드 실패:', error); this.pdfError = true; } finally { this.pdfLoading = false; } }, // PDF 에러 처리 handlePdfError() { console.error('PDF iframe 로드 오류'); this.pdfError = true; this.pdfLoading = false; }, // 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}