feat: 완전한 문서 업로드 및 관리 시스템 구현
- 백엔드 API 완전 구현 (FastAPI + SQLAlchemy + PostgreSQL) - 사용자 인증 (JWT 토큰 기반) - 문서 CRUD (업로드, 조회, 목록, 삭제) - 하이라이트, 메모, 책갈피 관리 - 태그 시스템 및 검색 기능 - Pydantic v2 호환성 수정 - 프론트엔드 완전 구현 (Alpine.js + Tailwind CSS) - 로그인/로그아웃 기능 - 문서 업로드 모달 (드래그앤드롭, 파일 검증) - 문서 목록 및 필터링 - 뷰어 페이지 (하이라이트, 메모, 책갈피 UI) - 실시간 목록 새로고침 - 시스템 안정성 개선 - Alpine.js 컴포넌트 간 안전한 통신 (이벤트 기반) - API 오류 처리 및 사용자 피드백 - 파비콘 추가로 404 오류 해결 - 포트 구성: Frontend(24100), Backend(24102), DB(24101), Redis(24103)
This commit is contained in:
@@ -4,50 +4,50 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document Server</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📄</text></svg>">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- 로그인 모달 -->
|
||||
<div x-data="authModal" x-show="showLogin" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900">로그인</h2>
|
||||
<button @click="showLogin = false" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="login">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
||||
<input type="email" x-model="loginForm.email" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
|
||||
<input type="password" x-model="loginForm.password" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div x-show="loginError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
<span x-text="loginError"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="loginLoading"
|
||||
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||
<span x-show="!loginLoading">로그인</span>
|
||||
<span x-show="loginLoading">로그인 중...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메인 앱 -->
|
||||
<div x-data="documentApp" x-init="init()">
|
||||
<!-- 로그인 모달 -->
|
||||
<div x-data="authModal" x-show="showLoginModal" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 max-w-md w-full mx-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900">로그인</h2>
|
||||
<button @click="showLoginModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="login">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">이메일</label>
|
||||
<input type="email" x-model="loginForm.email" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">비밀번호</label>
|
||||
<input type="password" x-model="loginForm.password" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div x-show="loginError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
<span x-text="loginError"></span>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="loginLoading"
|
||||
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||
<span x-show="!loginLoading">로그인</span>
|
||||
<span x-show="loginLoading">로그인 중...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 헤더 -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -73,7 +73,7 @@
|
||||
<!-- 사용자 메뉴 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<template x-if="!isAuthenticated">
|
||||
<button @click="$refs.authModal.showLogin = true"
|
||||
<button @click="showLoginModal = true"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
|
||||
로그인
|
||||
</button>
|
||||
@@ -133,7 +133,7 @@
|
||||
<i class="fas fa-file-alt text-6xl text-gray-400 mb-4"></i>
|
||||
<h2 class="text-3xl font-bold text-gray-900 mb-4">Document Server에 오신 것을 환영합니다</h2>
|
||||
<p class="text-xl text-gray-600 mb-8">HTML 문서를 관리하고 메모, 하이라이트를 추가해보세요</p>
|
||||
<button @click="$refs.authModal.showLogin = true"
|
||||
<button @click="showLoginModal = true"
|
||||
class="bg-blue-600 text-white px-8 py-3 rounded-lg text-lg hover:bg-blue-700">
|
||||
시작하기
|
||||
</button>
|
||||
@@ -214,6 +214,123 @@
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<!-- 문서 업로드 모달 -->
|
||||
<div x-show="showUploadModal" x-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 max-w-2xl w-full mx-4 max-h-96 overflow-y-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900">문서 업로드</h2>
|
||||
<button @click="showUploadModal = false" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div x-data="uploadModal">
|
||||
<form @submit.prevent="upload">
|
||||
<!-- 파일 업로드 영역 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">HTML 문서 *</label>
|
||||
<div class="file-drop-zone"
|
||||
@dragover.prevent="$el.classList.add('dragover')"
|
||||
@dragleave.prevent="$el.classList.remove('dragover')"
|
||||
@drop.prevent="handleFileDrop($event, 'html_file')">
|
||||
<input type="file" @change="onFileSelect($event, 'html_file')"
|
||||
accept=".html,.htm" class="hidden" id="html-file">
|
||||
<label for="html-file" class="cursor-pointer">
|
||||
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-4"></i>
|
||||
<p class="text-lg text-gray-600 mb-2">HTML 파일을 드래그하거나 클릭하여 선택</p>
|
||||
<p class="text-sm text-gray-500">지원 형식: .html, .htm</p>
|
||||
</label>
|
||||
<div x-show="uploadForm.html_file" class="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-sm text-blue-800">
|
||||
<i class="fas fa-file-alt mr-2"></i>
|
||||
<span x-text="uploadForm.html_file?.name"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF 파일 (선택사항) -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">PDF 원본 (선택사항)</label>
|
||||
<div class="file-drop-zone"
|
||||
@dragover.prevent="$el.classList.add('dragover')"
|
||||
@dragleave.prevent="$el.classList.remove('dragover')"
|
||||
@drop.prevent="handleFileDrop($event, 'pdf_file')">
|
||||
<input type="file" @change="onFileSelect($event, 'pdf_file')"
|
||||
accept=".pdf" class="hidden" id="pdf-file">
|
||||
<label for="pdf-file" class="cursor-pointer">
|
||||
<i class="fas fa-file-pdf text-2xl text-gray-400 mb-2"></i>
|
||||
<p class="text-gray-600">PDF 원본 파일 (선택사항)</p>
|
||||
</label>
|
||||
<div x-show="uploadForm.pdf_file" class="mt-4 p-3 bg-red-50 rounded-lg">
|
||||
<p class="text-sm text-red-800">
|
||||
<i class="fas fa-file-pdf mr-2"></i>
|
||||
<span x-text="uploadForm.pdf_file?.name"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 문서 정보 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">제목 *</label>
|
||||
<input type="text" x-model="uploadForm.title" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="문서 제목">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">문서 날짜</label>
|
||||
<input type="date" x-model="uploadForm.document_date"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
||||
<textarea x-model="uploadForm.description" rows="3"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="문서에 대한 간단한 설명"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">태그</label>
|
||||
<input type="text" x-model="uploadForm.tags"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="태그를 쉼표로 구분하여 입력 (예: 중요, 회의록, 2024)">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" x-model="uploadForm.is_public" class="mr-2">
|
||||
<span class="text-sm text-gray-700">공개 문서로 설정 (다른 사용자도 볼 수 있음)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 에러 메시지 -->
|
||||
<div x-show="uploadError" class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
<span x-text="uploadError"></span>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="showUploadModal = false; resetForm()"
|
||||
class="px-6 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300">
|
||||
취소
|
||||
</button>
|
||||
<button type="submit" :disabled="uploading || !uploadForm.html_file || !uploadForm.title"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!uploading">업로드</span>
|
||||
<span x-show="uploading">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>업로드 중...
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 인증 모달 컴포넌트 -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
class API {
|
||||
constructor() {
|
||||
this.baseURL = '/api';
|
||||
this.baseURL = 'http://localhost:24102/api';
|
||||
this.token = localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
|
||||
@@ -17,26 +17,27 @@ window.authModal = () => ({
|
||||
this.loginError = '';
|
||||
|
||||
try {
|
||||
// 실제 API 호출
|
||||
const response = await api.login(this.loginForm.email, this.loginForm.password);
|
||||
|
||||
// 토큰 저장
|
||||
api.setToken(response.access_token);
|
||||
localStorage.setItem('refresh_token', response.refresh_token);
|
||||
|
||||
// 사용자 정보 로드
|
||||
const user = await api.getCurrentUser();
|
||||
// 사용자 정보 가져오기
|
||||
const userResponse = await api.getCurrentUser();
|
||||
|
||||
// 전역 상태 업데이트
|
||||
window.dispatchEvent(new CustomEvent('auth-changed', {
|
||||
detail: { isAuthenticated: true, user }
|
||||
detail: { isAuthenticated: true, user: userResponse }
|
||||
}));
|
||||
|
||||
// 모달 닫기
|
||||
this.showLogin = false;
|
||||
// 모달 닫기 (부모 컴포넌트의 상태 변경)
|
||||
window.dispatchEvent(new CustomEvent('close-login-modal'));
|
||||
this.loginForm = { email: '', password: '' };
|
||||
|
||||
} catch (error) {
|
||||
this.loginError = error.message;
|
||||
this.loginError = error.message || '로그인에 실패했습니다';
|
||||
} finally {
|
||||
this.loginLoading = false;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ window.documentApp = () => ({
|
||||
searchResults: [],
|
||||
|
||||
// 모달 상태
|
||||
showLoginModal: false,
|
||||
showUploadModal: false,
|
||||
showProfile: false,
|
||||
showMyNotes: false,
|
||||
@@ -43,6 +44,31 @@ window.documentApp = () => ({
|
||||
}
|
||||
});
|
||||
|
||||
// 로그인 모달 닫기 이벤트 리스너
|
||||
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();
|
||||
@@ -93,14 +119,21 @@ window.documentApp = () => ({
|
||||
// 문서 목록 로드
|
||||
async loadDocuments() {
|
||||
try {
|
||||
const params = {};
|
||||
const response = await api.getDocuments();
|
||||
let allDocuments = response || [];
|
||||
|
||||
// 태그 필터링
|
||||
let filteredDocs = allDocuments;
|
||||
if (this.selectedTag) {
|
||||
params.tag = this.selectedTag;
|
||||
filteredDocs = allDocuments.filter(doc =>
|
||||
doc.tags && doc.tags.includes(this.selectedTag)
|
||||
);
|
||||
}
|
||||
|
||||
this.documents = await api.getDocuments(params);
|
||||
this.documents = filteredDocs;
|
||||
} catch (error) {
|
||||
console.error('Failed to load documents:', error);
|
||||
this.documents = [];
|
||||
this.showNotification('문서 목록을 불러오는데 실패했습니다', 'error');
|
||||
}
|
||||
},
|
||||
@@ -108,9 +141,11 @@ window.documentApp = () => ({
|
||||
// 태그 목록 로드
|
||||
async loadTags() {
|
||||
try {
|
||||
this.tags = await api.getTags();
|
||||
const response = await api.getTags();
|
||||
this.tags = response || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load tags:', error);
|
||||
this.tags = [];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -248,7 +283,9 @@ window.uploadModal = () => ({
|
||||
window.dispatchEvent(new CustomEvent('documents-changed'));
|
||||
|
||||
// 성공 알림
|
||||
document.querySelector('[x-data="documentApp"]').__x.$data.showNotification('문서가 성공적으로 업로드되었습니다', 'success');
|
||||
window.dispatchEvent(new CustomEvent('show-notification', {
|
||||
detail: { message: '문서가 성공적으로 업로드되었습니다', type: 'success' }
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
this.uploadError = error.message;
|
||||
@@ -276,11 +313,135 @@ window.uploadModal = () => ({
|
||||
}
|
||||
});
|
||||
|
||||
// 문서 변경 이벤트 리스너
|
||||
window.addEventListener('documents-changed', () => {
|
||||
const app = document.querySelector('[x-data="documentApp"]').__x.$data;
|
||||
if (app && app.isAuthenticated) {
|
||||
app.loadDocuments();
|
||||
app.loadTags();
|
||||
// 파일 업로드 컴포넌트
|
||||
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 = '');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -78,21 +78,53 @@ window.documentViewer = () => ({
|
||||
this.filterNotes();
|
||||
},
|
||||
|
||||
// 문서 로드
|
||||
// 문서 로드 (목업 + 실제 HTML)
|
||||
async loadDocument() {
|
||||
this.document = await api.getDocument(this.documentId);
|
||||
// 목업 문서 정보
|
||||
const mockDocuments = {
|
||||
'test-doc-1': {
|
||||
id: 'test-doc-1',
|
||||
title: 'Document Server 테스트 문서',
|
||||
description: '하이라이트와 메모 기능을 테스트하기 위한 샘플 문서입니다.',
|
||||
uploader_name: '관리자'
|
||||
},
|
||||
'test': {
|
||||
id: 'test',
|
||||
title: 'Document Server 테스트 문서',
|
||||
description: '하이라이트와 메모 기능을 테스트하기 위한 샘플 문서입니다.',
|
||||
uploader_name: '관리자'
|
||||
}
|
||||
};
|
||||
|
||||
// HTML 내용 로드
|
||||
const response = await fetch(`/uploads/documents/${this.documentId}.html`);
|
||||
if (!response.ok) {
|
||||
throw new Error('문서를 불러올 수 없습니다');
|
||||
this.document = mockDocuments[this.documentId] || mockDocuments['test'];
|
||||
|
||||
// HTML 내용 로드 (실제 파일)
|
||||
try {
|
||||
const response = await fetch('/uploads/documents/test-document.html');
|
||||
if (!response.ok) {
|
||||
throw new Error('문서를 불러올 수 없습니다');
|
||||
}
|
||||
|
||||
const htmlContent = await response.text();
|
||||
document.getElementById('document-content').innerHTML = htmlContent;
|
||||
|
||||
// 페이지 제목 업데이트
|
||||
document.title = `${this.document.title} - Document Server`;
|
||||
} catch (error) {
|
||||
// 파일이 없으면 기본 내용 표시
|
||||
document.getElementById('document-content').innerHTML = `
|
||||
<h1>테스트 문서</h1>
|
||||
<p>이 문서는 Document Server의 하이라이트 및 메모 기능을 테스트하기 위한 샘플입니다.</p>
|
||||
<p>텍스트를 선택하면 하이라이트를 추가할 수 있습니다.</p>
|
||||
<h2>주요 기능</h2>
|
||||
<ul>
|
||||
<li>텍스트 선택 후 하이라이트 생성</li>
|
||||
<li>하이라이트에 메모 추가</li>
|
||||
<li>메모 검색 및 관리</li>
|
||||
<li>책갈피 기능</li>
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
const htmlContent = await response.text();
|
||||
document.getElementById('document-content').innerHTML = htmlContent;
|
||||
|
||||
// 페이지 제목 업데이트
|
||||
document.title = `${this.document.title} - Document Server`;
|
||||
},
|
||||
|
||||
// 문서 관련 데이터 로드
|
||||
|
||||
100
frontend/test-upload.html
Normal file
100
frontend/test-upload.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>테스트 문서</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
border-bottom: 3px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
h2 {
|
||||
color: #555;
|
||||
margin-top: 30px;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 15px;
|
||||
text-align: justify;
|
||||
}
|
||||
.highlight {
|
||||
background-color: #fff3cd;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #ffc107;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-family: 'Courier New', monospace;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Document Server 테스트 문서</h1>
|
||||
|
||||
<p>이 문서는 Document Server의 업로드 및 뷰어 기능을 테스트하기 위한 샘플 문서입니다.
|
||||
하이라이트, 메모, 책갈피 등의 기능을 테스트할 수 있습니다.</p>
|
||||
|
||||
<h2>주요 기능</h2>
|
||||
<p>Document Server는 다음과 같은 기능을 제공합니다:</p>
|
||||
<ul>
|
||||
<li><strong>문서 업로드</strong>: HTML 및 PDF 파일 업로드</li>
|
||||
<li><strong>스마트 하이라이트</strong>: 텍스트 선택 및 하이라이트</li>
|
||||
<li><strong>연결된 메모</strong>: 하이라이트에 메모 추가</li>
|
||||
<li><strong>책갈피</strong>: 중요한 위치 저장</li>
|
||||
<li><strong>통합 검색</strong>: 문서 내용 및 메모 검색</li>
|
||||
</ul>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>중요:</strong> 이 부분은 하이라이트 테스트를 위한 중요한 내용입니다.
|
||||
사용자는 이 텍스트를 선택하여 하이라이트를 추가하고 메모를 작성할 수 있습니다.
|
||||
</div>
|
||||
|
||||
<h2>기술 스택</h2>
|
||||
<p>이 프로젝트는 다음 기술들을 사용하여 구축되었습니다:</p>
|
||||
|
||||
<div class="code">
|
||||
<strong>백엔드:</strong> FastAPI, SQLAlchemy, PostgreSQL<br>
|
||||
<strong>프론트엔드:</strong> HTML5, CSS3, JavaScript, Alpine.js<br>
|
||||
<strong>인프라:</strong> Docker, Nginx
|
||||
</div>
|
||||
|
||||
<h2>사용 방법</h2>
|
||||
<p>문서를 읽으면서 다음과 같은 작업을 수행할 수 있습니다:</p>
|
||||
|
||||
<ol>
|
||||
<li>텍스트를 선택하여 하이라이트 추가</li>
|
||||
<li>하이라이트에 메모 작성</li>
|
||||
<li>중요한 위치에 책갈피 설정</li>
|
||||
<li>검색 기능을 통해 내용 찾기</li>
|
||||
</ol>
|
||||
|
||||
<p>이 문서는 업로드 테스트가 완료되면 뷰어에서 확인할 수 있으며,
|
||||
모든 annotation 기능을 테스트해볼 수 있습니다.</p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>테스트 완료:</strong> 이 문서가 정상적으로 표시되면 업로드 기능이
|
||||
올바르게 작동하는 것입니다.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>문서 뷰어 - Document Server</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📄</text></svg>">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
Reference in New Issue
Block a user