- viewer.js: 텍스트 선택 → 하이라이트 생성 기능 구현 - viewer.js: 하이라이트 클릭 시 말풍선 UI로 메모 관리 - viewer.js: 다중 메모 지원, 실시간 메모 추가/삭제 - api.js: 하이라이트, 메모, 책갈피 API 함수 추가 - main.js: 문서 업로드 후 자동 새로고침, 뷰어 페이지 이동 - HTML: 인라인 SVG 파비콘 추가, 색상 버튼 개선 ✅ 하이라이트 생성/삭제 기능 완성 ✅ 메모 추가/편집 기능 완성 ✅ 말풍선 UI 구현 완성 ✅ Alpine.js 컴포넌트 간 안전한 통신
453 lines
13 KiB
JavaScript
453 lines
13 KiB
JavaScript
/**
|
|
* 메인 애플리케이션 Alpine.js 컴포넌트
|
|
*/
|
|
|
|
// 메인 문서 앱 컴포넌트
|
|
window.documentApp = () => ({
|
|
// 상태
|
|
isAuthenticated: false,
|
|
user: null,
|
|
loading: false,
|
|
|
|
// 문서 관련
|
|
documents: [],
|
|
tags: [],
|
|
selectedTag: '',
|
|
viewMode: 'grid', // 'grid' 또는 'list'
|
|
|
|
// 검색
|
|
searchQuery: '',
|
|
searchResults: [],
|
|
|
|
// 모달 상태
|
|
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);
|
|
}
|
|
});
|
|
|
|
// 초기 데이터 로드
|
|
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 response = await api.getDocuments();
|
|
let allDocuments = response || [];
|
|
|
|
// 태그 필터링
|
|
let filteredDocs = allDocuments;
|
|
if (this.selectedTag) {
|
|
filteredDocs = allDocuments.filter(doc =>
|
|
doc.tags && doc.tags.includes(this.selectedTag)
|
|
);
|
|
}
|
|
|
|
this.documents = filteredDocs;
|
|
} catch (error) {
|
|
console.error('Failed to load documents:', error);
|
|
this.documents = [];
|
|
this.showNotification('문서 목록을 불러오는데 실패했습니다', 'error');
|
|
}
|
|
},
|
|
|
|
// 태그 목록 로드
|
|
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}`;
|
|
},
|
|
|
|
// 로그아웃
|
|
async logout() {
|
|
try {
|
|
await api.logout();
|
|
this.isAuthenticated = false;
|
|
this.user = null;
|
|
this.resetData();
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
}
|
|
},
|
|
|
|
// 문서 뷰어 열기
|
|
openDocument(documentId) {
|
|
window.location.href = `/viewer.html?id=${documentId}`;
|
|
},
|
|
|
|
// 날짜 포맷팅
|
|
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'));
|
|
|
|
// 성공 알림
|
|
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 = '');
|
|
}
|
|
});
|
|
|
|
// 파일 업로드 컴포넌트
|
|
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 = '');
|
|
}
|
|
});
|
|
|
|
|