✨ Features implemented: - FastAPI backend with JWT authentication - PostgreSQL database with async SQLAlchemy - HTML document viewer with smart highlighting - Note system connected to highlights (1:1 relationship) - Bookmark system for quick navigation - Integrated search (documents + notes) - Tag system for document organization - Docker containerization with Nginx 🔧 Technical stack: - Backend: FastAPI + PostgreSQL + Redis - Frontend: Alpine.js + Tailwind CSS - Authentication: JWT tokens - File handling: HTML + PDF support - Search: Full-text search with relevance scoring 📋 Core functionality: - Text selection → Highlight creation - Highlight → Note attachment - Note management with search/filtering - Bookmark creation at scroll positions - Document upload with metadata - User management (admin creates accounts)
287 lines
7.7 KiB
JavaScript
287 lines
7.7 KiB
JavaScript
/**
|
|
* 메인 애플리케이션 Alpine.js 컴포넌트
|
|
*/
|
|
|
|
// 메인 문서 앱 컴포넌트
|
|
window.documentApp = () => ({
|
|
// 상태
|
|
isAuthenticated: false,
|
|
user: null,
|
|
loading: false,
|
|
|
|
// 문서 관련
|
|
documents: [],
|
|
tags: [],
|
|
selectedTag: '',
|
|
viewMode: 'grid', // 'grid' 또는 'list'
|
|
|
|
// 검색
|
|
searchQuery: '',
|
|
searchResults: [],
|
|
|
|
// 모달 상태
|
|
showUploadModal: false,
|
|
showProfile: false,
|
|
showMyNotes: false,
|
|
showBookmarks: false,
|
|
showAdmin: false,
|
|
|
|
// 초기화
|
|
async init() {
|
|
// 인증 상태 확인
|
|
await this.checkAuth();
|
|
|
|
// 인증 상태 변경 이벤트 리스너
|
|
window.addEventListener('auth-changed', (event) => {
|
|
this.isAuthenticated = event.detail.isAuthenticated;
|
|
this.user = event.detail.user;
|
|
|
|
if (this.isAuthenticated) {
|
|
this.loadInitialData();
|
|
} else {
|
|
this.resetData();
|
|
}
|
|
});
|
|
|
|
// 초기 데이터 로드
|
|
if (this.isAuthenticated) {
|
|
await this.loadInitialData();
|
|
}
|
|
},
|
|
|
|
// 인증 상태 확인
|
|
async checkAuth() {
|
|
if (!api.token) {
|
|
this.isAuthenticated = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.user = await api.getCurrentUser();
|
|
this.isAuthenticated = true;
|
|
} catch (error) {
|
|
console.error('Auth check failed:', error);
|
|
this.isAuthenticated = false;
|
|
api.setToken(null);
|
|
}
|
|
},
|
|
|
|
// 초기 데이터 로드
|
|
async loadInitialData() {
|
|
this.loading = true;
|
|
try {
|
|
await Promise.all([
|
|
this.loadDocuments(),
|
|
this.loadTags()
|
|
]);
|
|
} catch (error) {
|
|
console.error('Failed to load initial data:', error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// 데이터 리셋
|
|
resetData() {
|
|
this.documents = [];
|
|
this.tags = [];
|
|
this.selectedTag = '';
|
|
this.searchQuery = '';
|
|
this.searchResults = [];
|
|
},
|
|
|
|
// 문서 목록 로드
|
|
async loadDocuments() {
|
|
try {
|
|
const params = {};
|
|
if (this.selectedTag) {
|
|
params.tag = this.selectedTag;
|
|
}
|
|
|
|
this.documents = await api.getDocuments(params);
|
|
} catch (error) {
|
|
console.error('Failed to load documents:', error);
|
|
this.showNotification('문서 목록을 불러오는데 실패했습니다', 'error');
|
|
}
|
|
},
|
|
|
|
// 태그 목록 로드
|
|
async loadTags() {
|
|
try {
|
|
this.tags = await api.getTags();
|
|
} catch (error) {
|
|
console.error('Failed to load tags:', error);
|
|
}
|
|
},
|
|
|
|
// 문서 검색
|
|
async searchDocuments() {
|
|
if (!this.searchQuery.trim()) {
|
|
this.searchResults = [];
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const results = await api.search({
|
|
q: this.searchQuery,
|
|
limit: 20
|
|
});
|
|
this.searchResults = results.results;
|
|
} catch (error) {
|
|
console.error('Search failed:', error);
|
|
}
|
|
},
|
|
|
|
// 문서 열기
|
|
openDocument(document) {
|
|
// 문서 뷰어 페이지로 이동
|
|
window.location.href = `/viewer.html?id=${document.id}`;
|
|
},
|
|
|
|
// 로그아웃
|
|
async logout() {
|
|
try {
|
|
await api.logout();
|
|
this.isAuthenticated = false;
|
|
this.user = null;
|
|
this.resetData();
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
}
|
|
},
|
|
|
|
// 날짜 포맷팅
|
|
formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('ko-KR', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
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 = () => ({
|
|
show: false,
|
|
uploading: false,
|
|
uploadForm: {
|
|
title: '',
|
|
description: '',
|
|
tags: '',
|
|
is_public: false,
|
|
document_date: '',
|
|
html_file: null,
|
|
pdf_file: null
|
|
},
|
|
uploadError: '',
|
|
|
|
// 파일 선택
|
|
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(/\.[^/.]+$/, "");
|
|
}
|
|
}
|
|
},
|
|
|
|
// 업로드 실행
|
|
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 {
|
|
const formData = new FormData();
|
|
formData.append('title', this.uploadForm.title);
|
|
formData.append('description', this.uploadForm.description);
|
|
formData.append('tags', this.uploadForm.tags);
|
|
formData.append('is_public', this.uploadForm.is_public);
|
|
formData.append('html_file', this.uploadForm.html_file);
|
|
|
|
if (this.uploadForm.pdf_file) {
|
|
formData.append('pdf_file', this.uploadForm.pdf_file);
|
|
}
|
|
|
|
if (this.uploadForm.document_date) {
|
|
formData.append('document_date', this.uploadForm.document_date);
|
|
}
|
|
|
|
await api.uploadDocument(formData);
|
|
|
|
// 성공시 모달 닫기 및 목록 새로고침
|
|
this.show = false;
|
|
this.resetForm();
|
|
|
|
// 문서 목록 새로고침
|
|
window.dispatchEvent(new CustomEvent('documents-changed'));
|
|
|
|
// 성공 알림
|
|
document.querySelector('[x-data="documentApp"]').__x.$data.showNotification('문서가 성공적으로 업로드되었습니다', '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 = '');
|
|
}
|
|
});
|
|
|
|
// 문서 변경 이벤트 리스너
|
|
window.addEventListener('documents-changed', () => {
|
|
const app = document.querySelector('[x-data="documentApp"]').__x.$data;
|
|
if (app && app.isAuthenticated) {
|
|
app.loadDocuments();
|
|
app.loadTags();
|
|
}
|
|
});
|