Files
document-server/frontend/static/js/note-editor.js
Hyungi Ahn 3e0a03f149 🐛 Fix Alpine.js SyntaxError and backlink visibility issues
- Fix SyntaxError in viewer.js line 2868 (.bind(this) issue in setTimeout)
- Resolve Alpine.js 'Can't find variable' errors (documentViewer, goBack, etc.)
- Fix backlink rendering and persistence during temporary highlights
- Add backlink protection and restoration mechanism in highlightAndScrollToText
- Implement Note Management System with hierarchical notebooks
- Add note highlights and memos functionality
- Update cache version to force browser refresh (v=2025012641)
- Add comprehensive logging for debugging backlink issues
2025-08-26 23:50:48 +09:00

334 lines
12 KiB
JavaScript
Raw 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');
// 노트북 목록 로드
await this.loadNotebooks();
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, '개');
} 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;
}
});