Files
TK-BOM-Project/backend/app/services/material_classifier.py
Hyungi Ahn 12ecb93741 feat: 완전한 자재 분류 시스템 구현 (v1.0)
🎯 주요 기능:
- 재질 분류 모듈 (ASTM/ASME 규격 자동 인식)
- PIPE 분류 시스템 (제조방법, 끝가공, 스케줄, 절단계획)
- FITTING 분류 시스템 (10가지 타입, 연결방식, 압력등급)
- FLANGE 분류 시스템 (SPECIAL/STANDARD 구분, 면가공)
- 스풀 관리 시스템 (도면별 A,B,C 넘버링, 에리어 관리)

📁 새로 추가된 파일들:
- app/services/materials_schema.py (재질 규격 데이터베이스)
- app/services/material_classifier.py (공통 재질 분류 엔진)
- app/services/pipe_classifier.py (파이프 전용 분류기)
- app/services/fitting_classifier.py (피팅 전용 분류기)
- app/services/flange_classifier.py (플랜지 전용 분류기)
- app/services/spool_manager_v2.py (수정된 스풀 관리)
- app/services/test_*.py (각 시스템별 테스트 파일)

🔧 기술적 특징:
- 정규표현식 기반 패턴 매칭
- 신뢰도 점수 시스템 (0.0-1.0)
- 증거 기반 분류 (evidence tracking)
- 모듈화된 구조 (재사용 가능)

🎯 분류 정확도:
- 재질 분류: 90-95% 신뢰도
- PIPE 분류: 85-95% 신뢰도
- FITTING 분류: 85-95% 신뢰도
- FLANGE 분류: 85-95% 신뢰도

💾 데이터베이스 연동:
- 모든 분석 결과 자동 저장
- 프로젝트/도면 정보 자동 연결
- 스풀 정보 사용자 입력 대기

🧪 테스트 커버리지:
- 실제 BOM 데이터 기반 테스트
- 예외 케이스 처리
- 10+ 개 테스트 시나리오

Version: v1.0
Date: 2024-07-15
Author: hyungiahn
2025-07-15 09:43:39 +09:00

314 lines
11 KiB
Python

