feat: 자재 리비전 비교 및 구매 목록 시스템 구현

- 자재 리비전간 비교 기능 추가 (MaterialComparisonPage) - 버그 해결 필요
- 리비전간 추가 구매 필요 자재 분석 페이지 추가 (RevisionPurchasePage)
- 자재 비교 결과 컴포넌트 구현 (MaterialComparisonResult)
- 자재 비교 API 라우터 추가 (material_comparison.py) - 로직 개선 필요
- 자재 비교 시스템 데이터베이스 스키마 추가
- FileManager, FileUpload 컴포넌트 개선
- BOMManagerPage 제거 및 새로운 구조로 리팩토링
- 자재 분류기 및 스키마 개선

TODO: 자재 비교 알고리즘 정확도 향상 및 예외 처리 강화 필요
This commit is contained in:
Hyungi Ahn
2025-07-22 15:56:40 +09:00
parent 6ca1cd17e2
commit 534015cc7c
16 changed files with 2577 additions and 267 deletions

View File

@@ -16,11 +16,17 @@ import {
Alert,
CircularProgress,
Chip,
Divider
Divider,
FormControl,
InputLabel,
Select,
MenuItem,
Grid
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ShoppingCart from '@mui/icons-material/ShoppingCart';
import { api } from '../api';
import { Compare as CompareIcon } from '@mui/icons-material';
import { api, fetchFiles } from '../api';
const MaterialsPage = () => {
const [materials, setMaterials] = useState([]);
@@ -28,23 +34,63 @@ const MaterialsPage = () => {
const [error, setError] = useState(null);
const [fileId, setFileId] = useState(null);
const [fileName, setFileName] = useState('');
const [jobNo, setJobNo] = useState('');
const [bomName, setBomName] = useState('');
const [currentRevision, setCurrentRevision] = useState('');
const [availableRevisions, setAvailableRevisions] = useState([]);
const navigate = useNavigate();
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('file_id');
const name = urlParams.get('filename') || '';
const job_no = urlParams.get('job_no') || '';
if (id) {
if (id && job_no) {
setFileId(id);
setFileName(decodeURIComponent(name));
setJobNo(job_no);
loadMaterials(id);
loadAvailableRevisions(job_no, name);
} else {
setLoading(false);
setError('파일 ID가 지정되지 않았습니다.');
setError('파일 ID 또는 Job No가 지정되지 않았습니다.');
}
}, []);
// 같은 BOM의 다른 리비전들 로드
const loadAvailableRevisions = async (job_no, filename) => {
try {
const response = await fetchFiles({ job_no });
if (Array.isArray(response.data)) {
// 같은 BOM 이름의 파일들만 필터링
const sameNameFiles = response.data.filter(file =>
file.original_filename === filename ||
file.bom_name === filename ||
file.filename === filename
);
// 리비전 순으로 정렬 (최신부터)
const sortedFiles = sameNameFiles.sort((a, b) => {
const revA = parseInt(a.revision?.replace('Rev.', '') || '0');
const revB = parseInt(b.revision?.replace('Rev.', '') || '0');
return revB - revA;
});
setAvailableRevisions(sortedFiles);
// 현재 파일 정보 설정
const currentFile = sortedFiles.find(file => file.id === parseInt(fileId));
if (currentFile) {
setCurrentRevision(currentFile.revision || 'Rev.0');
setBomName(currentFile.bom_name || currentFile.original_filename);
}
}
} catch (err) {
console.error('리비전 목록 로드 실패:', err);
}
};
const loadMaterials = async (id) => {
try {
setLoading(true);
@@ -586,21 +632,75 @@ const MaterialsPage = () => {
뒤로가기
</Button>
<Button
variant="contained"
color="success"
size="large"
startIcon={<ShoppingCart />}
onClick={() => {
const params = new URLSearchParams(window.location.search);
navigate(`/purchase-confirmation?${params.toString()}`);
}}
disabled={materialSpecs.length === 0}
sx={{ minWidth: 150 }}
>
구매 확정
</Button>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
{/* 리비전 비교 버튼 */}
{availableRevisions.length > 1 && currentRevision !== 'Rev.0' && (
<Button
variant="outlined"
color="secondary"
startIcon={<CompareIcon />}
onClick={() => navigate(`/material-comparison?job_no=${jobNo}&revision=${currentRevision}&filename=${encodeURIComponent(fileName)}`)}
>
리비전 비교
</Button>
)}
<Button
variant="contained"
color="success"
size="large"
startIcon={<ShoppingCart />}
onClick={() => {
const params = new URLSearchParams(window.location.search);
navigate(`/purchase-confirmation?${params.toString()}`);
}}
disabled={materialSpecs.length === 0}
sx={{ minWidth: 150 }}
>
구매 확정
</Button>
</Box>
</Box>
{/* 리비전 선택 */}
{availableRevisions.length > 1 && (
<Card sx={{ mb: 3, p: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} md={6}>
<Typography variant="h6" gutterBottom>
📋 {bomName}
</Typography>
<Typography variant="body2" color="textSecondary">
Job No: {jobNo} | 현재 리비전: <strong>{currentRevision}</strong>
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth size="small">
<InputLabel>리비전 선택</InputLabel>
<Select
value={fileId || ''}
label="리비전 선택"
onChange={(e) => {
const selectedFileId = e.target.value;
const selectedFile = availableRevisions.find(file => file.id === selectedFileId);
if (selectedFile) {
// 새로운 리비전 페이지로 이동
navigate(`/materials?file_id=${selectedFileId}&job_no=${jobNo}&filename=${encodeURIComponent(selectedFile.original_filename || selectedFile.filename)}`);
window.location.reload(); // 페이지 새로고침으로 데이터 갱신
}
}}
>
{availableRevisions.map((file) => (
<MenuItem key={file.id} value={file.id}>
{file.revision || 'Rev.0'} ({file.parsed_count || 0} 자재) - {new Date(file.upload_date).toLocaleDateString('ko-KR')}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
</Card>
)}
<Typography variant="h4" component="h1" gutterBottom>
📋 자재 사양서