Files
ai-server/templates/documents.html
hyungi cb009f7393 feat: 완전한 웹 UI 구현 및 문서 처리 파이프라인 완성
 새로운 기능:
- FastAPI 기반 완전한 웹 UI 구현
- 반응형 디자인 (모바일/태블릿 지원)
- 드래그앤드롭 파일 업로드 인터페이스
- 실시간 AI 챗봇 인터페이스
- 문서 관리 및 검색 시스템
- 진행률 표시 및 상태 알림

🎨 UI 구성:
- 메인 대시보드: 서버 상태, 통계, 빠른 접근
- 파일 업로드: 드래그앤드롭, 처리 옵션, 진행률
- 문서 관리: 검색, 정렬, 미리보기, 다운로드
- AI 챗봇: 실시간 대화, 문서 기반 RAG, 빠른 질문

🔧 기술 스택:
- FastAPI + Jinja2 템플릿
- 모던 CSS (그라디언트, 애니메이션)
- Font Awesome 아이콘
- JavaScript (ES6+)

🚀 완성된 기능:
- 파일 업로드 → 텍스트 추출 → 임베딩 → 번역 → HTML 생성
- 벡터 검색 및 RAG 기반 질의응답
- 다중 모델 지원 (기본/부스팅/영어 전용)
- API 키 인증 및 CORS 설정
- NAS 연동 및 파일 내보내기
2025-08-14 08:09:48 +09:00

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 %}