- 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
589 lines
18 KiB
JavaScript
589 lines
18 KiB
JavaScript
// 메인 애플리케이션 컴포넌트
|
|
window.documentApp = () => ({
|
|
// 상태 관리
|
|
documents: [],
|
|
filteredDocuments: [],
|
|
groupedDocuments: [],
|
|
expandedBooks: [],
|
|
loading: false,
|
|
error: '',
|
|
|
|
// 인증 상태
|
|
isAuthenticated: false,
|
|
currentUser: null,
|
|
showLoginModal: false,
|
|
|
|
// 필터링 및 검색
|
|
searchQuery: '',
|
|
selectedTag: '',
|
|
availableTags: [],
|
|
|
|
// UI 상태
|
|
viewMode: 'grid', // 'grid' 또는 'books'
|
|
user: null, // currentUser의 별칭
|
|
tags: [], // availableTags의 별칭
|
|
|
|
// 모달 상태
|
|
showUploadModal: false,
|
|
|
|
// 로그인 관련 함수들
|
|
openLoginModal() {
|
|
this.showLoginModal = true;
|
|
},
|
|
|
|
// 초기화
|
|
async init() {
|
|
await this.checkAuthStatus();
|
|
if (this.isAuthenticated) {
|
|
await this.loadDocuments();
|
|
} else {
|
|
// 로그인하지 않은 경우에도 빈 배열로 초기화
|
|
this.groupedDocuments = [];
|
|
}
|
|
this.setupEventListeners();
|
|
|
|
// 커스텀 이벤트 리스너 등록
|
|
document.addEventListener('open-login-modal', () => {
|
|
console.log('📨 open-login-modal 이벤트 수신 (index.html)');
|
|
this.openLoginModal();
|
|
});
|
|
},
|
|
|
|
// 인증 상태 확인
|
|
async checkAuthStatus() {
|
|
try {
|
|
const token = localStorage.getItem('access_token');
|
|
if (token) {
|
|
window.api.setToken(token);
|
|
const user = await window.api.getCurrentUser();
|
|
this.isAuthenticated = true;
|
|
this.currentUser = user;
|
|
this.syncUIState(); // UI 상태 동기화
|
|
}
|
|
} catch (error) {
|
|
console.log('Not authenticated or token expired');
|
|
this.isAuthenticated = false;
|
|
this.currentUser = null;
|
|
localStorage.removeItem('access_token');
|
|
this.syncUIState(); // UI 상태 동기화
|
|
}
|
|
},
|
|
|
|
// 로그인 모달 열기
|
|
openLoginModal() {
|
|
this.showLoginModal = true;
|
|
},
|
|
|
|
// 로그아웃
|
|
async logout() {
|
|
try {
|
|
await window.api.logout();
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
} finally {
|
|
this.isAuthenticated = false;
|
|
this.currentUser = null;
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
this.documents = [];
|
|
this.filteredDocuments = [];
|
|
}
|
|
},
|
|
|
|
// 이벤트 리스너 설정
|
|
setupEventListeners() {
|
|
// 문서 변경 이벤트 리스너
|
|
window.addEventListener('documents-changed', () => {
|
|
this.loadDocuments();
|
|
});
|
|
|
|
// 알림 이벤트 리스너
|
|
window.addEventListener('show-notification', (event) => {
|
|
this.showNotification(event.detail.message, event.detail.type);
|
|
});
|
|
|
|
// 인증 상태 변경 이벤트 리스너
|
|
window.addEventListener('auth-changed', (event) => {
|
|
this.isAuthenticated = event.detail.isAuthenticated;
|
|
this.currentUser = event.detail.user;
|
|
this.showLoginModal = false;
|
|
this.syncUIState(); // UI 상태 동기화
|
|
if (this.isAuthenticated) {
|
|
this.loadDocuments();
|
|
}
|
|
});
|
|
|
|
// 로그인 모달 닫기 이벤트 리스너
|
|
window.addEventListener('close-login-modal', () => {
|
|
this.showLoginModal = false;
|
|
});
|
|
},
|
|
|
|
// 문서 목록 로드
|
|
async loadDocuments() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
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 상태 동기화
|
|
} catch (error) {
|
|
console.error('Failed to load documents:', error);
|
|
this.error = 'Failed to load documents: ' + error.message;
|
|
this.documents = [];
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// 사용 가능한 태그 업데이트
|
|
updateAvailableTags() {
|
|
const tagSet = new Set();
|
|
this.documents.forEach(doc => {
|
|
if (doc.tags) {
|
|
doc.tags.forEach(tag => tagSet.add(tag));
|
|
}
|
|
});
|
|
this.availableTags = Array.from(tagSet).sort();
|
|
this.tags = this.availableTags; // 별칭 동기화
|
|
},
|
|
|
|
// UI 상태 동기화
|
|
syncUIState() {
|
|
this.user = this.currentUser;
|
|
this.tags = this.availableTags;
|
|
},
|
|
|
|
// 문서 필터링
|
|
filterDocuments() {
|
|
let filtered = this.documents;
|
|
|
|
// 검색어 필터링
|
|
if (this.searchQuery) {
|
|
const query = this.searchQuery.toLowerCase();
|
|
filtered = filtered.filter(doc =>
|
|
doc.title.toLowerCase().includes(query) ||
|
|
(doc.description && doc.description.toLowerCase().includes(query)) ||
|
|
(doc.tags && doc.tags.some(tag => tag.toLowerCase().includes(query)))
|
|
);
|
|
}
|
|
|
|
// 태그 필터링
|
|
if (this.selectedTag) {
|
|
filtered = filtered.filter(doc =>
|
|
doc.tags && doc.tags.includes(this.selectedTag)
|
|
);
|
|
}
|
|
|
|
this.filteredDocuments = filtered;
|
|
this.groupDocumentsByBook();
|
|
},
|
|
|
|
// 검색어 변경 시
|
|
onSearchChange() {
|
|
this.filterDocuments();
|
|
},
|
|
|
|
// 필터 초기화
|
|
clearFilters() {
|
|
this.searchQuery = '';
|
|
this.selectedTag = '';
|
|
this.filterDocuments();
|
|
},
|
|
|
|
// 서적별 그룹화
|
|
groupDocumentsByBook() {
|
|
if (!this.filteredDocuments || this.filteredDocuments.length === 0) {
|
|
this.groupedDocuments = [];
|
|
return;
|
|
}
|
|
|
|
const grouped = {};
|
|
|
|
this.filteredDocuments.forEach(doc => {
|
|
const bookKey = doc.book_id || 'no-book';
|
|
if (!grouped[bookKey]) {
|
|
grouped[bookKey] = {
|
|
book: doc.book_id ? {
|
|
id: doc.book_id,
|
|
title: doc.book_title,
|
|
author: doc.book_author
|
|
} : null,
|
|
documents: []
|
|
};
|
|
}
|
|
grouped[bookKey].documents.push(doc);
|
|
});
|
|
|
|
// 배열로 변환하고 정렬 (서적 있는 것 먼저, 그 다음 서적명 순)
|
|
this.groupedDocuments = Object.values(grouped).sort((a, b) => {
|
|
if (!a.book && b.book) return 1;
|
|
if (a.book && !b.book) return -1;
|
|
if (!a.book && !b.book) return 0;
|
|
return a.book.title.localeCompare(b.book.title);
|
|
});
|
|
|
|
// 기본적으로 모든 서적을 펼친 상태로 설정
|
|
this.expandedBooks = this.groupedDocuments.map(group => group.book?.id || 'no-book');
|
|
},
|
|
|
|
// 서적 펼침/접힘 토글
|
|
toggleBookExpansion(bookId) {
|
|
const index = this.expandedBooks.indexOf(bookId);
|
|
if (index > -1) {
|
|
this.expandedBooks.splice(index, 1);
|
|
} else {
|
|
this.expandedBooks.push(bookId);
|
|
}
|
|
},
|
|
|
|
// 문서 검색 (HTML에서 사용)
|
|
searchDocuments() {
|
|
this.filterDocuments();
|
|
},
|
|
|
|
// 태그 선택 시
|
|
onTagSelect(tag) {
|
|
this.selectedTag = this.selectedTag === tag ? '' : tag;
|
|
this.filterDocuments();
|
|
},
|
|
|
|
// 태그 필터 초기화
|
|
clearTagFilter() {
|
|
this.selectedTag = '';
|
|
this.filterDocuments();
|
|
},
|
|
|
|
// 문서 삭제
|
|
async deleteDocument(documentId) {
|
|
if (!confirm('정말로 이 문서를 삭제하시겠습니까?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await window.api.deleteDocument(documentId);
|
|
await this.loadDocuments();
|
|
this.showNotification('문서가 삭제되었습니다', 'success');
|
|
} catch (error) {
|
|
console.error('Failed to delete document:', error);
|
|
this.showNotification('문서 삭제에 실패했습니다: ' + error.message, 'error');
|
|
}
|
|
},
|
|
|
|
// 문서 보기
|
|
viewDocument(documentId) {
|
|
// 현재 페이지 정보를 세션 스토리지에 저장
|
|
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
|
|
sessionStorage.setItem('previousPage', currentPage);
|
|
|
|
// from 파라미터 추가
|
|
const fromParam = currentPage === 'hierarchy.html' ? 'hierarchy' : 'index';
|
|
window.open(`/viewer.html?id=${documentId}&from=${fromParam}`, '_blank');
|
|
},
|
|
|
|
// 서적의 문서들 보기
|
|
openBookDocuments(book) {
|
|
if (book && book.id) {
|
|
// 서적 ID를 URL 파라미터로 전달하여 해당 서적의 문서들만 표시
|
|
window.location.href = `book-documents.html?bookId=${book.id}`;
|
|
} else {
|
|
// 서적 미분류 문서들 보기
|
|
window.location.href = `book-documents.html?bookId=none`;
|
|
}
|
|
},
|
|
|
|
// 업로드 페이지 열기
|
|
openUploadPage() {
|
|
window.location.href = 'upload.html';
|
|
},
|
|
|
|
// 문서 열기 (HTML에서 사용)
|
|
openDocument(documentId) {
|
|
this.viewDocument(documentId);
|
|
},
|
|
|
|
// 문서 수정 (HTML에서 사용)
|
|
editDocument(document) {
|
|
// TODO: 문서 수정 모달 구현
|
|
console.log('문서 수정:', document);
|
|
alert('문서 수정 기능은 곧 구현됩니다!');
|
|
},
|
|
|
|
// 업로드 모달 열기
|
|
openUploadModal() {
|
|
this.showUploadModal = true;
|
|
},
|
|
|
|
// 업로드 모달 닫기
|
|
closeUploadModal() {
|
|
this.showUploadModal = false;
|
|
},
|
|
|
|
// 날짜 포맷팅
|
|
formatDate(dateString) {
|
|
return new Date(dateString).toLocaleDateString('ko-KR', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
},
|
|
|
|
// 알림 표시
|
|
showNotification(message, type = 'info') {
|
|
// 간단한 알림 구현 (나중에 토스트 라이브러리로 교체 가능)
|
|
const notification = document.createElement('div');
|
|
notification.className = `fixed top-4 right-4 p-4 rounded-lg text-white z-50 ${
|
|
type === 'error' ? 'bg-red-500' :
|
|
type === 'success' ? 'bg-green-500' :
|
|
'bg-blue-500'
|
|
}`;
|
|
notification.textContent = message;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 3000);
|
|
}
|
|
});
|
|
|
|
// 파일 업로드 컴포넌트
|
|
window.uploadModal = () => ({
|
|
uploading: false,
|
|
uploadForm: {
|
|
title: '',
|
|
description: '',
|
|
tags: '',
|
|
is_public: false,
|
|
document_date: '',
|
|
html_file: null,
|
|
pdf_file: null
|
|
},
|
|
uploadError: '',
|
|
|
|
// 서적 관련 상태
|
|
bookSelectionMode: 'none', // 'existing', 'new', 'none'
|
|
bookSearchQuery: '',
|
|
searchedBooks: [],
|
|
selectedBook: null,
|
|
newBook: {
|
|
title: '',
|
|
author: '',
|
|
description: ''
|
|
},
|
|
suggestions: [],
|
|
searchTimeout: null,
|
|
|
|
// 파일 선택
|
|
onFileSelect(event, fileType) {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
this.uploadForm[fileType] = file;
|
|
|
|
// HTML 파일의 경우 제목 자동 설정
|
|
if (fileType === 'html_file' && !this.uploadForm.title) {
|
|
this.uploadForm.title = file.name.replace(/\.[^/.]+$/, "");
|
|
}
|
|
}
|
|
},
|
|
|
|
// 드래그 앤 드롭 처리
|
|
handleFileDrop(event, fileType) {
|
|
event.target.classList.remove('dragover');
|
|
const files = event.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
const file = files[0];
|
|
|
|
// 파일 타입 검증
|
|
if (fileType === 'html_file' && !file.name.match(/\.(html|htm)$/i)) {
|
|
this.uploadError = 'HTML 파일만 업로드 가능합니다';
|
|
return;
|
|
}
|
|
|
|
if (fileType === 'pdf_file' && !file.name.match(/\.pdf$/i)) {
|
|
this.uploadError = 'PDF 파일만 업로드 가능합니다';
|
|
return;
|
|
}
|
|
|
|
this.uploadForm[fileType] = file;
|
|
this.uploadError = '';
|
|
|
|
// HTML 파일의 경우 제목 자동 설정
|
|
if (fileType === 'html_file' && !this.uploadForm.title) {
|
|
this.uploadForm.title = file.name.replace(/\.[^/.]+$/, "");
|
|
}
|
|
}
|
|
},
|
|
|
|
// 업로드 실행
|
|
async upload() {
|
|
// 필수 필드 검증
|
|
if (!this.uploadForm.html_file) {
|
|
this.uploadError = 'HTML 파일을 선택해주세요';
|
|
return;
|
|
}
|
|
|
|
if (!this.uploadForm.title.trim()) {
|
|
this.uploadError = '문서 제목을 입력해주세요';
|
|
return;
|
|
}
|
|
|
|
this.uploading = true;
|
|
this.uploadError = '';
|
|
|
|
try {
|
|
let bookId = null;
|
|
|
|
// 서적 처리
|
|
if (this.bookSelectionMode === 'new' && this.newBook.title.trim()) {
|
|
const newBook = await window.api.createBook({
|
|
title: this.newBook.title,
|
|
author: this.newBook.author || null,
|
|
description: this.newBook.description || null,
|
|
language: this.uploadForm.language || 'ko',
|
|
is_public: this.uploadForm.is_public
|
|
});
|
|
bookId = newBook.id;
|
|
} else if (this.bookSelectionMode === 'existing' && this.selectedBook) {
|
|
bookId = this.selectedBook.id;
|
|
}
|
|
|
|
// FormData 생성
|
|
const formData = new FormData();
|
|
formData.append('title', this.uploadForm.title);
|
|
formData.append('description', this.uploadForm.description || '');
|
|
formData.append('html_file', this.uploadForm.html_file);
|
|
|
|
if (this.uploadForm.pdf_file) {
|
|
formData.append('pdf_file', this.uploadForm.pdf_file);
|
|
}
|
|
|
|
// 서적 ID 추가
|
|
if (bookId) {
|
|
formData.append('book_id', bookId);
|
|
}
|
|
|
|
formData.append('language', this.uploadForm.language || 'ko');
|
|
formData.append('is_public', this.uploadForm.is_public);
|
|
|
|
if (this.uploadForm.tags) {
|
|
formData.append('tags', this.uploadForm.tags);
|
|
}
|
|
|
|
if (this.uploadForm.document_date) {
|
|
formData.append('document_date', this.uploadForm.document_date);
|
|
}
|
|
|
|
// 업로드 실행
|
|
await window.api.uploadDocument(formData);
|
|
|
|
// 성공 처리
|
|
this.resetForm();
|
|
|
|
// 문서 목록 새로고침
|
|
window.dispatchEvent(new CustomEvent('documents-changed'));
|
|
|
|
// 성공 알림
|
|
window.dispatchEvent(new CustomEvent('show-notification', {
|
|
detail: { message: '문서가 성공적으로 업로드되었습니다', type: 'success' }
|
|
}));
|
|
|
|
} catch (error) {
|
|
this.uploadError = error.message;
|
|
} finally {
|
|
this.uploading = false;
|
|
}
|
|
},
|
|
|
|
// 폼 리셋
|
|
resetForm() {
|
|
this.uploadForm = {
|
|
title: '',
|
|
description: '',
|
|
tags: '',
|
|
is_public: false,
|
|
document_date: '',
|
|
html_file: null,
|
|
pdf_file: null
|
|
};
|
|
this.uploadError = '';
|
|
|
|
// 파일 입력 필드 리셋
|
|
const fileInputs = document.querySelectorAll('input[type="file"]');
|
|
fileInputs.forEach(input => input.value = '');
|
|
|
|
// 서적 관련 상태 리셋
|
|
this.bookSelectionMode = 'none';
|
|
this.bookSearchQuery = '';
|
|
this.searchedBooks = [];
|
|
this.selectedBook = null;
|
|
this.newBook = { title: '', author: '', description: '' };
|
|
this.suggestions = [];
|
|
if (this.searchTimeout) {
|
|
clearTimeout(this.searchTimeout);
|
|
this.searchTimeout = null;
|
|
}
|
|
},
|
|
|
|
// 서적 검색
|
|
async searchBooks() {
|
|
if (!this.bookSearchQuery.trim()) {
|
|
this.searchedBooks = [];
|
|
return;
|
|
}
|
|
try {
|
|
const books = await window.api.searchBooks(this.bookSearchQuery, 10);
|
|
this.searchedBooks = books;
|
|
} catch (error) {
|
|
console.error('서적 검색 실패:', error);
|
|
this.searchedBooks = [];
|
|
}
|
|
},
|
|
|
|
// 서적 선택
|
|
selectBook(book) {
|
|
this.selectedBook = book;
|
|
this.bookSearchQuery = book.title;
|
|
this.searchedBooks = [];
|
|
},
|
|
|
|
// 유사도 추천 가져오기
|
|
async getSuggestions() {
|
|
if (!this.newBook.title.trim()) {
|
|
this.suggestions = [];
|
|
return;
|
|
}
|
|
clearTimeout(this.searchTimeout);
|
|
this.searchTimeout = setTimeout(async () => {
|
|
try {
|
|
const suggestions = await window.api.getBookSuggestions(this.newBook.title, 3);
|
|
this.suggestions = suggestions.filter(s => s.similarity_score > 0.5); // 50% 이상 유사한 것만
|
|
} catch (error) {
|
|
console.error('추천 가져오기 실패:', error);
|
|
this.suggestions = [];
|
|
}
|
|
}, 300);
|
|
},
|
|
|
|
// 추천에서 기존 서적 선택
|
|
selectExistingFromSuggestion(suggestion) {
|
|
this.bookSelectionMode = 'existing';
|
|
this.selectedBook = suggestion;
|
|
this.bookSearchQuery = suggestion.title;
|
|
this.suggestions = [];
|
|
this.newBook = { title: '', author: '', description: '' };
|
|
}
|
|
}); |