🐛 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:
404
frontend/static/js/notes.js
Normal file
404
frontend/static/js/notes.js
Normal file
@@ -0,0 +1,404 @@
|
||||
// 노트 관리 애플리케이션 컴포넌트
|
||||
window.notesApp = () => ({
|
||||
// 상태 관리
|
||||
notes: [],
|
||||
stats: null,
|
||||
loading: false,
|
||||
error: '',
|
||||
|
||||
// 필터링
|
||||
searchQuery: '',
|
||||
selectedType: '',
|
||||
publishedOnly: false,
|
||||
selectedNotebook: '',
|
||||
|
||||
// 노트북 관련
|
||||
availableNotebooks: [],
|
||||
|
||||
// 일괄 선택 관련
|
||||
selectedNotes: [],
|
||||
bulkNotebookId: '',
|
||||
|
||||
// 노트북 생성 관련
|
||||
showCreateNotebookModal: false,
|
||||
creatingNotebook: false,
|
||||
newNotebookForm: {
|
||||
name: '',
|
||||
description: '',
|
||||
color: '#3B82F6',
|
||||
icon: 'book'
|
||||
},
|
||||
|
||||
// 색상 및 아이콘 옵션
|
||||
availableColors: [
|
||||
'#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
|
||||
'#06B6D4', '#84CC16', '#F97316', '#EC4899', '#6B7280'
|
||||
],
|
||||
availableIcons: [
|
||||
{ value: 'book', label: '📖 책' },
|
||||
{ value: 'sticky-note', label: '📝 노트' },
|
||||
{ value: 'lightbulb', label: '💡 아이디어' },
|
||||
{ value: 'graduation-cap', label: '🎓 학습' },
|
||||
{ value: 'briefcase', label: '💼 업무' },
|
||||
{ value: 'heart', label: '❤️ 개인' },
|
||||
{ value: 'code', label: '💻 개발' },
|
||||
{ value: 'palette', label: '🎨 창작' },
|
||||
{ value: 'flask', label: '🧪 연구' },
|
||||
{ value: 'star', label: '⭐ 즐겨찾기' }
|
||||
],
|
||||
|
||||
// 검색 디바운스
|
||||
searchTimeout: null,
|
||||
|
||||
// 인증 상태
|
||||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
|
||||
// API 클라이언트
|
||||
api: null,
|
||||
|
||||
// 초기화
|
||||
async init() {
|
||||
console.log('🚀 Notes App 초기화 시작');
|
||||
|
||||
// API 클라이언트 초기화
|
||||
this.api = new DocumentServerAPI();
|
||||
console.log('🔧 API 클라이언트 초기화됨:', this.api);
|
||||
console.log('🔧 getNotebooks 메서드 존재 여부:', typeof this.api.getNotebooks);
|
||||
|
||||
// URL 파라미터 확인 (노트북 필터)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const notebookId = urlParams.get('notebook_id');
|
||||
const notebookName = urlParams.get('notebook_name');
|
||||
|
||||
if (notebookId) {
|
||||
this.selectedNotebook = notebookId;
|
||||
console.log('🔍 노트북 필터 적용:', notebookName || notebookId);
|
||||
}
|
||||
|
||||
// 인증 상태 확인
|
||||
await this.checkAuthStatus();
|
||||
|
||||
if (this.isAuthenticated) {
|
||||
await this.loadNotebooks();
|
||||
await this.loadStats();
|
||||
await this.loadNotes();
|
||||
}
|
||||
|
||||
// 헤더 로드
|
||||
await this.loadHeader();
|
||||
},
|
||||
|
||||
// 인증 상태 확인
|
||||
async checkAuthStatus() {
|
||||
try {
|
||||
const user = await this.api.getCurrentUser();
|
||||
this.isAuthenticated = true;
|
||||
this.currentUser = user;
|
||||
console.log('✅ 인증됨:', user.username || user.email);
|
||||
} catch (error) {
|
||||
console.log('❌ 인증되지 않음');
|
||||
this.isAuthenticated = false;
|
||||
this.currentUser = null;
|
||||
// 로그인 페이지로 리다이렉트하지 않고 메인 페이지로
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
|
||||
// 헤더 로드
|
||||
async loadHeader() {
|
||||
try {
|
||||
await window.headerLoader.loadHeader();
|
||||
} catch (error) {
|
||||
console.error('헤더 로드 실패:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 노트북 목록 로드
|
||||
async loadNotebooks() {
|
||||
try {
|
||||
console.log('📚 노트북 로드 시작...');
|
||||
console.log('🔧 API 메서드 확인:', typeof this.api.getNotebooks);
|
||||
|
||||
if (typeof this.api.getNotebooks !== 'function') {
|
||||
throw new Error('getNotebooks 메서드가 존재하지 않습니다');
|
||||
}
|
||||
|
||||
// 임시: 직접 API 호출
|
||||
this.availableNotebooks = await this.api.get('/notebooks/', { active_only: true });
|
||||
console.log('📚 노트북 로드됨:', this.availableNotebooks.length, '개');
|
||||
} catch (error) {
|
||||
console.error('노트북 로드 실패:', error);
|
||||
this.availableNotebooks = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 통계 정보 로드
|
||||
async loadStats() {
|
||||
try {
|
||||
this.stats = await this.api.get('/note-documents/stats');
|
||||
console.log('📊 통계 로드됨:', this.stats);
|
||||
} catch (error) {
|
||||
console.error('통계 로드 실패:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 노트 목록 로드
|
||||
async loadNotes() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const queryParams = {};
|
||||
|
||||
if (this.searchQuery) {
|
||||
queryParams.search = this.searchQuery;
|
||||
}
|
||||
if (this.selectedType) {
|
||||
queryParams.note_type = this.selectedType;
|
||||
}
|
||||
if (this.publishedOnly) {
|
||||
queryParams.published_only = 'true';
|
||||
}
|
||||
if (this.selectedNotebook) {
|
||||
if (this.selectedNotebook === 'unassigned') {
|
||||
queryParams.notebook_id = 'null';
|
||||
} else {
|
||||
queryParams.notebook_id = this.selectedNotebook;
|
||||
}
|
||||
}
|
||||
|
||||
this.notes = await this.api.getNoteDocuments(queryParams);
|
||||
console.log('📝 노트 로드됨:', this.notes.length, '개');
|
||||
|
||||
} catch (error) {
|
||||
console.error('노트 로드 실패:', error);
|
||||
this.error = '노트를 불러오는데 실패했습니다: ' + error.message;
|
||||
this.notes = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 검색 디바운스
|
||||
debounceSearch() {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.loadNotes();
|
||||
}, 500);
|
||||
},
|
||||
|
||||
// 노트 새로고침
|
||||
async refreshNotes() {
|
||||
await Promise.all([
|
||||
this.loadStats(),
|
||||
this.loadNotes()
|
||||
]);
|
||||
},
|
||||
|
||||
// 새 노트 생성
|
||||
createNewNote() {
|
||||
window.location.href = '/note-editor.html';
|
||||
},
|
||||
|
||||
// 노트 보기 (뷰어 페이지로 이동)
|
||||
viewNote(noteId) {
|
||||
window.location.href = `/viewer.html?type=note&id=${noteId}`;
|
||||
},
|
||||
|
||||
// 노트 편집
|
||||
editNote(noteId) {
|
||||
window.location.href = `/note-editor.html?id=${noteId}`;
|
||||
},
|
||||
|
||||
// 노트 삭제
|
||||
async deleteNote(note) {
|
||||
if (!confirm(`"${note.title}" 노트를 삭제하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/note-documents/${note.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.showNotification('노트가 삭제되었습니다', 'success');
|
||||
await this.refreshNotes();
|
||||
} else {
|
||||
throw new Error('삭제 실패');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('노트 삭제 실패:', error);
|
||||
this.showNotification('노트 삭제에 실패했습니다: ' + error.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// 노트 타입 라벨
|
||||
getNoteTypeLabel(type) {
|
||||
const labels = {
|
||||
'note': '일반',
|
||||
'research': '연구',
|
||||
'summary': '요약',
|
||||
'idea': '아이디어',
|
||||
'guide': '가이드',
|
||||
'reference': '참고'
|
||||
};
|
||||
return labels[type] || type;
|
||||
},
|
||||
|
||||
// 날짜 포맷팅
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffTime = Math.abs(now - date);
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 1) {
|
||||
return '오늘';
|
||||
} else if (diffDays === 2) {
|
||||
return '어제';
|
||||
} else if (diffDays <= 7) {
|
||||
return `${diffDays - 1}일 전`;
|
||||
} else {
|
||||
return date.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 알림 표시
|
||||
showNotification(message, type = 'info') {
|
||||
console.log(`${type.toUpperCase()}: ${message}`);
|
||||
|
||||
// 간단한 토스트 알림 생성
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
|
||||
type === 'success' ? 'bg-green-600' :
|
||||
type === 'error' ? 'bg-red-600' : 'bg-blue-600'
|
||||
}`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 3초 후 제거
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
// === 일괄 선택 관련 메서드 ===
|
||||
|
||||
// 노트 선택/해제
|
||||
toggleNoteSelection(noteId) {
|
||||
const index = this.selectedNotes.indexOf(noteId);
|
||||
if (index > -1) {
|
||||
this.selectedNotes.splice(index, 1);
|
||||
} else {
|
||||
this.selectedNotes.push(noteId);
|
||||
}
|
||||
},
|
||||
|
||||
// 선택 해제
|
||||
clearSelection() {
|
||||
this.selectedNotes = [];
|
||||
this.bulkNotebookId = '';
|
||||
},
|
||||
|
||||
// 선택된 노트들을 노트북에 할당
|
||||
async assignToNotebook() {
|
||||
if (!this.bulkNotebookId || this.selectedNotes.length === 0) {
|
||||
this.showNotification('노트북을 선택하고 노트를 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 각 노트를 업데이트
|
||||
const updatePromises = this.selectedNotes.map(noteId =>
|
||||
this.api.put(`/note-documents/${noteId}`, { notebook_id: this.bulkNotebookId })
|
||||
);
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
this.showNotification(`${this.selectedNotes.length}개 노트가 노트북에 할당되었습니다.`, 'success');
|
||||
|
||||
// 선택 해제 및 새로고침
|
||||
this.clearSelection();
|
||||
await this.loadNotes();
|
||||
|
||||
} catch (error) {
|
||||
console.error('노트북 할당 실패:', error);
|
||||
this.showNotification('노트북 할당에 실패했습니다.', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// === 노트북 생성 관련 메서드 ===
|
||||
|
||||
// 노트북 생성 모달 닫기
|
||||
closeCreateNotebookModal() {
|
||||
this.showCreateNotebookModal = false;
|
||||
this.newNotebookForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
color: '#3B82F6',
|
||||
icon: 'book'
|
||||
};
|
||||
},
|
||||
|
||||
// 노트북 생성 및 노트 할당
|
||||
async createNotebookAndAssign() {
|
||||
if (!this.newNotebookForm.name.trim()) {
|
||||
this.showNotification('노트북 이름을 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.creatingNotebook = true;
|
||||
|
||||
try {
|
||||
// 1. 노트북 생성
|
||||
const newNotebook = await this.api.post('/notebooks/', this.newNotebookForm);
|
||||
console.log('📚 새 노트북 생성됨:', newNotebook.name);
|
||||
|
||||
// 2. 선택된 노트들이 있으면 할당
|
||||
if (this.selectedNotes.length > 0) {
|
||||
const updatePromises = this.selectedNotes.map(noteId =>
|
||||
this.api.put(`/note-documents/${noteId}`, { notebook_id: newNotebook.id })
|
||||
);
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
console.log(`📝 ${this.selectedNotes.length}개 노트가 새 노트북에 할당됨`);
|
||||
}
|
||||
|
||||
this.showNotification(
|
||||
`노트북 "${newNotebook.name}"이 생성되었습니다.${this.selectedNotes.length > 0 ? ` ${this.selectedNotes.length}개 노트가 할당되었습니다.` : ''}`,
|
||||
'success'
|
||||
);
|
||||
|
||||
// 3. 정리 및 새로고침
|
||||
this.closeCreateNotebookModal();
|
||||
this.clearSelection();
|
||||
await this.loadNotebooks();
|
||||
await this.loadNotes();
|
||||
|
||||
} catch (error) {
|
||||
console.error('노트북 생성 실패:', error);
|
||||
this.showNotification('노트북 생성에 실패했습니다.', 'error');
|
||||
} finally {
|
||||
this.creatingNotebook = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('📄 Notes 페이지 로드됨');
|
||||
});
|
||||
Reference in New Issue
Block a user