🎉 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:
265
frontend/static/js/api.js
Normal file
265
frontend/static/js/api.js
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* API 통신 유틸리티
|
||||
*/
|
||||
class API {
|
||||
constructor() {
|
||||
this.baseURL = '/api';
|
||||
this.token = localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
// 토큰 설정
|
||||
setToken(token) {
|
||||
this.token = token;
|
||||
if (token) {
|
||||
localStorage.setItem('access_token', token);
|
||||
} else {
|
||||
localStorage.removeItem('access_token');
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 요청 헤더
|
||||
getHeaders() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
// GET 요청
|
||||
async get(endpoint, params = {}) {
|
||||
const url = new URL(`${this.baseURL}${endpoint}`, window.location.origin);
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] !== null && params[key] !== undefined) {
|
||||
url.searchParams.append(key, params[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
}
|
||||
|
||||
// POST 요청
|
||||
async post(endpoint, data = {}) {
|
||||
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
}
|
||||
|
||||
// PUT 요청
|
||||
async put(endpoint, data = {}) {
|
||||
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
||||
method: 'PUT',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
}
|
||||
|
||||
// DELETE 요청
|
||||
async delete(endpoint) {
|
||||
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
}
|
||||
|
||||
// 파일 업로드
|
||||
async uploadFile(endpoint, formData) {
|
||||
const headers = {};
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: formData,
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
}
|
||||
|
||||
// 응답 처리
|
||||
async handleResponse(response) {
|
||||
if (response.status === 401) {
|
||||
// 토큰 만료 또는 인증 실패
|
||||
this.setToken(null);
|
||||
window.location.reload();
|
||||
throw new Error('인증이 필요합니다');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
// 인증 관련 API
|
||||
async login(email, password) {
|
||||
return await this.post('/auth/login', { email, password });
|
||||
}
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await this.post('/auth/logout');
|
||||
} finally {
|
||||
this.setToken(null);
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
return await this.get('/auth/me');
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken) {
|
||||
return await this.post('/auth/refresh', { refresh_token: refreshToken });
|
||||
}
|
||||
|
||||
// 문서 관련 API
|
||||
async getDocuments(params = {}) {
|
||||
return await this.get('/documents/', params);
|
||||
}
|
||||
|
||||
async getDocument(documentId) {
|
||||
return await this.get(`/documents/${documentId}`);
|
||||
}
|
||||
|
||||
async uploadDocument(formData) {
|
||||
return await this.uploadFile('/documents/', formData);
|
||||
}
|
||||
|
||||
async deleteDocument(documentId) {
|
||||
return await this.delete(`/documents/${documentId}`);
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
return await this.get('/documents/tags/');
|
||||
}
|
||||
|
||||
async createTag(tagData) {
|
||||
return await this.post('/documents/tags/', tagData);
|
||||
}
|
||||
|
||||
// 하이라이트 관련 API
|
||||
async createHighlight(highlightData) {
|
||||
return await this.post('/highlights/', highlightData);
|
||||
}
|
||||
|
||||
async getDocumentHighlights(documentId) {
|
||||
return await this.get(`/highlights/document/${documentId}`);
|
||||
}
|
||||
|
||||
async updateHighlight(highlightId, data) {
|
||||
return await this.put(`/highlights/${highlightId}`, data);
|
||||
}
|
||||
|
||||
async deleteHighlight(highlightId) {
|
||||
return await this.delete(`/highlights/${highlightId}`);
|
||||
}
|
||||
|
||||
// 메모 관련 API
|
||||
async createNote(noteData) {
|
||||
return await this.post('/notes/', noteData);
|
||||
}
|
||||
|
||||
async getNotes(params = {}) {
|
||||
return await this.get('/notes/', params);
|
||||
}
|
||||
|
||||
async getDocumentNotes(documentId) {
|
||||
return await this.get(`/notes/document/${documentId}`);
|
||||
}
|
||||
|
||||
async updateNote(noteId, data) {
|
||||
return await this.put(`/notes/${noteId}`, data);
|
||||
}
|
||||
|
||||
async deleteNote(noteId) {
|
||||
return await this.delete(`/notes/${noteId}`);
|
||||
}
|
||||
|
||||
async getPopularNoteTags() {
|
||||
return await this.get('/notes/tags/popular');
|
||||
}
|
||||
|
||||
// 책갈피 관련 API
|
||||
async createBookmark(bookmarkData) {
|
||||
return await this.post('/bookmarks/', bookmarkData);
|
||||
}
|
||||
|
||||
async getBookmarks(params = {}) {
|
||||
return await this.get('/bookmarks/', params);
|
||||
}
|
||||
|
||||
async getDocumentBookmarks(documentId) {
|
||||
return await this.get(`/bookmarks/document/${documentId}`);
|
||||
}
|
||||
|
||||
async updateBookmark(bookmarkId, data) {
|
||||
return await this.put(`/bookmarks/${bookmarkId}`, data);
|
||||
}
|
||||
|
||||
async deleteBookmark(bookmarkId) {
|
||||
return await this.delete(`/bookmarks/${bookmarkId}`);
|
||||
}
|
||||
|
||||
// 검색 관련 API
|
||||
async search(params = {}) {
|
||||
return await this.get('/search/', params);
|
||||
}
|
||||
|
||||
async getSearchSuggestions(query) {
|
||||
return await this.get('/search/suggestions', { q: query });
|
||||
}
|
||||
|
||||
// 사용자 관리 API
|
||||
async getUsers() {
|
||||
return await this.get('/users/');
|
||||
}
|
||||
|
||||
async createUser(userData) {
|
||||
return await this.post('/auth/create-user', userData);
|
||||
}
|
||||
|
||||
async updateUser(userId, userData) {
|
||||
return await this.put(`/users/${userId}`, userData);
|
||||
}
|
||||
|
||||
async deleteUser(userId) {
|
||||
return await this.delete(`/users/${userId}`);
|
||||
}
|
||||
|
||||
async updateProfile(profileData) {
|
||||
return await this.put('/users/profile', profileData);
|
||||
}
|
||||
|
||||
async changePassword(passwordData) {
|
||||
return await this.put('/auth/change-password', passwordData);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 API 인스턴스
|
||||
window.api = new API();
|
||||
Reference in New Issue
Block a user