Files
TK-BOM-Project/frontend/src/components/bom/tabs/BOMFilesTab.jsx
Hyungi Ahn 1dc735f362
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
리비전 페이지 제거 및 트랜잭션 오류 임시 수정
- frontend/src/pages/revision/ 폴더 완전 삭제
- EnhancedRevisionPage.css 제거
- support_details 저장 시 트랜잭션 오류로 인해 임시로 상세 정보 저장 비활성화
- 리비전 기능 재설계 예정
2025-10-21 12:11:57 +09:00

464 lines
15 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, useEffect } from 'react';
import api from '../../../api';
import RevisionUploadDialog from '../../RevisionUploadDialog';
const BOMFilesTab = ({
selectedProject,
user,
bomFiles,
setBomFiles,
selectedBOM,
onBOMSelect,
refreshTrigger
}) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [groupedFiles, setGroupedFiles] = useState({});
const [revisionDialog, setRevisionDialog] = useState({ open: false, parentFile: null });
// 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) => {
console.log('🔄 리비전 업로드 시작:', parentFile);
setRevisionDialog({ open: true, parentFile });
};
// 리비전 업로드 성공 처리
const handleRevisionUploadSuccess = async (uploadResult) => {
console.log('✅ 리비전 업로드 성공:', uploadResult);
// BOM 파일 목록 새로고침
try {
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));
// 성공 메시지 표시 (선택사항)
console.log(`새 리비전 ${uploadResult.revision} 업로드 완료!`);
} catch (err) {
console.error('파일 목록 새로고침 실패:', err);
setError('파일 목록을 새로고침하는데 실패했습니다.');
}
};
// 날짜 포맷팅
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>
{/* 리비전 업로드 다이얼로그 */}
<RevisionUploadDialog
isOpen={revisionDialog.open}
onClose={() => setRevisionDialog({ open: false, parentFile: null })}
parentFile={revisionDialog.parentFile}
selectedProject={selectedProject}
onUploadSuccess={handleRevisionUploadSuccess}
/>
</div>
);
};
export default BOMFilesTab;