// 업로드 애플리케이션 컴포넌트 window.uploadApp = () => ({ // 상태 관리 currentStep: 1, selectedFiles: [], uploadedDocuments: [], pdfFiles: [], // 업로드 상태 uploading: false, finalizing: false, // 인증 상태 isAuthenticated: false, currentUser: null, // 서적 관련 bookSelectionMode: 'none', bookSearchQuery: '', searchedBooks: [], selectedBook: null, newBook: { title: '', author: '', description: '' }, // Sortable 인스턴스 sortableInstance: null, // 초기화 async init() { console.log('🚀 Upload App 초기화 시작'); // 인증 상태 확인 await this.checkAuthStatus(); if (this.isAuthenticated) { // 헤더 로드 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); } }, // 드래그 오버 처리 handleDragOver(event) { event.dataTransfer.dropEffect = 'copy'; event.target.closest('.drag-area').classList.add('drag-over'); }, // 드래그 리브 처리 handleDragLeave(event) { event.target.closest('.drag-area').classList.remove('drag-over'); }, // 드롭 처리 handleDrop(event) { event.target.closest('.drag-area').classList.remove('drag-over'); const files = Array.from(event.dataTransfer.files); this.processFiles(files); }, // 파일 선택 처리 handleFileSelect(event) { const files = Array.from(event.target.files); this.processFiles(files); }, // 파일 처리 processFiles(files) { const validFiles = files.filter(file => { const isValid = file.type === 'text/html' || file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.html') || file.name.toLowerCase().endsWith('.htm') || file.name.toLowerCase().endsWith('.pdf'); if (!isValid) { console.warn('지원하지 않는 파일 형식:', file.name); } return isValid; }); // 기존 파일과 중복 체크 validFiles.forEach(file => { const isDuplicate = this.selectedFiles.some(existing => existing.name === file.name && existing.size === file.size ); if (!isDuplicate) { this.selectedFiles.push(file); } }); console.log('📁 선택된 파일:', this.selectedFiles.length, '개'); }, // 파일 제거 removeFile(index) { this.selectedFiles.splice(index, 1); }, // 파일 크기 포맷팅 formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, // 다음 단계 nextStep() { if (this.selectedFiles.length === 0) { alert('업로드할 파일을 선택해주세요.'); return; } this.currentStep = 2; }, // 이전 단계 prevStep() { this.currentStep = Math.max(1, this.currentStep - 1); }, // 서적 검색 async searchBooks() { if (!this.bookSearchQuery.trim()) { this.searchedBooks = []; return; } try { const books = await window.api.searchBooks(this.bookSearchQuery, 10); this.searchedBooks = books; } catch (error) { console.error('서적 검색 실패:', error); this.searchedBooks = []; } }, // 서적 선택 selectBook(book) { this.selectedBook = book; this.bookSearchQuery = book.title; this.searchedBooks = []; }, // 파일 업로드 async uploadFiles() { if (this.selectedFiles.length === 0) { alert('업로드할 파일이 없습니다.'); return; } // 서적 설정 검증 if (this.bookSelectionMode === 'new' && !this.newBook.title.trim()) { alert('새 서적을 생성하려면 제목을 입력해주세요.'); return; } if (this.bookSelectionMode === 'existing' && !this.selectedBook) { alert('기존 서적을 선택해주세요.'); return; } this.uploading = true; try { let bookId = null; // 서적 처리 if (this.bookSelectionMode === 'new' && this.newBook.title.trim()) { try { const newBook = await window.api.createBook({ title: this.newBook.title, author: this.newBook.author, description: this.newBook.description }); bookId = newBook.id; console.log('📚 새 서적 생성됨:', newBook.title); } catch (error) { if (error.message.includes('already exists')) { // 동일한 서적이 이미 존재하는 경우 - 선택 모달 표시 const choice = await this.showBookConflictModal(); if (choice === 'existing') { // 기존 서적 검색해서 사용 const existingBooks = await window.api.searchBooks(this.newBook.title, 10); const matchingBook = existingBooks.find(book => book.title === this.newBook.title && book.author === this.newBook.author ); if (matchingBook) { bookId = matchingBook.id; console.log('📚 기존 서적 사용:', matchingBook.title); } else { throw new Error('기존 서적을 찾을 수 없습니다.'); } } else if (choice === 'edition') { // 에디션 정보 입력받아서 새 서적 생성 const edition = await this.getEditionInfo(); if (edition) { const newBookWithEdition = await window.api.createBook({ title: `${this.newBook.title} (${edition})`, author: this.newBook.author, description: this.newBook.description }); bookId = newBookWithEdition.id; console.log('📚 에디션 서적 생성됨:', newBookWithEdition.title); } else { throw new Error('에디션 정보가 입력되지 않았습니다.'); } } else { throw new Error('사용자가 업로드를 취소했습니다.'); } } else { throw error; } } } else if (this.bookSelectionMode === 'existing' && this.selectedBook) { bookId = this.selectedBook.id; } // HTML과 PDF 파일 분리 const htmlFiles = this.selectedFiles.filter(file => file.type === 'text/html' || file.name.toLowerCase().endsWith('.html') || file.name.toLowerCase().endsWith('.htm') ); const pdfFiles = this.selectedFiles.filter(file => file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf') ); console.log('📄 HTML 파일:', htmlFiles.length, '개'); console.log('📕 PDF 파일:', pdfFiles.length, '개'); // 업로드할 파일들 처리 const uploadPromises = []; // HTML 파일 업로드 (PDF 파일이 있으면 함께 업로드) htmlFiles.forEach(async (file, index) => { const formData = new FormData(); formData.append('html_file', file); // 백엔드가 요구하는 필드명 formData.append('title', file.name.replace(/\.[^/.]+$/, "")); // 확장자 제거 formData.append('description', `업로드된 파일: ${file.name}`); 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); } 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); }); // 모든 업로드 완료 대기 const uploadedDocs = await Promise.all(uploadPromises); // HTML 문서와 PDF 문서 분리 const htmlDocuments = uploadedDocs.filter(doc => doc.html_path && doc.html_path !== null ); const pdfDocuments = uploadedDocs.filter(doc => (doc.original_filename && doc.original_filename.toLowerCase().endsWith('.pdf')) || (doc.pdf_path && !doc.html_path) ); // 업로드된 HTML 문서들만 정리 (순서 조정용) this.uploadedDocuments = htmlDocuments.map((doc, index) => ({ ...doc, display_order: index + 1, matched_pdf_id: null })); // PDF 파일들을 매칭용으로 저장 this.pdfFiles = pdfDocuments; console.log('🎉 모든 파일 업로드 완료!'); console.log('📄 HTML 문서:', this.uploadedDocuments.length, '개'); console.log('📕 PDF 문서:', this.pdfFiles.length, '개'); // 3단계로 이동 this.currentStep = 3; // 다음 틱에서 Sortable 초기화 this.$nextTick(() => { this.initSortable(); }); } catch (error) { console.error('업로드 실패:', error); alert('업로드 중 오류가 발생했습니다: ' + error.message); } finally { this.uploading = false; } }, // Sortable 초기화 initSortable() { const sortableList = document.getElementById('sortable-list'); if (sortableList && !this.sortableInstance) { this.sortableInstance = Sortable.create(sortableList, { animation: 150, ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', handle: '.cursor-move', onEnd: (evt) => { // 배열 순서 업데이트 const item = this.uploadedDocuments.splice(evt.oldIndex, 1)[0]; this.uploadedDocuments.splice(evt.newIndex, 0, item); // display_order 업데이트 this.updateDisplayOrder(); console.log('📋 드래그로 순서 변경됨'); } }); } }, // display_order 업데이트 updateDisplayOrder() { this.uploadedDocuments.forEach((doc, index) => { doc.display_order = index + 1; }); }, // 위로 이동 moveUp(index) { if (index > 0) { const item = this.uploadedDocuments.splice(index, 1)[0]; this.uploadedDocuments.splice(index - 1, 0, item); this.updateDisplayOrder(); console.log('📋 위로 이동:', item.title); } }, // 아래로 이동 moveDown(index) { if (index < this.uploadedDocuments.length - 1) { const item = this.uploadedDocuments.splice(index, 1)[0]; this.uploadedDocuments.splice(index + 1, 0, item); this.updateDisplayOrder(); console.log('📋 아래로 이동:', item.title); } }, // 이름순 정렬 autoSortByName() { this.uploadedDocuments.sort((a, b) => { return a.title.localeCompare(b.title, 'ko', { numeric: true }); }); this.updateDisplayOrder(); console.log('📋 이름순 정렬 완료'); }, // 순서 뒤집기 reverseOrder() { this.uploadedDocuments.reverse(); this.updateDisplayOrder(); console.log('📋 순서 뒤집기 완료'); }, // 섞기 shuffleDocuments() { for (let i = this.uploadedDocuments.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this.uploadedDocuments[i], this.uploadedDocuments[j]] = [this.uploadedDocuments[j], this.uploadedDocuments[i]]; } this.updateDisplayOrder(); console.log('📋 문서 섞기 완료'); }, // 최종 완료 처리 async finalizeUpload() { this.finalizing = true; try { // 문서 순서 및 PDF 매칭 정보 업데이트 const updatePromises = this.uploadedDocuments.map(async (doc) => { const updateData = { display_order: doc.display_order }; // PDF 매칭 정보 추가 (필요시 백엔드 API 확장) if (doc.matched_pdf_id) { updateData.matched_pdf_id = doc.matched_pdf_id; } return await window.api.updateDocument(doc.id, updateData); }); await Promise.all(updatePromises); console.log('🎉 업로드 완료 처리됨!'); alert('업로드가 완료되었습니다!'); // 메인 페이지로 이동 window.location.href = 'index.html'; } catch (error) { console.error('완료 처리 실패:', error); alert('완료 처리 중 오류가 발생했습니다: ' + error.message); } finally { this.finalizing = false; } }, // 업로드 재시작 resetUpload() { if (confirm('업로드를 다시 시작하시겠습니까? 현재 진행 상황이 초기화됩니다.')) { this.currentStep = 1; this.selectedFiles = []; this.uploadedDocuments = []; this.pdfFiles = []; this.bookSelectionMode = 'none'; this.selectedBook = null; this.newBook = { title: '', author: '', description: '' }; if (this.sortableInstance) { this.sortableInstance.destroy(); this.sortableInstance = null; } } }, // 뒤로가기 goBack() { if (this.currentStep > 1) { if (confirm('진행 중인 업로드를 취소하고 돌아가시겠습니까?')) { window.location.href = 'index.html'; } } else { window.location.href = 'index.html'; } }, // 서적 중복 시 선택 모달 showBookConflictModal() { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; modal.innerHTML = `

