feat: 통합 BOM 관리 시스템 구현
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

🎯 주요 변경사항:
- 통합 BOM 페이지 (UnifiedBOMPage) 신규 개발
- 탭 구조로 업로드 → 파일 관리 → 자재 관리 워크플로우 개선
- 컴포넌트 분리로 스파게티 코드 방지

📤 업로드 탭 (BOMUploadTab):
- 드래그 앤 드롭 파일 업로드
- 파일 검증 및 진행률 표시
- 업로드 완료 후 자동 Files 탭 이동

📊 파일 관리 탭 (BOMFilesTab):
- 프로젝트별 BOM 파일 목록 조회
- 리비전 히스토리 표시
- BOM 선택 후 자동 Materials 탭 이동

📋 자재 관리 탭 (BOMMaterialsTab):
- 기존 BOMManagementPage 래핑
- 선택된 BOM의 자재 분류 및 관리

🔧 백엔드 API 개선:
- /files/project/{project_code} 엔드포인트 추가
- 한글 프로젝트 코드 URL 인코딩 지원
- 프로젝트별 파일 조회 기능 구현

🎨 대시보드 개선:
- 3개 BOM 카드를 1개 통합 카드로 변경
- 기능 미리보기 태그 추가
- 더 직관적인 네비게이션

📁 코드 구조 개선:
- 기존 페이지들을 _deprecated 폴더로 이동
- 탭별 컴포넌트 분리 (components/bom/tabs/)
- PAGES_GUIDE.md 업데이트

 사용자 경험 개선:
- 자연스러운 워크플로우 (업로드 → 선택 → 관리)
- 탭 간 상태 공유 및 자동 네비게이션
- 통합된 인터페이스에서 모든 BOM 작업 처리
This commit is contained in:
hyungi
2025-10-17 14:44:17 +09:00
parent e0ad21bfad
commit ab607dfa9a
12 changed files with 2405 additions and 23 deletions

View File

