🐛 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
This commit is contained in:
333
frontend/static/js/note-editor.js
Normal file
333
frontend/static/js/note-editor.js
Normal file
@@ -0,0 +1,333 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user