🐛 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:
Hyungi Ahn
2025-08-26 23:50:48 +09:00
parent 8d7f4c04bb
commit 3e0a03f149
31 changed files with 5176 additions and 567 deletions

View File

@@ -3,12 +3,12 @@
*/
class DocumentServerAPI {
constructor() {
// 도커 백엔드 API (24102 포트)
this.baseURL = 'http://localhost:24102/api';
// nginx를 통한 프록시 API (24100 포트)
this.baseURL = 'http://localhost:24100/api';
this.token = localStorage.getItem('access_token');
console.log('🐳 API Base URL (DOCKER BACKEND):', this.baseURL);
console.log('🔧 도커 환경 설정 완료 - 버전 2025012415');
console.log('🌐 API Base URL (NGINX PROXY):', this.baseURL);
console.log('🔧 nginx 프록시 환경 설정 완료 - 버전 2025012607');
}
// 토큰 설정
@@ -221,7 +221,8 @@ class DocumentServerAPI {
return await this.delete(`/highlights/${highlightId}`);
}
// 메모 관련 API
// === 하이라이트 메모 (Highlight Memo) 관련 API ===
// 용어 정의: 하이라이트에 달리는 짧은 코멘트
async createNote(noteData) {
return await this.post('/notes/', noteData);
}
@@ -230,6 +231,8 @@ class DocumentServerAPI {
return await this.get('/notes/', params);
}
// === 문서 메모 조회 ===
// 용어 정의: 특정 문서의 모든 하이라이트 메모 조회
async getDocumentNotes(documentId) {
return await this.get(`/notes/document/${documentId}`);
}
@@ -319,6 +322,8 @@ class DocumentServerAPI {
}
// === 메모 관련 API ===
// === 문서 메모 조회 ===
// 용어 정의: 특정 문서의 모든 하이라이트 메모 조회
async getDocumentNotes(documentId) {
return await this.get(`/notes/document/${documentId}`);
}
@@ -441,6 +446,8 @@ class DocumentServerAPI {
}
// === 메모 관련 API ===
// === 문서 메모 조회 ===
// 용어 정의: 특정 문서의 모든 하이라이트 메모 조회
async getDocumentNotes(documentId) {
return await this.get(`/notes/document/${documentId}`);
}
@@ -553,6 +560,96 @@ class DocumentServerAPI {
async getDocumentLinkFragments(documentId) {
return await this.get(`/documents/${documentId}/link-fragments`);
}
// ===== 노트 문서 관련 API =====
// 모든 노트 조회
async getNoteDocuments(params = {}) {
return await this.get('/note-documents/', params);
}
// 특정 노트 조회
async getNoteDocument(noteId) {
return await this.get(`/note-documents/${noteId}`);
}
// === 노트 문서 (Note Document) 관련 API ===
// 용어 정의: 독립적인 문서 작성 (HTML 기반)
async createNoteDocument(noteData) {
return await this.post('/note-documents/', noteData);
}
// 노트 업데이트
async updateNoteDocument(noteId, noteData) {
return await this.put(`/note-documents/${noteId}`, noteData);
}
// 노트 삭제
async deleteNoteDocument(noteId) {
return await this.delete(`/note-documents/${noteId}`);
}
// 노트 HTML 내보내기
async exportNoteAsHTML(noteId) {
const response = await fetch(`${this.baseURL}/note-documents/${noteId}/export/html`, {
method: 'GET',
headers: this.getHeaders(),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
}
// ===== 노트북 관련 API =====
// 모든 노트북 조회
async getNotebooks(params = {}) {
return await this.get('/notebooks/', params);
}
// 특정 노트북 조회
async getNotebook(notebookId) {
return await this.get(`/notebooks/${notebookId}`);
}
// === 노트북 (Notebook) 관련 API ===
// 용어 정의: 노트 문서들을 그룹화하는 폴더
async createNotebook(notebookData) {
return await this.post('/notebooks/', notebookData);
}
// 노트북 업데이트
async updateNotebook(notebookId, notebookData) {
return await this.put(`/notebooks/${notebookId}`, notebookData);
}
// 노트북 삭제
async deleteNotebook(notebookId, force = false) {
return await this.delete(`/notebooks/${notebookId}?force=${force}`);
}
// 노트북 통계
async getNotebookStats() {
return await this.get('/notebooks/stats');
}
// 노트북의 노트들 조회
async getNotebookNotes(notebookId, params = {}) {
return await this.get(`/notebooks/${notebookId}/notes`, params);
}
// 노트를 노트북에 추가
async addNoteToNotebook(notebookId, noteId) {
return await this.post(`/notebooks/${notebookId}/notes/${noteId}`);
}
// 노트를 노트북에서 제거
async removeNoteFromNotebook(notebookId, noteId) {
return await this.delete(`/notebooks/${notebookId}/notes/${noteId}`);
}
}
// 전역 API 인스턴스

View File

@@ -125,7 +125,18 @@ window.documentApp = () => ({
this.error = '';
try {
this.documents = await window.api.getDocuments();
const allDocuments = await window.api.getDocuments();
// HTML 문서만 필터링 (PDF 파일 제외)
this.documents = allDocuments.filter(doc =>
doc.html_path &&
doc.html_path.includes('/documents/') // HTML은 documents 폴더에 저장됨
);
console.log('📄 전체 문서:', allDocuments.length, '개');
console.log('📄 HTML 문서:', this.documents.length, '개');
console.log('📄 PDF 파일:', allDocuments.length - this.documents.length, '개 (제외됨)');
this.updateAvailableTags();
this.filterDocuments();
this.syncUIState(); // UI 상태 동기화

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

View File

@@ -0,0 +1,277 @@
// 노트북 관리 애플리케이션 컴포넌트
window.notebooksApp = () => ({
// 상태 관리
notebooks: [],
stats: null,
loading: false,
saving: false,
error: '',
// 필터링
searchQuery: '',
activeOnly: true,
sortBy: 'updated_at',
// 검색 디바운스
searchTimeout: null,
// 모달 상태
showCreateModal: false,
showEditModal: false,
editingNotebook: null,
// 노트북 폼
notebookForm: {
title: '',
description: '',
color: '#3B82F6',
icon: 'book',
is_active: true,
sort_order: 0
},
// 인증 상태
isAuthenticated: false,
currentUser: null,
// API 클라이언트
api: null,
// 색상 옵션
availableColors: [
'#3B82F6', // blue
'#10B981', // emerald
'#F59E0B', // amber
'#EF4444', // red
'#8B5CF6', // violet
'#06B6D4', // cyan
'#84CC16', // lime
'#F97316', // orange
'#EC4899', // pink
'#6B7280' // gray
],
// 아이콘 옵션
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: '⭐ 즐겨찾기' }
],
// 초기화
async init() {
console.log('📚 Notebooks App 초기화 시작');
// API 클라이언트 초기화
this.api = new DocumentServerAPI();
// 인증 상태 확인
await this.checkAuthStatus();
if (this.isAuthenticated) {
await this.loadStats();
await this.loadNotebooks();
}
// 헤더 로드
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 {
if (typeof loadHeaderComponent === 'function') {
await loadHeaderComponent();
} else if (typeof window.loadHeaderComponent === 'function') {
await window.loadHeaderComponent();
} else {
console.warn('헤더 로더 함수를 찾을 수 없습니다.');
}
} catch (error) {
console.error('헤더 로드 실패:', error);
}
},
// 통계 정보 로드
async loadStats() {
try {
this.stats = await this.api.getNotebookStats();
console.log('📊 노트북 통계 로드됨:', this.stats);
} catch (error) {
console.error('통계 로드 실패:', error);
}
},
// 노트북 목록 로드
async loadNotebooks() {
this.loading = true;
this.error = '';
try {
const queryParams = {
active_only: this.activeOnly,
sort_by: this.sortBy,
order: 'desc'
};
if (this.searchQuery) {
queryParams.search = this.searchQuery;
}
this.notebooks = await this.api.getNotebooks(queryParams);
console.log('📚 노트북 로드됨:', this.notebooks.length, '개');
} catch (error) {
console.error('노트북 로드 실패:', error);
this.error = '노트북을 불러오는 중 오류가 발생했습니다.';
} finally {
this.loading = false;
}
},
// 검색 디바운스
debounceSearch() {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.loadNotebooks();
}, 300);
},
// 새로고침
async refreshNotebooks() {
await Promise.all([
this.loadStats(),
this.loadNotebooks()
]);
},
// 노트북 열기 (노트 목록으로 이동)
openNotebook(notebook) {
window.location.href = `/notes.html?notebook_id=${notebook.id}&notebook_name=${encodeURIComponent(notebook.name)}`;
},
// 노트북 편집
editNotebook(notebook) {
this.editingNotebook = notebook;
this.notebookForm = {
title: notebook.title,
description: notebook.description || '',
color: notebook.color,
icon: notebook.icon,
is_active: notebook.is_active,
sort_order: notebook.sort_order
};
this.showEditModal = true;
},
// 노트북 삭제
async deleteNotebook(notebook) {
if (!confirm(`"${notebook.title}" 노트북을 삭제하시겠습니까?\n\n${notebook.note_count > 0 ? `포함된 ${notebook.note_count}개의 노트는 미분류 상태가 됩니다.` : ''}`)) {
return;
}
try {
await this.api.deleteNotebook(notebook.id, true); // force=true
this.showNotification('노트북이 삭제되었습니다.', 'success');
await this.refreshNotebooks();
} catch (error) {
console.error('노트북 삭제 실패:', error);
this.showNotification('노트북 삭제에 실패했습니다.', 'error');
}
},
// 노트북 저장
async saveNotebook() {
if (!this.notebookForm.title.trim()) {
this.showNotification('제목을 입력해주세요.', 'error');
return;
}
this.saving = true;
try {
if (this.showEditModal && this.editingNotebook) {
// 편집
await this.api.updateNotebook(this.editingNotebook.id, this.notebookForm);
this.showNotification('노트북이 수정되었습니다.', 'success');
} else {
// 생성
await this.api.createNotebook(this.notebookForm);
this.showNotification('노트북이 생성되었습니다.', 'success');
}
this.closeModal();
await this.refreshNotebooks();
} catch (error) {
console.error('노트북 저장 실패:', error);
this.showNotification('노트북 저장에 실패했습니다.', 'error');
} finally {
this.saving = false;
}
},
// 모달 닫기
closeModal() {
this.showCreateModal = false;
this.showEditModal = false;
this.editingNotebook = null;
this.notebookForm = {
title: '',
description: '',
color: '#3B82F6',
icon: 'book',
is_active: true,
sort_order: 0
};
},
// 날짜 포맷팅
formatDate(dateString) {
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');
}
},
// 알림 표시
showNotification(message, type = 'info') {
if (type === 'error') {
alert('❌ ' + message);
} else if (type === 'success') {
alert('✅ ' + message);
} else {
alert(' ' + message);
}
}
});

404
frontend/static/js/notes.js Normal file
View 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 페이지 로드됨');
});

