Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
✅ 주요 개선사항: - Rev.0일 때 Revisions 카운트 0으로 정확히 표시 - 업로드 후 파일 목록 자동 새로고침 - 대시보드 계정 메뉴 zIndex 문제 해결 - 구매관리 페이지 500 오류 해결 및 대시보드 리다이렉트 - 구매신청 관리 페이지 버튼 텍스트 개선 🔧 기술적 수정: - purchase_requests API SQL 쿼리 테이블 구조에 맞게 수정 - UserMenu 드롭다운 zIndex 1050으로 상향 조정 - 프론트엔드 완전 재빌드로 최신 변경사항 반영 - 완전한 자동 마이그레이션 시스템 구축 (43개 테이블 스키마 동기화) 🚀 다음 단계: 리비전 기능 재도입 준비 완료
430 lines
14 KiB
JavaScript
430 lines
14 KiB
JavaScript
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> {Math.max(0, files.length - 1)}
|
||
</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;
|