🚀 배포용: 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:
310
frontend/static/js/notebooks.js
Normal file
310
frontend/static/js/notebooks.js
Normal file
@@ -0,0 +1,310 @@
|
||||
// 노트북 관리 애플리케이션 컴포넌트
|
||||
window.notebooksApp = () => ({
|
||||
// 상태 관리
|
||||
notebooks: [],
|
||||
stats: null,
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: '',
|
||||
|
||||
// 알림 시스템
|
||||
notification: {
|
||||
show: false,
|
||||
message: '',
|
||||
type: 'info' // 'success', 'error', 'info'
|
||||
},
|
||||
|
||||
// 필터링
|
||||
searchQuery: '',
|
||||
activeOnly: true,
|
||||
sortBy: 'updated_at',
|
||||
|
||||
// 검색 디바운스
|
||||
searchTimeout: null,
|
||||
|
||||
// 모달 상태
|
||||
showCreateModal: false,
|
||||
showEditModal: false,
|
||||
showDeleteModal: false,
|
||||
editingNotebook: null,
|
||||
deletingNotebook: null,
|
||||
deleting: false,
|
||||
|
||||
// 노트북 폼
|
||||
notebookForm: {
|
||||
title: '',
|
||||
description: '',
|
||||
color: '#3B82F6',
|
||||
icon: 'book',
|
||||
is_active: true,
|
||||
sort_order: 0
|
||||
},
|
||||
|
||||
// 인증 상태
|
||||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
|
||||
// API 클라이언트
|
||||
api: null,
|
||||
|
||||
// 색상 옵션
|
||||
availableColors: [
|
||||
'#3B82F6', // blue
|
||||
'#10B981', // emerald
|
||||
'#F59E0B', // amber
|
||||
'#EF4444', // red
|
||||
'#8B5CF6', // violet
|
||||
'#06B6D4', // cyan
|
||||
'#84CC16', // lime
|
||||
'#F97316', // orange
|
||||
'#EC4899', // pink
|
||||
'#6B7280' // gray
|
||||
],
|
||||
|
||||
// 아이콘 옵션
|
||||
availableIcons: [
|
||||
{ value: 'book', label: '📖 책' },
|
||||
{ value: 'sticky-note', label: '📝 노트' },
|
||||
{ value: 'lightbulb', label: '💡 아이디어' },
|
||||
{ value: 'graduation-cap', label: '🎓 학습' },
|
||||
{ value: 'briefcase', label: '💼 업무' },
|
||||
{ value: 'heart', label: '❤️ 개인' },
|
||||
{ value: 'code', label: '💻 개발' },
|
||||
{ value: 'palette', label: '🎨 창작' },
|
||||
{ value: 'flask', label: '🧪 연구' },
|
||||
{ value: 'star', label: '⭐ 즐겨찾기' }
|
||||
],
|
||||
|
||||
// 초기화
|
||||
async init() {
|
||||
console.log('📚 Notebooks App 초기화 시작');
|
||||
|
||||
// API 클라이언트 초기화
|
||||
this.api = new DocumentServerAPI();
|
||||
|
||||
// 인증 상태 확인
|
||||
await this.checkAuthStatus();
|
||||
|
||||
if (this.isAuthenticated) {
|
||||
await this.loadStats();
|
||||
await this.loadNotebooks();
|
||||
}
|
||||
|
||||
// 헤더 로드
|
||||
await this.loadHeader();
|
||||
},
|
||||
|
||||
// 인증 상태 확인
|
||||
async checkAuthStatus() {
|
||||
try {
|
||||
const user = await this.api.getCurrentUser();
|
||||
this.isAuthenticated = true;
|
||||
this.currentUser = user;
|
||||
console.log('✅ 인증됨:', user.username || user.email);
|
||||
} catch (error) {
|
||||
console.log('❌ 인증되지 않음');
|
||||
this.isAuthenticated = false;
|
||||
this.currentUser = null;
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
|
||||
// 헤더 로드
|
||||
async loadHeader() {
|
||||
try {
|
||||
if (typeof loadHeaderComponent === 'function') {
|
||||
await loadHeaderComponent();
|
||||
} else if (typeof window.loadHeaderComponent === 'function') {
|
||||
await window.loadHeaderComponent();
|
||||
} else {
|
||||
console.warn('헤더 로더 함수를 찾을 수 없습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('헤더 로드 실패:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 통계 정보 로드
|
||||
async loadStats() {
|
||||
try {
|
||||
this.stats = await this.api.getNotebookStats();
|
||||
console.log('📊 노트북 통계 로드됨:', this.stats);
|
||||
} catch (error) {
|
||||
console.error('통계 로드 실패:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 노트북 목록 로드
|
||||
async loadNotebooks() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const queryParams = {
|
||||
active_only: this.activeOnly,
|
||||
sort_by: this.sortBy,
|
||||
order: 'desc'
|
||||
};
|
||||
|
||||
if (this.searchQuery) {
|
||||
queryParams.search = this.searchQuery;
|
||||
}
|
||||
|
||||
this.notebooks = await this.api.getNotebooks(queryParams);
|
||||
console.log('📚 노트북 로드됨:', this.notebooks.length, '개');
|
||||
} catch (error) {
|
||||
console.error('노트북 로드 실패:', error);
|
||||
this.error = '노트북을 불러오는 중 오류가 발생했습니다.';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 검색 디바운스
|
||||
debounceSearch() {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.loadNotebooks();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// 새로고침
|
||||
async refreshNotebooks() {
|
||||
await Promise.all([
|
||||
this.loadStats(),
|
||||
this.loadNotebooks()
|
||||
]);
|
||||
},
|
||||
|
||||
// 노트북 열기 (노트 목록으로 이동)
|
||||
openNotebook(notebook) {
|
||||
window.location.href = `/notes.html?notebook_id=${notebook.id}¬ebook_name=${encodeURIComponent(notebook.title)}`;
|
||||
},
|
||||
|
||||
// 노트북에 노트 생성
|
||||
createNoteInNotebook(notebook) {
|
||||
window.location.href = `/note-editor.html?notebook_id=${notebook.id}¬ebook_name=${encodeURIComponent(notebook.title)}`;
|
||||
},
|
||||
|
||||
// 노트북 편집
|
||||
editNotebook(notebook) {
|
||||
this.editingNotebook = notebook;
|
||||
this.notebookForm = {
|
||||
title: notebook.title,
|
||||
description: notebook.description || '',
|
||||
color: notebook.color,
|
||||
icon: notebook.icon,
|
||||
is_active: notebook.is_active,
|
||||
sort_order: notebook.sort_order
|
||||
};
|
||||
this.showEditModal = true;
|
||||
},
|
||||
|
||||
// 노트북 삭제 (모달 표시)
|
||||
deleteNotebook(notebook) {
|
||||
this.deletingNotebook = notebook;
|
||||
this.showDeleteModal = true;
|
||||
},
|
||||
|
||||
// 삭제 확인
|
||||
async confirmDeleteNotebook() {
|
||||
if (!this.deletingNotebook) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
await this.api.deleteNotebook(this.deletingNotebook.id, true); // force=true
|
||||
this.showNotification('노트북이 삭제되었습니다.', 'success');
|
||||
this.closeDeleteModal();
|
||||
await this.refreshNotebooks();
|
||||
} catch (error) {
|
||||
console.error('노트북 삭제 실패:', error);
|
||||
this.showNotification('노트북 삭제에 실패했습니다.', 'error');
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 삭제 모달 닫기
|
||||
closeDeleteModal() {
|
||||
this.showDeleteModal = false;
|
||||
this.deletingNotebook = null;
|
||||
this.deleting = false;
|
||||
},
|
||||
|
||||
// 노트북 저장
|
||||
async saveNotebook() {
|
||||
if (!this.notebookForm.title.trim()) {
|
||||
this.showNotification('제목을 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
if (this.showEditModal && this.editingNotebook) {
|
||||
// 편집
|
||||
await this.api.updateNotebook(this.editingNotebook.id, this.notebookForm);
|
||||
this.showNotification('노트북이 수정되었습니다.', 'success');
|
||||
} else {
|
||||
// 생성
|
||||
await this.api.createNotebook(this.notebookForm);
|
||||
this.showNotification('노트북이 생성되었습니다.', 'success');
|
||||
}
|
||||
|
||||
this.closeModal();
|
||||
await this.refreshNotebooks();
|
||||
} catch (error) {
|
||||
console.error('노트북 저장 실패:', error);
|
||||
this.showNotification('노트북 저장에 실패했습니다.', 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 모달 닫기
|
||||
closeModal() {
|
||||
this.showCreateModal = false;
|
||||
this.showEditModal = false;
|
||||
this.editingNotebook = null;
|
||||
this.notebookForm = {
|
||||
title: '',
|
||||
description: '',
|
||||
color: '#3B82F6',
|
||||
icon: 'book',
|
||||
is_active: true,
|
||||
sort_order: 0
|
||||
};
|
||||
},
|
||||
|
||||
// 날짜 포맷팅
|
||||
formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffTime = Math.abs(now - date);
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 1) {
|
||||
return '오늘';
|
||||
} else if (diffDays === 2) {
|
||||
return '어제';
|
||||
} else if (diffDays <= 7) {
|
||||
return `${diffDays - 1}일 전`;
|
||||
} else {
|
||||
return date.toLocaleDateString('ko-KR');
|
||||
}
|
||||
},
|
||||
|
||||
// 알림 표시
|
||||
showNotification(message, type = 'info') {
|
||||
this.notification = {
|
||||
show: true,
|
||||
message: message,
|
||||
type: type
|
||||
};
|
||||
|
||||
// 3초 후 자동으로 숨김
|
||||
setTimeout(() => {
|
||||
this.notification.show = false;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user