feat: PDF/HTML 폴더 분리 및 필터링 개선

- 업로드 시 HTML과 PDF를 별도 폴더에 저장 (/documents/, /pdfs/)
- 프론트엔드 필터링을 폴더 경로 기준으로 단순화
- PDF 삭제 시 외래키 참조 해제 로직 추가
- book-documents.js, book-editor.js 필터링 통일
- HTML 문서 목록에서 PDF 완전 분리
This commit is contained in:
Hyungi Ahn
2025-08-26 07:44:25 +09:00
parent 4038040faa
commit 04ae64fc4d
20 changed files with 1334 additions and 73 deletions

View File

@@ -8,7 +8,7 @@ class DocumentServerAPI {
this.token = localStorage.getItem('access_token');
console.log('🐳 API Base URL (DOCKER BACKEND):', this.baseURL);
console.log('🔧 도커 환경 설정 완료 - 버전 2025012380');
console.log('🔧 도커 환경 설정 완료 - 버전 2025012384');
}
// 토큰 설정
@@ -383,6 +383,10 @@ class DocumentServerAPI {
return await this.get(`/books/${bookId}`);
}
async updateBook(bookId, bookData) {
return await this.put(`/books/${bookId}`, bookData);
}
async searchBooks(query, limit = 10) {
const params = new URLSearchParams({ q: query, limit });
return await this.get(`/books/search/?${params}`);

View File

@@ -2,6 +2,7 @@
window.bookDocumentsApp = () => ({
// 상태 관리
documents: [],
availablePDFs: [],
bookInfo: {},
loading: false,
error: '',
@@ -73,15 +74,38 @@ window.bookDocumentsApp = () => ({
const allDocuments = await window.api.getDocuments();
if (this.bookId === 'none') {
// 서적 미분류 문서들
this.documents = allDocuments.filter(doc => !doc.book_id);
// 서적 미분류 HTML 문서들만 (폴더로 구분)
this.documents = allDocuments.filter(doc =>
!doc.book_id &&
doc.html_path &&
!doc.pdf_path?.includes('/pdfs/') // pdfs 폴더에 있는 건 제외
);
// 서적 미분류 PDF 문서들 (매칭용)
this.availablePDFs = allDocuments.filter(doc =>
!doc.book_id &&
doc.pdf_path &&
doc.pdf_path.includes('/pdfs/') // PDF는 pdfs 폴더에 저장됨
);
this.bookInfo = {
title: '서적 미분류',
description: '서적에 속하지 않은 문서들입니다.'
};
} else {
// 특정 서적의 문서들
this.documents = allDocuments.filter(doc => doc.book_id === this.bookId);
// 특정 서적의 HTML 문서들만 (폴더로 구분)
this.documents = allDocuments.filter(doc =>
doc.book_id === this.bookId &&
doc.html_path &&
!doc.pdf_path?.includes('/pdfs/') // pdfs 폴더에 있는 건 제외
);
// 특정 서적의 PDF 문서들 (매칭용)
this.availablePDFs = allDocuments.filter(doc =>
doc.book_id === this.bookId &&
doc.pdf_path &&
doc.pdf_path.includes('/pdfs/') // PDF는 pdfs 폴더에 저장됨
);
if (this.documents.length > 0) {
// 첫 번째 문서에서 서적 정보 추출
@@ -107,6 +131,18 @@ window.bookDocumentsApp = () => ({
}
console.log('📚 서적 문서 로드 완료:', this.documents.length, '개');
console.log('📕 사용 가능한 PDF:', this.availablePDFs.length, '개');
// 디버깅: 문서들의 original_filename 확인
console.log('🔍 문서들 확인:');
this.documents.slice(0, 5).forEach(doc => {
console.log(`- ${doc.title}: ${doc.original_filename}`);
});
console.log('🔍 PDF들 확인:');
this.availablePDFs.slice(0, 5).forEach(doc => {
console.log(`- ${doc.title}: ${doc.original_filename}`);
});
} catch (error) {
console.error('서적 문서 로드 실패:', error);
this.error = '문서를 불러오는데 실패했습니다: ' + error.message;
@@ -125,6 +161,15 @@ window.bookDocumentsApp = () => ({
window.open(`/viewer.html?id=${documentId}&from=book`, '_blank');
},
// 서적 편집 페이지 열기
openBookEditor() {
if (this.bookId === 'none') {
alert('서적 미분류 문서들은 편집할 수 없습니다.');
return;
}
window.location.href = `book-editor.html?bookId=${this.bookId}`;
},
// 문서 수정
editDocument(doc) {
// TODO: 문서 수정 모달 또는 페이지로 이동

View File

@@ -0,0 +1,246 @@
// 서적 편집 애플리케이션 컴포넌트
window.bookEditorApp = () => ({
// 상태 관리
documents: [],
bookInfo: {},
availablePDFs: [],
loading: false,
saving: false,
error: '',
// 인증 상태
isAuthenticated: false,
currentUser: null,
// URL 파라미터
bookId: null,
// SortableJS 인스턴스
sortableInstance: null,
// 초기화
async init() {
console.log('🚀 Book Editor App 초기화 시작');
// URL 파라미터 파싱
this.parseUrlParams();
// 인증 상태 확인
await this.checkAuthStatus();
if (this.isAuthenticated) {
await this.loadBookData();
this.initSortable();
}
// 헤더 로드
await this.loadHeader();
},
// URL 파라미터 파싱
parseUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
this.bookId = urlParams.get('bookId');
console.log('📖 편집할 서적 ID:', this.bookId);
},
// 인증 상태 확인
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);
}
},
// 서적 데이터 로드
async loadBookData() {
this.loading = true;
this.error = '';
try {
// 서적 정보 로드
this.bookInfo = await window.api.getBook(this.bookId);
console.log('📚 서적 정보 로드:', this.bookInfo);
// 모든 문서 가져와서 이 서적에 속한 HTML 문서들만 필터링 (폴더로 구분)
const allDocuments = await window.api.getDocuments();
this.documents = allDocuments
.filter(doc =>
doc.book_id === this.bookId &&
doc.html_path &&
!doc.pdf_path?.includes('/pdfs/') // pdfs 폴더에 있는 건 제외
)
.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); // 순서대로 정렬
console.log('📄 서적 문서들:', this.documents.length, '개');
// 사용 가능한 PDF 문서들 로드 (PDF 타입 문서들)
// PDF 문서들만 필터링 (폴더 경로 기준)
this.availablePDFs = allDocuments.filter(doc =>
doc.pdf_path &&
doc.pdf_path.includes('/pdfs/') // PDF는 pdfs 폴더에 저장됨
);
console.log('📎 사용 가능한 PDF:', this.availablePDFs.length, '개');
} catch (error) {
console.error('서적 데이터 로드 실패:', error);
this.error = '데이터를 불러오는데 실패했습니다: ' + error.message;
} finally {
this.loading = false;
}
},
// SortableJS 초기화
initSortable() {
this.$nextTick(() => {
const sortableList = document.getElementById('sortable-list');
if (sortableList && !this.sortableInstance) {
this.sortableInstance = Sortable.create(sortableList, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
handle: '.fa-grip-vertical',
onEnd: (evt) => {
// 배열 순서 업데이트
const item = this.documents.splice(evt.oldIndex, 1)[0];
this.documents.splice(evt.newIndex, 0, item);
this.updateDisplayOrder();
}
});
console.log('✅ SortableJS 초기화 완료');
}
});
},
// 표시 순서 업데이트
updateDisplayOrder() {
this.documents.forEach((doc, index) => {
doc.sort_order = index + 1;
});
console.log('🔢 표시 순서 업데이트됨');
},
// 위로 이동
moveUp(index) {
if (index > 0) {
const item = this.documents.splice(index, 1)[0];
this.documents.splice(index - 1, 0, item);
this.updateDisplayOrder();
}
},
// 아래로 이동
moveDown(index) {
if (index < this.documents.length - 1) {
const item = this.documents.splice(index, 1)[0];
this.documents.splice(index + 1, 0, item);
this.updateDisplayOrder();
}
},
// 이름순 정렬
autoSortByName() {
this.documents.sort((a, b) => {
return a.title.localeCompare(b.title, 'ko', { numeric: true });
});
this.updateDisplayOrder();
console.log('📝 이름순 정렬 완료');
},
// 순서 뒤집기
reverseOrder() {
this.documents.reverse();
this.updateDisplayOrder();
console.log('🔄 순서 뒤집기 완료');
},
// 변경사항 저장
async saveChanges() {
if (this.saving) return;
this.saving = true;
try {
// 서적 정보 업데이트
await window.api.updateBook(this.bookId, {
title: this.bookInfo.title,
author: this.bookInfo.author,
description: this.bookInfo.description
});
// 각 문서의 순서와 PDF 매칭 정보 업데이트
const updatePromises = this.documents.map(doc => {
return window.api.updateDocument(doc.id, {
sort_order: doc.sort_order,
matched_pdf_id: doc.matched_pdf_id || null
});
});
await Promise.all(updatePromises);
console.log('✅ 모든 변경사항 저장 완료');
this.showNotification('변경사항이 저장되었습니다', 'success');
// 잠시 후 서적 페이지로 돌아가기
setTimeout(() => {
this.goBack();
}, 1500);
} catch (error) {
console.error('저장 실패:', error);
this.showNotification('저장에 실패했습니다: ' + error.message, 'error');
} finally {
this.saving = false;
}
},
// 뒤로가기
goBack() {
window.location.href = `book-documents.html?bookId=${this.bookId}`;
},
// 알림 표시
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('📄 Book Editor 페이지 로드됨');
});

View File

@@ -0,0 +1,229 @@
// PDF 관리 애플리케이션 컴포넌트
window.pdfManagerApp = () => ({
// 상태 관리
pdfDocuments: [],
allDocuments: [],
loading: false,
error: '',
filterType: 'all', // 'all', 'linked', 'standalone'
// 인증 상태
isAuthenticated: false,
currentUser: null,
// 초기화
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;
});
console.log('📕 PDF 문서들:', this.pdfDocuments.length, '개');
} catch (error) {
console.error('PDF 로드 실패:', error);
this.error = 'PDF 파일을 불러오는데 실패했습니다: ' + error.message;
this.pdfDocuments = [];
} finally {
this.loading = false;
}
},
// 필터링된 PDF 목록
get filteredPDFs() {
switch (this.filterType) {
case 'linked':
return this.pdfDocuments.filter(pdf => pdf.isLinked);
case 'standalone':
return this.pdfDocuments.filter(pdf => !pdf.isLinked);
default:
return this.pdfDocuments;
}
},
// 통계 계산
get linkedPDFs() {
return this.pdfDocuments.filter(pdf => pdf.isLinked).length;
},
get standalonePDFs() {
return this.pdfDocuments.filter(pdf => !pdf.isLinked).length;
},
// PDF 새로고침
async refreshPDFs() {
await this.loadPDFs();
},
// 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');
}
},
// 날짜 포맷팅
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 페이지 로드됨');
});