"""
재질 분류를 위한 공통 함수
materials_schema.py의 데이터를 사용하여 재질을 분류
"""
import re
from typing import Dict, List, Optional, Tuple
from .materials_schema import (
MATERIAL_STANDARDS,
SPECIAL_MATERIALS,
MANUFACTURING_MATERIAL_MAP,
GENERIC_MATERIAL_KEYWORDS
)
def classify_material(description: str) -> Dict:
"""
공통 재질 분류 함수
Args:
description: 자재 설명 (DESCRIPTION 필드)
Returns:
재질 분류 결과 딕셔너리
"""
desc_upper = description.upper().strip()
# 1단계: 특수 재질 우선 확인 (가장 구체적)
special_result = check_special_materials(desc_upper)
if special_result['confidence'] > 0.9:
return special_result
# 2단계: ASTM/ASME 규격 확인
astm_result = check_astm_materials(desc_upper)
if astm_result['confidence'] > 0.8:
return astm_result
# 3단계: KS 규격 확인
ks_result = check_ks_materials(desc_upper)
if ks_result['confidence'] > 0.8:
return ks_result
# 4단계: JIS 규격 확인
jis_result = check_jis_materials(desc_upper)
if jis_result['confidence'] > 0.8:
return jis_result
# 5단계: 일반 키워드 확인
generic_result = check_generic_materials(desc_upper)
return generic_result
def check_special_materials(description: str) -> Dict:
"""특수 재질 확인"""
# SUPER ALLOYS 확인
for alloy_family, alloy_data in SPECIAL_MATERIALS["SUPER_ALLOYS"].items():
for pattern in alloy_data["patterns"]:
match = re.search(pattern, description)
if match:
grade = match.group(1) if match.groups() else "STANDARD"
grade_info = alloy_data["grades"].get(grade, {})
return {
"standard": f"{alloy_family}",
"grade": f"{alloy_family} {grade}",
"material_type": "SUPER_ALLOY",
"manufacturing": alloy_data.get("manufacturing", "SPECIAL"),
"composition": grade_info.get("composition", ""),
"applications": grade_info.get("applications", ""),
"confidence": 0.95,
"evidence": [f"SPECIAL_MATERIAL: {alloy_family} {grade}"]
}
# TITANIUM 확인
titanium_data = SPECIAL_MATERIALS["TITANIUM"]
for pattern in titanium_data["patterns"]:
match = re.search(pattern, description)
if match:
grade = match.group(1) if match.groups() else "2"
grade_info = titanium_data["grades"].get(grade, {})
return {
"standard": "TITANIUM",
"grade": f"Titanium Grade {grade}",
"material_type": "TITANIUM",
"manufacturing": "FORGED_OR_SEAMLESS",
"composition": grade_info.get("composition", f"Ti Grade {grade}"),
"confidence": 0.95,
"evidence": [f"TITANIUM: Grade {grade}"]
}
return {"confidence": 0.0}
def check_astm_materials(description: str) -> Dict:
"""ASTM/ASME 규격 확인"""
astm_data = MATERIAL_STANDARDS["ASTM_ASME"]
# FORGED 등급 확인
for standard, standard_data in astm_data["FORGED_GRADES"].items():
result = check_astm_standard(description, standard, standard_data)
if result["confidence"] > 0.8:
return result
# WELDED 등급 확인
for standard, standard_data in astm_data["WELDED_GRADES"].items():
result = check_astm_standard(description, standard, standard_data)
if result["confidence"] > 0.8:
return result
# CAST 등급 확인
for standard, standard_data in astm_data["CAST_GRADES"].items():
result = check_astm_standard(description, standard, standard_data)
if result["confidence"] > 0.8:
return result
# PIPE 등급 확인
for standard, standard_data in astm_data["PIPE_GRADES"].items():
result = check_astm_standard(description, standard, standard_data)
if result["confidence"] > 0.8:
return result
return {"confidence": 0.0}
def check_astm_standard(description: str, standard: str, standard_data: Dict) -> Dict:
"""개별 ASTM 규격 확인"""
# 직접 패턴이 있는 경우 (A105 등)
if "patterns" in standard_data:
for pattern in standard_data["patterns"]:
match = re.search(pattern, description)
if match:
return {
"standard": f"ASTM {standard}",
"grade": f"ASTM {standard}",
"material_type": determine_material_type(standard, ""),
"manufacturing": standard_data.get("manufacturing", "UNKNOWN"),
"confidence": 0.9,
"evidence": [f"ASTM_{standard}: Direct Match"]
}
# 하위 분류가 있는 경우 (A182, A234 등)
else:
for subtype, subtype_data in standard_data.items():
for pattern in subtype_data["patterns"]:
match = re.search(pattern, description)
if match:
grade_code = match.group(1) if match.groups() else ""
grade_info = subtype_data["grades"].get(grade_code, {})
return {
"standard": f"ASTM {standard}",
"grade": f"ASTM {standard} {grade_code}",
"material_type": determine_material_type(standard, grade_code),
"manufacturing": subtype_data.get("manufacturing", "UNKNOWN"),
"composition": grade_info.get("composition", ""),
"applications": grade_info.get("applications", ""),
"confidence": 0.9,
"evidence": [f"ASTM_{standard}: {grade_code}"]
}
return {"confidence": 0.0}
def check_ks_materials(description: str) -> Dict:
"""KS 규격 확인"""
ks_data = MATERIAL_STANDARDS["KS"]
for category, standards in ks_data.items():
for standard, standard_data in standards.items():
for pattern in standard_data["patterns"]:
match = re.search(pattern, description)
if match:
return {
"standard": f"KS {standard}",
"grade": f"KS {standard}",
"material_type": determine_material_type_from_description(description),
"manufacturing": standard_data.get("manufacturing", "UNKNOWN"),
"description": standard_data["description"],
"confidence": 0.85,
"evidence": [f"KS_{standard}"]
}
return {"confidence": 0.0}
def check_jis_materials(description: str) -> Dict:
"""JIS 규격 확인"""
jis_data = MATERIAL_STANDARDS["JIS"]
for category, standards in jis_data.items():
for standard, standard_data in standards.items():
for pattern in standard_data["patterns"]:
match = re.search(pattern, description)
if match:
return {
"standard": f"JIS {standard}",
"grade": f"JIS {standard}",
"material_type": determine_material_type_from_description(description),
"manufacturing": standard_data.get("manufacturing", "UNKNOWN"),
"description": standard_data["description"],
"confidence": 0.85,
"evidence": [f"JIS_{standard}"]
}
return {"confidence": 0.0}
def check_generic_materials(description: str) -> Dict:
"""일반 재질 키워드 확인"""
for material_type, keywords in GENERIC_MATERIAL_KEYWORDS.items():
for keyword in keywords:
if keyword in description:
return {
"standard": "GENERIC",
"grade": keyword,
"material_type": material_type,
"manufacturing": "UNKNOWN",
"confidence": 0.6,
"evidence": [f"GENERIC: {keyword}"]
}
return {
"standard": "UNKNOWN",
"grade": "UNKNOWN",
"material_type": "UNKNOWN",
"manufacturing": "UNKNOWN",
"confidence": 0.0,
"evidence": ["NO_MATERIAL_FOUND"]
}
def determine_material_type(standard: str, grade: str) -> str:
"""규격과 등급으로 재질 타입 결정"""
# 스테인리스 등급
stainless_patterns = ["304", "316", "321", "347", "F304", "F316", "WP304", "CF8"]
if any(pattern in grade for pattern in stainless_patterns):
return "STAINLESS_STEEL"
# 합금강 등급
alloy_patterns = ["F1", "F5", "F11", "F22", "F91", "WP1", "WP5", "WP11", "WP22", "WP91"]
if any(pattern in grade for pattern in alloy_patterns):
return "ALLOY_STEEL"
# 주조품
if standard in ["A216", "A351"]:
return "CAST_STEEL"
# 기본값은 탄소강
return "CARBON_STEEL"
def determine_material_type_from_description(description: str) -> str:
"""설명에서 재질 타입 추정"""
desc_upper = description.upper()
if any(keyword in desc_upper for keyword in ["SS", "STS", "STAINLESS", "304", "316"]):
return "STAINLESS_STEEL"
elif any(keyword in desc_upper for keyword in ["ALLOY", "합금", "CR", "MO"]):
return "ALLOY_STEEL"
elif any(keyword in desc_upper for keyword in ["CAST", "주조"]):
return "CAST_STEEL"
else:
return "CARBON_STEEL"
def get_manufacturing_method_from_material(material_result: Dict) -> str:
"""재질 정보로부터 제작방법 추정"""
if material_result.get("confidence", 0) < 0.5:
return "UNKNOWN"
material_standard = material_result.get('standard', '')
# 직접 매핑
if 'A182' in material_standard or 'A105' in material_standard:
return 'FORGED'
elif 'A234' in material_standard or 'A403' in material_standard:
return 'WELDED_FABRICATED'
elif 'A216' in material_standard or 'A351' in material_standard:
return 'CAST'
elif 'A106' in material_standard or 'A312' in material_standard:
return 'SEAMLESS'
elif 'A53' in material_standard:
return 'WELDED_OR_SEAMLESS'
# manufacturing 필드가 있으면 직접 사용
manufacturing = material_result.get("manufacturing", "UNKNOWN")
if manufacturing != "UNKNOWN":
return manufacturing
return "UNKNOWN"
def get_material_confidence_factors(material_result: Dict) -> List[str]:
"""재질 분류 신뢰도 영향 요소 반환"""
factors = []
confidence = material_result.get("confidence", 0)
if confidence >= 0.9:
factors.append("HIGH_CONFIDENCE")
elif confidence >= 0.7:
factors.append("MEDIUM_CONFIDENCE")
else:
factors.append("LOW_CONFIDENCE")
if material_result.get("standard") == "UNKNOWN":
factors.append("NO_STANDARD_FOUND")
if material_result.get("manufacturing") == "UNKNOWN":
factors.append("MANUFACTURING_UNCLEAR")
return factors