/** * 통합 검색 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, // 인증 상태 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 === 'note') { // 전체 내용 로드 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; }, // 검색 결과 열기 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 로드 완료');