Files
TK-BOM-Project/frontend/src/components/bom/tabs/BOMFilesTab.jsx
Hyungi Ahn c258303bb7
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
🎯 UI/UX 개선 및 안정성 강화
 주요 개선사항:
- Rev.0일 때 Revisions 카운트 0으로 정확히 표시
- 업로드 후 파일 목록 자동 새로고침
- 대시보드 계정 메뉴 zIndex 문제 해결
- 구매관리 페이지 500 오류 해결 및 대시보드 리다이렉트
- 구매신청 관리 페이지 버튼 텍스트 개선

🔧 기술적 수정:
- purchase_requests API SQL 쿼리 테이블 구조에 맞게 수정
- UserMenu 드롭다운 zIndex 1050으로 상향 조정
- 프론트엔드 완전 재빌드로 최신 변경사항 반영
- 완전한 자동 마이그레이션 시스템 구축 (43개 테이블 스키마 동기화)

🚀 다음 단계: 리비전 기능 재도입 준비 완료
2025-10-21 15:44:43 +09:00

430 lines
14 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';
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;