Files
document-server/frontend/static/js/note-editor.js
hyungi cfb9485d4f 🚀 배포용: 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 컴포넌트 초기화 최적화
2025-09-05 07:13:49 +09:00

350 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function noteEditorApp() {
return {
// 상태 관리
noteData: {
title: '',
content: '',
note_type: 'note',
tags: [],
is_published: false,
parent_note_id: null,
sort_order: 0,
notebook_id: null
},
// 노트북 관련
availableNotebooks: [],
// UI 상태
loading: false,
saving: false,
error: null,
isEditing: false,
noteId: null,
// 에디터 관련
quillEditor: null,
editorMode: 'wysiwyg', // 'wysiwyg' 또는 'html'
tagInput: '',
// 인증 관련
isAuthenticated: false,
currentUser: null,
// API 클라이언트
api: null,
async init() {
console.log('📝 노트 에디터 초기화 시작');
try {
// API 클라이언트 초기화
this.api = new DocumentServerAPI();
console.log('🔧 API 클라이언트 초기화됨:', this.api);
console.log('🔧 getNotebooks 메서드 존재 여부:', typeof this.api.getNotebooks);
// 헤더 로드
await this.loadHeader();
// 인증 상태 확인
await this.checkAuthStatus();
if (!this.isAuthenticated) {
window.location.href = '/';
return;
}
// URL에서 노트 ID 및 노트북 정보 확인
const urlParams = new URLSearchParams(window.location.search);
this.noteId = urlParams.get('id');
const notebookId = urlParams.get('notebook_id');
const notebookName = urlParams.get('notebook_name');
// 노트북 목록 로드
await this.loadNotebooks();
// URL에서 노트북이 지정된 경우 자동 설정
if (notebookId && !this.noteId) { // 새 노트 생성 시에만
this.noteData.notebook_id = notebookId;
console.log('📚 노트북 자동 설정:', notebookName || notebookId);
}
if (this.noteId) {
this.isEditing = true;
await this.loadNote(this.noteId);
}
// Quill 에디터 초기화
this.initQuillEditor();
console.log('✅ 노트 에디터 초기화 완료');
} catch (error) {
console.error('❌ 노트 에디터 초기화 실패:', error);
this.error = '노트 에디터를 초기화하는 중 오류가 발생했습니다.';
}
},
async loadHeader() {
try {
if (typeof loadHeaderComponent === 'function') {
await loadHeaderComponent();
} else if (typeof window.loadHeaderComponent === 'function') {
await window.loadHeaderComponent();
} else {
console.warn('헤더 로더 함수를 찾을 수 없습니다. 수동으로 헤더를 로드합니다.');
// 수동으로 헤더 로드
const headerContainer = document.getElementById('header-container');
if (headerContainer) {
const response = await fetch('/components/header.html');
const headerHTML = await response.text();
headerContainer.innerHTML = headerHTML;
}
}
} catch (error) {
console.error('헤더 로드 실패:', error);
}
},
async checkAuthStatus() {
try {
const response = await this.api.getCurrentUser();
this.isAuthenticated = true;
this.currentUser = response;
console.log('✅ 인증된 사용자:', this.currentUser.username);
} catch (error) {
console.log('❌ 인증되지 않은 사용자');
this.isAuthenticated = false;
this.currentUser = null;
}
},
async loadNotebooks() {
try {
console.log('📚 노트북 로드 시작...');
console.log('🔧 API 메서드 확인:', typeof this.api.getNotebooks);
// 임시: 직접 API 호출
this.availableNotebooks = await this.api.get('/notebooks/', { active_only: true });
console.log('📚 노트북 로드됨:', this.availableNotebooks.length, '개');
console.log('📚 노트북 데이터 상세:', this.availableNotebooks);
// 각 노트북의 필드 확인
if (this.availableNotebooks.length > 0) {
console.log('📚 첫 번째 노트북 필드:', Object.keys(this.availableNotebooks[0]));
console.log('📚 첫 번째 노트북 title:', this.availableNotebooks[0].title);
console.log('📚 첫 번째 노트북 name:', this.availableNotebooks[0].name);
}
} catch (error) {
console.error('노트북 로드 실패:', error);
this.availableNotebooks = [];
}
},
initQuillEditor() {
// Quill 에디터 설정
const toolbarOptions = [
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'font': [] }],
[{ 'size': ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'align': [] }],
['blockquote', 'code-block'],
['link', 'image', 'video'],
['clean']
];
this.quillEditor = new Quill('#quill-editor', {
theme: 'snow',
modules: {
toolbar: toolbarOptions
},
placeholder: '노트 내용을 작성하세요...'
});
// 에디터 내용 변경 시 동기화
this.quillEditor.on('text-change', () => {
if (this.editorMode === 'wysiwyg') {
this.noteData.content = this.quillEditor.root.innerHTML;
}
});
// 기존 내용이 있으면 로드
if (this.noteData.content) {
this.quillEditor.root.innerHTML = this.noteData.content;
}
},
async loadNote(noteId) {
this.loading = true;
this.error = null;
try {
console.log('📖 노트 로드 중:', noteId);
const note = await this.api.getNoteDocument(noteId);
this.noteData = {
title: note.title || '',
content: note.content || '',
note_type: note.note_type || 'note',
tags: note.tags || [],
is_published: note.is_published || false,
parent_note_id: note.parent_note_id || null,
sort_order: note.sort_order || 0
};
console.log('✅ 노트 로드 완료:', this.noteData.title);
} catch (error) {
console.error('❌ 노트 로드 실패:', error);
this.error = '노트를 불러오는 중 오류가 발생했습니다.';
} finally {
this.loading = false;
}
},
async saveNote() {
if (!this.noteData.title.trim()) {
this.showNotification('제목을 입력해주세요.', 'error');
return;
}
this.saving = true;
this.error = null;
try {
// WYSIWYG 모드에서 HTML 동기화
if (this.editorMode === 'wysiwyg' && this.quillEditor) {
this.noteData.content = this.quillEditor.root.innerHTML;
}
console.log('💾 노트 저장 중:', this.noteData.title);
let result;
if (this.isEditing && this.noteId) {
// 기존 노트 업데이트
result = await this.api.updateNoteDocument(this.noteId, this.noteData);
console.log('✅ 노트 업데이트 완료');
} else {
// 새 노트 생성
result = await this.api.createNoteDocument(this.noteData);
console.log('✅ 새 노트 생성 완료');
// 편집 모드로 전환
this.isEditing = true;
this.noteId = result.id;
// URL 업데이트 (새로고침 없이)
const newUrl = `${window.location.pathname}?id=${result.id}`;
window.history.replaceState({}, '', newUrl);
}
this.showNotification('노트가 성공적으로 저장되었습니다.', 'success');
} catch (error) {
console.error('❌ 노트 저장 실패:', error);
this.error = '노트 저장 중 오류가 발생했습니다.';
this.showNotification('노트 저장에 실패했습니다.', 'error');
} finally {
this.saving = false;
}
},
toggleEditorMode() {
if (this.editorMode === 'wysiwyg') {
// WYSIWYG → HTML 코드
if (this.quillEditor) {
this.noteData.content = this.quillEditor.root.innerHTML;
}
this.editorMode = 'html';
} else {
// HTML 코드 → WYSIWYG
if (this.quillEditor) {
this.quillEditor.root.innerHTML = this.noteData.content || '';
}
this.editorMode = 'wysiwyg';
}
},
addTag() {
const tag = this.tagInput.trim();
if (tag && !this.noteData.tags.includes(tag)) {
this.noteData.tags.push(tag);
this.tagInput = '';
}
},
removeTag(index) {
this.noteData.tags.splice(index, 1);
},
getWordCount() {
if (!this.noteData.content) return 0;
// HTML 태그 제거 후 단어 수 계산
const textContent = this.noteData.content.replace(/<[^>]*>/g, '');
return textContent.length;
},
goBack() {
// 변경사항이 있으면 확인
if (this.hasUnsavedChanges()) {
if (!confirm('저장하지 않은 변경사항이 있습니다. 정말 나가시겠습니까?')) {
return;
}
}
window.location.href = '/notes.html';
},
hasUnsavedChanges() {
// 간단한 변경사항 감지 (실제로는 더 정교하게 구현 가능)
return this.noteData.title.trim() !== '' || this.noteData.content.trim() !== '';
},
showNotification(message, type = 'info') {
// 간단한 알림 (나중에 더 정교한 토스트 시스템으로 교체 가능)
if (type === 'error') {
alert('❌ ' + message);
} else if (type === 'success') {
alert('✅ ' + message);
} else {
alert(' ' + message);
}
},
// 키보드 단축키
handleKeydown(event) {
// Ctrl+S (또는 Cmd+S): 저장
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
this.saveNote();
}
}
};
}
// 키보드 이벤트 리스너 등록
document.addEventListener('keydown', function(event) {
// Alpine.js 컴포넌트에 접근
const app = Alpine.$data(document.querySelector('[x-data]'));
if (app && app.handleKeydown) {
app.handleKeydown(event);
}
});
// 페이지 떠날 때 확인
window.addEventListener('beforeunload', function(event) {
const app = Alpine.$data(document.querySelector('[x-data]'));
if (app && app.hasUnsavedChanges && app.hasUnsavedChanges()) {
event.preventDefault();
event.returnValue = '저장하지 않은 변경사항이 있습니다.';
return event.returnValue;
}
});