🎯 주요 기능: - 재질 분류 모듈 (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
314 lines
11 KiB
Python
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
|