Files
TK-BOM-Project/frontend/src/pages/_backup/MaterialComparisonPage.jsx
Hyungi Ahn 83b90ef05c
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 자재 관리 페이지 대규모 개선
- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산)
- UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인)
- 자재별 그룹핑 로직 개선:
  * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시
  * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등)
  * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시
  * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계)
  * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시
  * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑
- 전체 카테고리 버튼 제거 (표시 복잡도 감소)
- 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
2025-09-09 09:24:45 +09:00

518 lines
18 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import {
Box,
Container,
Typography,
Button,
CircularProgress,
Alert,
Breadcrumbs,
Link,
Stack,
Card,
CardContent,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Chip,
Grid,
Divider,
Tabs,
Tab
} from '@mui/material';
import {
ArrowBack,
Refresh,
History,
Download
} from '@mui/icons-material';
import MaterialComparisonResult from '../components/MaterialComparisonResult';
import { compareMaterialRevisions, confirmMaterialPurchase, fetchMaterials, api } from '../api';
import { exportComparisonToExcel } from '../utils/excelExport';
const MaterialComparisonPage = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [loading, setLoading] = useState(true);
const [confirmLoading, setConfirmLoading] = useState(false);
const [error, setError] = useState(null);
const [comparisonResult, setComparisonResult] = useState(null);
const [selectedTab, setSelectedTab] = useState(0);
// URL 파라미터에서 정보 추출
const jobNo = searchParams.get('job_no');
const currentRevision = searchParams.get('revision');
const previousRevision = searchParams.get('prev_revision');
const filename = searchParams.get('filename');
useEffect(() => {
if (jobNo && currentRevision) {
loadComparison();
} else {
setError('필수 파라미터가 누락되었습니다 (job_no, revision)');
setLoading(false);
}
}, [jobNo, currentRevision, previousRevision]);
const loadComparison = async () => {
try {
setLoading(true);
setError(null);
console.log('🔍 자재 비교 실행 - 파라미터:', {
jobNo,
currentRevision,
previousRevision,
filename
});
// 🚨 테스트: MaterialsPage API 직접 호출해서 길이 정보 확인
try {
// ✅ API 함수 사용 - 테스트용 자재 조회
const testResult = await fetchMaterials({
job_no: jobNo,
revision: currentRevision,
limit: 10
});
const pipeData = testResult.data.materials?.filter(m => m.classified_category === 'PIPE');
console.log('🧪 MaterialsPage API 테스트 (길이 있는지 확인):', pipeData);
if (pipeData && pipeData.length > 0) {
console.log('🧪 첫 번째 파이프 상세:', JSON.stringify(pipeData[0], null, 2));
}
} catch (e) {
console.log('🧪 MaterialsPage API 테스트 실패:', e);
}
const result = await compareMaterialRevisions(
jobNo,
currentRevision,
previousRevision,
true // 결과 저장
);
console.log('✅ 비교 결과 성공:', result);
console.log('🔍 전체 데이터 구조:', JSON.stringify(result.data || result, null, 2));
setComparisonResult(result.data || result);
} catch (err) {
console.error('❌ 자재 비교 실패:', {
message: err.message,
response: err.response?.data,
status: err.response?.status,
params: { jobNo, currentRevision, previousRevision }
});
setError(err.response?.data?.detail || err.message || '자재 비교 중 오류가 발생했습니다');
} finally {
setLoading(false);
}
};
const handleConfirmPurchase = async (confirmations) => {
try {
setConfirmLoading(true);
console.log('발주 확정 실행:', { jobNo, currentRevision, confirmations });
const result = await confirmMaterialPurchase(
jobNo,
currentRevision,
confirmations,
'user'
);
console.log('발주 확정 결과:', result);
// 성공 메시지 표시 후 비교 결과 새로고침
alert(`${result.confirmed_items?.length || confirmations.length}개 항목의 발주가 확정되었습니다!`);
// 비교 결과 새로고침 (재고 상태가 변경되었을 수 있음)
await loadComparison();
} catch (err) {
console.error('발주 확정 실패:', err);
alert('발주 확정 중 오류가 발생했습니다: ' + (err.response?.data?.detail || err.message));
} finally {
setConfirmLoading(false);
}
};
const handleRefresh = () => {
loadComparison();
};
const handleGoBack = () => {
// BOM 상태 페이지로 이동
if (jobNo) {
navigate(`/bom-status?job_no=${jobNo}`);
} else {
navigate(-1);
}
};
const handleExportToExcel = () => {
if (!comparisonResult) {
alert('내보낼 비교 데이터가 없습니다.');
return;
}
const additionalInfo = {
jobNo: jobNo,
currentRevision: currentRevision,
previousRevision: previousRevision,
filename: filename
};
const baseFilename = `리비전비교_${jobNo}_${currentRevision}_vs_${previousRevision}`;
exportComparisonToExcel(comparisonResult, baseFilename, additionalInfo);
};
const renderComparisonResults = () => {
const { summary, new_items = [], modified_items = [], removed_items = [] } = comparisonResult;
return (
<Box>
{/* 요약 통계 카드 */}
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid item xs={12} md={3}>
<Card>
<CardContent sx={{ textAlign: 'center' }}>
<Typography variant="h4" color="primary" gutterBottom>
{summary?.new_items_count || 0}
</Typography>
<Typography variant="h6">신규 자재</Typography>
<Typography variant="body2" color="text.secondary">
새로 추가된 자재
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={3}>
<Card>
<CardContent sx={{ textAlign: 'center' }}>
<Typography variant="h4" color="warning.main" gutterBottom>
{summary?.modified_items_count || 0}
</Typography>
<Typography variant="h6">변경 자재</Typography>
<Typography variant="body2" color="text.secondary">
수량이 변경된 자재
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={3}>
<Card>
<CardContent sx={{ textAlign: 'center' }}>
<Typography variant="h4" color="error.main" gutterBottom>
{summary?.removed_items_count || 0}
</Typography>
<Typography variant="h6">삭제 자재</Typography>
<Typography variant="body2" color="text.secondary">
제거된 자재
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={3}>
<Card>
<CardContent sx={{ textAlign: 'center' }}>
<Typography variant="h4" color="success.main" gutterBottom>
{summary?.total_current_items || 0}
</Typography>
<Typography variant="h6"> 자재</Typography>
<Typography variant="body2" color="text.secondary">
현재 리비전 전체
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* 탭으로 구분된 자재 목록 */}
<Card>
<Tabs
value={selectedTab}
onChange={(e, newValue) => setSelectedTab(newValue)}
variant="fullWidth"
>
<Tab label={`신규 자재 (${new_items.length})`} />
<Tab label={`변경 자재 (${modified_items.length})`} />
<Tab label={`삭제 자재 (${removed_items.length})`} />
</Tabs>
<CardContent>
{selectedTab === 0 && renderMaterialTable(new_items, 'new')}
{selectedTab === 1 && renderMaterialTable(modified_items, 'modified')}
{selectedTab === 2 && renderMaterialTable(removed_items, 'removed')}
</CardContent>
</Card>
</Box>
);
};
const renderMaterialTable = (items, type) => {
if (items.length === 0) {
return (
<Alert severity="info">
{type === 'new' && '새로 추가된 자재가 없습니다.'}
{type === 'modified' && '수량이 변경된 자재가 없습니다.'}
{type === 'removed' && '삭제된 자재가 없습니다.'}
</Alert>
);
}
console.log(`🔍 ${type} 테이블 렌더링:`, items); // 디버깅용
return (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>카테고리</TableCell>
<TableCell>자재 설명</TableCell>
<TableCell>사이즈</TableCell>
<TableCell>재질</TableCell>
{type === 'modified' && (
<>
<TableCell align="center">이전 수량</TableCell>
<TableCell align="center">현재 수량</TableCell>
<TableCell align="center">변경량</TableCell>
</>
)}
{type !== 'modified' && <TableCell align="center">수량</TableCell>}
<TableCell>단위</TableCell>
<TableCell>길이(mm)</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item, index) => {
console.log(`🔍 항목 ${index}:`, item); // 각 항목 확인
// 파이프인 경우 길이 정보 표시
console.log(`🔧 길이 확인 - ${item.category}:`, item.pipe_details); // 디버깅
console.log(`🔧 전체 아이템:`, item); // 전체 구조 확인
let lengthInfo = '-';
if (item.category === 'PIPE' && item.pipe_details?.length_mm && item.pipe_details.length_mm > 0) {
const avgUnitLength = item.pipe_details.length_mm;
const currentTotalLength = item.pipe_details.total_length_mm || (item.quantity || 0) * avgUnitLength;
if (type === 'modified') {
// 변경된 파이프: 백엔드에서 계산된 실제 길이 사용
let prevTotalLength, lengthChange;
if (item.previous_pipe_details && item.previous_pipe_details.total_length_mm) {
// 백엔드에서 실제 이전 총길이를 제공한 경우
prevTotalLength = item.previous_pipe_details.total_length_mm;
lengthChange = currentTotalLength - prevTotalLength;
} else {
// 백업: 비율 계산
const prevRatio = (item.previous_quantity || 0) / (item.current_quantity || item.quantity || 1);
prevTotalLength = currentTotalLength * prevRatio;
lengthChange = currentTotalLength - prevTotalLength;
}
lengthInfo = (
<Box>
<Typography variant="body2">
이전: {Math.round(prevTotalLength).toLocaleString()}mm 현재: {Math.round(currentTotalLength).toLocaleString()}mm
</Typography>
<Typography
variant="body2"
fontWeight="bold"
color={lengthChange > 0 ? 'success.main' : lengthChange < 0 ? 'error.main' : 'text.primary'}
>
변화: {lengthChange > 0 ? '+' : ''}{Math.round(lengthChange).toLocaleString()}mm
</Typography>
</Box>
);
} else {
// 신규/삭제된 파이프: 실제 총길이 사용
lengthInfo = (
<Box>
<Typography variant="body2" fontWeight="bold">
길이: {Math.round(currentTotalLength).toLocaleString()}mm
</Typography>
</Box>
);
}
} else if (item.category === 'PIPE') {
lengthInfo = '길이 정보 없음';
}
return (
<TableRow key={index}>
<TableCell>
<Chip
label={item.category}
size="small"
color={type === 'new' ? 'primary' : type === 'modified' ? 'warning' : 'error'}
/>
</TableCell>
<TableCell>{item.description}</TableCell>
<TableCell>{item.size_spec || '-'}</TableCell>
<TableCell>{item.material_grade || '-'}</TableCell>
{type === 'modified' && (
<>
<TableCell align="center">{item.previous_quantity}</TableCell>
<TableCell align="center">{item.current_quantity}</TableCell>
<TableCell align="center">
<Typography
variant="body2"
fontWeight="bold"
color={item.quantity_change > 0 ? 'success.main' : 'error.main'}
>
{item.quantity_change > 0 ? '+' : ''}{item.quantity_change}
</Typography>
</TableCell>
</>
)}
{type !== 'modified' && (
<TableCell align="center">
<Typography variant="body2" fontWeight="bold">
{item.quantity}
</Typography>
</TableCell>
)}
<TableCell>{item.unit || 'EA'}</TableCell>
<TableCell align="center">
<Typography variant="body2" color={lengthInfo !== '-' ? 'primary.main' : 'text.secondary'}>
{lengthInfo}
</Typography>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
};
const renderHeader = () => (
<Box sx={{ mb: 3 }}>
<Breadcrumbs sx={{ mb: 2 }}>
<Link
component="button"
variant="body2"
onClick={() => navigate('/jobs')}
sx={{ textDecoration: 'none' }}
>
프로젝트 목록
</Link>
<Link
component="button"
variant="body2"
onClick={() => navigate(`/materials?job_no=${jobNo}`)}
sx={{ textDecoration: 'none' }}
>
{jobNo}
</Link>
<Typography variant="body2" color="textPrimary">
자재 비교
</Typography>
</Breadcrumbs>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography variant="h4" gutterBottom>
자재 리비전 비교
</Typography>
<Typography variant="body1" color="textSecondary">
{filename && `파일: ${filename}`}
<br />
{previousRevision ?
`${previousRevision}${currentRevision} 비교` :
`${currentRevision} (이전 리비전 없음)`
}
</Typography>
</Box>
<Stack direction="row" spacing={2}>
<Button
variant="outlined"
startIcon={<Refresh />}
onClick={handleRefresh}
disabled={loading}
>
새로고침
</Button>
<Button
variant="outlined"
color="primary"
startIcon={<Download />}
onClick={handleExportToExcel}
disabled={!comparisonResult}
>
엑셀 내보내기
</Button>
<Button
variant="outlined"
startIcon={<ArrowBack />}
onClick={handleGoBack}
>
BOM 목록으로
</Button>
</Stack>
</Stack>
</Box>
);
if (loading) {
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
{renderHeader()}
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
<Stack alignItems="center" spacing={2}>
<CircularProgress size={60} />
<Typography variant="h6" color="textSecondary">
자재 비교 ...
</Typography>
<Typography variant="body2" color="textSecondary">
리비전간 차이점을 분석하고 있습니다
</Typography>
</Stack>
</Box>
</Container>
);
}
if (error) {
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
{renderHeader()}
<Alert severity="error" sx={{ mb: 3 }}>
<Typography variant="h6" gutterBottom>
자재 비교 실패
</Typography>
<Typography variant="body2">
{error}
</Typography>
</Alert>
<Button
variant="contained"
startIcon={<Refresh />}
onClick={handleRefresh}
>
다시 시도
</Button>
</Container>
);
}
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
{renderHeader()}
{comparisonResult && renderComparisonResults()}
</Container>
);
};
export default MaterialComparisonPage;