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 (
{/* 요약 통계 카드 */}
{summary?.new_items_count || 0}
신규 자재
새로 추가된 자재
{summary?.modified_items_count || 0}
변경 자재
수량이 변경된 자재
{summary?.removed_items_count || 0}
삭제 자재
제거된 자재
{summary?.total_current_items || 0}
총 자재
현재 리비전 전체
{/* 탭으로 구분된 자재 목록 */}
setSelectedTab(newValue)}
variant="fullWidth"
>
{selectedTab === 0 && renderMaterialTable(new_items, 'new')}
{selectedTab === 1 && renderMaterialTable(modified_items, 'modified')}
{selectedTab === 2 && renderMaterialTable(removed_items, 'removed')}
);
};
const renderMaterialTable = (items, type) => {
if (items.length === 0) {
return (
{type === 'new' && '새로 추가된 자재가 없습니다.'}
{type === 'modified' && '수량이 변경된 자재가 없습니다.'}
{type === 'removed' && '삭제된 자재가 없습니다.'}
);
}
console.log(`🔍 ${type} 테이블 렌더링:`, items); // 디버깅용
return (
카테고리
자재 설명
사이즈
재질
{type === 'modified' && (
<>
이전 수량
현재 수량
변경량
>
)}
{type !== 'modified' && 수량}
단위
길이(mm)
{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 = (
이전: {Math.round(prevTotalLength).toLocaleString()}mm → 현재: {Math.round(currentTotalLength).toLocaleString()}mm
0 ? 'success.main' : lengthChange < 0 ? 'error.main' : 'text.primary'}
>
변화: {lengthChange > 0 ? '+' : ''}{Math.round(lengthChange).toLocaleString()}mm
);
} else {
// 신규/삭제된 파이프: 실제 총길이 사용
lengthInfo = (
총 길이: {Math.round(currentTotalLength).toLocaleString()}mm
);
}
} else if (item.category === 'PIPE') {
lengthInfo = '길이 정보 없음';
}
return (
{item.description}
{item.size_spec || '-'}
{item.material_grade || '-'}
{type === 'modified' && (
<>
{item.previous_quantity}
{item.current_quantity}
0 ? 'success.main' : 'error.main'}
>
{item.quantity_change > 0 ? '+' : ''}{item.quantity_change}
>
)}
{type !== 'modified' && (
{item.quantity}
)}
{item.unit || 'EA'}
{lengthInfo}
);
})}
);
};
const renderHeader = () => (
navigate('/jobs')}
sx={{ textDecoration: 'none' }}
>
프로젝트 목록
navigate(`/materials?job_no=${jobNo}`)}
sx={{ textDecoration: 'none' }}
>
{jobNo}
자재 비교
자재 리비전 비교
{filename && `파일: ${filename}`}
{previousRevision ?
`${previousRevision} → ${currentRevision} 비교` :
`${currentRevision} (이전 리비전 없음)`
}
}
onClick={handleRefresh}
disabled={loading}
>
새로고침
}
onClick={handleExportToExcel}
disabled={!comparisonResult}
>
엑셀 내보내기
}
onClick={handleGoBack}
>
BOM 목록으로
);
if (loading) {
return (
{renderHeader()}
자재 비교 중...
리비전간 차이점을 분석하고 있습니다
);
}
if (error) {
return (
{renderHeader()}
자재 비교 실패
{error}
}
onClick={handleRefresh}
>
다시 시도
);
}
return (
{renderHeader()}
{comparisonResult && renderComparisonResults()}
);
};
export default MaterialComparisonPage;