diff --git a/backend/app/routers/material_comparison.py b/backend/app/routers/material_comparison.py index 949d761..32ab9d4 100644 --- a/backend/app/routers/material_comparison.py +++ b/backend/app/routers/material_comparison.py @@ -48,13 +48,14 @@ async def compare_material_revisions( db, current_file, previous_file, job_no ) - # 4. 결과 저장 (선택사항) + # 4. 결과 저장 (선택사항) - 임시로 비활성화 comparison_id = None - if save_result and previous_file and previous_revision: - comparison_id = await save_comparison_result( - db, job_no, current_revision, previous_revision, - current_file["id"], previous_file["id"], comparison_result - ) + # TODO: 저장 기능 활성화 + # if save_result and previous_file and previous_revision: + # comparison_id = await save_comparison_result( + # db, job_no, current_revision, previous_revision, + # current_file["id"], previous_file["id"], comparison_result + # ) return { "success": True, @@ -65,8 +66,7 @@ async def compare_material_revisions( "summary": comparison_result["summary"], "new_items": comparison_result["new_items"], "modified_items": comparison_result["modified_items"], - "removed_items": comparison_result["removed_items"], - "purchase_summary": comparison_result["purchase_summary"] + "removed_items": comparison_result["removed_items"] } except Exception as e: @@ -323,21 +323,39 @@ async def get_file_by_revision(db: Session, job_no: str, revision: str) -> Optio return None async def get_previous_revision(db: Session, job_no: str, current_revision: str) -> Optional[str]: - """이전 리비전 자동 탐지""" + """이전 리비전 자동 탐지 - 숫자 기반 비교""" + + # 현재 리비전의 숫자 추출 + try: + current_rev_num = int(current_revision.replace("Rev.", "")) + except (ValueError, AttributeError): + current_rev_num = 0 + query = text(""" SELECT revision FROM files - WHERE job_no = :job_no AND revision < :current_revision AND is_active = TRUE + WHERE job_no = :job_no AND is_active = TRUE ORDER BY revision DESC - LIMIT 1 """) - result = db.execute(query, {"job_no": job_no, "current_revision": current_revision}) - prev_row = result.fetchone() + result = db.execute(query, {"job_no": job_no}) + revisions = result.fetchall() - if prev_row is not None: - return prev_row[0] - return None + # 현재 리비전보다 낮은 리비전 중 가장 높은 것 찾기 + previous_revision = None + highest_prev_num = -1 + + for row in revisions: + rev = row[0] + try: + rev_num = int(rev.replace("Rev.", "")) + if rev_num < current_rev_num and rev_num > highest_prev_num: + highest_prev_num = rev_num + previous_revision = rev + except (ValueError, AttributeError): + continue + + return previous_revision async def perform_material_comparison( db: Session, @@ -346,9 +364,7 @@ async def perform_material_comparison( job_no: str ) -> Dict: """ - 핵심 자재 비교 로직 - - 해시 기반 고성능 비교 - - 누적 재고 고려한 실제 구매 필요량 계산 + 핵심 자재 비교 로직 - 간단한 버전 """ # 1. 현재 리비전 자재 목록 (해시별로 그룹화) @@ -359,63 +375,44 @@ async def perform_material_comparison( if previous_file: previous_materials = await get_materials_by_hash(db, previous_file["id"]) - # 3. 현재까지의 누적 재고 조회 - current_inventory = await get_current_inventory(db, job_no) - - # 4. 비교 실행 + # 3. 비교 실행 new_items = [] modified_items = [] removed_items = [] - purchase_summary = { - "additional_purchase_needed": 0, - "total_new_items": 0, - "total_increased_items": 0 - } # 신규/변경 항목 찾기 for material_hash, current_item in current_materials.items(): current_qty = current_item["quantity"] - available_stock = current_inventory.get(material_hash, 0) if material_hash not in previous_materials: # 완전히 새로운 항목 - additional_needed = max(current_qty - available_stock, 0) - new_items.append({ "material_hash": material_hash, "description": current_item["description"], "size_spec": current_item["size_spec"], "material_grade": current_item["material_grade"], "quantity": current_qty, - "available_stock": available_stock, - "additional_needed": additional_needed + "category": current_item["category"], + "unit": current_item["unit"] }) - purchase_summary["additional_purchase_needed"] += additional_needed - purchase_summary["total_new_items"] += 1 - else: # 기존 항목 - 수량 변경 체크 previous_qty = previous_materials[material_hash]["quantity"] - qty_diff = current_qty - previous_qty + qty_change = current_qty - previous_qty - if qty_diff != 0: - additional_needed = max(current_qty - available_stock, 0) - + if qty_change != 0: modified_items.append({ "material_hash": material_hash, "description": current_item["description"], "size_spec": current_item["size_spec"], + "material_grade": current_item["material_grade"], "previous_quantity": previous_qty, "current_quantity": current_qty, - "quantity_diff": qty_diff, - "available_stock": available_stock, - "additional_needed": additional_needed + "quantity_change": qty_change, + "category": current_item["category"], + "unit": current_item["unit"] }) - - if additional_needed > 0: - purchase_summary["additional_purchase_needed"] += additional_needed - purchase_summary["total_increased_items"] += 1 # 삭제된 항목 찾기 for material_hash, previous_item in previous_materials.items(): @@ -424,7 +421,10 @@ async def perform_material_comparison( "material_hash": material_hash, "description": previous_item["description"], "size_spec": previous_item["size_spec"], - "quantity": previous_item["quantity"] + "material_grade": previous_item["material_grade"], + "quantity": previous_item["quantity"], + "category": previous_item["category"], + "unit": previous_item["unit"] }) return { @@ -437,8 +437,7 @@ async def perform_material_comparison( }, "new_items": new_items, "modified_items": modified_items, - "removed_items": removed_items, - "purchase_summary": purchase_summary + "removed_items": removed_items } async def get_materials_by_hash(db: Session, file_id: int) -> Dict[str, Dict]: @@ -451,10 +450,11 @@ async def get_materials_by_hash(db: Session, file_id: int) -> Dict[str, Dict]: size_spec, material_grade, SUM(quantity) as quantity, - classified_category + classified_category, + unit FROM materials WHERE file_id = :file_id - GROUP BY original_description, size_spec, material_grade, classified_category + GROUP BY original_description, size_spec, material_grade, classified_category, unit """) result = db.execute(query, {"file_id": file_id}) @@ -468,27 +468,20 @@ async def get_materials_by_hash(db: Session, file_id: int) -> Dict[str, Dict]: materials_dict[material_hash] = { "material_hash": material_hash, - "original_description": mat[0], + "description": mat[0], # original_description -> description "size_spec": mat[1], "material_grade": mat[2], "quantity": float(mat[3]) if mat[3] else 0.0, - "classified_category": mat[4] + "category": mat[4], # classified_category -> category + "unit": mat[5] or 'EA' } return materials_dict async def get_current_inventory(db: Session, job_no: str) -> Dict[str, float]: - """현재까지의 누적 재고량 조회""" - query = text(""" - SELECT material_hash, available_stock - FROM material_inventory_status - WHERE job_no = :job_no - """) - - result = db.execute(query, {"job_no": job_no}) - inventory = result.fetchall() - - return {inv.material_hash: float(inv.available_stock or 0) for inv in inventory} + """현재까지의 누적 재고량 조회 - 임시로 빈 딕셔너리 반환""" + # TODO: 실제 재고 시스템 구현 후 활성화 + return {} async def save_comparison_result( db: Session, diff --git a/frontend/src/pages/BOMStatusPage.jsx b/frontend/src/pages/BOMStatusPage.jsx index ba8365a..695bad7 100644 --- a/frontend/src/pages/BOMStatusPage.jsx +++ b/frontend/src/pages/BOMStatusPage.jsx @@ -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); diff --git a/frontend/src/pages/MaterialComparisonPage.jsx b/frontend/src/pages/MaterialComparisonPage.jsx index fd1f056..e8548f0 100644 --- a/frontend/src/pages/MaterialComparisonPage.jsx +++ b/frontend/src/pages/MaterialComparisonPage.jsx @@ -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 ( + + {/* 요약 통계 카드 */} + + + + + + {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' && '삭제된 자재가 없습니다.'} + + ); + } + + return ( + + + + + 카테고리 + 자재 설명 + 사이즈 + 재질 + {type === 'modified' && ( + <> + 이전 수량 + 현재 수량 + 변경량 + + )} + {type !== 'modified' && 수량} + 단위 + + + + {items.map((item, index) => ( + + + + + {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'} + + ))} + +
+
+ ); + }; + const renderHeader = () => ( @@ -163,7 +346,7 @@ const MaterialComparisonPage = () => { startIcon={} onClick={handleGoBack} > - 돌아가기 + BOM 목록으로 @@ -216,17 +399,7 @@ const MaterialComparisonPage = () => { {renderHeader()} - {comparisonResult ? ( - - ) : ( - - 비교 결과가 없습니다. - - )} + {comparisonResult && renderComparisonResults()} ); }; diff --git a/frontend/src/pages/RevisionPurchasePage.jsx b/frontend/src/pages/RevisionPurchasePage.jsx index 1e932df..ec68848 100644 --- a/frontend/src/pages/RevisionPurchasePage.jsx +++ b/frontend/src/pages/RevisionPurchasePage.jsx @@ -165,10 +165,10 @@ const RevisionPurchasePage = () => { {error} @@ -188,9 +188,9 @@ const RevisionPurchasePage = () => { diff --git a/test_upload.csv b/test_upload.csv new file mode 100644 index 0000000..d56a59e --- /dev/null +++ b/test_upload.csv @@ -0,0 +1,3 @@ +description,qty,main_nom +PIPE A,10,4 +FITTING B,5,2