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,431 @@
import React, { useState, useEffect } from 'react';
import SimpleFileUpload from '../components/SimpleFileUpload';
import MaterialList from '../components/MaterialList';
import { fetchMaterials, fetchFiles } from '../api';
const BOMManagementPage = ({ user }) => {
const [activeTab, setActiveTab] = useState('upload');
const [projects, setProjects] = useState([]);
const [selectedProject, setSelectedProject] = useState(null);
const [files, setFiles] = useState([]);
const [materials, setMaterials] = useState([]);
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState({
totalFiles: 0,
totalMaterials: 0,
recentUploads: []
});
useEffect(() => {
loadProjects();
}, []);
useEffect(() => {
if (selectedProject) {
loadProjectFiles();
}
}, [selectedProject]);
useEffect(() => {
loadStats();
}, [files, materials]);
const loadProjects = async () => {
try {
const response = await fetch('/api/jobs/');
const data = await response.json();
if (data.success) {
setProjects(data.jobs);
}
} catch (error) {
console.error('프로젝트 로딩 실패:', error);
}
};
const loadProjectFiles = async () => {
if (!selectedProject) return;
setLoading(true);
try {
// 기존 API 함수 사용 - 파일 목록 로딩
const filesResponse = await fetchFiles({ job_no: selectedProject.job_no });
setFiles(Array.isArray(filesResponse.data) ? filesResponse.data : []);
// 기존 API 함수 사용 - 자재 목록 로딩
const materialsResponse = await fetchMaterials({ job_no: selectedProject.job_no, limit: 1000 });
setMaterials(materialsResponse.data?.materials || []);
} catch (error) {
console.error('프로젝트 데이터 로딩 실패:', error);
} finally {
setLoading(false);
}
};
const loadStats = async () => {
try {
// 실제 통계 계산 - 더미 데이터 없이
const totalFiles = files.length;
const totalMaterials = materials.length;
setStats({
totalFiles,
totalMaterials,
recentUploads: files.slice(0, 5) // 최근 5개 파일
});
} catch (error) {
console.error('통계 로딩 실패:', error);
}
};
const handleFileUpload = async (uploadData) => {
try {
setLoading(true);
// 기존 FileUpload 컴포넌트의 업로드 로직 활용
await loadProjectFiles(); // 업로드 후 데이터 새로고침
await loadStats();
} catch (error) {
console.error('파일 업로드 후 새로고침 실패:', error);
} finally {
setLoading(false);
}
};
const StatCard = ({ title, value, icon, color = '#667eea' }) => (
<div style={{
background: 'white',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
border: '1px solid #e2e8f0',
display: 'flex',
alignItems: 'center',
gap: '16px'
}}>
<div style={{
width: '48px',
height: '48px',
borderRadius: '12px',
background: color + '20',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px'
}}>
{icon}
</div>
<div>
<div style={{
fontSize: '24px',
fontWeight: '700',
color: '#2d3748',
marginBottom: '4px'
}}>
{value}
</div>
<div style={{
fontSize: '14px',
color: '#718096'
}}>
{title}
</div>
</div>
</div>
);
return (
<div style={{
padding: '32px',
background: '#f7fafc',
minHeight: '100vh'
}}>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
{/* 헤더 */}
<div style={{ marginBottom: '32px' }}>
<h1 style={{
fontSize: '28px',
fontWeight: '700',
color: '#2d3748',
margin: '0 0 8px 0'
}}>
🔧 BOM 관리
</h1>
<p style={{
color: '#718096',
fontSize: '16px',
margin: '0'
}}>
Bill of Materials 업로드, 분석 관리를 수행하세요.
</p>
</div>
{/* 통계 카드들 */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '20px',
marginBottom: '32px'
}}>
<StatCard
title="총 업로드 파일"
value={stats.totalFiles}
icon="📄"
color="#667eea"
/>
<StatCard
title="분석된 자재"
value={stats.totalMaterials}
icon="🔧"
color="#48bb78"
/>
<StatCard
title="활성 프로젝트"
value={projects.length}
icon="📋"
color="#ed8936"
/>
</div>
{/* 탭 네비게이션 */}
<div style={{
background: 'white',
borderRadius: '12px',
border: '1px solid #e2e8f0',
overflow: 'hidden',
marginBottom: '24px'
}}>
<div style={{
display: 'flex',
borderBottom: '1px solid #e2e8f0'
}}>
{[
{ id: 'upload', label: '📤 파일 업로드', icon: '📤' },
{ id: 'files', label: '📁 파일 관리', icon: '📁' },
{ id: 'materials', label: '🔧 자재 목록', icon: '🔧' },
{ id: 'analysis', label: '📊 분석 결과', icon: '📊' }
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
style={{
flex: 1,
padding: '16px 20px',
background: activeTab === tab.id ? '#f7fafc' : 'transparent',
border: 'none',
borderBottom: activeTab === tab.id ? '2px solid #667eea' : '2px solid transparent',
cursor: 'pointer',
fontSize: '14px',
fontWeight: activeTab === tab.id ? '600' : '500',
color: activeTab === tab.id ? '#667eea' : '#4a5568',
transition: 'all 0.2s ease'
}}
>
{tab.label}
</button>
))}
</div>
{/* 탭 콘텐츠 */}
<div style={{ padding: '24px' }}>
{activeTab === 'upload' && (
<div>
<h3 style={{
fontSize: '18px',
fontWeight: '600',
color: '#2d3748',
margin: '0 0 20px 0'
}}>
📤 BOM 파일 업로드
</h3>
{/* 프로젝트 선택 */}
<div style={{ marginBottom: '24px' }}>
<label style={{
display: 'block',
fontSize: '14px',
fontWeight: '600',
color: '#4a5568',
marginBottom: '8px'
}}>
프로젝트 선택
</label>
<select
value={selectedProject?.job_no || ''}
onChange={(e) => {
const project = projects.find(p => p.job_no === e.target.value);
setSelectedProject(project);
}}
style={{
width: '100%',
padding: '12px',
border: '1px solid #e2e8f0',
borderRadius: '8px',
fontSize: '14px',
background: 'white'
}}
>
<option value="">프로젝트를 선택하세요</option>
{projects.map(project => (
<option key={project.job_no} value={project.job_no}>
{project.job_no} - {project.job_name}
</option>
))}
</select>
</div>
{selectedProject ? (
<div>
<div style={{
background: '#f7fafc',
border: '1px solid #e2e8f0',
borderRadius: '8px',
padding: '16px',
marginBottom: '24px'
}}>
<h4 style={{ margin: '0 0 8px 0', color: '#2d3748' }}>
선택된 프로젝트: {selectedProject.job_name}
</h4>
<p style={{ margin: '0', fontSize: '14px', color: '#718096' }}>
Job No: {selectedProject.job_no} |
고객사: {selectedProject.client_name} |
상태: {selectedProject.status}
</p>
</div>
<SimpleFileUpload
selectedProject={selectedProject}
onUploadComplete={handleFileUpload}
/>
</div>
) : (
<div style={{
textAlign: 'center',
padding: '40px',
color: '#718096'
}}>
먼저 프로젝트를 선택해주세요.
</div>
)}
</div>
)}
{activeTab === 'files' && (
<div>
<h3 style={{
fontSize: '18px',
fontWeight: '600',
color: '#2d3748',
margin: '0 0 20px 0'
}}>
📁 업로드된 파일 목록
</h3>
{selectedProject ? (
loading ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#718096' }}>
파일 목록을 불러오는 ...
</div>
) : files.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{files.map((file, index) => (
<div key={index} style={{
background: '#f7fafc',
border: '1px solid #e2e8f0',
borderRadius: '8px',
padding: '16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<div style={{ fontWeight: '600', color: '#2d3748' }}>
{file.original_filename || file.filename}
</div>
<div style={{ fontSize: '12px', color: '#718096' }}>
업로드: {new Date(file.created_at).toLocaleString()} |
자재 : {file.parsed_count || 0}
</div>
</div>
<div style={{
padding: '4px 8px',
background: '#48bb78',
color: 'white',
borderRadius: '4px',
fontSize: '12px'
}}>
{file.revision || 'Rev.0'}
</div>
</div>
))}
</div>
) : (
<div style={{ textAlign: 'center', padding: '40px', color: '#718096' }}>
업로드된 파일이 없습니다.
</div>
)
) : (
<div style={{ textAlign: 'center', padding: '40px', color: '#718096' }}>
프로젝트를 선택해주세요.
</div>
)}
</div>
)}
{activeTab === 'materials' && (
<div>
<h3 style={{
fontSize: '18px',
fontWeight: '600',
color: '#2d3748',
margin: '0 0 20px 0'
}}>
🔧 자재 목록
</h3>
{selectedProject ? (
<MaterialList
selectedProject={selectedProject}
key={selectedProject.job_no} // 프로젝트 변경 컴포넌트 재렌더링
/>
) : (
<div style={{ textAlign: 'center', padding: '40px', color: '#718096' }}>
프로젝트를 선택해주세요.
</div>
)}
</div>
)}
{activeTab === 'analysis' && (
<div>
<h3 style={{
fontSize: '18px',
fontWeight: '600',
color: '#2d3748',
margin: '0 0 20px 0'
}}>
📊 분석 결과
</h3>
<div style={{
background: '#fff3cd',
border: '1px solid #ffeaa7',
borderRadius: '8px',
padding: '16px',
textAlign: 'center'
}}>
<div style={{ fontSize: '16px', color: '#856404' }}>
🚧 분석 결과 페이지는 구현될 예정입니다.
</div>
<div style={{ fontSize: '14px', color: '#856404', marginTop: '8px' }}>
자재 분류, 통계, 비교 분석 기능이 추가됩니다.
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default BOMManagementPage;