@@ -0,0 +1,429 @@
import React, { useState, useEffect } from 'react';
import api from '../../../api';
const BOMFilesTab = ({
selectedProject,
user,
bomFiles,
setBomFiles,
selectedBOM,
onBOMSelect,
refreshTrigger
}) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [groupedFiles, setGroupedFiles] = useState({});
// BOM 파일 목록 로드
useEffect(() => {
const loadBOMFiles = async () => {
if (!selectedProject) return;
try {
setLoading(true);
setError('');
const projectCode = selectedProject.official_project_code || selectedProject.job_no;
const encodedProjectCode = encodeURIComponent(projectCode);
const response = await api.get(`/files/project/${encodedProjectCode}`);
const files = response.data || [];
setBomFiles(files);
// BOM 이름별로 그룹화
const groups = groupFilesByBOM(files);
setGroupedFiles(groups);
} catch (err) {
console.error('BOM 파일 로드 실패:', err);
setError('BOM 파일을 불러오는데 실패했습니다.');
} finally {
setLoading(false);
}
};
loadBOMFiles();
}, [selectedProject, refreshTrigger, setBomFiles]);
// 파일을 BOM 이름별로 그룹화
const groupFilesByBOM = (fileList) => {
const groups = {};
fileList.forEach(file => {
const bomName = file.bom_name || file.original_filename;
if (!groups[bomName]) {
groups[bomName] = [];
}
groups[bomName].push(file);
});
// 각 그룹 내에서 리비전 번호로 정렬
Object.keys(groups).forEach(bomName => {
groups[bomName].sort((a, b) => {
const revA = parseInt(a.revision?.replace('Rev.', '') || '0');
const revB = parseInt(b.revision?.replace('Rev.', '') || '0');
return revB - revA; // 최신 리비전이 위로
});
});
return groups;
};
// BOM 선택 처리
const handleBOMClick = (bomFile) => {
if (onBOMSelect) {
onBOMSelect(bomFile);
}
};
// 파일 삭제
const handleDeleteFile = async (fileId, bomName) => {
if (!window.confirm(`이 파일을 삭제하시겠습니까?`)) {
return;
}
try {
await api.delete(`/files/delete/${fileId}`);
// 파일 목록 새로고침
const projectCode = selectedProject.official_project_code || selectedProject.job_no;
const encodedProjectCode = encodeURIComponent(projectCode);
const response = await api.get(`/files/project/${encodedProjectCode}`);
const files = response.data || [];
setBomFiles(files);
setGroupedFiles(groupFilesByBOM(files));
} catch (err) {
console.error('파일 삭제 실패:', err);
setError('파일 삭제에 실패했습니다.');
}
};
// 리비전 업로드 (향후 구현)
const handleRevisionUpload = (parentFile) => {
// TODO: 리비전 업로드 기능 구현
alert('리비전 업로드 기능은 향후 구현 예정입니다.');
};
// 날짜 포맷팅
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
try {
return new Date(dateString).toLocaleDateString('ko-KR');
} catch {
return dateString;
}
};
if (loading) {
return (
<div style={{
padding: '60px',
textAlign: 'center',
color: '#6b7280'
}}>
<div style={{ fontSize: '24px', marginBottom: '16px' }}></div>
<div>Loading BOM files...</div>
</div>
);
}
if (error) {
return (
<div style={{
padding: '40px',
textAlign: 'center'
}}>
<div style={{
background: '#fee2e2',
border: '1px solid #fecaca',
borderRadius: '8px',
padding: '16px',
color: '#dc2626'
}}>
<div style={{ fontSize: '20px', marginBottom: '8px' }}></div>
{error}
</div>
</div>
);
}
if (bomFiles.length === 0) {
return (
<div style={{
padding: '60px',
textAlign: 'center',
color: '#6b7280'
}}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📄</div>
<h3 style={{ fontSize: '20px', fontWeight: '600', marginBottom: '8px' }}>
No BOM Files Found
</h3>
<p style={{ fontSize: '16px', margin: 0 }}>
Upload your first BOM file using the Upload tab
</p>
</div>
);
}
return (
<div style={{ padding: '40px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '32px'
}}>
<div>
<h2 style={{
fontSize: '24px',
fontWeight: '700',
color: '#0f172a',
margin: '0 0 8px 0'
}}>
BOM Files & Revisions
</h2>
<p style={{
fontSize: '16px',
color: '#64748b',
margin: 0
}}>
Select a BOM file to manage its materials
</p>
</div>
<div style={{
background: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
padding: '12px 20px',
borderRadius: '12px',
fontSize: '14px',
fontWeight: '600',
color: '#1d4ed8'
}}>
{Object.keys(groupedFiles).length} BOM Groups {bomFiles.length} Total Files
</div>
</div>
{/* BOM 파일 그룹 목록 */}
<div style={{ display: 'grid', gap: '24px' }}>
{Object.entries(groupedFiles).map(([bomName, files]) => {
const latestFile = files[0]; // 최신 리비전
const isSelected = selectedBOM?.id === latestFile.id;
return (
<div key={bomName} style={{
background: isSelected ? '#eff6ff' : 'white',
border: isSelected ? '2px solid #3b82f6' : '1px solid #e5e7eb',
borderRadius: '16px',
padding: '24px',
transition: 'all 0.2s ease',
cursor: 'pointer'
}}
onClick={() => handleBOMClick(latestFile)}
>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '16px'
}}>
<div style={{ flex: 1 }}>
<h3 style={{
fontSize: '20px',
fontWeight: '600',
color: isSelected ? '#1d4ed8' : '#374151',
margin: '0 0 8px 0',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
<span style={{ fontSize: '24px' }}>📋</span>
{bomName}
{isSelected && (
<span style={{
background: '#3b82f6',
color: 'white',
fontSize: '12px',
padding: '4px 8px',
borderRadius: '6px',
fontWeight: '500'
}}>
SELECTED
</span>
)}
</h3>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: '12px',
fontSize: '14px',
color: '#6b7280'
}}>
<div>
<span style={{ fontWeight: '500' }}>Latest:</span> {latestFile.revision || 'Rev.0'}
</div>
<div>
<span style={{ fontWeight: '500' }}>Revisions:</span> {files.length}
</div>
<div>
<span style={{ fontWeight: '500' }}>Updated:</span> {formatDate(latestFile.upload_date)}
</div>
<div>
<span style={{ fontWeight: '500' }}>Size:</span> {latestFile.file_size ? `${Math.round(latestFile.file_size / 1024)} KB` : 'N/A'}
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '8px', marginLeft: '16px' }}>
<button
onClick={(e) => {
e.stopPropagation();
handleRevisionUpload(latestFile);
}}
style={{
padding: '8px 12px',
background: 'white',
color: '#f59e0b',
border: '1px solid #f59e0b',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '12px',
fontWeight: '500',
transition: 'all 0.2s ease'
}}
>
📝 New Revision
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteFile(latestFile.id, bomName);
}}
style={{
padding: '8px 12px',
background: '#fee2e2',
color: '#dc2626',
border: '1px solid #fecaca',
borderRadius: '8px',
cursor: 'pointer',
fontSize: '12px',
fontWeight: '500'
}}
>
🗑 Delete
</button>
</div>
</div>
{/* 리비전 히스토리 */}
{files.length > 1 && (
<div style={{
background: '#f8fafc',
borderRadius: '8px',
padding: '12px',
marginTop: '16px'
}}>
<h4 style={{
fontSize: '14px',
fontWeight: '600',
color: '#374151',
margin: '0 0 8px 0'
}}>
Revision History
</h4>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{files.map((file, index) => (
<div key={file.id} style={{
background: index === 0 ? '#dbeafe' : 'white',
color: index === 0 ? '#1d4ed8' : '#6b7280',
padding: '4px 8px',
borderRadius: '6px',
fontSize: '12px',
fontWeight: '500',
border: '1px solid #e5e7eb'
}}>
{file.revision || 'Rev.0'}
{index === 0 && ' (Latest)'}
</div>
))}
</div>
</div>
)}
{/* 선택 안내 */}
{!isSelected && (
<div style={{
marginTop: '16px',
padding: '12px',
background: 'rgba(59, 130, 246, 0.05)',
borderRadius: '8px',
textAlign: 'center',
fontSize: '14px',
color: '#3b82f6',
fontWeight: '500'
}}>
Click to select this BOM for material management
</div>
)}
</div>
);
})}
</div>
{/* 향후 기능 안내 */}
<div style={{
marginTop: '40px',
padding: '24px',
background: '#f8fafc',
borderRadius: '12px',
border: '1px solid #e2e8f0'
}}>
<h3 style={{
fontSize: '18px',
fontWeight: '600',
color: '#374151',
marginBottom: '16px'
}}>
🚧 Coming Soon: Advanced Revision Features
</h3>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '16px'
}}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '8px' }}>📊</div>
<div style={{ fontSize: '14px', fontWeight: '500', color: '#374151' }}>
Visual Timeline
</div>
<div style={{ fontSize: '12px', color: '#6b7280' }}>
Interactive revision history
</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '8px' }}>🔍</div>
<div style={{ fontSize: '14px', fontWeight: '500', color: '#374151' }}>
Diff Comparison
</div>
<div style={{ fontSize: '12px', color: '#6b7280' }}>
Compare changes between revisions
</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '8px' }}></div>
<div style={{ fontSize: '14px', fontWeight: '500', color: '#374151' }}>
Rollback System
</div>
<div style={{ fontSize: '12px', color: '#6b7280' }}>
Restore previous versions
</div>
</div>
</div>
</div>
</div>
);
};
export default BOMFilesTab;

