✨ 새로운 기능: - FastAPI 기반 완전한 웹 UI 구현 - 반응형 디자인 (모바일/태블릿 지원) - 드래그앤드롭 파일 업로드 인터페이스 - 실시간 AI 챗봇 인터페이스 - 문서 관리 및 검색 시스템 - 진행률 표시 및 상태 알림 🎨 UI 구성: - 메인 대시보드: 서버 상태, 통계, 빠른 접근 - 파일 업로드: 드래그앤드롭, 처리 옵션, 진행률 - 문서 관리: 검색, 정렬, 미리보기, 다운로드 - AI 챗봇: 실시간 대화, 문서 기반 RAG, 빠른 질문 🔧 기술 스택: - FastAPI + Jinja2 템플릿 - 모던 CSS (그라디언트, 애니메이션) - Font Awesome 아이콘 - JavaScript (ES6+) 🚀 완성된 기능: - 파일 업로드 → 텍스트 추출 → 임베딩 → 번역 → HTML 생성 - 벡터 검색 및 RAG 기반 질의응답 - 다중 모델 지원 (기본/부스팅/영어 전용) - API 키 인증 및 CORS 설정 - NAS 연동 및 파일 내보내기
348 lines
10 KiB
HTML
348 lines
10 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}문서 관리 - AI 문서 처리 서버{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h1 class="card-title">
|
|
<i class="fas fa-folder-open"></i> 문서 관리
|
|
</h1>
|
|
<p class="card-subtitle">처리된 문서들을 확인하고 관리하세요</p>
|
|
</div>
|
|
|
|
<!-- 통계 요약 -->
|
|
<div class="grid grid-3 mb-4">
|
|
<div class="card">
|
|
<h3><i class="fas fa-file-alt"></i> 총 문서 수</h3>
|
|
<div class="stat-number">{{ documents|length }}</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3><i class="fas fa-database"></i> 총 용량</h3>
|
|
<div class="stat-number" id="totalSize">계산 중...</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3><i class="fas fa-calendar"></i> 최근 업로드</h3>
|
|
<div class="stat-number">
|
|
{% if documents %}
|
|
{{ documents[0].created.split(' ')[0] }}
|
|
{% else %}
|
|
없음
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 검색 및 필터 -->
|
|
<div class="search-controls mb-4">
|
|
<div class="grid grid-2">
|
|
<div>
|
|
<input type="text" class="form-control" id="searchInput"
|
|
placeholder="파일명으로 검색..." onkeyup="filterDocuments()">
|
|
</div>
|
|
<div>
|
|
<select class="form-control" id="sortSelect" onchange="sortDocuments()">
|
|
<option value="name">이름순</option>
|
|
<option value="date" selected>날짜순 (최신)</option>
|
|
<option value="size">크기순</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 문서 목록 -->
|
|
{% if documents %}
|
|
<div class="table-responsive">
|
|
<table class="table" id="documentsTable">
|
|
<thead>
|
|
<tr>
|
|
<th><i class="fas fa-file"></i> 파일명</th>
|
|
<th><i class="fas fa-calendar"></i> 생성일시</th>
|
|
<th><i class="fas fa-weight"></i> 크기</th>
|
|
<th><i class="fas fa-cogs"></i> 액션</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for doc in documents %}
|
|
<tr class="document-row" data-name="{{ doc.name|lower }}" data-date="{{ doc.created }}" data-size="{{ doc.size }}">
|
|
<td>
|
|
<div class="file-info">
|
|
<i class="fas fa-file-alt text-primary"></i>
|
|
<strong>{{ doc.name }}</strong>
|
|
<br>
|
|
<small class="text-muted">{{ formatFileSize(doc.size) }}</small>
|
|
</div>
|
|
</td>
|
|
<td>{{ doc.created }}</td>
|
|
<td>
|
|
<span class="file-size" data-bytes="{{ doc.size }}">{{ formatFileSize(doc.size) }}</span>
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<a href="/html/{{ doc.name }}" class="btn btn-sm btn-primary" target="_blank" title="HTML 보기">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<button onclick="openChatWithDoc('{{ doc.name }}')" class="btn btn-sm btn-success" title="이 문서로 대화하기">
|
|
<i class="fas fa-comments"></i>
|
|
</button>
|
|
<button onclick="downloadDoc('{{ doc.name }}')" class="btn btn-sm btn-secondary" title="다운로드">
|
|
<i class="fas fa-download"></i>
|
|
</button>
|
|
<button onclick="deleteDoc('{{ doc.name }}')" class="btn btn-sm btn-danger" title="삭제">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<i class="fas fa-inbox fa-4x text-muted mb-3"></i>
|
|
<h3>처리된 문서가 없습니다</h3>
|
|
<p class="text-muted mb-4">파일을 업로드하여 AI 처리를 시작하세요.</p>
|
|
<a href="/upload" class="btn btn-primary btn-lg">
|
|
<i class="fas fa-cloud-upload-alt"></i> 첫 문서 업로드하기
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- 파일 미리보기 모달 -->
|
|
<div class="modal" id="previewModal" style="display: none;">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 id="previewTitle">문서 미리보기</h3>
|
|
<button onclick="closePreview()" class="btn btn-sm btn-secondary">
|
|
<i class="fas fa-times"></i> 닫기
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<iframe id="previewFrame" style="width: 100%; height: 500px; border: none;"></iframe>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.search-controls {
|
|
background-color: #f8f9fa;
|
|
padding: 1.5rem;
|
|
border-radius: 8px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.file-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.file-info i {
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
}
|
|
|
|
.modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal-content {
|
|
background: white;
|
|
border-radius: 10px;
|
|
width: 90%;
|
|
max-width: 900px;
|
|
max-height: 90%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid #e9ecef;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.mb-4 { margin-bottom: 2rem; }
|
|
.text-primary { color: #667eea; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let allDocuments = [];
|
|
|
|
// 페이지 로드 시 초기화
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// 문서 데이터 수집
|
|
const rows = document.querySelectorAll('.document-row');
|
|
allDocuments = Array.from(rows).map(row => ({
|
|
element: row,
|
|
name: row.dataset.name,
|
|
date: new Date(row.dataset.date),
|
|
size: parseInt(row.dataset.size)
|
|
}));
|
|
|
|
// 총 용량 계산
|
|
const totalBytes = allDocuments.reduce((sum, doc) => sum + doc.size, 0);
|
|
document.getElementById('totalSize').textContent = formatFileSize(totalBytes);
|
|
|
|
// 기본 정렬 (날짜순)
|
|
sortDocuments();
|
|
});
|
|
|
|
// 파일 크기 포맷팅
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
// 문서 검색 필터링
|
|
function filterDocuments() {
|
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
const rows = document.querySelectorAll('.document-row');
|
|
|
|
rows.forEach(row => {
|
|
const fileName = row.dataset.name;
|
|
if (fileName.includes(searchTerm)) {
|
|
row.style.display = '';
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
// 문서 정렬
|
|
function sortDocuments() {
|
|
const sortBy = document.getElementById('sortSelect').value;
|
|
const tbody = document.querySelector('#documentsTable tbody');
|
|
|
|
allDocuments.sort((a, b) => {
|
|
switch (sortBy) {
|
|
case 'name':
|
|
return a.name.localeCompare(b.name);
|
|
case 'date':
|
|
return b.date - a.date; // 최신순
|
|
case 'size':
|
|
return b.size - a.size; // 큰 것부터
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
// DOM 재정렬
|
|
allDocuments.forEach(doc => {
|
|
tbody.appendChild(doc.element);
|
|
});
|
|
}
|
|
|
|
// 문서로 채팅 시작
|
|
function openChatWithDoc(fileName) {
|
|
const docName = fileName.replace('.html', '');
|
|
window.location.href = `/chat?doc=${encodeURIComponent(docName)}`;
|
|
}
|
|
|
|
// 문서 다운로드
|
|
function downloadDoc(fileName) {
|
|
const link = document.createElement('a');
|
|
link.href = `/html/${fileName}`;
|
|
link.download = fileName;
|
|
link.click();
|
|
}
|
|
|
|
// 문서 삭제
|
|
async function deleteDoc(fileName) {
|
|
if (!confirm(`정말로 "${fileName}" 문서를 삭제하시겠습니까?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 실제로는 DELETE API를 구현해야 함
|
|
showToast('삭제 기능은 아직 구현되지 않았습니다.', 'info');
|
|
// TODO: DELETE /api/documents/{fileName} 구현
|
|
} catch (error) {
|
|
console.error('Delete error:', error);
|
|
showToast('삭제 중 오류가 발생했습니다.', 'danger');
|
|
}
|
|
}
|
|
|
|
// 미리보기 열기
|
|
function openPreview(fileName) {
|
|
document.getElementById('previewTitle').textContent = `${fileName} 미리보기`;
|
|
document.getElementById('previewFrame').src = `/html/${fileName}`;
|
|
document.getElementById('previewModal').style.display = 'flex';
|
|
}
|
|
|
|
// 미리보기 닫기
|
|
function closePreview() {
|
|
document.getElementById('previewModal').style.display = 'none';
|
|
document.getElementById('previewFrame').src = '';
|
|
}
|
|
|
|
// 토스트 알림 (main.js에서 가져옴)
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.createElement('div');
|
|
toast.className = `alert alert-${type}`;
|
|
toast.textContent = message;
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 1001;
|
|
min-width: 300px;
|
|
animation: slideIn 0.3s ease;
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideOut 0.3s ease';
|
|
setTimeout(() => {
|
|
if (document.body.contains(toast)) {
|
|
document.body.removeChild(toast);
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
</script>
|
|
{% endblock %} |