📚 서적 중복 발견

"${this.newBook.title}"${this.newBook.author ? ` (${this.newBook.author})` : ''} 서적이 이미 존재합니다.

어떻게 처리하시겠습니까?

`; document.body.appendChild(modal); // 이벤트 리스너 modal.querySelector('#use-existing').onclick = () => { document.body.removeChild(modal); resolve('existing'); }; modal.querySelector('#add-edition').onclick = () => { document.body.removeChild(modal); resolve('edition'); }; modal.querySelector('#cancel-upload').onclick = () => { document.body.removeChild(modal); resolve('cancel'); }; }); }, // 에디션 정보 입력 getEditionInfo() { return new Promise((resolve) => { const modal = document.createElement('div'); modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; modal.innerHTML = `

📖 에디션 정보 입력

서적을 구분할 에디션 정보를 입력해주세요.

💡 예시: "2nd Edition", "2024년판", "개정판", "Ver 2.0"

`; document.body.appendChild(modal); const input = modal.querySelector('#edition-input'); input.focus(); // 이벤트 리스너 const confirm = () => { const edition = input.value.trim(); document.body.removeChild(modal); resolve(edition || null); }; const cancel = () => { document.body.removeChild(modal); resolve(null); }; modal.querySelector('#confirm-edition').onclick = confirm; modal.querySelector('#cancel-edition').onclick = cancel; // Enter 키 처리 input.onkeypress = (e) => { if (e.key === 'Enter') { confirm(); } }; }); } }); // 페이지 로드 시 초기화 document.addEventListener('DOMContentLoaded', () => { console.log('📄 Upload 페이지 로드됨'); });