From bef0d8bf7cd6b8278c93d29c8b8a96b982abfd24 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Wed, 23 Jul 2025 06:58:31 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9E=90=EC=9E=AC=20=EB=A6=AC=EB=B9=84?= =?UTF-8?q?=EC=A0=84=20=EB=B9=84=EA=B5=90=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=99=84=EC=A0=84=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20UI=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ”ง ๋ฐฑ์—”๋“œ ์ˆ˜์ •: - ์ด์ „ ๋ฆฌ๋น„์ „ ํƒ์ง€ ๋กœ์ง์„ ๋ฌธ์ž์—ด์—์„œ ์ˆซ์ž ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐœ์„  (Rev.1์—์„œ Rev.0 ์ •์ƒ ํƒ์ง€) - get_materials_by_hash ํ•จ์ˆ˜ ํ•„๋“œ๋ช… ์ˆ˜์ • ๋ฐ ๋‹จ์ˆœํ™” - ์ž์žฌ ๋น„๊ต API ์‘๋‹ต ๊ตฌ์กฐ ๊ฐœ์„  ๐ŸŽจ ํ”„๋ก ํŠธ์—”๋“œ UI ๋Œ€ํญ ๊ฐœ์„ : - MaterialComparisonPage๋ฅผ MaterialsPage์™€ ๋™์ผํ•œ ๊น”๋”ํ•œ ๋””์ž์ธ์œผ๋กœ ๋ฆฌ๋‰ด์–ผ - ์š”์•ฝ ํ†ต๊ณ„ ์นด๋“œ 4๊ฐœ ์ถ”๊ฐ€ (์‹ ๊ทœ/๋ณ€๊ฒฝ/์‚ญ์ œ/์ด ์ž์žฌ) - ํƒญ์œผ๋กœ ๊ตฌ๋ถ„๋œ ์ž์žฌ ๋ชฉ๋ก (์‹ ๊ทœ/๋ณ€๊ฒฝ/์‚ญ์ œ) - ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ๋น„๊ต ๊ฒฐ๊ณผ ํ‘œ์‹œ (์ด์ „์ˆ˜๋Ÿ‰ โ†’ ํ˜„์žฌ์ˆ˜๋Ÿ‰ โ†’ ๋ณ€๊ฒฝ๋Ÿ‰) - ์ƒ‰์ƒ๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ Chip๊ณผ ๋ณ€๊ฒฝ๋Ÿ‰ ๊ฐ•์กฐ ํ‘œ์‹œ ๐Ÿ”„ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ฐœ์„ : - MaterialComparisonPage ๋Œ์•„๊ฐ€๊ธฐ ๋ฒ„ํŠผ์„ BOM ์ƒํƒœ ํŽ˜์ด์ง€๋กœ ์ˆ˜์ • - RevisionPurchasePage ๋ฒ„ํŠผ ํ…์ŠคํŠธ๋ฅผ 'BOM ๋ชฉ๋ก์œผ๋กœ'๋กœ ๋ช…ํ™•ํ™” - ๋ชจ๋“  ๋น„๊ต ๊ด€๋ จ ํŽ˜์ด์ง€์˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ผ๊ด€์„ฑ ํ™•๋ณด ๐Ÿ› ๋ฒ„๊ทธ ์ˆ˜์ •: - BOMStatusPage ๋ฆฌ๋น„์ „ ์—…๋กœ๋“œ ์‹œ ๋ถˆํ•„์š”ํ•œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ (๋„คํŠธ์›Œํฌ ์—๋Ÿฌ ํ•ด๊ฒฐ) - API ์‘๋‹ต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  (result.data ์ฒ˜๋ฆฌ) - ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋ฐ ๋””๋ฒ„๊น… ๋กœ๊ทธ ๊ฐ•ํ™” โœ… ์™„์ „ ์ž‘๋™ํ•˜๋Š” ๋ฆฌ๋น„์ „ ๋น„๊ต ์‹œ์Šคํ…œ ์™„์„ฑ --- backend/app/routers/material_comparison.py | 123 +++++----- frontend/src/pages/BOMStatusPage.jsx | 2 - frontend/src/pages/MaterialComparisonPage.jsx | 211 ++++++++++++++++-- frontend/src/pages/RevisionPurchasePage.jsx | 8 +- test_upload.csv | 3 + 5 files changed, 257 insertions(+), 90 deletions(-) create mode 100644 test_upload.csv 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