🎉 Initial commit: Document Server MVP
✨ 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)
This commit is contained in:
286
frontend/static/js/main.js
Normal file
286
frontend/static/js/main.js
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 메인 애플리케이션 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user