feat: SWG 가스켓 전체 구성 정보 표시 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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:
431
frontend/src/pages/BOMManagementPage.jsx
Normal file
431
frontend/src/pages/BOMManagementPage.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user