Files
document-server/frontend/static/js/api.js
Hyungi Ahn 46546da55f feat: 계층구조 뷰 및 완전한 하이라이트/메모 시스템 구현
주요 기능:
- 📚 Book 및 BookCategory 모델 추가 (서적 그룹화)
- 🏗️ 계층구조 뷰 (Book > Category > Document) 구현
- 🎨 완전한 하이라이트 시스템 (생성, 표시, 삭제)
- 📝 통합 메모 관리 (추가, 수정, 삭제)
- 🔄 그리드 뷰와 계층구조 뷰 간 완전 동기화
- 🛡️ 관리자 전용 문서 삭제 기능
- 🔧 모든 CORS 및 500 오류 해결

기술적 개선:
- API 베이스 URL을 Nginx 프록시로 변경 (/api)
- 외래키 제약 조건 해결 (삭제 순서 최적화)
- SQLAlchemy 관계 로딩 최적화 (selectinload)
- 프론트엔드 캐시 무효화 시스템
- Alpine.js 컴포넌트 구조 개선

UI/UX:
- 계층구조 네비게이션 (사이드바 + 트리 구조)
- 하이라이트 모드 토글 스위치
- 완전한 툴팁 기반 메모 관리 인터페이스
- 반응형 하이라이트 메뉴 (색상 선택)
- 스마트 툴팁 위치 조정 (화면 경계 고려)
2025-08-23 14:31:30 +09:00

421 lines
11 KiB
JavaScript

/**
* API 통신 유틸리티
*/
class DocumentServerAPI {
constructor() {
this.baseURL = '/api'; // Nginx 프록시를 통해 접근
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 getDocumentsHierarchy() {
return await this.get('/documents/hierarchy/structured');
}
async getDocument(documentId) {
return await this.get(`/documents/${documentId}`);
}
async getDocumentContent(documentId) {
return await this.get(`/documents/${documentId}/content`);
}
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 ===
async getDocumentHighlights(documentId) {
return await this.get(`/highlights/document/${documentId}`);
}
async createHighlight(highlightData) {
return await this.post('/highlights/', highlightData);
}
async updateHighlight(highlightId, highlightData) {
return await this.put(`/highlights/${highlightId}`, highlightData);
}
async deleteHighlight(highlightId) {
return await this.delete(`/highlights/${highlightId}`);
}
// === 메모 관련 API ===
async getDocumentNotes(documentId) {
return await this.get(`/notes/document/${documentId}`);
}
async createNote(noteData) {
return await this.post('/notes/', noteData);
}
async updateNote(noteId, noteData) {
return await this.put(`/notes/${noteId}`, noteData);
}
async deleteNote(noteId) {
return await this.delete(`/notes/${noteId}`);
}
async getNotesByHighlight(highlightId) {
return await this.get(`/notes/highlight/${highlightId}`);
}
// === 책갈피 관련 API ===
async getDocumentBookmarks(documentId) {
return await this.get(`/bookmarks/document/${documentId}`);
}
async createBookmark(bookmarkData) {
return await this.post('/bookmarks/', bookmarkData);
}
async updateBookmark(bookmarkId, bookmarkData) {
return await this.put(`/bookmarks/${bookmarkId}`, bookmarkData);
}
async deleteBookmark(bookmarkId) {
return await this.delete(`/bookmarks/${bookmarkId}`);
}
// === 검색 관련 API ===
async searchDocuments(query, filters = {}) {
const params = new URLSearchParams({ q: query, ...filters });
return await this.get(`/search/documents?${params}`);
}
async searchNotes(query, documentId = null) {
const params = new URLSearchParams({ q: query });
if (documentId) params.append('document_id', documentId);
return await this.get(`/search/notes?${params}`);
}
// === 서적 관련 API ===
async getBooks(skip = 0, limit = 50, search = null) {
const params = new URLSearchParams({ skip, limit });
if (search) params.append('search', search);
return await this.get(`/books?${params}`);
}
async createBook(bookData) {
return await this.post('/books', bookData);
}
async getBook(bookId) {
return await this.get(`/books/${bookId}`);
}
async searchBooks(query, limit = 10) {
const params = new URLSearchParams({ q: query, limit });
return await this.get(`/books/search/?${params}`);
}
async getBookSuggestions(title, limit = 5) {
const params = new URLSearchParams({ title, limit });
return await this.get(`/books/suggestions/?${params}`);
}
// === 서적 소분류 관련 API ===
async createBookCategory(categoryData) {
return await this.post('/book-categories/', categoryData);
}
async getBookCategories(bookId) {
return await this.get(`/book-categories/book/${bookId}`);
}
async updateBookCategory(categoryId, categoryData) {
return await this.put(`/book-categories/${categoryId}`, categoryData);
}
async deleteBookCategory(categoryId) {
return await this.delete(`/book-categories/${categoryId}`);
}
async updateDocumentOrder(orderData) {
return await this.put('/book-categories/documents/reorder', orderData);
}
// === 하이라이트 관련 API ===
async getDocumentHighlights(documentId) {
return await this.get(`/highlights/document/${documentId}`);
}
async createHighlight(highlightData) {
return await this.post('/highlights/', highlightData);
}
async updateHighlight(highlightId, highlightData) {
return await this.put(`/highlights/${highlightId}`, highlightData);
}
async deleteHighlight(highlightId) {
return await this.delete(`/highlights/${highlightId}`);
}
// === 메모 관련 API ===
async getDocumentNotes(documentId) {
return await this.get(`/notes/document/${documentId}`);
}
async createNote(noteData) {
return await this.post('/notes/', noteData);
}
async updateNote(noteId, noteData) {
return await this.put(`/notes/${noteId}`, noteData);
}
async deleteNote(noteId) {
return await this.delete(`/notes/${noteId}`);
}
}
// 전역 API 인스턴스
window.api = new DocumentServerAPI();