feat: SPECIAL/UNCLASSIFIED 카테고리 추가 및 WELD GAP 자동 제외
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

주요 변경사항:
- SPECIAL 카테고리 추가: 특수 제작 품목 관리 (Type, Drawing, Detail1-4)
- UNCLASSIFIED 카테고리 추가: 미분류 자재 원본 그대로 표시
- UNKNOWN → UNCLASSIFIED 통합: 기존 UNKNOWN 카테고리 제거
- WELD GAP 자동 제외: BOM 업로드 시 WELD GAP 항목 자동 필터링

백엔드:
- integrated_classifier.py: UNKNOWN → UNCLASSIFIED 변경, SPECIAL 우선순위 분류
- files.py: parse_dataframe에서 WELD GAP 필터링, UNKNOWN 참조 제거
- exclude_classifier.py: WELD GAP 제외 로직 유지

프론트엔드:
- SpecialMaterialsView.jsx: 특수 제작 품목 관리 컴포넌트
- UnclassifiedMaterialsView.jsx: 미분류 자재 관리 컴포넌트
- BOMManagementPage.jsx: 새 카테고리 추가 및 라우팅
- excelExport.js: SPECIAL/UNCLASSIFIED 엑셀 내보내기 지원
- 모든 UNKNOWN 참조를 UNCLASSIFIED로 변경

기능 개선:
- 저장 기능: 모든 카테고리에 추가요청사항 저장/편집 기능
- P열 납기일 규칙: 모든 카테고리 엑셀 내보내기 통일
- UI 개선: Detail1-4 컬럼명으로 혼동 방지
- 데이터 정리: 모든 프로젝트 및 BOM 데이터 초기화
This commit is contained in:
hyungi
2025-10-17 13:48:48 +09:00
parent f336b5a4a8
commit e0ad21bfad
14 changed files with 1335 additions and 41 deletions

View File

