feat: 계층구조 뷰 및 완전한 하이라이트/메모 시스템 구현
주요 기능: - 📚 Book 및 BookCategory 모델 추가 (서적 그룹화) - 🏗️ 계층구조 뷰 (Book > Category > Document) 구현 - 🎨 완전한 하이라이트 시스템 (생성, 표시, 삭제) - 📝 통합 메모 관리 (추가, 수정, 삭제) - 🔄 그리드 뷰와 계층구조 뷰 간 완전 동기화 - 🛡️ 관리자 전용 문서 삭제 기능 - 🔧 모든 CORS 및 500 오류 해결 기술적 개선: - API 베이스 URL을 Nginx 프록시로 변경 (/api) - 외래키 제약 조건 해결 (삭제 순서 최적화) - SQLAlchemy 관계 로딩 최적화 (selectinload) - 프론트엔드 캐시 무효화 시스템 - Alpine.js 컴포넌트 구조 개선 UI/UX: - 계층구조 네비게이션 (사이드바 + 트리 구조) - 하이라이트 모드 토글 스위치 - 완전한 툴팁 기반 메모 관리 인터페이스 - 반응형 하이라이트 메뉴 (색상 선택) - 스마트 툴팁 위치 조정 (화면 경계 고려)
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* API 통신 유틸리티
|
||||
*/
|
||||
class API {
|
||||
class DocumentServerAPI {
|
||||
constructor() {
|
||||
this.baseURL = 'http://localhost:24102/api';
|
||||
this.baseURL = '/api'; // Nginx 프록시를 통해 접근
|
||||
this.token = localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
@@ -143,10 +143,18 @@ class API {
|
||||
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);
|
||||
}
|
||||
@@ -326,7 +334,87 @@ class API {
|
||||
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 API();
|
||||
window.api = new DocumentServerAPI();
|
||||
|
||||
@@ -18,14 +18,14 @@ window.authModal = () => ({
|
||||
|
||||
try {
|
||||
// 실제 API 호출
|
||||
const response = await api.login(this.loginForm.email, this.loginForm.password);
|
||||
const response = await window.api.login(this.loginForm.email, this.loginForm.password);
|
||||
|
||||
// 토큰 저장
|
||||
api.setToken(response.access_token);
|
||||
window.api.setToken(response.access_token);
|
||||
localStorage.setItem('refresh_token', response.refresh_token);
|
||||
|
||||
// 사용자 정보 가져오기
|
||||
const userResponse = await api.getCurrentUser();
|
||||
const userResponse = await window.api.getCurrentUser();
|
||||
|
||||
// 전역 상태 업데이트
|
||||
window.dispatchEvent(new CustomEvent('auth-changed', {
|
||||
@@ -45,7 +45,7 @@ window.authModal = () => ({
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await api.logout();
|
||||
await window.api.logout();
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
} finally {
|
||||
@@ -79,7 +79,7 @@ async function refreshTokenIfNeeded() {
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error);
|
||||
// 갱신 실패시 로그아웃
|
||||
api.setToken(null);
|
||||
window.api.setToken(null);
|
||||
localStorage.removeItem('refresh_token');
|
||||
window.dispatchEvent(new CustomEvent('auth-changed', {
|
||||
detail: { isAuthenticated: false, user: null }
|
||||
|
||||
1115
frontend/static/js/hierarchy.js
Normal file
1115
frontend/static/js/hierarchy.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,201 +1,239 @@
|
||||
/**
|
||||
* 메인 애플리케이션 Alpine.js 컴포넌트
|
||||
*/
|
||||
|
||||
// 메인 문서 앱 컴포넌트
|
||||
// 메인 애플리케이션 컴포넌트
|
||||
window.documentApp = () => ({
|
||||
// 상태
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: false,
|
||||
|
||||
// 문서 관련
|
||||
// 상태 관리
|
||||
documents: [],
|
||||
tags: [],
|
||||
selectedTag: '',
|
||||
viewMode: 'grid', // 'grid' 또는 'list'
|
||||
filteredDocuments: [],
|
||||
loading: false,
|
||||
error: '',
|
||||
|
||||
// 검색
|
||||
// 인증 상태
|
||||
isAuthenticated: false,
|
||||
currentUser: null,
|
||||
showLoginModal: false,
|
||||
|
||||
// 필터링 및 검색
|
||||
searchQuery: '',
|
||||
searchResults: [],
|
||||
selectedTag: '',
|
||||
availableTags: [],
|
||||
|
||||
// UI 상태
|
||||
viewMode: 'grid', // 'grid' 또는 'list'
|
||||
user: null, // currentUser의 별칭
|
||||
tags: [], // availableTags의 별칭
|
||||
|
||||
// 모달 상태
|
||||
showLoginModal: false,
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// 로그인 모달 닫기 이벤트 리스너
|
||||
window.addEventListener('close-login-modal', () => {
|
||||
this.showLoginModal = false;
|
||||
});
|
||||
|
||||
// 문서 변경 이벤트 리스너
|
||||
window.addEventListener('documents-changed', () => {
|
||||
if (this.isAuthenticated) {
|
||||
this.loadDocuments();
|
||||
this.loadTags();
|
||||
}
|
||||
});
|
||||
|
||||
// 업로드 모달 닫기 이벤트 리스너
|
||||
window.addEventListener('close-upload-modal', () => {
|
||||
this.showUploadModal = false;
|
||||
});
|
||||
|
||||
// 알림 표시 이벤트 리스너
|
||||
window.addEventListener('show-notification', (event) => {
|
||||
if (this.showNotification) {
|
||||
this.showNotification(event.detail.message, event.detail.type);
|
||||
}
|
||||
});
|
||||
|
||||
// 초기 데이터 로드
|
||||
await this.checkAuthStatus();
|
||||
if (this.isAuthenticated) {
|
||||
await this.loadInitialData();
|
||||
await this.loadDocuments();
|
||||
}
|
||||
this.setupEventListeners();
|
||||
},
|
||||
|
||||
// 인증 상태 확인
|
||||
async checkAuth() {
|
||||
if (!api.token) {
|
||||
this.isAuthenticated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
async checkAuthStatus() {
|
||||
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 response = await api.getDocuments();
|
||||
let allDocuments = response || [];
|
||||
|
||||
// 태그 필터링
|
||||
let filteredDocs = allDocuments;
|
||||
if (this.selectedTag) {
|
||||
filteredDocs = allDocuments.filter(doc =>
|
||||
doc.tags && doc.tags.includes(this.selectedTag)
|
||||
);
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
window.api.setToken(token);
|
||||
const user = await window.api.getCurrentUser();
|
||||
this.isAuthenticated = true;
|
||||
this.currentUser = user;
|
||||
this.syncUIState(); // UI 상태 동기화
|
||||
}
|
||||
|
||||
this.documents = filteredDocs;
|
||||
} catch (error) {
|
||||
console.error('Failed to load documents:', error);
|
||||
this.documents = [];
|
||||
this.showNotification('문서 목록을 불러오는데 실패했습니다', 'error');
|
||||
console.log('Not authenticated or token expired');
|
||||
this.isAuthenticated = false;
|
||||
this.currentUser = null;
|
||||
localStorage.removeItem('access_token');
|
||||
this.syncUIState(); // UI 상태 동기화
|
||||
}
|
||||
},
|
||||
|
||||
// 태그 목록 로드
|
||||
async loadTags() {
|
||||
try {
|
||||
const response = await api.getTags();
|
||||
this.tags = response || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load tags:', error);
|
||||
this.tags = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 문서 검색
|
||||
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}`;
|
||||
// 로그인 모달 열기
|
||||
openLoginModal() {
|
||||
this.showLoginModal = true;
|
||||
},
|
||||
|
||||
// 로그아웃
|
||||
async logout() {
|
||||
try {
|
||||
await api.logout();
|
||||
this.isAuthenticated = false;
|
||||
this.user = null;
|
||||
this.resetData();
|
||||
await window.api.logout();
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
console.error('Logout error:', error);
|
||||
} finally {
|
||||
this.isAuthenticated = false;
|
||||
this.currentUser = null;
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
this.documents = [];
|
||||
this.filteredDocuments = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
setupEventListeners() {
|
||||
// 문서 변경 이벤트 리스너
|
||||
window.addEventListener('documents-changed', () => {
|
||||
this.loadDocuments();
|
||||
});
|
||||
|
||||
// 알림 이벤트 리스너
|
||||
window.addEventListener('show-notification', (event) => {
|
||||
this.showNotification(event.detail.message, event.detail.type);
|
||||
});
|
||||
|
||||
// 인증 상태 변경 이벤트 리스너
|
||||
window.addEventListener('auth-changed', (event) => {
|
||||
this.isAuthenticated = event.detail.isAuthenticated;
|
||||
this.currentUser = event.detail.user;
|
||||
this.showLoginModal = false;
|
||||
this.syncUIState(); // UI 상태 동기화
|
||||
if (this.isAuthenticated) {
|
||||
this.loadDocuments();
|
||||
}
|
||||
});
|
||||
|
||||
// 로그인 모달 닫기 이벤트 리스너
|
||||
window.addEventListener('close-login-modal', () => {
|
||||
this.showLoginModal = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 문서 목록 로드
|
||||
async loadDocuments() {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
this.documents = await window.api.getDocuments();
|
||||
this.updateAvailableTags();
|
||||
this.filterDocuments();
|
||||
this.syncUIState(); // UI 상태 동기화
|
||||
} catch (error) {
|
||||
console.error('Failed to load documents:', error);
|
||||
this.error = 'Failed to load documents: ' + error.message;
|
||||
this.documents = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 문서 뷰어 열기
|
||||
// 사용 가능한 태그 업데이트
|
||||
updateAvailableTags() {
|
||||
const tagSet = new Set();
|
||||
this.documents.forEach(doc => {
|
||||
if (doc.tags) {
|
||||
doc.tags.forEach(tag => tagSet.add(tag));
|
||||
}
|
||||
});
|
||||
this.availableTags = Array.from(tagSet).sort();
|
||||
this.tags = this.availableTags; // 별칭 동기화
|
||||
},
|
||||
|
||||
// UI 상태 동기화
|
||||
syncUIState() {
|
||||
this.user = this.currentUser;
|
||||
this.tags = this.availableTags;
|
||||
},
|
||||
|
||||
// 문서 필터링
|
||||
filterDocuments() {
|
||||
let filtered = this.documents;
|
||||
|
||||
// 검색어 필터링
|
||||
if (this.searchQuery) {
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(doc =>
|
||||
doc.title.toLowerCase().includes(query) ||
|
||||
(doc.description && doc.description.toLowerCase().includes(query)) ||
|
||||
(doc.tags && doc.tags.some(tag => tag.toLowerCase().includes(query)))
|
||||
);
|
||||
}
|
||||
|
||||
// 태그 필터링
|
||||
if (this.selectedTag) {
|
||||
filtered = filtered.filter(doc =>
|
||||
doc.tags && doc.tags.includes(this.selectedTag)
|
||||
);
|
||||
}
|
||||
|
||||
this.filteredDocuments = filtered;
|
||||
},
|
||||
|
||||
// 검색어 변경 시
|
||||
onSearchChange() {
|
||||
this.filterDocuments();
|
||||
},
|
||||
|
||||
// 문서 검색 (HTML에서 사용)
|
||||
searchDocuments() {
|
||||
this.filterDocuments();
|
||||
},
|
||||
|
||||
// 태그 선택 시
|
||||
onTagSelect(tag) {
|
||||
this.selectedTag = this.selectedTag === tag ? '' : tag;
|
||||
this.filterDocuments();
|
||||
},
|
||||
|
||||
// 태그 필터 초기화
|
||||
clearTagFilter() {
|
||||
this.selectedTag = '';
|
||||
this.filterDocuments();
|
||||
},
|
||||
|
||||
// 문서 삭제
|
||||
async deleteDocument(documentId) {
|
||||
if (!confirm('정말로 이 문서를 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.api.deleteDocument(documentId);
|
||||
await this.loadDocuments();
|
||||
this.showNotification('문서가 삭제되었습니다', 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete document:', error);
|
||||
this.showNotification('문서 삭제에 실패했습니다: ' + error.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// 문서 보기
|
||||
viewDocument(documentId) {
|
||||
window.open(`/viewer.html?id=${documentId}`, '_blank');
|
||||
},
|
||||
|
||||
// 문서 열기 (HTML에서 사용)
|
||||
openDocument(documentId) {
|
||||
window.location.href = `/viewer.html?id=${documentId}`;
|
||||
this.viewDocument(documentId);
|
||||
},
|
||||
|
||||
// 문서 수정 (HTML에서 사용)
|
||||
editDocument(document) {
|
||||
// TODO: 문서 수정 모달 구현
|
||||
console.log('문서 수정:', document);
|
||||
alert('문서 수정 기능은 곧 구현됩니다!');
|
||||
},
|
||||
|
||||
// 업로드 모달 열기
|
||||
openUploadModal() {
|
||||
this.showUploadModal = true;
|
||||
},
|
||||
|
||||
// 업로드 모달 닫기
|
||||
closeUploadModal() {
|
||||
this.showUploadModal = false;
|
||||
},
|
||||
|
||||
// 날짜 포맷팅
|
||||
formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR', {
|
||||
return new Date(dateString).toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
@@ -221,7 +259,6 @@ window.documentApp = () => ({
|
||||
|
||||
// 파일 업로드 컴포넌트
|
||||
window.uploadModal = () => ({
|
||||
show: false,
|
||||
uploading: false,
|
||||
uploadForm: {
|
||||
title: '',
|
||||
@@ -233,6 +270,19 @@ window.uploadModal = () => ({
|
||||
pdf_file: null
|
||||
},
|
||||
uploadError: '',
|
||||
|
||||
// 서적 관련 상태
|
||||
bookSelectionMode: 'none', // 'existing', 'new', 'none'
|
||||
bookSearchQuery: '',
|
||||
searchedBooks: [],
|
||||
selectedBook: null,
|
||||
newBook: {
|
||||
title: '',
|
||||
author: '',
|
||||
description: ''
|
||||
},
|
||||
suggestions: [],
|
||||
searchTimeout: null,
|
||||
|
||||
// 파일 선택
|
||||
onFileSelect(event, fileType) {
|
||||
@@ -247,15 +297,44 @@ window.uploadModal = () => ({
|
||||
}
|
||||
},
|
||||
|
||||
// 드래그 앤 드롭 처리
|
||||
handleFileDrop(event, fileType) {
|
||||
event.target.classList.remove('dragover');
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
|
||||
// 파일 타입 검증
|
||||
if (fileType === 'html_file' && !file.name.match(/\.(html|htm)$/i)) {
|
||||
this.uploadError = 'HTML 파일만 업로드 가능합니다';
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileType === 'pdf_file' && !file.name.match(/\.pdf$/i)) {
|
||||
this.uploadError = 'PDF 파일만 업로드 가능합니다';
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadForm[fileType] = file;
|
||||
this.uploadError = '';
|
||||
|
||||
// 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 = '제목을 입력해주세요';
|
||||
this.uploadError = '문서 제목을 입력해주세요';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -263,25 +342,52 @@ window.uploadModal = () => ({
|
||||
this.uploadError = '';
|
||||
|
||||
try {
|
||||
let bookId = null;
|
||||
|
||||
// 서적 처리
|
||||
if (this.bookSelectionMode === 'new' && this.newBook.title.trim()) {
|
||||
const newBook = await window.api.createBook({
|
||||
title: this.newBook.title,
|
||||
author: this.newBook.author || null,
|
||||
description: this.newBook.description || null,
|
||||
language: this.uploadForm.language || 'ko',
|
||||
is_public: this.uploadForm.is_public
|
||||
});
|
||||
bookId = newBook.id;
|
||||
} else if (this.bookSelectionMode === 'existing' && this.selectedBook) {
|
||||
bookId = this.selectedBook.id;
|
||||
}
|
||||
|
||||
// FormData 생성
|
||||
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('description', this.uploadForm.description || '');
|
||||
formData.append('html_file', this.uploadForm.html_file);
|
||||
|
||||
if (this.uploadForm.pdf_file) {
|
||||
formData.append('pdf_file', this.uploadForm.pdf_file);
|
||||
}
|
||||
|
||||
// 서적 ID 추가
|
||||
if (bookId) {
|
||||
formData.append('book_id', bookId);
|
||||
}
|
||||
|
||||
formData.append('language', this.uploadForm.language || 'ko');
|
||||
formData.append('is_public', this.uploadForm.is_public);
|
||||
|
||||
if (this.uploadForm.tags) {
|
||||
formData.append('tags', this.uploadForm.tags);
|
||||
}
|
||||
|
||||
if (this.uploadForm.document_date) {
|
||||
formData.append('document_date', this.uploadForm.document_date);
|
||||
}
|
||||
|
||||
await api.uploadDocument(formData);
|
||||
// 업로드 실행
|
||||
await window.api.uploadDocument(formData);
|
||||
|
||||
// 성공시 모달 닫기 및 목록 새로고침
|
||||
this.show = false;
|
||||
// 성공 처리
|
||||
this.resetForm();
|
||||
|
||||
// 문서 목록 새로고침
|
||||
@@ -315,138 +421,66 @@ window.uploadModal = () => ({
|
||||
// 파일 입력 필드 리셋
|
||||
const fileInputs = document.querySelectorAll('input[type="file"]');
|
||||
fileInputs.forEach(input => input.value = '');
|
||||
}
|
||||
});
|
||||
|
||||
// 파일 업로드 컴포넌트
|
||||
window.uploadModal = () => ({
|
||||
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(/\.[^/.]+$/, "");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 드래그 앤 드롭 처리
|
||||
handleFileDrop(event, fileType) {
|
||||
event.target.classList.remove('dragover');
|
||||
const files = event.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
|
||||
// 파일 타입 검증
|
||||
if (fileType === 'html_file' && !file.name.match(/\.(html|htm)$/i)) {
|
||||
this.uploadError = 'HTML 파일만 업로드 가능합니다';
|
||||
return;
|
||||
}
|
||||
if (fileType === 'pdf_file' && !file.name.match(/\.pdf$/i)) {
|
||||
this.uploadError = 'PDF 파일만 업로드 가능합니다';
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadForm[fileType] = file;
|
||||
this.uploadError = '';
|
||||
|
||||
// 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 {
|
||||
// FormData 생성
|
||||
const formData = new FormData();
|
||||
formData.append('title', this.uploadForm.title);
|
||||
formData.append('description', this.uploadForm.description || '');
|
||||
formData.append('html_file', this.uploadForm.html_file);
|
||||
|
||||
if (this.uploadForm.pdf_file) {
|
||||
formData.append('pdf_file', this.uploadForm.pdf_file);
|
||||
}
|
||||
|
||||
formData.append('language', this.uploadForm.language);
|
||||
formData.append('is_public', this.uploadForm.is_public);
|
||||
|
||||
// 태그 추가
|
||||
if (this.uploadForm.tags && this.uploadForm.tags.length > 0) {
|
||||
this.uploadForm.tags.forEach(tag => {
|
||||
formData.append('tags', tag);
|
||||
});
|
||||
}
|
||||
|
||||
// 실제 API 호출
|
||||
await api.uploadDocument(formData);
|
||||
|
||||
// 성공시 모달 닫기 및 목록 새로고침
|
||||
window.dispatchEvent(new CustomEvent('close-upload-modal'));
|
||||
this.resetForm();
|
||||
|
||||
// 문서 목록 새로고침
|
||||
window.dispatchEvent(new CustomEvent('documents-changed'));
|
||||
|
||||
// 성공 알림
|
||||
window.dispatchEvent(new CustomEvent('show-notification', {
|
||||
detail: { message: '문서가 성공적으로 업로드되었습니다', type: '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 = '');
|
||||
// 서적 관련 상태 리셋
|
||||
this.bookSelectionMode = 'none';
|
||||
this.bookSearchQuery = '';
|
||||
this.searchedBooks = [];
|
||||
this.selectedBook = null;
|
||||
this.newBook = { title: '', author: '', description: '' };
|
||||
this.suggestions = [];
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 서적 검색
|
||||
async searchBooks() {
|
||||
if (!this.bookSearchQuery.trim()) {
|
||||
this.searchedBooks = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const books = await window.api.searchBooks(this.bookSearchQuery, 10);
|
||||
this.searchedBooks = books;
|
||||
} catch (error) {
|
||||
console.error('서적 검색 실패:', error);
|
||||
this.searchedBooks = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 서적 선택
|
||||
selectBook(book) {
|
||||
this.selectedBook = book;
|
||||
this.bookSearchQuery = book.title;
|
||||
this.searchedBooks = [];
|
||||
},
|
||||
|
||||
// 유사도 추천 가져오기
|
||||
async getSuggestions() {
|
||||
if (!this.newBook.title.trim()) {
|
||||
this.suggestions = [];
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const suggestions = await window.api.getBookSuggestions(this.newBook.title, 3);
|
||||
this.suggestions = suggestions.filter(s => s.similarity_score > 0.5); // 50% 이상 유사한 것만
|
||||
} catch (error) {
|
||||
console.error('추천 가져오기 실패:', error);
|
||||
this.suggestions = [];
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// 추천에서 기존 서적 선택
|
||||
selectExistingFromSuggestion(suggestion) {
|
||||
this.bookSelectionMode = 'existing';
|
||||
this.selectedBook = suggestion;
|
||||
this.bookSearchQuery = suggestion.title;
|
||||
this.suggestions = [];
|
||||
this.newBook = { title: '', author: '', description: '' };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user