Files
ai-server/templates/upload.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

395 lines
13 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-cloud-upload-alt"></i> 파일 업로드
</h1>
<p class="card-subtitle">PDF 또는 TXT 파일을 업로드하여 AI 처리를 시작하세요</p>
</div>
<form id="uploadForm" enctype="multipart/form-data">
<div class="grid grid-2">
<!-- 파일 선택 영역 -->
<div>
<h3><i class="fas fa-file"></i> 파일 선택</h3>
<div class="drop-zone" id="dropZone">
<i class="fas fa-cloud-upload-alt"></i>
<h4>파일을 드래그하거나 클릭하여 선택</h4>
<p>PDF, TXT 파일 지원 (최대 200MB)</p>
<input type="file" id="fileInput" name="file" accept=".pdf,.txt" style="display: none;">
<button type="button" class="btn btn-primary mt-2" onclick="document.getElementById('fileInput').click()">
<i class="fas fa-folder-open"></i> 파일 선택
</button>
</div>
<div id="fileInfo" class="file-info" style="display: none;">
<h4><i class="fas fa-file-check"></i> 선택된 파일</h4>
<div id="fileDetails"></div>
<button type="button" class="btn btn-secondary btn-sm mt-2" onclick="clearFile()">
<i class="fas fa-times"></i> 다른 파일 선택
</button>
</div>
</div>
<!-- 옵션 설정 -->
<div>
<h3><i class="fas fa-cogs"></i> 처리 옵션</h3>
<div class="form-group">
<label class="form-label" for="docId">문서 ID</label>
<input type="text" class="form-control" id="docId" name="doc_id"
placeholder="예: report-2025-001" required>
<small class="text-muted">고유한 문서 식별자를 입력하세요</small>
</div>
<div class="form-group">
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="generateHtml" name="generate_html" checked>
<i class="fas fa-code"></i> HTML 파일 생성
</label>
<small class="text-muted">읽기 쉬운 HTML 형태로 변환합니다</small>
</div>
</div>
<div class="form-group">
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="translate" name="translate">
<i class="fas fa-language"></i> 한국어로 번역
</label>
<small class="text-muted">영어 문서를 한국어로 번역합니다</small>
</div>
</div>
<div class="form-group" id="targetLangGroup" style="display: none;">
<label class="form-label" for="targetLanguage">번역 언어</label>
<select class="form-control" id="targetLanguage" name="target_language">
<option value="ko">한국어</option>
<option value="en">영어</option>
<option value="ja">일본어</option>
<option value="zh">중국어</option>
</select>
</div>
<div class="form-group">
<div class="checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="summarize" name="summarize">
<i class="fas fa-compress-alt"></i> 요약 생성
</label>
<small class="text-muted">긴 문서를 요약하여 처리합니다</small>
</div>
</div>
<div class="form-group" id="summaryGroup" style="display: none;">
<label class="form-label" for="summarySentences">요약 문장 수</label>
<input type="number" class="form-control" id="summarySentences"
name="summary_sentences" value="5" min="3" max="10">
</div>
</div>
</div>
<div class="upload-actions">
<button type="submit" class="btn btn-success btn-lg" id="uploadBtn" disabled>
<i class="fas fa-rocket"></i> 업로드 및 처리 시작
</button>
<button type="button" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-redo"></i> 초기화
</button>
</div>
</form>
<!-- 진행 상황 -->
<div id="progressSection" style="display: none;">
<h3><i class="fas fa-tasks"></i> 처리 진행 상황</h3>
<div class="progress">
<div class="progress-bar" id="progressBar" style="width: 0%"></div>
</div>
<div id="progressText">준비 중...</div>
</div>
<!-- 결과 -->
<div id="resultSection" style="display: none;">
<h3><i class="fas fa-check-circle"></i> 처리 완료</h3>
<div id="resultContent"></div>
</div>
</div>
<style>
.checkbox-group {
margin-bottom: 0.5rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-weight: 500;
}
.checkbox-label input[type="checkbox"] {
margin: 0;
}
.file-info {
margin-top: 1rem;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.file-details {
display: grid;
gap: 0.5rem;
margin-top: 0.5rem;
}
.upload-actions {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #e9ecef;
display: flex;
gap: 1rem;
justify-content: center;
}
#progressSection {
margin-top: 2rem;
padding: 1.5rem;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
#resultSection {
margin-top: 2rem;
padding: 1.5rem;
background-color: #d4edda;
border-radius: 8px;
border: 1px solid #c3e6cb;
}
.btn-lg {
padding: 1rem 2rem;
font-size: 1.1rem;
}
</style>
{% endblock %}
{% block scripts %}
<script>
let selectedFile = null;
// DOM 요소들
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const fileInfo = document.getElementById('fileInfo');
const fileDetails = document.getElementById('fileDetails');
const uploadBtn = document.getElementById('uploadBtn');
const uploadForm = document.getElementById('uploadForm');
const progressSection = document.getElementById('progressSection');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const resultSection = document.getElementById('resultSection');
const resultContent = document.getElementById('resultContent');
// 체크박스 이벤트
document.getElementById('translate').addEventListener('change', function() {
const targetLangGroup = document.getElementById('targetLangGroup');
targetLangGroup.style.display = this.checked ? 'block' : 'none';
});
document.getElementById('summarize').addEventListener('change', function() {
const summaryGroup = document.getElementById('summaryGroup');
summaryGroup.style.display = this.checked ? 'block' : 'none';
});
// 드래그 앤 드롭 이벤트
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelect(files[0]);
}
});
// 파일 선택 이벤트
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelect(e.target.files[0]);
}
});
// 파일 처리 함수
function handleFileSelect(file) {
// 파일 타입 검증
const allowedTypes = ['application/pdf', 'text/plain'];
if (!allowedTypes.includes(file.type) &&
!file.name.toLowerCase().endsWith('.pdf') &&
!file.name.toLowerCase().endsWith('.txt')) {
showToast('PDF 또는 TXT 파일만 업로드할 수 있습니다.', 'danger');
return;
}
// 파일 크기 검증 (200MB)
if (file.size > 200 * 1024 * 1024) {
showToast('파일 크기는 200MB를 초과할 수 없습니다.', 'danger');
return;
}
selectedFile = file;
// 파일 정보 표시
fileDetails.innerHTML = `
<div class="file-details">
<div><strong>파일명:</strong> ${file.name}</div>
<div><strong>크기:</strong> ${formatFileSize(file.size)}</div>
<div><strong>타입:</strong> ${file.type || '알 수 없음'}</div>
</div>
`;
dropZone.style.display = 'none';
fileInfo.style.display = 'block';
uploadBtn.disabled = false;
// 문서 ID 자동 생성
const timestamp = new Date().toISOString().slice(0, 10);
const baseName = file.name.replace(/\.[^/.]+$/, "");
document.getElementById('docId').value = `${baseName}-${timestamp}`;
}
// 파일 선택 취소
function clearFile() {
selectedFile = null;
fileInput.value = '';
dropZone.style.display = 'block';
fileInfo.style.display = 'none';
uploadBtn.disabled = true;
progressSection.style.display = 'none';
resultSection.style.display = 'none';
}
// 폼 초기화
function resetForm() {
clearFile();
uploadForm.reset();
document.getElementById('generateHtml').checked = true;
document.getElementById('targetLangGroup').style.display = 'none';
document.getElementById('summaryGroup').style.display = 'none';
}
// 폼 제출
uploadForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!selectedFile) {
showToast('파일을 선택해주세요.', 'danger');
return;
}
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('doc_id', document.getElementById('docId').value);
formData.append('generate_html', document.getElementById('generateHtml').checked);
formData.append('translate', document.getElementById('translate').checked);
formData.append('target_language', document.getElementById('targetLanguage').value);
// 진행 상황 표시
progressSection.style.display = 'block';
resultSection.style.display = 'none';
setLoading(uploadBtn);
try {
updateProgress(20, '파일 업로드 중...');
const response = await fetch('/pipeline/ingest_file', {
method: 'POST',
headers: {
'X-API-Key': '{{ api_key }}'
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
updateProgress(80, '처리 중...');
const result = await response.json();
updateProgress(100, '완료!');
// 결과 표시
setTimeout(() => {
showResult(result);
}, 500);
showToast('파일이 성공적으로 처리되었습니다!', 'success');
} catch (error) {
console.error('Upload error:', error);
showToast('업로드 중 오류가 발생했습니다: ' + error.message, 'danger');
progressSection.style.display = 'none';
} finally {
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="fas fa-rocket"></i> 업로드 및 처리 시작';
}
});
function updateProgress(percent, text) {
progressBar.style.width = percent + '%';
progressText.textContent = text;
}
function showResult(result) {
resultContent.innerHTML = `
<div class="result-details">
<h4><i class="fas fa-info-circle"></i> 처리 결과</h4>
<div class="grid grid-2" style="margin-top: 1rem;">
<div>
<strong>문서 ID:</strong> ${result.doc_id}<br>
<strong>처리된 청크:</strong> ${result.added}개<br>
<strong>전체 청크:</strong> ${result.chunks}
</div>
<div class="result-actions">
${result.html_path ? `
<a href="/html/${result.html_path.split('/').pop()}"
class="btn btn-primary" target="_blank">
<i class="fas fa-eye"></i> HTML 보기
</a>
` : ''}
<a href="/documents" class="btn btn-secondary">
<i class="fas fa-list"></i> 문서 목록
</a>
<a href="/chat" class="btn btn-success">
<i class="fas fa-comments"></i> 문서로 대화하기
</a>
</div>
</div>
</div>
`;
resultSection.style.display = 'block';
progressSection.style.display = 'none';
}
</script>
{% endblock %}