🎨 데본씽크 스타일 통합 미리보기 완성

📱 PDF 미리보기 (PDF.js):
- 실제 PDF 렌더링 (Canvas 기반)
- 줌 인/아웃 (50% ~ 300%)
- 페이지 네비게이션 (이전/다음/직접입력)
- 고해상도 디스플레이 지원
- 검색어 위치로 뷰어 연동

📄 HTML 문서 미리보기:
- iframe 렌더링 뷰 / 소스 코드 뷰 토글
- 검색어 자동 하이라이트 (iframe 내부)
- 문법 하이라이트된 소스 코드 표시
- 안전한 sandbox 모드

📝 노트 문서 미리보기:
- 제목, 생성일시 표시
- 검색어 하이라이트
- 편집기에서 열기 버튼
- 깔끔한 카드 스타일 UI

🌳 메모 트리 노드 미리보기:
- 메모 제목과 내용 표시
- 트리 정보 (소속 트리명)
- 검색어 하이라이트
- 구조화된 레이아웃

🎯 UX 개선:
- 타입별 아이콘과 색상 구분
- ESC 키 / 배경 클릭으로 닫기
- 로딩 상태 표시
- 에러 처리 및 fallback
- 반응형 디자인
This commit is contained in:
Hyungi Ahn
2025-09-02 17:14:09 +09:00
parent 4b65d45584
commit 960ee84356
2 changed files with 418 additions and 16 deletions

View File

@@ -26,6 +26,18 @@ window.searchApp = function() {
previewLoading: false,
pdfError: false,
// PDF 뷰어 상태
pdfLoading: false,
pdfDoc: null,
pdfCurrentPage: 1,
pdfTotalPages: 0,
pdfZoom: 1.0,
// HTML 뷰어 상태
htmlLoading: false,
htmlRawMode: false,
htmlSourceCode: '',
// 인증 상태
isAuthenticated: false,
currentUser: null,
@@ -194,9 +206,27 @@ window.searchApp = function() {
this.previewLoading = true;
try {
// 추가 내용 로드 (필요한 경우)
if (result.type === 'document' || result.type === 'note') {
// 전체 내용 로드
// 타입별 미리보기 로드
if (result.type === 'document_content' && result.highlight_info?.has_pdf) {
// PDF 미리보기
await this.loadPdfPreview(result.document_id);
} else if ((result.type === 'document' || result.type === 'document_content') && !result.highlight_info?.has_pdf) {
// HTML 문서 미리보기
await this.loadHtmlPreview(result.document_id);
} else if (result.type === 'note') {
// 노트 미리보기 - 전체 내용 로드
const fullContent = await this.loadFullContent(result);
if (fullContent) {
this.previewResult = { ...result, content: fullContent };
}
} else if (result.type === 'memo') {
// 메모 미리보기 - 전체 내용 로드
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 };
@@ -283,6 +313,229 @@ window.searchApp = function() {
this.previewResult = null;
this.previewLoading = false;
this.pdfError = false;
// PDF 리소스 정리
this.pdfDoc = null;
this.pdfCurrentPage = 1;
this.pdfTotalPages = 0;
this.pdfZoom = 1.0;
this.pdfLoading = false;
// HTML 리소스 정리
this.htmlLoading = false;
this.htmlRawMode = false;
this.htmlSourceCode = '';
},
// PDF 미리보기 로드
async loadPdfPreview(documentId) {
this.pdfLoading = true;
this.pdfError = false;
try {
// PDF.js 워커 설정
if (typeof pdfjsLib !== 'undefined') {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
}
const pdfUrl = `/api/documents/${documentId}/pdf`;
// PDF 문서 로드
const loadingTask = pdfjsLib.getDocument({
url: pdfUrl,
httpHeaders: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
this.pdfDoc = await loadingTask.promise;
this.pdfTotalPages = this.pdfDoc.numPages;
this.pdfCurrentPage = 1;
console.log('PDF 로드 완료:', this.pdfTotalPages, '페이지');
// 첫 페이지 렌더링
await this.renderPdfPage(1);
} catch (error) {
console.error('PDF 로드 실패:', error);
this.pdfError = true;
} finally {
this.pdfLoading = false;
}
},
// PDF 페이지 렌더링
async renderPdfPage(pageNum) {
if (!this.pdfDoc) return;
try {
const page = await this.pdfDoc.getPage(pageNum);
const canvas = document.getElementById('pdfCanvas');
const context = canvas.getContext('2d');
// 뷰포트 설정 (줌 적용)
const viewport = page.getViewport({ scale: this.pdfZoom });
// 캔버스 크기 설정
canvas.height = viewport.height;
canvas.width = viewport.width;
// 고해상도 디스플레이 지원
const devicePixelRatio = window.devicePixelRatio || 1;
canvas.style.width = viewport.width + 'px';
canvas.style.height = viewport.height + 'px';
canvas.width = viewport.width * devicePixelRatio;
canvas.height = viewport.height * devicePixelRatio;
context.scale(devicePixelRatio, devicePixelRatio);
// 페이지 렌더링
const renderContext = {
canvasContext: context,
viewport: viewport
};
await page.render(renderContext).promise;
console.log('페이지 렌더링 완료:', pageNum);
} catch (error) {
console.error('페이지 렌더링 실패:', error);
this.pdfError = true;
}
},
// 줌 인
async zoomIn() {
if (this.pdfZoom < 3.0) {
this.pdfZoom += 0.25;
await this.renderPdfPage(this.pdfCurrentPage);
}
},
// 줌 아웃
async zoomOut() {
if (this.pdfZoom > 0.5) {
this.pdfZoom -= 0.25;
await this.renderPdfPage(this.pdfCurrentPage);
}
},
// 이전 페이지
async prevPage() {
if (this.pdfCurrentPage > 1) {
this.pdfCurrentPage--;
await this.renderPdfPage(this.pdfCurrentPage);
}
},
// 다음 페이지
async nextPage() {
if (this.pdfCurrentPage < this.pdfTotalPages) {
this.pdfCurrentPage++;
await this.renderPdfPage(this.pdfCurrentPage);
}
},
// 특정 페이지로 이동
async goToPage(pageNum) {
const page = parseInt(pageNum);
if (page >= 1 && page <= this.pdfTotalPages) {
this.pdfCurrentPage = page;
await this.renderPdfPage(this.pdfCurrentPage);
}
},
// HTML 미리보기 로드
async loadHtmlPreview(documentId) {
this.htmlLoading = true;
try {
// HTML 내용 가져오기
const response = await fetch(`/api/documents/${documentId}/content`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
const htmlContent = await response.text();
this.htmlSourceCode = this.escapeHtml(htmlContent);
// iframe에 HTML 로드
const iframe = document.getElementById('htmlPreviewFrame');
if (iframe) {
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(htmlContent);
doc.close();
// 검색어 하이라이트 (iframe 내부)
if (this.searchQuery) {
this.highlightInIframe(iframe, this.searchQuery);
}
}
} else {
throw new Error('HTML 로드 실패');
}
} catch (error) {
console.error('HTML 미리보기 로드 실패:', error);
} 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에서 검색