View File

@@ -261,8 +261,11 @@ window.uploadApp = () => ({
console.log('📄 HTML 파일:', htmlFiles.length, '개');
console.log('📕 PDF 파일:', pdfFiles.length, '개');
// HTML 파일 업로드 (백엔드 API에 맞게)
const uploadPromises = htmlFiles.map(async (file, index) => {
// 업로드할 파일들 처리
const uploadPromises = [];
// HTML 파일 업로드 (PDF 파일이 있으면 함께 업로드)
htmlFiles.forEach(async (file, index) => {
const formData = new FormData();
formData.append('html_file', file); // 백엔드가 요구하는 필드명
formData.append('title', file.name.replace(/\.[^/.]+$/, "")); // 확장자 제거
@@ -270,52 +273,96 @@ window.uploadApp = () => ({
formData.append('language', 'ko');
formData.append('is_public', 'false');
// 같은 이름의 PDF 파일이 있는지 확인
const htmlBaseName = file.name.replace(/\.[^/.]+$/, "");
const matchingPdf = pdfFiles.find(pdfFile => {
const pdfBaseName = pdfFile.name.replace(/\.[^/.]+$/, "");
return pdfBaseName === htmlBaseName;
});
if (matchingPdf) {
formData.append('pdf_file', matchingPdf);
console.log('📎 매칭된 PDF 파일 함께 업로드:', matchingPdf.name);
}
if (bookId) {
formData.append('book_id', bookId);
}
try {
const response = await window.api.uploadDocument(formData);
console.log('✅ HTML 파일 업로드 완료:', file.name);
return response;
} catch (error) {
console.error('❌ HTML 파일 업로드 실패:', file.name, error);
throw error;
const uploadPromise = (async () => {
try {
const response = await window.api.uploadDocument(formData);
console.log('✅ HTML 파일 업로드 완료:', file.name);
return response;
} catch (error) {
console.error('❌ HTML 파일 업로드 실패:', file.name, error);
throw error;
}
})();
uploadPromises.push(uploadPromise);
});
// HTML과 매칭되지 않은 PDF 파일들을 별도로 업로드
const unmatchedPdfs = pdfFiles.filter(pdfFile => {
const pdfBaseName = pdfFile.name.replace(/\.[^/.]+$/, "");
return !htmlFiles.some(htmlFile => {
const htmlBaseName = htmlFile.name.replace(/\.[^/.]+$/, "");
return htmlBaseName === pdfBaseName;
});
});
unmatchedPdfs.forEach(async (file, index) => {
const formData = new FormData();
formData.append('html_file', file); // PDF도 html_file로 전송 (백엔드에서 처리)
formData.append('title', file.name.replace(/\.[^/.]+$/, "")); // 확장자 제거
formData.append('description', `PDF 파일: ${file.name}`);
formData.append('language', 'ko');
formData.append('is_public', 'false');
if (bookId) {
formData.append('book_id', bookId);
}
const uploadPromise = (async () => {
try {
const response = await window.api.uploadDocument(formData);
console.log('✅ PDF 파일 업로드 완료:', file.name);
return response;
} catch (error) {
console.error('❌ PDF 파일 업로드 실패:', file.name, error);
throw error;
}
})();
uploadPromises.push(uploadPromise);
});
// PDF 파일은 별도로 처리 (나중에 HTML과 매칭)
const pdfUploadPromises = pdfFiles.map(async (file, index) => {
// PDF 전용 업로드 로직 (임시로 HTML 파일로 처리하지 않음)
console.log('📕 PDF 파일 대기 중:', file.name);
return {
id: `pdf-${Date.now()}-${index}`,
title: file.name.replace(/\.[^/.]+$/, ""),
original_filename: file.name,
file_type: 'pdf',
file: file // 실제 파일 객체 보관
};
});
// 모든 업로드 완료 대기
const uploadedDocs = await Promise.all(uploadPromises);
// HTML 파일 업로드 완료 대기
const uploadedHtmlDocs = await Promise.all(uploadPromises);
// HTML 문서와 PDF 문서 분리
const htmlDocuments = uploadedDocs.filter(doc =>
doc.html_path && doc.html_path !== null
);
// PDF 파일 처리 (실제 업로드는 하지 않고 매칭용으로만 보관)
const pdfDocs = await Promise.all(pdfUploadPromises);
const pdfDocuments = uploadedDocs.filter(doc =>
(doc.original_filename && doc.original_filename.toLowerCase().endsWith('.pdf')) ||
(doc.pdf_path && !doc.html_path)
);
// 업로드된 HTML 문서 정리
this.uploadedDocuments = uploadedHtmlDocs.map((doc, index) => ({
// 업로드된 HTML 문서들만 정리 (순서 조정용)
this.uploadedDocuments = htmlDocuments.map((doc, index) => ({
...doc,
display_order: index + 1,
matched_pdf_id: null,
file_type: 'html'
matched_pdf_id: null
}));
// PDF 파일 목록 (매칭용)
this.pdfFiles = pdfDocs;
// PDF 파일들을 매칭용으로 저장
this.pdfFiles = pdfDocuments;
console.log('🎉 모든 파일 업로드 완료!');
console.log('📄 HTML 문서:', this.uploadedDocuments.filter(doc => doc.file_type === 'html').length, '개');
console.log('📄 HTML 문서:', this.uploadedDocuments.length, '개');
console.log('📕 PDF 문서:', this.pdfFiles.length, '개');
// 3단계로 이동

View File

@@ -1095,7 +1095,7 @@ window.documentViewer = () => ({
const response = await fetch(downloadUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
@@ -1124,5 +1124,66 @@ window.documentViewer = () => ({
console.error('❌ PDF 다운로드 실패:', error);
alert('PDF 다운로드에 실패했습니다: ' + error.message);
}
},
// 원본 파일 다운로드 (연결된 PDF 파일)
async downloadOriginalFile() {
if (!this.document || !this.document.id) {
console.warn('문서 정보가 없습니다');
return;
}
// 연결된 PDF가 있는지 확인
if (!this.document.matched_pdf_id) {
alert('연결된 원본 PDF 파일이 없습니다.\n\n서적 편집 페이지에서 PDF 파일을 연결해주세요.');
return;
}
try {
console.log('📕 연결된 PDF 다운로드 시작:', this.document.matched_pdf_id);
// 연결된 PDF 문서 정보 가져오기
const pdfDocument = await window.api.getDocument(this.document.matched_pdf_id);
if (!pdfDocument) {
throw new Error('연결된 PDF 문서를 찾을 수 없습니다');
}
// PDF 파일 다운로드 URL 생성
const downloadUrl = `/api/documents/${this.document.matched_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 = pdfDocument.original_filename || `${pdfDocument.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);
}
}
});