Files
TK-BOM-Project/frontend/src/components/SimpleFileUpload.jsx
Hyungi Ahn 50570e4624
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 사용자 요구사항 기능 완전 구현 및 전체 카테고리 추가
- 사용자 요구사항 저장/로드/엑셀 내보내기 기능 완전 구현
- 백엔드 API 수정: Request Body 방식으로 변경
- 데이터베이스 스키마: material_id 컬럼 추가
- 프론트엔드 상태 관리 개선: 저장 후 자동 리로드
- 입력 필드 연결 문제 해결: 누락된 onChange 핸들러 추가
- NewMaterialsPage에 '전체' 카테고리 버튼 추가 (기본 선택)
- Docker 환경 개선: 프론트엔드 볼륨 마운트 및 포트 수정
- UI 개선: 벌레 이모지 제거, 디버그 코드 정리
2025-09-30 08:55:20 +09:00

324 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import api from '../api';
const SimpleFileUpload = ({ selectedProject, onUploadComplete }) => {
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadResult, setUploadResult] = useState(null);
const [error, setError] = useState('');
const [dragActive, setDragActive] = useState(false);
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFileUpload(e.dataTransfer.files[0]);
}
};
const handleFileSelect = (e) => {
if (e.target.files && e.target.files[0]) {
handleFileUpload(e.target.files[0]);
}
};
const handleFileUpload = async (file) => {
if (!selectedProject) {
setError('프로젝트를 먼저 선택해주세요.');
return;
}
// 파일 유효성 검사
const allowedTypes = ['.xlsx', '.xls', '.csv'];
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedTypes.includes(fileExtension)) {
setError(`지원하지 않는 파일 형식입니다. 허용된 확장자: ${allowedTypes.join(', ')}`);
return;
}
if (file.size > 10 * 1024 * 1024) {
setError('파일 크기는 10MB를 초과할 수 없습니다.');
return;
}
setUploading(true);
setError('');
setUploadResult(null);
setUploadProgress(0);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('job_no', selectedProject.job_no);
formData.append('revision', 'Rev.0');
// 업로드 진행률 시뮬레이션
const progressInterval = setInterval(() => {
setUploadProgress(prev => {
if (prev >= 90) {
clearInterval(progressInterval);
return 90;
}
return prev + 10;
});
}, 200);
const response = await api.post('/files/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
clearInterval(progressInterval);
setUploadProgress(100);
if (response.data.success) {
setUploadResult({
success: true,
message: response.data.message,
file: response.data.file,
job: response.data.job,
sampleMaterials: response.data.sample_materials || []
});
// 업로드 완료 콜백 호출
if (onUploadComplete) {
onUploadComplete(response.data);
}
} else {
throw new Error(response.data.message || '업로드 실패');
}
} catch (err) {
console.error('업로드 에러:', err);
setError(err.response?.data?.detail || err.message || '파일 업로드에 실패했습니다.');
setUploadProgress(0);
} finally {
setUploading(false);
}
};
return (
<div>
{/* 드래그 앤 드롭 영역 */}
<div
style={{
border: `2px dashed ${dragActive ? '#667eea' : '#e2e8f0'}`,
borderRadius: '12px',
padding: '40px 20px',
textAlign: 'center',
background: dragActive ? '#f7fafc' : 'white',
transition: 'all 0.2s ease',
cursor: 'pointer',
marginBottom: '20px'
}}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
onClick={() => document.getElementById('file-input').click()}
>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>
{uploading ? '⏳' : '📤'}
</div>
<div style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', marginBottom: '8px' }}>
{uploading ? '업로드 중...' : 'BOM 파일을 업로드하세요'}
</div>
<div style={{ fontSize: '14px', color: '#718096', marginBottom: '16px' }}>
파일을 드래그하거나 클릭하여 선택하세요
</div>
<div style={{ fontSize: '12px', color: '#a0aec0' }}>
지원 형식: Excel (.xlsx, .xls), CSV (.csv) | 최대 크기: 10MB
</div>
<input
id="file-input"
type="file"
accept=".xlsx,.xls,.csv"
onChange={handleFileSelect}
style={{ display: 'none' }}
disabled={uploading}
/>
</div>
{/* 업로드 진행률 */}
{uploading && (
<div style={{ marginBottom: '20px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px'
}}>
<span style={{ fontSize: '14px', fontWeight: '600', color: '#2d3748' }}>
업로드 진행률
</span>
<span style={{ fontSize: '14px', color: '#667eea' }}>
{uploadProgress}%
</span>
</div>
<div style={{
width: '100%',
height: '8px',
background: '#e2e8f0',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: `${uploadProgress}%`,
height: '100%',
background: 'linear-gradient(90deg, #667eea, #764ba2)',
transition: 'width 0.3s ease'
}} />
</div>
</div>
)}
{/* 에러 메시지 */}
{error && (
<div style={{
background: '#fed7d7',
border: '1px solid #fc8181',
borderRadius: '8px',
padding: '12px 16px',
marginBottom: '20px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span style={{ color: '#c53030', fontSize: '16px' }}></span>
<span style={{ color: '#c53030', fontSize: '14px' }}>{error}</span>
<button
onClick={() => setError('')}
style={{
marginLeft: 'auto',
background: 'none',
border: 'none',
color: '#c53030',
cursor: 'pointer',
fontSize: '16px'
}}
>
</button>
</div>
)}
{/* 업로드 성공 결과 */}
{uploadResult && uploadResult.success && (
<div style={{
background: '#c6f6d5',
border: '1px solid #68d391',
borderRadius: '12px',
padding: '20px',
marginBottom: '20px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
<span style={{ color: '#2f855a', fontSize: '20px' }}></span>
<span style={{ color: '#2f855a', fontSize: '16px', fontWeight: '600' }}>
업로드 완료!
</span>
</div>
<div style={{ color: '#2f855a', fontSize: '14px', marginBottom: '16px' }}>
{uploadResult.message}
</div>
{/* 파일 정보 */}
<div style={{
background: 'white',
borderRadius: '8px',
padding: '16px',
marginBottom: '16px'
}}>
<h4 style={{ margin: '0 0 12px 0', color: '#2d3748', fontSize: '14px' }}>
📄 파일 정보
</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', fontSize: '12px' }}>
<div><strong>파일명:</strong> {uploadResult.file?.original_filename}</div>
<div><strong>분석된 자재:</strong> {uploadResult.file?.parsed_count}</div>
<div><strong>저장된 자재:</strong> {uploadResult.file?.saved_count}</div>
<div><strong>프로젝트:</strong> {uploadResult.job?.job_name}</div>
</div>
</div>
{/* 샘플 자재 미리보기 */}
{uploadResult.sampleMaterials && uploadResult.sampleMaterials.length > 0 && (
<div style={{
background: 'white',
borderRadius: '8px',
padding: '16px'
}}>
<h4 style={{ margin: '0 0 12px 0', color: '#2d3748', fontSize: '14px' }}>
🔧 자재 샘플 (처음 3)
</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{uploadResult.sampleMaterials.map((material, index) => (
<div key={index} style={{
padding: '8px 12px',
background: '#f7fafc',
borderRadius: '6px',
fontSize: '12px',
color: '#4a5568'
}}>
<strong>{material.description || material.item_code}</strong>
{material.category && (
<span style={{
marginLeft: '8px',
padding: '2px 6px',
background: '#667eea',
color: 'white',
borderRadius: '3px',
fontSize: '10px'
}}>
{material.category}
</span>
)}
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
};
export default SimpleFileUpload;