fix: 자재 리비전 비교 시스템 완전 수정 및 UI 개선

🔧 백엔드 수정:
- 이전 리비전 탐지 로직을 문자열에서 숫자 기반으로 개선 (Rev.1에서 Rev.0 정상 탐지)
- get_materials_by_hash 함수 필드명 수정 및 단순화
- 자재 비교 API 응답 구조 개선

🎨 프론트엔드 UI 대폭 개선:
- MaterialComparisonPage를 MaterialsPage와 동일한 깔끔한 디자인으로 리뉴얼
- 요약 통계 카드 4개 추가 (신규/변경/삭제/총 자재)
- 탭으로 구분된 자재 목록 (신규/변경/삭제)
- 테이블 형태로 비교 결과 표시 (이전수량 → 현재수량 → 변경량)
- 색상별 카테고리 Chip과 변경량 강조 표시

🔄 네비게이션 개선:
- MaterialComparisonPage 돌아가기 버튼을 BOM 상태 페이지로 수정
- RevisionPurchasePage 버튼 텍스트를 'BOM 목록으로'로 명확화
- 모든 비교 관련 페이지의 네비게이션 일관성 확보

🐛 버그 수정:
- BOMStatusPage 리비전 업로드 시 불필요한 파라미터 제거 (네트워크 에러 해결)
- API 응답 데이터 처리 로직 개선 (result.data 처리)
- 에러 핸들링 및 디버깅 로그 강화

 완전 작동하는 리비전 비교 시스템 완성
This commit is contained in:
Hyungi Ahn
2025-07-23 06:58:31 +09:00
parent 534015cc7c
commit bef0d8bf7c
5 changed files with 257 additions and 90 deletions

View File

@@ -135,8 +135,6 @@ const BOMStatusPage = () => {
formData.append('job_no', jobNo);
formData.append('revision', 'Rev.0'); // 백엔드에서 자동 증가
formData.append('bom_name', revisionDialog.bomName);
formData.append('bom_type', 'excel');
formData.append('description', '');
formData.append('parent_file_id', revisionDialog.parentId);
const response = await uploadFileApi(formData);

View File

@@ -9,7 +9,21 @@ import {
Alert,
Breadcrumbs,
Link,
Stack
Stack,
Card,
CardContent,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Chip,
Grid,
Divider,
Tabs,
Tab
} from '@mui/material';
import {
ArrowBack,
@@ -27,6 +41,7 @@ const MaterialComparisonPage = () => {
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');
@@ -48,7 +63,12 @@ const MaterialComparisonPage = () => {
setLoading(true);
setError(null);
console.log('자재 비교 실행:', { jobNo, currentRevision, previousRevision });
console.log('🔍 자재 비교 실행 - 파라미터:', {
jobNo,
currentRevision,
previousRevision,
filename
});
const result = await compareMaterialRevisions(
jobNo,
@@ -57,11 +77,16 @@ const MaterialComparisonPage = () => {
true // 결과 저장
);
console.log('비교 결과:', result);
setComparisonResult(result);
console.log('비교 결과 성공:', result);
setComparisonResult(result.data || result);
} catch (err) {
console.error('자재 비교 실패:', 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);
@@ -102,14 +127,172 @@ const MaterialComparisonPage = () => {
};
const handleGoBack = () => {
// 이전 페이지로 이동 (대부분 파일 업로드 완료 페이지)
// BOM 상태 페이지로 이동
if (jobNo) {
navigate(`/materials?job_no=${jobNo}`);
navigate(`/bom-status?job_no=${jobNo}`);
} else {
navigate(-1);
}
};
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>
);
}
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>
</TableRow>
</TableHead>
<TableBody>
{items.map((item, index) => (
<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>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
const renderHeader = () => (
<Box sx={{ mb: 3 }}>
<Breadcrumbs sx={{ mb: 2 }}>
@@ -163,7 +346,7 @@ const MaterialComparisonPage = () => {
startIcon={<ArrowBack />}
onClick={handleGoBack}
>
돌아가기
BOM 목록으로
</Button>
</Stack>
</Stack>
@@ -216,17 +399,7 @@ const MaterialComparisonPage = () => {
<Container maxWidth="xl" sx={{ py: 4 }}>
{renderHeader()}
{comparisonResult ? (
<MaterialComparisonResult
comparison={comparisonResult}
onConfirmPurchase={handleConfirmPurchase}
loading={confirmLoading}
/>
) : (
<Alert severity="info">
비교 결과가 없습니다.
</Alert>
)}
{comparisonResult && renderComparisonResults()}
</Container>
);
};

View File

@@ -165,10 +165,10 @@ const RevisionPurchasePage = () => {
<Button
variant="outlined"
startIcon={<ArrowBack />}
onClick={() => navigate(-1)}
onClick={() => navigate(`/bom-status?job_no=${jobNo}`)}
sx={{ mb: 2 }}
>
뒤로가기
BOM 목록으로
</Button>
<Alert severity="error">{error}</Alert>
</Box>
@@ -188,9 +188,9 @@ const RevisionPurchasePage = () => {
<Button
variant="outlined"
startIcon={<ArrowBack />}
onClick={() => navigate(-1)}
onClick={() => navigate(`/bom-status?job_no=${jobNo}`)}
>
뒤로가기
BOM 목록으로
</Button>
<Box sx={{ display: 'flex', gap: 2 }}>