프론트엔드 작성중

This commit is contained in:
Hyungi Ahn
2025-07-16 15:44:50 +09:00
parent 5ac9d562d5
commit ea111433e4
25 changed files with 7286 additions and 2043 deletions

View 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;