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; } });