@@ -231,6 +231,14 @@ def parse_dataframe(df):
materials = []
for index, row in df.iterrows():
description = str(row.get(mapped_columns.get('description', ''), ''))
# WELD GAP 항목은 업로드 단계에서 제외 (불필요한 계산용 항목)
description_upper = description.upper()
if ('WELD GAP' in description_upper or 'WELDING GAP' in description_upper or
'웰드갭' in description_upper or '용접갭' in description_upper):
print(f"⚠️ WELD GAP 항목 제외: {description}")
continue
quantity_raw = row.get(mapped_columns.get('quantity', ''), 0)
try:
@@ -547,7 +555,7 @@ async def upload_file(
materials_to_insert.append({
"file_id": file_id,
"original_description": material_data["original_description"],
"classified_category": previous_item.get("category", "UNKNOWN"),
"classified_category": previous_item.get("category", "UNCLASSIFIED"),
"confidence": 1.0, # 확정된 자료이므로 신뢰도 100%
"quantity": material_data["quantity"],
"unit": material_data.get("unit", "EA"),
@@ -659,7 +667,7 @@ async def upload_file(
# 1. 통합 분류기로 자재 타입 결정
integrated_result = classify_material_integrated(description, main_nom or "", red_nom or "", length_value)
print(f"[분류] {description}")
print(f"통합 분류 결과: {integrated_result.get('category', 'UNKNOWN')} (신뢰도: {integrated_result.get('confidence', 0)}, Level: {integrated_result.get('classification_level', 'NONE')})")
print(f"통합 분류 결과: {integrated_result.get('category', 'UNCLASSIFIED')} (신뢰도: {integrated_result.get('confidence', 0)}, Level: {integrated_result.get('classification_level', 'NONE')})")
# 2. 제외 대상 확인
if should_exclude_material(description):
@@ -670,7 +678,7 @@ async def upload_file(
}
else:
# 3. 타입별 상세 분류기 실행
material_type = integrated_result.get('category', 'UNKNOWN')
material_type = integrated_result.get('category', 'UNCLASSIFIED')
if material_type == "PIPE":
from ..services.pipe_classifier import classify_pipe_for_purchase
@@ -714,9 +722,9 @@ async def upload_file(
}
}
else:
# UNKNOWN 처리
# UNCLASSIFIED 처리
classification_result = {
"category": "UNKNOWN",
"category": "UNCLASSIFIED",
"overall_confidence": integrated_result.get('confidence', 0.0),
"reason": f"분류 불가: {integrated_result.get('evidence', [])}"
}
@@ -728,7 +736,7 @@ async def upload_file(
integrated_result.get('confidence', 0.0) + 0.2
)
print(f"최종 분류 결과: {classification_result.get('category', 'UNKNOWN')}")
print(f"최종 분류 결과: {classification_result.get('category', 'UNCLASSIFIED')}")
# 전체 재질명 추출
from ..services.material_grade_extractor import extract_full_material_grade
@@ -758,7 +766,7 @@ async def upload_file(
print(f"첫 번째 자재 저장:")
print(f" size_spec: '{material_data['size_spec']}'")
print(f" original_description: {material_data['original_description']}")
print(f" category: {classification_result.get('category', 'UNKNOWN')}")
print(f" category: {classification_result.get('category', 'UNCLASSIFIED')}")
print(f" drawing_name: {material_data.get('dwg_name')}")
print(f" line_no: {material_data.get('line_num')}")
@@ -774,7 +782,7 @@ async def upload_file(
"full_material_grade": full_material_grade,
"line_number": material_data["line_number"],
"row_number": material_data["row_number"],
"classified_category": classification_result.get("category", "UNKNOWN"),
"classified_category": classification_result.get("category", "UNCLASSIFIED"),
"classification_confidence": classification_result.get("overall_confidence", 0.0),
"is_verified": False,
"drawing_name": material_data.get("dwg_name"),
@@ -889,7 +897,7 @@ async def upload_file(
material_grade_from_classifier = material_info.get("grade", "")
# 분류기에서 더 상세한 재질 정보가 나왔으면 업데이트
if material_grade_from_classifier and material_grade_from_classifier != "UNKNOWN":
if material_grade_from_classifier and material_grade_from_classifier not in ["UNKNOWN", "UNCLASSIFIED"]:
material_spec = material_grade_from_classifier
# materials 테이블의 material_grade도 업데이트
@@ -958,7 +966,7 @@ async def upload_file(
material_grade = material_info.get("grade", "")
# 분류기에서 더 상세한 재질 정보가 나왔으면 업데이트
if material_grade and material_grade != "UNKNOWN":
if material_grade and material_grade not in ["UNKNOWN", "UNCLASSIFIED"]:
db.execute(text("""
UPDATE materials
SET material_grade = :new_material_grade
@@ -1055,7 +1063,7 @@ async def upload_file(
material_grade = material_info.get("grade", "")
# 분류기에서 더 상세한 재질 정보가 나왔으면 업데이트
if material_grade and material_grade != "UNKNOWN":
if material_grade and material_grade not in ["UNKNOWN", "UNCLASSIFIED"]:
db.execute(text("""
UPDATE materials
SET material_grade = :new_material_grade
@@ -1245,7 +1253,7 @@ async def upload_file(
material_grade = material_info.get("grade", "")
# 분류기에서 더 상세한 재질 정보가 나왔으면 업데이트
if material_grade and material_grade != "UNKNOWN":
if material_grade and material_grade not in ["UNKNOWN", "UNCLASSIFIED"]:
db.execute(text("""
UPDATE materials
SET material_grade = :new_material_grade

View File

@@ -8,11 +8,6 @@ from typing import Dict, List, Optional
# ========== 제외 대상 타입 ==========
EXCLUDE_TYPES = {
"WELD_GAP": {
"description_keywords": ["WELD GAP", "WELDING GAP", "GAP", "용접갭", "웰드갭"],
"characteristics": "용접 시 수축 고려용 계산 항목",
"reason": "실제 자재 아님 - 용접 갭 계산용"
},
"CUTTING_LOSS": {
"description_keywords": ["CUTTING LOSS", "CUT LOSS", "절단로스", "컷팅로스"],
"characteristics": "절단 시 손실 고려용 계산 항목",

View File

@@ -110,6 +110,7 @@ def classify_material_integrated(description: str, main_nom: str = "",
"reason": "스페셜 키워드 발견"
}
# VALVE 카테고리 우선 확인 (SIGHT GLASS, STRAINER)
if ('SIGHT GLASS' in desc_upper or 'STRAINER' in desc_upper or
'사이트글라스' in desc_upper or '스트레이너' in desc_upper):
@@ -295,7 +296,7 @@ def classify_material_integrated(description: str, main_nom: str = "",
# 분류 실패
return {
"category": "UNKNOWN",
"category": "UNCLASSIFIED",
"confidence": 0.0,
"evidence": ["NO_CLASSIFICATION_POSSIBLE"],
"classification_level": "NONE"