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 = () => {
}
- onClick={() => navigate(-1)}
+ onClick={() => navigate(`/bom-status?job_no=${jobNo}`)}
sx={{ mb: 2 }}
>
- 뒤로가기
+ BOM 목록으로
{error}
@@ -188,9 +188,9 @@ const RevisionPurchasePage = () => {
}
- onClick={() => navigate(-1)}
+ onClick={() => navigate(`/bom-status?job_no=${jobNo}`)}
>
- 뒤로가기
+ BOM 목록으로
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