프론트엔드 작성중
This commit is contained in:
256
frontend/src/pages/MaterialsPage.jsx
Normal file
256
frontend/src/pages/MaterialsPage.jsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Chip,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { fetchMaterialsSummary } from '../api';
|
||||
|
||||
const MaterialsPage = () => {
|
||||
const [materials, setMaterials] = useState([]);
|
||||
const [summary, setSummary] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fileId = searchParams.get('file_id');
|
||||
const jobNo = searchParams.get('job_no');
|
||||
const filename = searchParams.get('filename');
|
||||
|
||||
// 자재 목록 불러오기
|
||||
const loadMaterials = async () => {
|
||||
if (!fileId) return;
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
// 자재 목록 조회
|
||||
const response = await fetch(`http://localhost:8000/files/materials?file_id=${fileId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.materials && Array.isArray(data.materials)) {
|
||||
// 동일 항목 그룹화 (품명 + 사이즈 + 재질이 같은 것들)
|
||||
const groupedMaterials = groupMaterialsByItem(data.materials);
|
||||
setMaterials(groupedMaterials);
|
||||
} else {
|
||||
setMaterials([]);
|
||||
}
|
||||
|
||||
// 요약 정보 조회
|
||||
const summaryResponse = await fetchMaterialsSummary({ file_id: fileId });
|
||||
if (summaryResponse.data.success) {
|
||||
setSummary(summaryResponse.data.summary);
|
||||
}
|
||||
} catch (e) {
|
||||
setError('자재 목록을 불러오지 못했습니다.');
|
||||
console.error('자재 로드 에러:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 동일 항목 그룹화 함수
|
||||
const groupMaterialsByItem = (materials) => {
|
||||
const grouped = {};
|
||||
|
||||
materials.forEach(material => {
|
||||
// 그룹화 키: 품명 + 사이즈 + 재질 + 분류
|
||||
const key = `${material.original_description}_${material.size_spec || ''}_${material.material_grade || ''}_${material.classified_category || ''}`;
|
||||
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = {
|
||||
...material,
|
||||
totalQuantity: 0,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
|
||||
grouped[key].totalQuantity += material.quantity || 0;
|
||||
grouped[key].items.push(material);
|
||||
});
|
||||
|
||||
return Object.values(grouped).sort((a, b) => b.totalQuantity - a.totalQuantity);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadMaterials();
|
||||
}, [fileId]);
|
||||
|
||||
// 분류별 색상 지정
|
||||
const getCategoryColor = (category) => {
|
||||
const colors = {
|
||||
'PIPE': 'primary',
|
||||
'FITTING': 'secondary',
|
||||
'VALVE': 'error',
|
||||
'FLANGE': 'warning',
|
||||
'BOLT': 'info',
|
||||
'GASKET': 'success',
|
||||
'INSTRUMENT': 'default'
|
||||
};
|
||||
return colors[category] || 'default';
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 1200, mx: 'auto', mt: 4, p: 2 }}>
|
||||
{/* 헤더 */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => navigate(`/bom-manager?job_no=${jobNo}`)}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
← BOM 관리로 돌아가기
|
||||
</Button>
|
||||
|
||||
<Typography variant="h5" gutterBottom>
|
||||
자재 목록 - {filename}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
Job No: {jobNo}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 요약 정보 */}
|
||||
{summary && (
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
총 항목 수
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{summary.total_items}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
고유 품명 수
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{summary.unique_descriptions}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
총 수량
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{summary.total_quantity}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
고유 재질 수
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{summary.unique_materials}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{loading && <CircularProgress sx={{ mt: 4 }} />}
|
||||
|
||||
{/* 자재 목록 테이블 */}
|
||||
{!loading && materials.length > 0 && (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>분류</TableCell>
|
||||
<TableCell>품명</TableCell>
|
||||
<TableCell>사이즈</TableCell>
|
||||
<TableCell>재질</TableCell>
|
||||
<TableCell align="right">총 수량</TableCell>
|
||||
<TableCell>단위</TableCell>
|
||||
<TableCell align="center">항목 수</TableCell>
|
||||
<TableCell>신뢰도</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{materials.map((material, index) => (
|
||||
<TableRow key={index} hover>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={material.classified_category || 'OTHER'}
|
||||
color={getCategoryColor(material.classified_category)}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'medium' }}>
|
||||
{material.original_description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{material.size_spec || '-'}</TableCell>
|
||||
<TableCell>{material.material_grade || '-'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
|
||||
{material.totalQuantity}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{material.unit || 'EA'}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={material.items.length}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={material.classification_confidence > 0.7 ? 'success.main' : 'warning.main'}
|
||||
>
|
||||
{Math.round((material.classification_confidence || 0) * 100)}%
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
|
||||
{!loading && materials.length === 0 && fileId && (
|
||||
<Alert severity="info" sx={{ mt: 4 }}>
|
||||
해당 파일에 자재 정보가 없습니다.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterialsPage;
|
||||
Reference in New Issue
Block a user