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} (이전 리비전 없음)` }
); if (loading) { return ( {renderHeader()} 자재 비교 중... 리비전간 차이점을 분석하고 있습니다 ); } if (error) { return ( {renderHeader()} 자재 비교 실패 {error} ); } return ( {renderHeader()} {comparisonResult && renderComparisonResults()} ); }; export default MaterialComparisonPage;