feat: SWG 가스켓 전체 구성 정보 표시 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- H/F/I/O SS304/GRAPHITE/CS/CS 패턴에서 4개 구성요소 모두 표시
- 기존 SS304 + GRAPHITE → SS304/GRAPHITE/CS/CS로 완전한 구성 표시
- 외부링/필러/내부링/추가구성 모든 정보 포함
- 구매수량 계산 모달에서 정확한 재질 정보 확인 가능
This commit is contained in:
Hyungi Ahn
2025-08-30 14:23:01 +09:00
parent 78d90c7a8f
commit 4f8e395f87
84 changed files with 16297 additions and 2161 deletions

View File

@@ -0,0 +1,301 @@
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;