- FastAPI 라우터에서 슬래시 문제로 인한 307 리다이렉트 수정 - Nginx 프록시 설정에서 경로 중복 문제 해결 - 계정 관리 시스템 구현 (로그인, 사용자 관리, 권한 설정) - 노트북 연결 기능 수정 (notebook_id 필드 추가) - 메모 트리 UI 개선 (수평 레이아웃, 드래그 기능 제거) - 헤더 UI 개선 및 고정 위치 설정 - 백업/복원 스크립트 추가 - PDF 미리보기 토큰 인증 지원
706 lines
21 KiB
JavaScript
706 lines
21 KiB
JavaScript
/**
|
|
* API 통신 유틸리티
|
|
*/
|
|
class DocumentServerAPI {
|
|
constructor() {
|
|
// nginx 프록시를 통한 API 호출 (절대 경로로 강제)
|
|
this.baseURL = `${window.location.origin}/api`;
|
|
this.token = localStorage.getItem('access_token');
|
|
|
|
console.log('🌐 API Base URL (NGINX PROXY):', this.baseURL);
|
|
console.log('🔧 현재 브라우저 위치:', window.location.origin);
|
|
console.log('🔧 현재 브라우저 전체 URL:', window.location.href);
|
|
console.log('🔧 nginx 프록시 환경 설정 완료 - 상대 경로 사용');
|
|
}
|
|
|
|
// 토큰 설정
|
|
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 = {}) {
|
|
// URL 생성 시 포트 유지를 위해 단순 문자열 연결 사용
|
|
let url = `${this.baseURL}${endpoint}`;
|
|
|
|
// 쿼리 파라미터 추가
|
|
if (Object.keys(params).length > 0) {
|
|
const searchParams = new URLSearchParams();
|
|
Object.keys(params).forEach(key => {
|
|
if (params[key] !== null && params[key] !== undefined) {
|
|
searchParams.append(key, params[key]);
|
|
}
|
|
});
|
|
url += `?${searchParams.toString()}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: this.getHeaders(),
|
|
mode: 'cors',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
return this.handleResponse(response);
|
|
}
|
|
|
|
// POST 요청
|
|
async post(endpoint, data = {}) {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
console.log('🌐 POST 요청 시작');
|
|
console.log(' - baseURL:', this.baseURL);
|
|
console.log(' - endpoint:', endpoint);
|
|
console.log(' - 최종 URL:', url);
|
|
console.log(' - 데이터:', data);
|
|
|
|
console.log('🔍 fetch 호출 직전 URL 검증:', url);
|
|
console.log('🔍 URL 타입:', typeof url);
|
|
console.log('🔍 URL 절대/상대 여부:', url.startsWith('http') ? '절대경로' : '상대경로');
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: this.getHeaders(),
|
|
body: JSON.stringify(data),
|
|
mode: 'cors',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
console.log('📡 POST 응답 받음:', response.url, response.status);
|
|
console.log('📡 실제 요청된 URL:', response.url);
|
|
return this.handleResponse(response);
|
|
}
|
|
|
|
// PUT 요청
|
|
async put(endpoint, data = {}) {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
console.log('🌐 PUT 요청 URL:', url); // 디버깅용
|
|
|
|
const response = await fetch(url, {
|
|
method: 'PUT',
|
|
headers: this.getHeaders(),
|
|
body: JSON.stringify(data),
|
|
mode: 'cors',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
return this.handleResponse(response);
|
|
}
|
|
|
|
// DELETE 요청
|
|
async delete(endpoint) {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
console.log('🌐 DELETE 요청 URL:', url); // 디버깅용
|
|
|
|
const response = await fetch(url, {
|
|
method: 'DELETE',
|
|
headers: this.getHeaders(),
|
|
mode: 'cors',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
return this.handleResponse(response);
|
|
}
|
|
|
|
// 파일 업로드
|
|
async uploadFile(endpoint, formData) {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
console.log('🌐 UPLOAD 요청 URL:', url); // 디버깅용
|
|
|
|
const headers = {};
|
|
if (this.token) {
|
|
headers['Authorization'] = `Bearer ${this.token}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: formData,
|
|
mode: 'cors',
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
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) {
|
|
const response = await this.post('/auth/login', { email, password });
|
|
|
|
// 토큰 저장
|
|
if (response.access_token) {
|
|
this.setToken(response.access_token);
|
|
|
|
// 사용자 정보 가져오기
|
|
try {
|
|
const user = await this.getCurrentUser();
|
|
return {
|
|
success: true,
|
|
user: user,
|
|
token: response.access_token
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: '사용자 정보를 가져올 수 없습니다.'
|
|
};
|
|
}
|
|
} else {
|
|
return {
|
|
success: false,
|
|
message: '로그인에 실패했습니다.'
|
|
};
|
|
}
|
|
}
|
|
|
|
async logout() {
|
|
try {
|
|
await this.post('/auth/logout');
|
|
} finally {
|
|
this.setToken(null);
|
|
}
|
|
}
|
|
|
|
async getCurrentUser() {
|
|
return await this.get('/users/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 updateDocument(documentId, updateData) {
|
|
return await this.put(`/documents/${documentId}`, updateData);
|
|
}
|
|
|
|
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) {
|
|
console.log('🎨 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}`);
|
|
}
|
|
|
|
// === 하이라이트 메모 (Highlight Memo) 관련 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) {
|
|
console.log('🎨 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 updateBook(bookId, bookData) {
|
|
return await this.put(`/books/${bookId}`, bookData);
|
|
}
|
|
|
|
// 문서 네비게이션 정보 조회
|
|
async getDocumentNavigation(documentId) {
|
|
return await this.get(`/documents/${documentId}/navigation`);
|
|
}
|
|
|
|
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) {
|
|
console.log('🎨 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
|
|
// ============================================================================
|
|
|
|
// 메모 트리 관리
|
|
async getUserMemoTrees(includeArchived = false) {
|
|
const params = includeArchived ? '?include_archived=true' : '';
|
|
return await this.get(`/memo-trees/${params}`);
|
|
}
|
|
|
|
async createMemoTree(treeData) {
|
|
return await this.post('/memo-trees/', treeData);
|
|
}
|
|
|
|
async getMemoTree(treeId) {
|
|
return await this.get(`/memo-trees/${treeId}`);
|
|
}
|
|
|
|
async updateMemoTree(treeId, treeData) {
|
|
return await this.put(`/memo-trees/${treeId}`, treeData);
|
|
}
|
|
|
|
async deleteMemoTree(treeId) {
|
|
return await this.delete(`/memo-trees/${treeId}`);
|
|
}
|
|
|
|
// 메모 노드 관리
|
|
async getMemoTreeNodes(treeId) {
|
|
return await this.get(`/memo-trees/${treeId}/nodes`);
|
|
}
|
|
|
|
async createMemoNode(nodeData) {
|
|
return await this.post(`/memo-trees/${nodeData.tree_id}/nodes`, nodeData);
|
|
}
|
|
|
|
async getMemoNode(nodeId) {
|
|
return await this.get(`/memo-trees/nodes/${nodeId}`);
|
|
}
|
|
|
|
async updateMemoNode(nodeId, nodeData) {
|
|
return await this.put(`/memo-trees/nodes/${nodeId}`, nodeData);
|
|
}
|
|
|
|
async deleteMemoNode(nodeId) {
|
|
return await this.delete(`/memo-trees/nodes/${nodeId}`);
|
|
}
|
|
|
|
// 노드 이동
|
|
async moveMemoNode(nodeId, moveData) {
|
|
return await this.put(`/memo-trees/nodes/${nodeId}/move`, moveData);
|
|
}
|
|
|
|
// 트리 통계
|
|
async getMemoTreeStats(treeId) {
|
|
return await this.get(`/memo-trees/${treeId}/stats`);
|
|
}
|
|
|
|
// 검색
|
|
async searchMemoNodes(searchData) {
|
|
return await this.post('/memo-trees/search', searchData);
|
|
}
|
|
|
|
// 내보내기
|
|
async exportMemoTree(exportData) {
|
|
return await this.post('/memo-trees/export', exportData);
|
|
}
|
|
|
|
// 문서 링크 관련 API
|
|
async createDocumentLink(documentId, linkData) {
|
|
return await this.post(`/documents/${documentId}/links`, linkData);
|
|
}
|
|
|
|
async getDocumentLinks(documentId) {
|
|
return await this.get(`/documents/${documentId}/links`);
|
|
}
|
|
|
|
async getLinkableDocuments(documentId) {
|
|
return await this.get(`/documents/${documentId}/linkable-documents`);
|
|
}
|
|
|
|
async updateDocumentLink(linkId, linkData) {
|
|
return await this.put(`/documents/links/${linkId}`, linkData);
|
|
}
|
|
|
|
async deleteDocumentLink(linkId) {
|
|
return await this.delete(`/documents/links/${linkId}`);
|
|
}
|
|
|
|
// 백링크 관련 API
|
|
async getDocumentBacklinks(documentId) {
|
|
return await this.get(`/documents/${documentId}/backlinks`);
|
|
}
|
|
|
|
async getDocumentLinkFragments(documentId) {
|
|
return await this.get(`/documents/${documentId}/link-fragments`);
|
|
}
|
|
|
|
// ===== 노트 문서 관련 API =====
|
|
|
|
// 모든 노트 조회
|
|
async getNoteDocuments(params = {}) {
|
|
return await this.get('/note-documents/', params);
|
|
}
|
|
|
|
// 특정 노트 조회
|
|
async getNoteDocument(noteId) {
|
|
return await this.get(`/note-documents/${noteId}`);
|
|
}
|
|
|
|
// 특정 노트북의 노트들 조회
|
|
async getNotesInNotebook(notebookId) {
|
|
return await this.get('/note-documents/', { notebook_id: notebookId });
|
|
}
|
|
|
|
// === 노트 문서 (Note Document) 관련 API ===
|
|
// 용어 정의: 독립적인 문서 작성 (HTML 기반)
|
|
async createNoteDocument(noteData) {
|
|
return await this.post('/note-documents/', noteData);
|
|
}
|
|
|
|
// 노트 업데이트
|
|
async updateNoteDocument(noteId, noteData) {
|
|
return await this.put(`/note-documents/${noteId}`, noteData);
|
|
}
|
|
|
|
// 노트 삭제
|
|
async deleteNoteDocument(noteId) {
|
|
return await this.delete(`/note-documents/${noteId}`);
|
|
}
|
|
|
|
// 노트 HTML 내보내기
|
|
async exportNoteAsHTML(noteId) {
|
|
const response = await fetch(`${this.baseURL}/note-documents/${noteId}/export/html`, {
|
|
method: 'GET',
|
|
headers: this.getHeaders(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
return response.blob();
|
|
}
|
|
|
|
// ===== 노트북 관련 API =====
|
|
|
|
// 모든 노트북 조회
|
|
async getNotebooks(params = {}) {
|
|
return await this.get('/notebooks/', params);
|
|
}
|
|
|
|
// 특정 노트북 조회
|
|
async getNotebook(notebookId) {
|
|
return await this.get(`/notebooks/${notebookId}`);
|
|
}
|
|
|
|
// === 노트북 (Notebook) 관련 API ===
|
|
// 용어 정의: 노트 문서들을 그룹화하는 폴더
|
|
async createNotebook(notebookData) {
|
|
return await this.post('/notebooks/', notebookData);
|
|
}
|
|
|
|
// 노트북 업데이트
|
|
async updateNotebook(notebookId, notebookData) {
|
|
return await this.put(`/notebooks/${notebookId}`, notebookData);
|
|
}
|
|
|
|
// 노트북 삭제
|
|
async deleteNotebook(notebookId, force = false) {
|
|
return await this.delete(`/notebooks/${notebookId}?force=${force}`);
|
|
}
|
|
|
|
// 노트북 통계
|
|
async getNotebookStats() {
|
|
return await this.get('/notebooks/stats');
|
|
}
|
|
|
|
// 노트북의 노트들 조회
|
|
async getNotebookNotes(notebookId, params = {}) {
|
|
return await this.get(`/notebooks/${notebookId}/notes`, params);
|
|
}
|
|
|
|
// 노트를 노트북에 추가
|
|
async addNoteToNotebook(notebookId, noteId) {
|
|
return await this.post(`/notebooks/${notebookId}/notes/${noteId}`);
|
|
}
|
|
|
|
// 노트를 노트북에서 제거
|
|
async removeNoteFromNotebook(notebookId, noteId) {
|
|
return await this.delete(`/notebooks/${notebookId}/notes/${noteId}`);
|
|
}
|
|
}
|
|
|
|
// 전역 API 인스턴스
|
|
window.api = new DocumentServerAPI();
|