✨ 새로운 기능: - FastAPI 기반 완전한 웹 UI 구현 - 반응형 디자인 (모바일/태블릿 지원) - 드래그앤드롭 파일 업로드 인터페이스 - 실시간 AI 챗봇 인터페이스 - 문서 관리 및 검색 시스템 - 진행률 표시 및 상태 알림 🎨 UI 구성: - 메인 대시보드: 서버 상태, 통계, 빠른 접근 - 파일 업로드: 드래그앤드롭, 처리 옵션, 진행률 - 문서 관리: 검색, 정렬, 미리보기, 다운로드 - AI 챗봇: 실시간 대화, 문서 기반 RAG, 빠른 질문 🔧 기술 스택: - FastAPI + Jinja2 템플릿 - 모던 CSS (그라디언트, 애니메이션) - Font Awesome 아이콘 - JavaScript (ES6+) 🚀 완성된 기능: - 파일 업로드 → 텍스트 추출 → 임베딩 → 번역 → HTML 생성 - 벡터 검색 및 RAG 기반 질의응답 - 다중 모델 지원 (기본/부스팅/영어 전용) - API 키 인증 및 CORS 설정 - NAS 연동 및 파일 내보내기
395 lines
13 KiB
HTML
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 %} |