🚀 배포용: PDF 뷰어 개선 및 서적별 UI 데본씽크 스타일 적용
✨ 주요 개선사항: - PDF API 500 에러 수정 (한글 파일명 UTF-8 인코딩 처리) - PDF 뷰어 기능 완전 구현 (PDF.js 통합, 네비게이션, 확대/축소) - 서적별 문서 그룹화 UI 데본씽크 스타일로 개선 - PDF Manager 페이지 서적별 보기 기능 추가 - Alpine.js 로드 순서 최적화로 JavaScript 에러 해결 🎨 UI/UX 개선: - 확장/축소 가능한 아코디언 스타일 서적 목록 - 간결하고 직관적인 데본씽크 스타일 인터페이스 - PDF 상태 표시 (HTML 연결, 서적 분류) - 반응형 디자인 및 부드러운 애니메이션 🔧 기술적 개선: - PDF.js 워커 설정 및 토큰 인증 처리 - 서적별 PDF 자동 그룹화 로직 - Alpine.js 컴포넌트 초기화 최적화
This commit is contained in:
692
frontend/static/js/search.js
Normal file
692
frontend/static/js/search.js
Normal file
@@ -0,0 +1,692 @@
|
||||
/**
|
||||
* 통합 검색 JavaScript
|
||||
*/
|
||||
|
||||
// 검색 애플리케이션 Alpine.js 컴포넌트
|
||||
window.searchApp = function() {
|
||||
return {
|
||||
// 상태 관리
|
||||
searchQuery: '',
|
||||
searchResults: [],
|
||||
filteredResults: [],
|
||||
loading: false,
|
||||
hasSearched: false,
|
||||
searchTime: 0,
|
||||
|
||||
// 필터링
|
||||
typeFilter: '', // '', 'document', 'note', 'memo', 'highlight'
|
||||
fileTypeFilter: '', // '', 'PDF', 'HTML'
|
||||
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];
|
||||
|
||||
// 중복 ID 제거 (같은 문서의 document와 document_content가 중복될 수 있음)
|
||||
const uniqueResults = [];
|
||||
const seenIds = new Set();
|
||||
|
||||
results.forEach(result => {
|
||||
const uniqueKey = `${result.type}-${result.id}`;
|
||||
if (!seenIds.has(uniqueKey)) {
|
||||
seenIds.add(uniqueKey);
|
||||
uniqueResults.push({
|
||||
...result,
|
||||
unique_id: uniqueKey // Alpine.js x-for 키로 사용
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
results = uniqueResults;
|
||||
|
||||
// 타입 필터
|
||||
if (this.typeFilter) {
|
||||
results = results.filter(result => {
|
||||
// 문서 타입은 document와 document_content 모두 포함
|
||||
if (this.typeFilter === 'document') {
|
||||
return result.type === 'document' || result.type === 'document_content';
|
||||
}
|
||||
// 하이라이트 타입은 highlight와 highlight_note 모두 포함
|
||||
if (this.typeFilter === 'highlight') {
|
||||
return result.type === 'highlight' || result.type === 'highlight_note';
|
||||
}
|
||||
return result.type === this.typeFilter;
|
||||
});
|
||||
}
|
||||
|
||||
// 파일 타입 필터
|
||||
if (this.fileTypeFilter) {
|
||||
results = results.filter(result => {
|
||||
return result.highlight_info?.file_type === this.fileTypeFilter;
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
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, '개 결과 (타입:', this.typeFilter, ', 파일타입:', this.fileTypeFilter, ')');
|
||||
},
|
||||
|
||||
// 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 파일 src 직접 설정 (HEAD 요청 대신)
|
||||
const token = localStorage.getItem('access_token');
|
||||
console.log('🔍 토큰 디버깅:', {
|
||||
token: token,
|
||||
tokenType: typeof token,
|
||||
tokenLength: token ? token.length : 0,
|
||||
isNull: token === null,
|
||||
isStringNull: token === 'null',
|
||||
localStorage: Object.keys(localStorage)
|
||||
});
|
||||
|
||||
if (!token || token === 'null' || token === null) {
|
||||
console.error('❌ 토큰 문제:', token);
|
||||
throw new Error('인증 토큰이 없습니다. 다시 로그인해주세요.');
|
||||
}
|
||||
this.pdfSrc = `/api/documents/${documentId}/pdf?_token=${encodeURIComponent(token)}`;
|
||||
console.log('✅ PDF 미리보기 준비 완료:', this.pdfSrc);
|
||||
} 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;
|
||||
},
|
||||
|
||||
// PDF에서 검색어 찾기 (브라우저 내장 검색 활용)
|
||||
searchInPdf() {
|
||||
if (this.searchQuery && this.pdfLoaded) {
|
||||
// iframe 내에서 검색 실행 (Ctrl+F 시뮬레이션)
|
||||
const iframe = document.querySelector('#pdf-preview-iframe');
|
||||
if (iframe && iframe.contentWindow) {
|
||||
try {
|
||||
iframe.contentWindow.focus();
|
||||
// 브라우저 검색 창 열기 시도
|
||||
if (iframe.contentWindow.find) {
|
||||
iframe.contentWindow.find(this.searchQuery);
|
||||
} else {
|
||||
// 대안: 사용자에게 수동 검색 안내
|
||||
this.showNotification(`PDF에서 "${this.searchQuery}"를 찾으려면 Ctrl+F를 눌러주세요.`, 'info');
|
||||
}
|
||||
} catch (e) {
|
||||
// 보안상 직접 접근이 안 되는 경우, 사용자에게 안내
|
||||
this.showNotification(`PDF에서 "${this.searchQuery}"를 찾으려면 Ctrl+F를 눌러주세요.`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 알림 표시 (간단한 토스트)
|
||||
showNotification(message, type = 'info') {
|
||||
// 간단한 알림 구현 (실제로는 더 정교한 토스트 시스템을 사용할 수 있음)
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 px-4 py-2 rounded-lg text-white z-50 ${
|
||||
type === 'info' ? 'bg-blue-500' :
|
||||
type === 'success' ? 'bg-green-500' :
|
||||
type === 'error' ? 'bg-red-500' : 'bg-gray-500'
|
||||
}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 3초 후 자동 제거
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
// 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('access_token');
|
||||
console.log('🔍 HTML 미리보기 토큰:', token ? '있음' : '없음', token);
|
||||
if (!token || token === 'null' || token === null) {
|
||||
console.error('❌ HTML 미리보기 토큰 문제:', token);
|
||||
throw new Error('인증 토큰이 없습니다.');
|
||||
}
|
||||
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 = `<div class="p-4 text-center text-gray-500">
|
||||
<i class="fas fa-exclamation-triangle text-2xl mb-2"></i>
|
||||
<p>HTML 내용을 로드할 수 없습니다.</p>
|
||||
<p class="text-sm">${error.message}</p>
|
||||
</div>`;
|
||||
} 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, '<mark style="background: yellow; color: black;">$1</mark>');
|
||||
|
||||
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, '<span class="highlight-text">$1</span>');
|
||||
},
|
||||
|
||||
// 정규식 이스케이프
|
||||
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 로드 완료');
|
||||
Reference in New Issue
Block a user