Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 사용자 요구사항 저장/로드/엑셀 내보내기 기능 완전 구현 - 백엔드 API 수정: Request Body 방식으로 변경 - 데이터베이스 스키마: material_id 컬럼 추가 - 프론트엔드 상태 관리 개선: 저장 후 자동 리로드 - 입력 필드 연결 문제 해결: 누락된 onChange 핸들러 추가 - NewMaterialsPage에 '전체' 카테고리 버튼 추가 (기본 선택) - Docker 환경 개선: 프론트엔드 볼륨 마운트 및 포트 수정 - UI 개선: 벌레 이모지 제거, 디버그 코드 정리
324 lines
12 KiB
JavaScript
324 lines
12 KiB
JavaScript
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;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|