View File

@@ -0,0 +1,105 @@
import React from 'react';
import BOMManagementPage from '../../../pages/BOMManagementPage';
const BOMMaterialsTab = ({
selectedProject,
user,
selectedBOM,
onNavigate
}) => {
// BOMManagementPage에 필요한 props 구성
const bomManagementProps = {
onNavigate,
user,
selectedProject,
fileId: selectedBOM?.id,
jobNo: selectedBOM?.job_no || selectedProject?.official_project_code || selectedProject?.job_no,
bomName: selectedBOM?.bom_name || selectedBOM?.original_filename,
revision: selectedBOM?.revision || 'Rev.0',
filename: selectedBOM?.original_filename
};
return (
<div style={{
background: 'white',
minHeight: '600px'
}}>
{/* 헤더 정보 */}
<div style={{
padding: '24px 40px',
borderBottom: '1px solid #e5e7eb',
background: '#f8fafc'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2 style={{
fontSize: '24px',
fontWeight: '700',
color: '#0f172a',
margin: '0 0 8px 0'
}}>
Material Management
</h2>
<p style={{
fontSize: '16px',
color: '#64748b',
margin: 0
}}>
BOM: {selectedBOM?.bom_name || selectedBOM?.original_filename} {selectedBOM?.revision || 'Rev.0'}
</p>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
gap: '12px',
fontSize: '12px'
}}>
<div style={{
background: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
padding: '8px 12px',
borderRadius: '8px',
textAlign: 'center'
}}>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#1d4ed8' }}>
{selectedBOM?.id || 'N/A'}
</div>
<div style={{ color: '#1d4ed8', fontWeight: '500' }}>
File ID
</div>
</div>
<div style={{
background: 'linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%)',
padding: '8px 12px',
borderRadius: '8px',
textAlign: 'center'
}}>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#059669' }}>
{selectedBOM?.revision || 'Rev.0'}
</div>
<div style={{ color: '#059669', fontWeight: '500' }}>
Revision
</div>
</div>
</div>
</div>
</div>
{/* BOM 관리 페이지 임베드 */}
<div style={{
background: 'white',
// BOMManagementPage의 기본 패딩을 제거하기 위한 스타일 오버라이드
'& > div': {
padding: '0 !important',
background: 'transparent !important',
minHeight: 'auto !important'
}
}}>
<BOMManagementPage {...bomManagementProps} />
</div>
</div>
);
};
export default BOMMaterialsTab;