View File

@@ -0,0 +1,92 @@
/**
* 간단한 테스트용 documentViewer
*/
window.documentViewer = () => ({
// 기본 상태
loading: false,
error: null,
// 네비게이션
navigation: null,
// 검색
searchQuery: '',
// 데이터
notes: [],
bookmarks: [],
documentLinks: [],
backlinks: [],
// UI 상태
activeFeatureMenu: null,
selectedHighlightColor: '#FFFF00',
// 모달 상태
showLinksModal: false,
showLinkModal: false,
showNotesModal: false,
showBookmarksModal: false,
showBacklinksModal: false,
// 폼 데이터
linkForm: {
target_document_id: '',
selected_text: '',
book_scope: 'same',
target_book_id: '',
link_type: 'document',
target_text: '',
description: ''
},
// 기타 데이터
availableBooks: [],
filteredDocuments: [],
// 초기화
init() {
console.log('🔧 간단한 documentViewer 로드됨');
this.documentId = new URLSearchParams(window.location.search).get('id');
console.log('📋 문서 ID:', this.documentId);
},
// 뒤로가기
goBack() {
console.log('🔙 뒤로가기 클릭됨');
const urlParams = new URLSearchParams(window.location.search);
const fromPage = urlParams.get('from');
if (fromPage === 'index') {
window.location.href = '/index.html';
} else if (fromPage === 'hierarchy') {
window.location.href = '/hierarchy.html';
} else {
window.location.href = '/index.html';
}
},
// 기본 함수들
toggleFeatureMenu(feature) {
console.log('🎯 기능 메뉴 토글:', feature);
this.activeFeatureMenu = this.activeFeatureMenu === feature ? null : feature;
},
searchInDocument() {
console.log('🔍 문서 검색:', this.searchQuery);
},
// 빈 함수들 (오류 방지용)
navigateToDocument() { console.log('네비게이션 함수 호출됨'); },
goToBookContents() { console.log('목차로 이동 함수 호출됨'); },
createHighlightWithColor() { console.log('하이라이트 생성 함수 호출됨'); },
resetTargetSelection() { console.log('타겟 선택 리셋 함수 호출됨'); },
loadDocumentsFromBook() { console.log('서적 문서 로드 함수 호출됨'); },
onTargetDocumentChange() { console.log('타겟 문서 변경 함수 호출됨'); },
openTargetDocumentSelector() { console.log('타겟 문서 선택기 열기 함수 호출됨'); },
saveDocumentLink() { console.log('문서 링크 저장 함수 호출됨'); },
closeLinkModal() { console.log('링크 모달 닫기 함수 호출됨'); },
getSelectedBookTitle() { return '테스트 서적'; }
});
console.log('✅ 테스트용 documentViewer 정의됨');

File diff suppressed because it is too large Load Diff