✨ 주요 개선사항: - PDF API 500 에러 수정 (한글 파일명 UTF-8 인코딩 처리) - PDF 뷰어 기능 완전 구현 (PDF.js 통합, 네비게이션, 확대/축소) - 서적별 문서 그룹화 UI 데본씽크 스타일로 개선 - PDF Manager 페이지 서적별 보기 기능 추가 - Alpine.js 로드 순서 최적화로 JavaScript 에러 해결 🎨 UI/UX 개선: - 확장/축소 가능한 아코디언 스타일 서적 목록 - 간결하고 직관적인 데본씽크 스타일 인터페이스 - PDF 상태 표시 (HTML 연결, 서적 분류) - 반응형 디자인 및 부드러운 애니메이션 🔧 기술적 개선: - PDF.js 워커 설정 및 토큰 인증 처리 - 서적별 PDF 자동 그룹화 로직 - Alpine.js 컴포넌트 초기화 최적화
350 lines
13 KiB
JavaScript
350 lines
13 KiB
JavaScript
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;
|
||
}
|
||
});
|