🚀 배포용: 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:
362
frontend/static/js/pdf-manager.js
Normal file
362
frontend/static/js/pdf-manager.js
Normal file
@@ -0,0 +1,362 @@
|
||||
// PDF 관리 애플리케이션 컴포넌트
|
||||
window.pdfManagerApp = () => ({
|
||||
// 상태 관리
|
||||
pdfDocuments: [],
|
||||
allDocuments: [],
|
||||
loading: false,
|
||||
error: '',
|
||||
filterType: 'all', // 'all', 'book', 'linked', 'standalone'
|
||||
viewMode: 'books', // 'list', 'books'
|
||||
groupedPDFs: [], // 서적별 그룹화된 PDF 목록
|
||||
|
||||
// 인증 상태
|
||||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
|
||||
// PDF 미리보기 상태
|
||||
showPreviewModal: false,
|
||||
previewPdf: null,
|
||||
pdfPreviewSrc: '',
|
||||
pdfPreviewLoading: false,
|
||||
pdfPreviewError: false,
|
||||
pdfPreviewLoaded: false,
|
||||
|
||||
// 초기화
|
||||
async init() {
|
||||
console.log('🚀 PDF Manager App 초기화 시작');
|
||||
|
||||
// 인증 상태 확인
|
||||
await this.checkAuthStatus();
|
||||
|
||||
if (this.isAuthenticated) {
|
||||
await this.loadPDFs();
|
||||
}
|
||||
|
||||
// 헤더 로드
|
||||
await this.loadHeader();
|
||||
},
|
||||
|
||||
// 인증 상태 확인
|
||||
async checkAuthStatus() {
|
||||
try {
|
||||
const user = await window.api.getCurrentUser();
|
||||
this.isAuthenticated = true;
|
||||
this.currentUser = user;
|
||||
console.log('✅ 인증됨:', user.username);
|
||||
} catch (error) {
|
||||
console.log('❌ 인증되지 않음');
|
||||
this.isAuthenticated = false;
|
||||
this.currentUser = null;
|
||||
window.location.href = '/login.html';
|
||||
}
|
||||
},
|
||||
|
||||
// 헤더 로드
|
||||
async loadHeader() {
|
||||
try {
|
||||
await window.headerLoader.loadHeader();
|
||||
} catch (error) {
|
||||
console.error('헤더 로드 실패:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// PDF 파일들 로드
|
||||
async loadPDFs() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
// 모든 문서 가져오기
|
||||
this.allDocuments = await window.api.getDocuments();
|
||||
|
||||
// PDF 파일들만 필터링
|
||||
this.pdfDocuments = this.allDocuments.filter(doc =>
|
||||
(doc.original_filename && doc.original_filename.toLowerCase().endsWith('.pdf')) ||
|
||||
(doc.pdf_path && doc.pdf_path !== '') ||
|
||||
(doc.html_path === null && doc.pdf_path) // PDF만 업로드된 경우
|
||||
);
|
||||
|
||||
// 연결 상태 및 서적 정보 확인
|
||||
this.pdfDocuments.forEach(pdf => {
|
||||
// 이 PDF를 참조하는 다른 문서가 있는지 확인
|
||||
const linkedDocuments = this.allDocuments.filter(doc =>
|
||||
doc.matched_pdf_id === pdf.id
|
||||
);
|
||||
pdf.isLinked = linkedDocuments.length > 0;
|
||||
pdf.linkedDocuments = linkedDocuments;
|
||||
|
||||
// 서적 정보 추가 (PDF가 속한 서적 또는 연결된 문서의 서적)
|
||||
if (pdf.book_title) {
|
||||
// PDF 자체가 서적에 속한 경우
|
||||
pdf.book_title = pdf.book_title;
|
||||
} else if (linkedDocuments.length > 0) {
|
||||
// 연결된 문서가 있는 경우, 첫 번째 연결 문서의 서적 정보 사용
|
||||
const firstLinked = linkedDocuments[0];
|
||||
pdf.book_title = firstLinked.book_title;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📕 PDF 문서들:', this.pdfDocuments.length, '개');
|
||||
console.log('📚 서적 포함 PDF:', this.bookPDFs, '개');
|
||||
console.log('🔗 HTML 연결 PDF:', this.linkedPDFs, '개');
|
||||
console.log('📄 독립 PDF:', this.standalonePDFs, '개');
|
||||
|
||||
// 디버깅: PDF 서적 정보 확인
|
||||
this.pdfDocuments.slice(0, 5).forEach(pdf => {
|
||||
console.log(`📋 ${pdf.title}: 서적=${pdf.book_title || '없음'}, 연결=${pdf.isLinked ? '예' : '아니오'}`);
|
||||
});
|
||||
|
||||
// 서적별 그룹화 실행
|
||||
this.groupPDFsByBook();
|
||||
|
||||
} catch (error) {
|
||||
console.error('PDF 로드 실패:', error);
|
||||
this.error = 'PDF 파일을 불러오는데 실패했습니다: ' + error.message;
|
||||
this.pdfDocuments = [];
|
||||
this.groupedPDFs = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 필터링된 PDF 목록
|
||||
get filteredPDFs() {
|
||||
switch (this.filterType) {
|
||||
case 'book':
|
||||
return this.pdfDocuments.filter(pdf => pdf.book_title);
|
||||
case 'linked':
|
||||
return this.pdfDocuments.filter(pdf => pdf.isLinked);
|
||||
case 'standalone':
|
||||
return this.pdfDocuments.filter(pdf => !pdf.isLinked && !pdf.book_title);
|
||||
default:
|
||||
return this.pdfDocuments;
|
||||
}
|
||||
},
|
||||
|
||||
// 통계 계산
|
||||
get bookPDFs() {
|
||||
return this.pdfDocuments.filter(pdf => pdf.book_title).length;
|
||||
},
|
||||
|
||||
get linkedPDFs() {
|
||||
return this.pdfDocuments.filter(pdf => pdf.isLinked).length;
|
||||
},
|
||||
|
||||
get standalonePDFs() {
|
||||
return this.pdfDocuments.filter(pdf => !pdf.isLinked && !pdf.book_title).length;
|
||||
},
|
||||
|
||||
// PDF 새로고침
|
||||
async refreshPDFs() {
|
||||
await this.loadPDFs();
|
||||
},
|
||||
|
||||
// 서적별 PDF 그룹화
|
||||
groupPDFsByBook() {
|
||||
if (!this.pdfDocuments || this.pdfDocuments.length === 0) {
|
||||
this.groupedPDFs = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const grouped = {};
|
||||
|
||||
this.pdfDocuments.forEach(pdf => {
|
||||
const bookKey = pdf.book_id || 'no-book';
|
||||
if (!grouped[bookKey]) {
|
||||
grouped[bookKey] = {
|
||||
book: pdf.book_id ? {
|
||||
id: pdf.book_id,
|
||||
title: pdf.book_title,
|
||||
author: pdf.book_author
|
||||
} : null,
|
||||
pdfs: [],
|
||||
linkedCount: 0,
|
||||
expanded: false // 기본적으로 축소된 상태
|
||||
};
|
||||
}
|
||||
grouped[bookKey].pdfs.push(pdf);
|
||||
if (pdf.isLinked) {
|
||||
grouped[bookKey].linkedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
// 배열로 변환하고 정렬 (서적 있는 것 먼저, 그 다음 서적명 순)
|
||||
this.groupedPDFs = Object.values(grouped).sort((a, b) => {
|
||||
if (!a.book && b.book) return 1;
|
||||
if (a.book && !b.book) return -1;
|
||||
if (!a.book && !b.book) return 0;
|
||||
return a.book.title.localeCompare(b.book.title);
|
||||
});
|
||||
|
||||
console.log('📚 서적별 PDF 그룹화 완료:', this.groupedPDFs.length, '개 그룹');
|
||||
|
||||
// 디버깅: 그룹화 결과 확인
|
||||
this.groupedPDFs.forEach((group, index) => {
|
||||
console.log(`📖 그룹 ${index + 1}: ${group.book?.title || 'PDF 미분류'} (${group.pdfs.length}개 PDF, ${group.linkedCount}개 연결됨)`);
|
||||
});
|
||||
},
|
||||
|
||||
// PDF 다운로드
|
||||
async downloadPDF(pdf) {
|
||||
try {
|
||||
console.log('📕 PDF 다운로드 시작:', pdf.id);
|
||||
|
||||
// PDF 파일 다운로드 URL 생성
|
||||
const downloadUrl = `/api/documents/${pdf.id}/download`;
|
||||
|
||||
// 인증 헤더 추가를 위해 fetch 사용
|
||||
const response = await fetch(downloadUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('PDF 다운로드에 실패했습니다');
|
||||
}
|
||||
|
||||
// Blob으로 변환하여 다운로드
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// 다운로드 링크 생성 및 클릭
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = pdf.original_filename || `${pdf.title}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// URL 정리
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
console.log('✅ PDF 다운로드 완료');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ PDF 다운로드 실패:', error);
|
||||
alert('PDF 다운로드에 실패했습니다: ' + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
// PDF 삭제
|
||||
async deletePDF(pdf) {
|
||||
// 연결된 문서가 있는지 확인
|
||||
if (pdf.isLinked && pdf.linkedDocuments.length > 0) {
|
||||
const linkedTitles = pdf.linkedDocuments.map(doc => doc.title).join('\n- ');
|
||||
const confirmMessage = `이 PDF는 다음 문서들과 연결되어 있습니다:\n\n- ${linkedTitles}\n\n정말 삭제하시겠습니까? 연결된 문서들의 PDF 링크가 해제됩니다.`;
|
||||
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!confirm(`"${pdf.title}" PDF 파일을 삭제하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await window.api.deleteDocument(pdf.id);
|
||||
|
||||
// 목록에서 제거
|
||||
this.pdfDocuments = this.pdfDocuments.filter(p => p.id !== pdf.id);
|
||||
|
||||
this.showNotification('PDF 파일이 삭제되었습니다', 'success');
|
||||
|
||||
// 목록 새로고침 (연결 상태 업데이트를 위해)
|
||||
await this.loadPDFs();
|
||||
|
||||
} catch (error) {
|
||||
console.error('PDF 삭제 실패:', error);
|
||||
this.showNotification('PDF 삭제에 실패했습니다: ' + error.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== PDF 미리보기 관련 ====================
|
||||
async previewPDF(pdf) {
|
||||
console.log('👁️ PDF 미리보기:', pdf.title);
|
||||
|
||||
this.previewPdf = pdf;
|
||||
this.showPreviewModal = true;
|
||||
this.pdfPreviewLoading = true;
|
||||
this.pdfPreviewError = false;
|
||||
this.pdfPreviewLoaded = false;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token || token === 'null' || token === null) {
|
||||
throw new Error('인증 토큰이 없습니다. 다시 로그인해주세요.');
|
||||
}
|
||||
|
||||
// PDF 미리보기 URL 설정
|
||||
this.pdfPreviewSrc = `/api/documents/${pdf.id}/pdf?_token=${encodeURIComponent(token)}`;
|
||||
console.log('✅ PDF 미리보기 준비 완료:', this.pdfPreviewSrc);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ PDF 미리보기 로드 실패:', error);
|
||||
this.pdfPreviewError = true;
|
||||
this.showNotification('PDF 미리보기 로드에 실패했습니다: ' + error.message, 'error');
|
||||
} finally {
|
||||
this.pdfPreviewLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
closePreview() {
|
||||
this.showPreviewModal = false;
|
||||
this.previewPdf = null;
|
||||
this.pdfPreviewSrc = '';
|
||||
this.pdfPreviewLoading = false;
|
||||
this.pdfPreviewError = false;
|
||||
this.pdfPreviewLoaded = false;
|
||||
},
|
||||
|
||||
handlePdfPreviewError() {
|
||||
console.error('❌ PDF 미리보기 iframe 로드 오류');
|
||||
this.pdfPreviewError = true;
|
||||
this.pdfPreviewLoading = false;
|
||||
},
|
||||
|
||||
async retryPdfPreview() {
|
||||
if (this.previewPdf) {
|
||||
await this.previewPDF(this.previewPdf);
|
||||
}
|
||||
},
|
||||
|
||||
// 날짜 포맷팅
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
// 알림 표시
|
||||
showNotification(message, type = 'info') {
|
||||
console.log(`${type.toUpperCase()}: ${message}`);
|
||||
|
||||
// 간단한 토스트 알림 생성
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
|
||||
type === 'success' ? 'bg-green-600' :
|
||||
type === 'error' ? 'bg-red-600' : 'bg-blue-600'
|
||||
}`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 3초 후 제거
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('📄 PDF Manager 페이지 로드됨');
|
||||
});
|
||||
Reference in New Issue
Block a user