✨ 주요 개선사항: - PDF API 500 에러 수정 (한글 파일명 UTF-8 인코딩 처리) - PDF 뷰어 기능 완전 구현 (PDF.js 통합, 네비게이션, 확대/축소) - 서적별 문서 그룹화 UI 데본씽크 스타일로 개선 - PDF Manager 페이지 서적별 보기 기능 추가 - Alpine.js 로드 순서 최적화로 JavaScript 에러 해결 🎨 UI/UX 개선: - 확장/축소 가능한 아코디언 스타일 서적 목록 - 간결하고 직관적인 데본씽크 스타일 인터페이스 - PDF 상태 표시 (HTML 연결, 서적 분류) - 반응형 디자인 및 부드러운 애니메이션 🔧 기술적 개선: - PDF.js 워커 설정 및 토큰 인증 처리 - 서적별 PDF 자동 그룹화 로직 - Alpine.js 컴포넌트 초기화 최적화
330 lines
12 KiB
JavaScript
330 lines
12 KiB
JavaScript
// 서적 편집 애플리케이션 컴포넌트
|
|
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.html_path.includes('/documents/') // HTML은 documents 폴더에 저장됨
|
|
)
|
|
.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); // 순서대로 정렬
|
|
|
|
console.log('📄 서적 문서들:', this.documents.length, '개');
|
|
|
|
// 각 문서의 PDF 매칭 상태 확인
|
|
this.documents.forEach((doc, index) => {
|
|
console.log(`📄 문서 ${index + 1}: ${doc.title}`);
|
|
console.log(` - matched_pdf_id: ${doc.matched_pdf_id || 'null'}`);
|
|
console.log(` - sort_order: ${doc.sort_order || 'null'}`);
|
|
|
|
// null 값을 빈 문자열로 변환 (UI 바인딩을 위해)
|
|
if (doc.matched_pdf_id === null) {
|
|
doc.matched_pdf_id = "";
|
|
}
|
|
|
|
// 디버깅: 실제 값과 타입 확인
|
|
console.log(` - matched_pdf_id 타입: ${typeof doc.matched_pdf_id}`);
|
|
console.log(` - matched_pdf_id 값: "${doc.matched_pdf_id}"`);
|
|
console.log(` - 빈 문자열인가? ${doc.matched_pdf_id === ""}`);
|
|
console.log(` - null인가? ${doc.matched_pdf_id === null}`);
|
|
console.log(` - undefined인가? ${doc.matched_pdf_id === undefined}`);
|
|
});
|
|
|
|
// 사용 가능한 PDF 문서들 로드 (현재 서적의 PDF만)
|
|
console.log('🔍 현재 서적 ID:', this.bookId);
|
|
console.log('🔍 전체 문서 수:', allDocuments.length);
|
|
|
|
// PDF 문서들 먼저 필터링
|
|
const allPDFs = allDocuments.filter(doc =>
|
|
doc.pdf_path &&
|
|
doc.pdf_path.includes('/pdfs/') // PDF는 pdfs 폴더에 저장됨
|
|
);
|
|
console.log('🔍 전체 PDF 문서 수:', allPDFs.length);
|
|
|
|
// 같은 서적의 PDF 문서들만 필터링
|
|
this.availablePDFs = allPDFs.filter(doc => {
|
|
const match = String(doc.book_id) === String(this.bookId);
|
|
if (!match && allPDFs.indexOf(doc) < 5) {
|
|
console.log(`🔍 PDF "${doc.title}": book_id="${doc.book_id}" (${typeof doc.book_id}) vs bookId="${this.bookId}" (${typeof this.bookId})`);
|
|
}
|
|
return match;
|
|
});
|
|
|
|
console.log('📎 현재 서적의 PDF:', this.availablePDFs.length, '개');
|
|
console.log('📎 현재 서적 PDF 목록:', this.availablePDFs.map(pdf => ({
|
|
id: pdf.id,
|
|
title: pdf.title,
|
|
book_id: pdf.book_id,
|
|
book_title: pdf.book_title
|
|
})));
|
|
|
|
// 각 PDF의 ID 확인
|
|
this.availablePDFs.forEach((pdf, index) => {
|
|
console.log(`📎 PDF ${index + 1}: ID="${pdf.id}", 제목="${pdf.title}"`);
|
|
});
|
|
|
|
// 디버깅: 다른 서적의 PDF들도 확인
|
|
const otherBookPDFs = allPDFs.filter(doc => doc.book_id !== this.bookId);
|
|
console.log('🔍 다른 서적의 PDF:', otherBookPDFs.length, '개');
|
|
if (otherBookPDFs.length > 0) {
|
|
console.log('🔍 다른 서적 PDF 예시:', otherBookPDFs.slice(0, 3).map(pdf => ({
|
|
title: pdf.title,
|
|
book_id: pdf.book_id,
|
|
book_title: pdf.book_title
|
|
})));
|
|
}
|
|
|
|
// Alpine.js DOM 업데이트 강제 실행
|
|
this.$nextTick(() => {
|
|
console.log('🔄 Alpine.js DOM 업데이트 완료');
|
|
// DOM이 완전히 렌더링된 후 실행
|
|
setTimeout(() => {
|
|
this.documents.forEach((doc, index) => {
|
|
if (doc.matched_pdf_id) {
|
|
console.log(`🔧 문서 ${index + 1} 강제 업데이트: ${doc.matched_pdf_id}`);
|
|
// Alpine.js 반응성 트리거
|
|
const oldValue = doc.matched_pdf_id;
|
|
doc.matched_pdf_id = "";
|
|
doc.matched_pdf_id = oldValue;
|
|
}
|
|
});
|
|
}, 100);
|
|
});
|
|
|
|
} 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;
|
|
console.log('💾 저장 시작...');
|
|
|
|
try {
|
|
// 저장 전에 순서 업데이트
|
|
this.updateDisplayOrder();
|
|
|
|
// 서적 정보 업데이트
|
|
console.log('📚 서적 정보 업데이트 중...');
|
|
await window.api.updateBook(this.bookId, {
|
|
title: this.bookInfo.title,
|
|
author: this.bookInfo.author,
|
|
description: this.bookInfo.description
|
|
});
|
|
console.log('✅ 서적 정보 업데이트 완료');
|
|
|
|
// 각 문서의 순서와 PDF 매칭 정보 업데이트
|
|
console.log('📄 문서 업데이트 시작...');
|
|
const updatePromises = this.documents.map((doc, index) => {
|
|
console.log(`📄 문서 ${index + 1}/${this.documents.length}: ${doc.title}`);
|
|
console.log(` - sort_order: ${doc.sort_order}`);
|
|
console.log(` - matched_pdf_id: ${doc.matched_pdf_id || 'null'}`);
|
|
|
|
return window.api.updateDocument(doc.id, {
|
|
sort_order: doc.sort_order,
|
|
matched_pdf_id: doc.matched_pdf_id === "" || doc.matched_pdf_id === null ? null : doc.matched_pdf_id
|
|
});
|
|
});
|
|
|
|
const results = await Promise.all(updatePromises);
|
|
console.log('✅ 모든 문서 업데이트 완료:', results.length, '개');
|
|
|
|
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 페이지 로드됨');
|
|
});
|