View File

@@ -0,0 +1,496 @@
import React, { useState, useRef, useCallback } from 'react';
import api from '../../../api';
const BOMUploadTab = ({
selectedProject,
user,
onUploadSuccess,
onNavigate
}) => {
const [dragOver, setDragOver] = useState(false);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [selectedFiles, setSelectedFiles] = useState([]);
const [bomName, setBomName] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const fileInputRef = useRef(null);
// 파일 검증
const validateFile = (file) => {
const allowedTypes = [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'
];
const maxSize = 50 * 1024 * 1024; // 50MB
if (!allowedTypes.includes(file.type) && !file.name.match(/\.(xlsx|xls|csv)$/i)) {
return '지원되지 않는 파일 형식입니다. Excel 또는 CSV 파일만 업로드 가능합니다.';
}
if (file.size > maxSize) {
return '파일 크기가 너무 큽니다. 50MB 이하의 파일만 업로드 가능합니다.';
}
return null;
};
// 파일 선택 처리
const handleFileSelect = useCallback((files) => {
const fileList = Array.from(files);
const validFiles = [];
const errors = [];
fileList.forEach(file => {
const error = validateFile(file);
if (error) {
errors.push(`${file.name}: ${error}`);
} else {
validFiles.push(file);
}
});
if (errors.length > 0) {
setError(errors.join('\n'));
return;
}
setSelectedFiles(validFiles);
setError('');
// 첫 번째 파일명을 기본 BOM 이름으로 설정 (확장자 제거)
if (validFiles.length > 0 && !bomName) {
const fileName = validFiles[0].name;
const nameWithoutExt = fileName.replace(/\.[^/.]+$/, '');
setBomName(nameWithoutExt);
}
}, [bomName]);
// 드래그 앤 드롭 처리
const handleDragOver = useCallback((e) => {
e.preventDefault();
setDragOver(true);
}, []);
const handleDragLeave = useCallback((e) => {
e.preventDefault();
setDragOver(false);
}, []);
const handleDrop = useCallback((e) => {
e.preventDefault();
setDragOver(false);
handleFileSelect(e.dataTransfer.files);
}, [handleFileSelect]);
// 파일 선택 버튼 클릭
const handleFileButtonClick = () => {
fileInputRef.current?.click();
};
// 파일 업로드
const handleUpload = async () => {
if (selectedFiles.length === 0) {
setError('업로드할 파일을 선택해주세요.');
return;
}
if (!bomName.trim()) {
setError('BOM 이름을 입력해주세요.');
return;
}
if (!selectedProject) {
setError('프로젝트를 선택해주세요.');
return;
}
try {
setUploading(true);
setUploadProgress(0);
setError('');
setSuccess('');
let uploadedFile = null;
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i];
const formData = new FormData();
formData.append('file', file);
formData.append('job_no', selectedProject.official_project_code || selectedProject.job_no);
formData.append('bom_name', bomName.trim());
formData.append('revision', 'Rev.0'); // 새 업로드는 항상 Rev.0
const response = await api.post('/files/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
const progress = Math.round(
((i * 100) + (progressEvent.loaded * 100) / progressEvent.total) / selectedFiles.length
);
setUploadProgress(progress);
}
});
if (!response.data?.success) {
throw new Error(response.data?.message || '업로드 실패');
}
// 첫 번째 파일의 정보를 저장
if (i === 0) {
uploadedFile = {
id: response.data.file_id,
bom_name: bomName.trim(),
revision: 'Rev.0',
job_no: selectedProject.official_project_code || selectedProject.job_no,
original_filename: file.name
};
}
}
setSuccess(`${selectedFiles.length}개 파일이 성공적으로 업로드되었습니다!`);
// 파일 초기화
setSelectedFiles([]);
setBomName('');
// 2초 후 Files 탭으로 이동
setTimeout(() => {
if (onUploadSuccess) {
onUploadSuccess(uploadedFile);
}
}, 2000);
} catch (err) {
console.error('업로드 실패:', err);
setError(`업로드 실패: ${err.response?.data?.detail || err.message}`);
} finally {
setUploading(false);
setUploadProgress(0);
}
};
// 파일 제거
const removeFile = (index) => {
const newFiles = selectedFiles.filter((_, i) => i !== index);
setSelectedFiles(newFiles);
if (newFiles.length === 0) {
setBomName('');
}
};
// 파일 크기 포맷팅
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
return (
<div style={{ padding: '40px' }}>
{/* BOM 이름 입력 */}
<div style={{ marginBottom: '32px' }}>
<label style={{
display: 'block',
fontSize: '16px',
fontWeight: '600',
color: '#374151',
marginBottom: '8px'
}}>
BOM Name
</label>
<input
type="text"
value={bomName}
onChange={(e) => setBomName(e.target.value)}
placeholder="Enter BOM name..."
style={{
width: '100%',
padding: '12px 16px',
border: '2px solid #e5e7eb',
borderRadius: '12px',
fontSize: '16px',
transition: 'border-color 0.2s ease',
outline: 'none'
}}
onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
onBlur={(e) => e.target.style.borderColor = '#e5e7eb'}
/>
</div>
{/* 파일 드롭 영역 */}
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
border: `3px dashed ${dragOver ? '#3b82f6' : '#d1d5db'}`,
borderRadius: '16px',
padding: '60px 40px',
textAlign: 'center',
background: dragOver ? '#eff6ff' : '#f9fafb',
transition: 'all 0.3s ease',
cursor: 'pointer',
marginBottom: '24px'
}}
onClick={handleFileButtonClick}
>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>
{dragOver ? '📁' : '📄'}
</div>
<h3 style={{
fontSize: '20px',
fontWeight: '600',
color: '#374151',
margin: '0 0 8px 0'
}}>
{dragOver ? 'Drop files here' : 'Upload BOM Files'}
</h3>
<p style={{
fontSize: '16px',
color: '#6b7280',
margin: '0 0 16px 0'
}}>
Drag and drop your Excel or CSV files here, or click to browse
</p>
<div style={{
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
padding: '8px 16px',
background: 'rgba(59, 130, 246, 0.1)',
borderRadius: '8px',
fontSize: '14px',
color: '#3b82f6'
}}>
<span>📋</span>
Supported: .xlsx, .xls, .csv (Max 50MB)
</div>
</div>
{/* 숨겨진 파일 입력 */}
<input
ref={fileInputRef}
type="file"
multiple
accept=".xlsx,.xls,.csv"
onChange={(e) => handleFileSelect(e.target.files)}
style={{ display: 'none' }}
/>
{/* 선택된 파일 목록 */}
{selectedFiles.length > 0 && (
<div style={{ marginBottom: '24px' }}>
<h4 style={{
fontSize: '18px',
fontWeight: '600',
color: '#374151',
marginBottom: '16px'
}}>
Selected Files ({selectedFiles.length})
</h4>
<div style={{
background: '#f8fafc',
borderRadius: '12px',
padding: '16px'
}}>
{selectedFiles.map((file, index) => (
<div key={index} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
background: 'white',
borderRadius: '8px',
marginBottom: index < selectedFiles.length - 1 ? '8px' : '0',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '20px' }}>📄</span>
<div>
<div style={{ fontWeight: '500', color: '#374151' }}>
{file.name}
</div>
<div style={{ fontSize: '14px', color: '#6b7280' }}>
{formatFileSize(file.size)}
</div>
</div>
</div>
<button
onClick={() => removeFile(index)}
style={{
background: '#fee2e2',
color: '#dc2626',
border: 'none',
borderRadius: '6px',
padding: '6px 12px',
cursor: 'pointer',
fontSize: '12px',
fontWeight: '500'
}}
>
Remove
</button>
</div>
))}
</div>
</div>
)}
{/* 업로드 진행률 */}
{uploading && (
<div style={{ marginBottom: '24px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px'
}}>
<span style={{ fontSize: '14px', fontWeight: '500', color: '#374151' }}>
Uploading...
</span>
<span style={{ fontSize: '14px', fontWeight: '500', color: '#3b82f6' }}>
{uploadProgress}%
</span>
</div>
<div style={{
width: '100%',
height: '8px',
background: '#e5e7eb',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: `${uploadProgress}%`,
height: '100%',
background: 'linear-gradient(90deg, #3b82f6 0%, #1d4ed8 100%)',
transition: 'width 0.3s ease'
}} />
</div>
</div>
)}
{/* 에러 메시지 */}
{error && (
<div style={{
background: '#fee2e2',
border: '1px solid #fecaca',
borderRadius: '8px',
padding: '12px 16px',
marginBottom: '24px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '16px' }}></span>
<div style={{ fontSize: '14px', color: '#dc2626', whiteSpace: 'pre-line' }}>
{error}
</div>
</div>
</div>
)}
{/* 성공 메시지 */}
{success && (
<div style={{
background: '#dcfce7',
border: '1px solid #bbf7d0',
borderRadius: '8px',
padding: '12px 16px',
marginBottom: '24px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '16px' }}></span>
<div style={{ fontSize: '14px', color: '#059669' }}>
{success}
</div>
</div>
</div>
)}
{/* 업로드 버튼 */}
<div style={{ display: 'flex', gap: '16px', justifyContent: 'flex-end' }}>
<button
onClick={handleUpload}
disabled={uploading || selectedFiles.length === 0 || !bomName.trim()}
style={{
padding: '12px 32px',
background: (uploading || selectedFiles.length === 0 || !bomName.trim())
? '#d1d5db'
: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
color: 'white',
border: 'none',
borderRadius: '12px',
cursor: (uploading || selectedFiles.length === 0 || !bomName.trim())
? 'not-allowed'
: 'pointer',
fontSize: '16px',
fontWeight: '600',
transition: 'all 0.2s ease'
}}
>
{uploading ? 'Uploading...' : 'Upload BOM'}
</button>
</div>
{/* 가이드 정보 */}
<div style={{
marginTop: '40px',
padding: '24px',
background: '#f8fafc',
borderRadius: '12px',
border: '1px solid #e2e8f0'
}}>
<h3 style={{
fontSize: '18px',
fontWeight: '600',
color: '#374151',
marginBottom: '16px'
}}>
📋 Upload Guidelines
</h3>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '20px'
}}>
<div>
<h4 style={{ fontSize: '14px', fontWeight: '600', color: '#059669', marginBottom: '8px' }}>
Supported Formats
</h4>
<ul style={{ margin: 0, paddingLeft: '16px', color: '#6b7280', fontSize: '14px' }}>
<li>Excel files (.xlsx, .xls)</li>
<li>CSV files (.csv)</li>
<li>Maximum file size: 50MB</li>
</ul>
</div>
<div>
<h4 style={{ fontSize: '14px', fontWeight: '600', color: '#3b82f6', marginBottom: '8px' }}>
📊 Required Columns
</h4>
<ul style={{ margin: 0, paddingLeft: '16px', color: '#6b7280', fontSize: '14px' }}>
<li>Description (자재명/품명)</li>
<li>Quantity (수량)</li>
<li>Size information (사이즈)</li>
</ul>
</div>
<div>
<h4 style={{ fontSize: '14px', fontWeight: '600', color: '#f59e0b', marginBottom: '8px' }}>
Auto Processing
</h4>
<ul style={{ margin: 0, paddingLeft: '16px', color: '#6b7280', fontSize: '14px' }}>
<li>Automatic material classification</li>
<li>WELD GAP items excluded</li>
<li>Ready for material management</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default BOMUploadTab;