- note-editor.html: notebook.name → notebook.title 수정 - notes.html: 모든 notebook.name → notebook.title 수정 - note-editor.js: 디버깅 로그 추가하여 노트북 데이터 구조 확인 백엔드 API에서는 'title' 필드를 사용하는데 프론트엔드에서 'name' 필드를 참조하여 노트북 선택창이 빈칸으로 표시되는 문제 해결
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;
|
||
}
|
||
});
|