""" BOLT 분류 시스템 볼트, 너트, 와셔, 스터드 등 체결용 부품 분류 """ import re from typing import Dict, List, Optional from .material_classifier import classify_material def classify_bolt_material(description: str) -> Dict: """볼트용 재질 분류 (ASTM A193, A194 등)""" desc_upper = description.upper() # A320/A194M 동시 처리 (예: "ASTM A320/A194M GR B8/8") - 저온용 볼트 조합 if "A320" in desc_upper and "A194" in desc_upper: # B8/8 등급 추출 bolt_grade = "UNKNOWN" nut_grade = "UNKNOWN" if "B8" in desc_upper: bolt_grade = "B8" nut_grade = "8" # A320/A194M의 경우 보통 B8/8 조합 elif "L7" in desc_upper: bolt_grade = "L7" elif "B8M" in desc_upper: bolt_grade = "B8M" combined_grade = f"{bolt_grade}/{nut_grade}" if bolt_grade != "UNKNOWN" and nut_grade != "UNKNOWN" else f"{bolt_grade}" if bolt_grade != "UNKNOWN" else "A320/A194M" return { "standard": "ASTM A320/A194M", "grade": combined_grade, "material_type": "LOW_TEMP_STAINLESS", # 저온용 스테인리스 "manufacturing": "FORGED", "confidence": 0.95, "evidence": ["ASTM_A320_A194M_COMBINED"] } # A193/A194 동시 처리 (예: "ASTM A193/A194 GR B7/2H") if "A193" in desc_upper and "A194" in desc_upper: # B7/2H 등급 추출 bolt_grade = "UNKNOWN" nut_grade = "UNKNOWN" if "B7" in desc_upper: bolt_grade = "B7" if "2H" in desc_upper: nut_grade = "2H" elif " 8" in desc_upper or "GR 8" in desc_upper: nut_grade = "8" combined_grade = f"{bolt_grade}/{nut_grade}" if bolt_grade != "UNKNOWN" and nut_grade != "UNKNOWN" else "A193/A194" return { "standard": "ASTM A193/A194", "grade": combined_grade, "material_type": "ALLOY_STEEL", # B7/2H 조합은 보통 합금강 "manufacturing": "FORGED", "confidence": 0.95, "evidence": ["ASTM_A193_A194_COMBINED"] } # ASTM A193 (볼트용 강재) if any(pattern in desc_upper for pattern in ["A193", "ASTM A193"]): # B7, B8 등 등급 추출 (GR B7/2H 형태도 지원) grade = "UNKNOWN" if "GR B7" in desc_upper or " B7" in desc_upper: grade = "B7" elif "GR B8" in desc_upper or " B8" in desc_upper: grade = "B8" elif "GR B16" in desc_upper or " B16" in desc_upper: grade = "B16" return { "standard": "ASTM A193", "grade": grade if grade != "UNKNOWN" else "ASTM A193", "material_type": "ALLOY_STEEL" if "B7" in grade else "STAINLESS_STEEL", "manufacturing": "FORGED", "confidence": 0.95, "evidence": ["ASTM_A193_BOLT_MATERIAL"] } # ASTM A194 (너트용 강재) if any(pattern in desc_upper for pattern in ["A194", "ASTM A194"]): grade = "UNKNOWN" if "GR 2H" in desc_upper or " 2H" in desc_upper or "/2H" in desc_upper: grade = "2H" elif "GR 8" in desc_upper or " 8" in desc_upper: grade = "8" return { "standard": "ASTM A194", "grade": grade if grade != "UNKNOWN" else "ASTM A194", "material_type": "ALLOY_STEEL" if "2H" in grade else "STAINLESS_STEEL", "manufacturing": "FORGED", "confidence": 0.95, "evidence": ["ASTM_A194_NUT_MATERIAL"] } # ASTM A320 (저온용 볼트) if any(pattern in desc_upper for pattern in ["A320", "ASTM A320"]): grade = "UNKNOWN" if "L7" in desc_upper: grade = "L7" elif "L43" in desc_upper: grade = "L43" elif "B8M" in desc_upper: grade = "B8M" return { "standard": "ASTM A320", "grade": grade if grade != "UNKNOWN" else "ASTM A320", "material_type": "LOW_TEMP_STEEL", "manufacturing": "FORGED", "confidence": 0.95, "evidence": ["ASTM_A320_LOW_TEMP_BOLT"] } # ASTM A325 (구조용 볼트) if any(pattern in desc_upper for pattern in ["A325", "ASTM A325"]): return { "standard": "ASTM A325", "grade": "ASTM A325", "material_type": "STRUCTURAL_STEEL", "manufacturing": "HEAT_TREATED", "confidence": 0.95, "evidence": ["ASTM_A325_STRUCTURAL_BOLT"] } # ASTM A490 (고강도 구조용 볼트) if any(pattern in desc_upper for pattern in ["A490", "ASTM A490"]): return { "standard": "ASTM A490", "grade": "ASTM A490", "material_type": "HIGH_STRENGTH_STEEL", "manufacturing": "HEAT_TREATED", "confidence": 0.95, "evidence": ["ASTM_A490_HIGH_STRENGTH_BOLT"] } # DIN 934 (DIN 너트) if any(pattern in desc_upper for pattern in ["DIN 934", "DIN934"]): return { "standard": "DIN 934", "grade": "DIN 934", "material_type": "CARBON_STEEL", "manufacturing": "FORGED", "confidence": 0.90, "evidence": ["DIN_934_NUT"] } # ISO 4762 (소켓 헤드 캡 스크류) if any(pattern in desc_upper for pattern in ["ISO 4762", "ISO4762", "DIN 912", "DIN912"]): return { "standard": "ISO 4762", "grade": "ISO 4762", "material_type": "ALLOY_STEEL", "manufacturing": "HEAT_TREATED", "confidence": 0.90, "evidence": ["ISO_4762_SOCKET_SCREW"] } # 일반적인 볼트 재질 패턴 추가 확인 if "B7" in desc_upper and "2H" in desc_upper: return { "standard": "ASTM A193/A194", "grade": "B7/2H", "material_type": "ALLOY_STEEL", "manufacturing": "FORGED", "confidence": 0.85, "evidence": ["B7_2H_PATTERN"] } # 단독 B7 패턴 if "B7" in desc_upper: return { "standard": "ASTM A193", "grade": "B7", "material_type": "ALLOY_STEEL", "manufacturing": "FORGED", "confidence": 0.80, "evidence": ["B7_PATTERN"] } # 단독 2H 패턴 if "2H" in desc_upper: return { "standard": "ASTM A194", "grade": "2H", "material_type": "ALLOY_STEEL", "manufacturing": "FORGED", "confidence": 0.80, "evidence": ["2H_PATTERN"] } # 기본 재질 분류기 호출 (materials_schema 문제가 있어도 우회) try: return classify_material(description) except: # materials_schema에 문제가 있으면 기본값 반환 return { "standard": "UNKNOWN", "grade": "UNKNOWN", "material_type": "UNKNOWN", "manufacturing": "UNKNOWN", "confidence": 0.0, "evidence": ["MATERIAL_SCHEMA_ERROR"] } # ========== 볼트 타입별 분류 ========== BOLT_TYPES = { "HEX_BOLT": { "dat_file_patterns": ["BOLT_HEX", "HEX_BOLT", "HEXB_"], "description_keywords": ["HEX BOLT", "HEXAGON BOLT", "육각볼트", "HEX HEAD"], "characteristics": "육각 머리 볼트", "applications": "일반 체결용", "head_type": "HEXAGON" }, "SOCKET_HEAD_CAP": { "dat_file_patterns": ["SHCS_", "SOCKET_", "CAP_BOLT"], "description_keywords": ["SOCKET HEAD CAP", "SHCS", "소켓헤드", "알렌볼트"], "characteristics": "소켓 헤드 캡 스크류", "applications": "정밀 체결용", "head_type": "SOCKET" }, "STUD_BOLT": { "dat_file_patterns": ["STUD_", "STUD_BOLT", "_TK", "BLT_"], "description_keywords": ["STUD BOLT", "STUD", "스터드볼트", "전나사", "BLT"], "characteristics": "양끝 나사 스터드", "applications": "플랜지 체결용", "head_type": "NONE" }, "FLANGE_BOLT": { "dat_file_patterns": ["FLG_BOLT", "FLANGE_BOLT"], "description_keywords": ["FLANGE BOLT", "플랜지볼트"], "characteristics": "플랜지 전용 볼트", "applications": "플랜지 체결 전용", "head_type": "HEXAGON" }, "PSV_BOLT": { "dat_file_patterns": ["PSV_BOLT", "PSV_BLT"], "description_keywords": ["PSV", "PRESSURE SAFETY VALVE BOLT"], "characteristics": "압력안전밸브용 특수 볼트", "applications": "PSV 체결 전용", "head_type": "HEXAGON", "special_application": "PSV" }, "LT_BOLT": { "dat_file_patterns": ["LT_BOLT", "LT_BLT"], "description_keywords": ["LT BOLT", "LT BLT", "LOW TEMP", "저온용"], "characteristics": "저온용 특수 볼트", "applications": "저온 환경 체결용", "head_type": "HEXAGON", "special_application": "LT" }, "CK_BOLT": { "dat_file_patterns": ["CK_BOLT", "CK_BLT", "CHECK_BOLT"], "description_keywords": ["CK", "CHECK VALVE BOLT"], "characteristics": "체크밸브용 특수 볼트", "applications": "체크밸브 체결 전용", "head_type": "HEXAGON", "special_application": "CK" }, "ORI_BOLT": { "dat_file_patterns": ["ORI_BOLT", "ORI_BLT", "ORIFICE_BOLT"], "description_keywords": ["ORI", "ORIFICE", "오리피스"], "characteristics": "오리피스용 특수 볼트", "applications": "오리피스 체결 전용", "head_type": "HEXAGON", "special_application": "ORI" }, "MACHINE_SCREW": { "dat_file_patterns": ["MACH_SCR", "M_SCR"], "description_keywords": ["MACHINE SCREW", "머신스크류", "기계나사"], "characteristics": "기계용 나사", "applications": "기계 부품 체결", "head_type": "VARIOUS" }, "SET_SCREW": { "dat_file_patterns": ["SET_SCR", "GRUB_"], "description_keywords": ["SET SCREW", "GRUB SCREW", "세트스크류", "고정나사"], "characteristics": "고정용 나사", "applications": "축 고정, 위치 고정", "head_type": "SOCKET_OR_NONE" }, "U_BOLT": { "dat_file_patterns": ["U_BOLT", "UBOLT_"], "description_keywords": ["U-BOLT", "U BOLT", "유볼트"], "characteristics": "U자형 볼트", "applications": "파이프 고정용", "head_type": "NONE" }, "EYE_BOLT": { "dat_file_patterns": ["EYE_BOLT", "EYEB_"], "description_keywords": ["EYE BOLT", "아이볼트", "고리볼트"], "characteristics": "고리 형태 볼트", "applications": "인양, 고정용", "head_type": "EYE" } } # ========== 너트 타입별 분류 ========== NUT_TYPES = { "HEX_NUT": { "dat_file_patterns": ["NUT_HEX", "HEX_NUT"], "description_keywords": ["HEX NUT", "HEXAGON NUT", "육각너트"], "characteristics": "육각 너트", "applications": "일반 체결용" }, "HEAVY_HEX_NUT": { "dat_file_patterns": ["HEAVY_NUT", "HVY_NUT"], "description_keywords": ["HEAVY HEX NUT", "HEAVY NUT", "헤비너트"], "characteristics": "두꺼운 육각 너트", "applications": "고강도 체결용" }, "LOCK_NUT": { "dat_file_patterns": ["LOCK_NUT", "LOCKN_"], "description_keywords": ["LOCK NUT", "잠금너트", "록너트"], "characteristics": "잠금 기능 너트", "applications": "진동 방지용" }, "WING_NUT": { "dat_file_patterns": ["WING_NUT", "WINGN_"], "description_keywords": ["WING NUT", "윙너트", "나비너트"], "characteristics": "날개형 너트", "applications": "수동 체결용" }, "COUPLING_NUT": { "dat_file_patterns": ["COUPL_NUT", "CONN_NUT"], "description_keywords": ["COUPLING NUT", "커플링너트", "연결너트"], "characteristics": "연결용 너트", "applications": "스터드 연결용" } } # ========== 와셔 타입별 분류 ========== WASHER_TYPES = { "FLAT_WASHER": { "dat_file_patterns": ["WASH_FLAT", "FLAT_WASH"], "description_keywords": ["FLAT WASHER", "평와셔", "플랫와셔"], "characteristics": "평판형 와셔", "applications": "하중 분산용" }, "SPRING_WASHER": { "dat_file_patterns": ["SPRING_WASH", "SPR_WASH"], "description_keywords": ["SPRING WASHER", "스프링와셔", "탄성와셔"], "characteristics": "탄성 와셔", "applications": "진동 방지용" }, "LOCK_WASHER": { "dat_file_patterns": ["LOCK_WASH", "LOCKW_"], "description_keywords": ["LOCK WASHER", "록와셔", "잠금와셔"], "characteristics": "잠금 와셔", "applications": "풀림 방지용" }, "BELLEVILLE_WASHER": { "dat_file_patterns": ["BELL_WASH", "BELLEV_"], "description_keywords": ["BELLEVILLE WASHER", "벨레빌와셔", "접시와셔"], "characteristics": "접시형 스프링 와셔", "applications": "고하중 탄성용" } } # ========== 나사 규격별 분류 ========== THREAD_STANDARDS = { "METRIC": { "patterns": [r"M(\d+)(?:X(\d+(?:\.\d+)?))?", r"(\d+)MM"], "description": "미터 나사", "pitch_patterns": [r"X(\d+(?:\.\d+)?)"], "common_sizes": ["M6", "M8", "M10", "M12", "M16", "M20", "M24", "M30", "M36"] }, "INCH": { "patterns": [ r"(\d+(?:/\d+)?)\s*[\"\']\s*UNC", # 1/2" UNC r"(\d+(?:/\d+)?)\s*[\"\']\s*UNF", # 1/2" UNF r"(\d+(?:/\d+)?)\s*[\"\']-(\d+)", # 1/2"-13 r"(\d+\.\d+)", # 0.625 (소수점 인치) r"(\d+(?:/\d+)?)\s*INCH", # 1/2 INCH r"(\d+(?:/\d+)?)\s*IN" # 1/2 IN ], "description": "인치 나사", "thread_types": ["UNC", "UNF"], "common_sizes": ["1/4\"", "5/16\"", "3/8\"", "1/2\"", "5/8\"", "0.625\"", "3/4\"", "7/8\"", "1\""] }, "BSW": { "patterns": [r"(\d+(?:/\d+)?)\s*[\"\']\s*BSW", r"(\d+(?:/\d+)?)\s*[\"\']\s*BSF"], "description": "영국 표준 나사", "thread_types": ["BSW", "BSF"], "common_sizes": ["1/4\"", "5/16\"", "3/8\"", "1/2\"", "5/8\"", "3/4\""] } } # ========== 길이 및 등급 분류 ========== BOLT_GRADES = { "METRIC": { "8.8": {"tensile_strength": "800 MPa", "yield_strength": "640 MPa"}, "10.9": {"tensile_strength": "1000 MPa", "yield_strength": "900 MPa"}, "12.9": {"tensile_strength": "1200 MPa", "yield_strength": "1080 MPa"} }, "INCH": { "A307": {"grade": "A", "tensile_strength": "60 ksi"}, "A325": {"type": "1", "tensile_strength": "120 ksi"}, "A490": {"type": "1", "tensile_strength": "150 ksi"} } } def convert_decimal_to_fraction(decimal_str: str) -> str: """소수점 인치를 분수로 변환 (현장 표준)""" try: decimal = float(decimal_str) # 일반적인 인치 분수 변환표 inch_fractions = { 0.125: "1/8", 0.1875: "3/16", 0.25: "1/4", 0.3125: "5/16", 0.375: "3/8", 0.4375: "7/16", 0.5: "1/2", 0.5625: "9/16", 0.625: "5/8", 0.6875: "11/16", 0.75: "3/4", 0.8125: "13/16", 0.875: "7/8", 0.9375: "15/16", 1.0: "1", 1.125: "1-1/8", 1.25: "1-1/4", 1.375: "1-3/8", 1.5: "1-1/2", 1.625: "1-5/8", 1.75: "1-3/4", 1.875: "1-7/8", 2.0: "2" } # 정확한 매칭 (소수점 오차 고려) for dec_val, fraction in inch_fractions.items(): if abs(decimal - dec_val) < 0.001: # 1mm 오차 허용 return fraction # 정확한 매칭이 없으면 가장 가까운 값 찾기 closest_decimal = min(inch_fractions.keys(), key=lambda x: abs(x - decimal)) if abs(closest_decimal - decimal) < 0.0625: # 1/16" 이내 오차만 허용 return inch_fractions[closest_decimal] # 변환할 수 없으면 원래 값 반환 return str(decimal) except ValueError: return decimal_str def classify_surface_treatment(description: str) -> Dict: """볼트 표면처리 분류 (아연도금, 스테인리스 등)""" desc_upper = description.upper() treatments = [] # 전기아연도금 if any(keyword in desc_upper for keyword in ["ELEC.GALV", "ELEC GALV", "ELECTRO GALV", "전기아연도금"]): treatments.append({ "type": "ELECTRO_GALVANIZING", "description": "전기아연도금", "code": "ELEC.GALV", "corrosion_resistance": "보통" }) # 용융아연도금 if any(keyword in desc_upper for keyword in ["HOT DIP GALV", "HDG", "용융아연도금"]): treatments.append({ "type": "HOT_DIP_GALVANIZING", "description": "용융아연도금", "code": "HDG", "corrosion_resistance": "높음" }) # 스테인리스 (표면처리 불필요) if any(keyword in desc_upper for keyword in ["STAINLESS", "STS", "스테인리스"]): treatments.append({ "type": "STAINLESS_STEEL", "description": "스테인리스강", "code": "STS", "corrosion_resistance": "매우높음" }) # 니켈도금 if any(keyword in desc_upper for keyword in ["NICKEL", "NI PLATING", "니켈도금"]): treatments.append({ "type": "NICKEL_PLATING", "description": "니켈도금", "code": "NI", "corrosion_resistance": "높음" }) # 크롬도금 if any(keyword in desc_upper for keyword in ["CHROME", "CR PLATING", "크롬도금"]): treatments.append({ "type": "CHROME_PLATING", "description": "크롬도금", "code": "CR", "corrosion_resistance": "매우높음" }) return { "treatments": treatments, "has_treatment": len(treatments) > 0, "treatment_count": len(treatments), "primary_treatment": treatments[0] if treatments else None } def classify_special_application_bolts(description: str) -> Dict: """ 특수 용도 볼트 분류 및 카운팅 (PSV, LT, CK) 주의: 이 함수는 이미 BOLT로 분류된 아이템에서만 호출되어야 함 PSV, LT, CK는 해당 장비용 볼트를 의미하며, 장비 자체가 아님 """ desc_upper = description.upper() special_applications = [] special_details = {} # PSV 볼트 확인 (압력안전밸브용 볼트) psv_patterns = [ r'\bPSV\b', # 단어 경계로 PSV만 r'PRESSURE\s+SAFETY\s+VALVE', r'압력안전밸브', r'PSV\s+BOLT', r'PSV\s+BLT' ] import re if any(re.search(pattern, desc_upper) for pattern in psv_patterns): special_applications.append("PSV") special_details["PSV"] = { "type": "압력안전밸브용 볼트", "application": "PSV 체결 전용", "critical": True # 안전 장비용으로 중요 } # LT 볼트 확인 (저온용 볼트) lt_patterns = [ r'\bLT\s', # LT 다음에 공백이 있는 경우만 (LT BOLT, LT BLT) r'^LT\b', # 문장 시작의 LT만 r'LOW\s+TEMP', r'저온용', r'CRYOGENIC', r'LT\s+BOLT', r'LT\s+BLT' ] if any(re.search(pattern, desc_upper) for pattern in lt_patterns): special_applications.append("LT") special_details["LT"] = { "type": "저온용 볼트", "application": "저온 환경 체결용", "critical": True # 저온 환경용으로 중요 } # CK 볼트 확인 (체크밸브용 볼트) ck_patterns = [ r'\bCK\b', # 단어 경계로 CK만 r'CHECK\s+VALVE', r'체크밸브', r'CK\s+BOLT', r'CK\s+BLT' ] if any(re.search(pattern, desc_upper) for pattern in ck_patterns): special_applications.append("CK") special_details["CK"] = { "type": "체크밸브용 볼트", "application": "체크밸브 체결 전용", "critical": False # 일반적 } # ORI 볼트 확인 (오리피스용 볼트) ori_patterns = [ r'\bORI\b', # 단어 경계로 ORI만 r'ORIFICE', r'오리피스', r'ORI\s+BOLT', r'ORI\s+BLT' ] if any(re.search(pattern, desc_upper) for pattern in ori_patterns): special_applications.append("ORI") special_details["ORI"] = { "type": "오리피스용 볼트", "application": "오리피스 체결 전용", "critical": True # 유량 측정용으로 중요 } return { "detected_applications": special_applications, "special_details": special_details, "is_special_bolt": len(special_applications) > 0, "special_count": len(special_applications), "classification_note": "특수 장비용 볼트 (장비 자체 아님)" } def classify_bolt(dat_file: str, description: str, main_nom: str, length: Optional[float] = None) -> Dict: """ 완전한 BOLT 분류 Args: dat_file: DAT_FILE 필드 description: DESCRIPTION 필드 main_nom: MAIN_NOM 필드 (나사 사이즈) Returns: 완전한 볼트 분류 결과 """ # 1. 재질 분류 (볼트 전용 버전) material_result = classify_bolt_material(description) # 2. 체결재 타입 분류 (볼트/너트/와셔) fastener_category = classify_fastener_category(dat_file, description) # 3. 구체적 타입 분류 specific_type_result = classify_specific_fastener_type( dat_file, description, fastener_category ) # 4. 나사 규격 분류 thread_result = classify_thread_specification(main_nom, description) # 5. 길이 및 치수 추출 dimensions_result = extract_bolt_dimensions(main_nom, description) # 6. 등급 및 강도 분류 grade_result = classify_bolt_grade(description, thread_result) # 7. 특수 용도 볼트 분류 (PSV, LT, CK) special_result = classify_special_application_bolts(description) # 8. 표면처리 분류 (ELEC.GALV 등) surface_result = classify_surface_treatment(description) # 9. 최종 결과 조합 return { "category": "BOLT", # 재질 정보 (공통 모듈) "material": { "standard": material_result.get('standard', 'UNKNOWN'), "grade": material_result.get('grade', 'UNKNOWN'), "material_type": material_result.get('material_type', 'UNKNOWN'), "confidence": material_result.get('confidence', 0.0) }, # 체결재 분류 정보 "fastener_category": { "category": fastener_category.get('category', 'UNKNOWN'), "confidence": fastener_category.get('confidence', 0.0) }, "fastener_type": { "type": specific_type_result.get('type', 'UNKNOWN'), "characteristics": specific_type_result.get('characteristics', ''), "confidence": specific_type_result.get('confidence', 0.0), "evidence": specific_type_result.get('evidence', []), "applications": specific_type_result.get('applications', ''), "head_type": specific_type_result.get('head_type', '') }, "thread_specification": { "standard": thread_result.get('standard', 'UNKNOWN'), "size": thread_result.get('size', ''), "size_fraction": thread_result.get('size_fraction', ''), "pitch": thread_result.get('pitch', ''), "thread_type": thread_result.get('thread_type', ''), "confidence": thread_result.get('confidence', 0.0) }, "dimensions": { "nominal_size": dimensions_result.get('nominal_size', main_nom), "nominal_size_fraction": dimensions_result.get('nominal_size_fraction', main_nom), "length": dimensions_result.get('length', ''), "diameter": dimensions_result.get('diameter', ''), "dimension_description": dimensions_result.get('dimension_description', '') }, "grade_strength": { "grade": grade_result.get('grade', 'UNKNOWN'), "tensile_strength": grade_result.get('tensile_strength', ''), "yield_strength": grade_result.get('yield_strength', ''), "confidence": grade_result.get('confidence', 0.0) }, # 특수 용도 볼트 정보 "special_applications": { "is_special_bolt": special_result.get('is_special_bolt', False), "detected_applications": special_result.get('detected_applications', []), "special_details": special_result.get('special_details', {}), "special_count": special_result.get('special_count', 0), "classification_note": special_result.get('classification_note', '') }, # 표면처리 정보 "surface_treatment": { "has_treatment": surface_result.get('has_treatment', False), "treatments": surface_result.get('treatments', []), "treatment_count": surface_result.get('treatment_count', 0), "primary_treatment": surface_result.get('primary_treatment', None) }, # 전체 신뢰도 "overall_confidence": calculate_bolt_confidence({ "material": material_result.get('confidence', 0), "fastener_type": specific_type_result.get('confidence', 0), "thread": thread_result.get('confidence', 0), "grade": grade_result.get('confidence', 0) }) } def classify_fastener_category(dat_file: str, description: str) -> Dict: """체결재 카테고리 분류 (볼트/너트/와셔)""" dat_upper = dat_file.upper() desc_upper = description.upper() combined_text = f"{dat_upper} {desc_upper}" # 볼트 키워드 bolt_keywords = ["BOLT", "SCREW", "STUD", "BLT", "볼트", "나사", "스크류", "A193", "A194", "A320", "A325", "A490"] if any(keyword in combined_text for keyword in bolt_keywords): return { "category": "BOLT", "confidence": 0.9, "evidence": ["BOLT_KEYWORDS"] } # 너트 키워드 nut_keywords = ["NUT", "너트"] if any(keyword in combined_text for keyword in nut_keywords): return { "category": "NUT", "confidence": 0.9, "evidence": ["NUT_KEYWORDS"] } # 와셔 키워드 washer_keywords = ["WASHER", "WASH", "와셔"] if any(keyword in combined_text for keyword in washer_keywords): return { "category": "WASHER", "confidence": 0.9, "evidence": ["WASHER_KEYWORDS"] } # 볼트가 아닌 것 같은 키워드들 체크 non_bolt_keywords = [ "PIPE", "TUBE", "파이프", "배관", # 파이프 "ELBOW", "TEE", "REDUCER", "CAP", "엘보", "티", "리듀서", # 피팅 "VALVE", "GATE", "BALL", "GLOBE", "CHECK", "밸브", # 밸브 "FLANGE", "FLG", "플랜지", # 플랜지 "GASKET", "GASK", "가스켓", # 가스켓 "GAUGE", "METER", "TRANSMITTER", "INDICATOR", "SENSOR", "계기", "게이지", # 계기 "THERMOWELL", "ORIFICE", "MANOMETER" # 특수 계기 ] if any(keyword in combined_text for keyword in non_bolt_keywords): return { "category": "UNKNOWN", "confidence": 0.1, "evidence": ["NON_BOLT_KEYWORDS_DETECTED"] } # 기본값: BOLT (하지만 낮은 신뢰도) return { "category": "BOLT", "confidence": 0.1, # 0.6에서 0.1로 낮춤 "evidence": ["DEFAULT_BOLT_LOW_CONFIDENCE"] } def classify_specific_fastener_type(dat_file: str, description: str, fastener_category: Dict) -> Dict: """구체적 체결재 타입 분류""" category = fastener_category.get('category', 'BOLT') dat_upper = dat_file.upper() desc_upper = description.upper() if category == "BOLT": type_dict = BOLT_TYPES elif category == "NUT": type_dict = NUT_TYPES elif category == "WASHER": type_dict = WASHER_TYPES else: type_dict = BOLT_TYPES # 기본값 # DAT_FILE 패턴 확인 for fastener_type, type_data in type_dict.items(): for pattern in type_data.get("dat_file_patterns", []): if pattern in dat_upper: return { "type": fastener_type, "characteristics": type_data["characteristics"], "confidence": 0.95, "evidence": [f"DAT_FILE_PATTERN: {pattern}"], "applications": type_data["applications"], "head_type": type_data.get("head_type", "") } # DESCRIPTION 키워드 확인 for fastener_type, type_data in type_dict.items(): for keyword in type_data.get("description_keywords", []): if keyword in desc_upper: return { "type": fastener_type, "characteristics": type_data["characteristics"], "confidence": 0.85, "evidence": [f"DESCRIPTION_KEYWORD: {keyword}"], "applications": type_data["applications"], "head_type": type_data.get("head_type", "") } # 기본값 default_type = f"{category}_GENERAL" return { "type": default_type, "characteristics": f"일반 {category.lower()}", "confidence": 0.6, "evidence": ["DEFAULT_TYPE"], "applications": "일반용", "head_type": "" } def classify_thread_specification(main_nom: str, description: str) -> Dict: """나사 규격 분류""" combined_text = f"{main_nom} {description}".upper() # 각 표준별 패턴 확인 for standard, standard_data in THREAD_STANDARDS.items(): for pattern in standard_data["patterns"]: match = re.search(pattern, combined_text) if match: size = match.group(1) # 피치 정보 추출 (미터 나사) pitch = "" if standard == "METRIC" and len(match.groups()) > 1 and match.group(2): pitch = match.group(2) elif standard == "INCH" and len(match.groups()) > 1 and match.group(2): pitch = match.group(2) # TPI (Threads Per Inch) # 나사 타입 확인 thread_type = "" if standard in ["INCH", "BSW"]: for t_type in standard_data.get("thread_types", []): if t_type in combined_text: thread_type = t_type break # 인치 사이즈를 분수로 변환 size_fraction = size if standard == "INCH": try: if '.' in size and size.replace('.', '').isdigit(): size_fraction = convert_decimal_to_fraction(size).replace('"', '') except: size_fraction = size return { "standard": standard, "size": size, # 원래 값 "size_fraction": size_fraction, # 분수 변환값 "pitch": pitch, "thread_type": thread_type, "confidence": 0.9, "matched_pattern": pattern, "description": standard_data["description"] } return { "standard": "UNKNOWN", "size": main_nom, "pitch": "", "thread_type": "", "confidence": 0.0, "description": "" } def extract_bolt_dimensions(main_nom: str, description: str) -> Dict: """볼트 치수 정보 추출""" desc_upper = description.upper() actual_bolt_size = main_nom # 실제 BOM 형태: "ORI, 0.75, 145.0000 LG, 300LB, ASTM A193/A194 GR B7/2H, ELEC.GALV" # 첫 번째 숫자가 실제 볼트 사이즈 (접두사 건너뛰기) import re # 설명에서 첫 번째 숫자 추출 (볼트 사이즈) first_number_match = re.search(r'(\d+(?:\.\d+)?)', description) if first_number_match: actual_bolt_size = first_number_match.group(1) # 플랜지 볼트의 경우 실제 볼트 직경을 description에서 추출 if "FLANGE BOLT" in desc_upper or "FLG_BOLT" in desc_upper: # 플랜지 볼트에서 실제 볼트 사이즈 패턴 찾기 # 예: "FLANGE BOLT 6" 150LB M16" → M16 # 예: "FLANGE BOLT 1-1/2" 5/8" x 100mm" → 5/8 bolt_size_patterns = [ r'M(\d+)', # M16, M20 등 메트릭 r'(\d+-\d+/\d+)\s*["\']?\s*X', # 1-1/2" X 등 (복합 분수) r'(\d+/\d+)\s*["\']?\s*X', # 5/8" X, 3/4" X 등 (단순 분수) r'(\d+(?:\.\d+)?)\s*["\']?\s*X', # 0.625" X 등 (소수) r'(\d+-\d+/\d+)\s*["\']?\s*DIA', # 1-1/2" DIA 등 (복합 분수) r'(\d+/\d+)\s*["\']?\s*DIA', # 5/8" DIA 등 (단순 분수) r'(\d+(?:\.\d+)?)\s*["\']?\s*DIA', # 0.625" DIA 등 (소수) r'(\d+-\d+/\d+)\s*["\']?\s*(?:LONG|LG|LENGTH)', # 복합 분수 + 길이 r'(\d+/\d+)\s*["\']?\s*(?:LONG|LG|LENGTH)', # 단순 분수 + 길이 r'(\d+(?:\.\d+)?)\s*["\']?\s*(?:LONG|LG|LENGTH)', # 소수 + 길이 r'(\d+(?:\.\d+)?)\s*MM\s*DIA', # 16MM DIA 등 ] for pattern in bolt_size_patterns: match = re.search(pattern, desc_upper) if match: extracted_size = match.group(1) # M16 같은 메트릭은 M 제거 if pattern.startswith(r'M'): actual_bolt_size = extracted_size else: actual_bolt_size = extracted_size break # 볼트 사이즈를 분수로 변환 (인치인 경우) nominal_size_fraction = actual_bolt_size try: # 소수점 인치를 분수로 변환 if '.' in actual_bolt_size and actual_bolt_size.replace('.', '').isdigit(): nominal_size_fraction = convert_decimal_to_fraction(actual_bolt_size) except: nominal_size_fraction = actual_bolt_size dimensions = { "nominal_size": actual_bolt_size, # 실제 볼트 사이즈 "nominal_size_fraction": nominal_size_fraction, # 분수 변환값 "length": "", "diameter": "", "dimension_description": nominal_size_fraction # 분수로 표시 } # 길이 정보 추출 (개선된 패턴) length_patterns = [ r'(\d+(?:\.\d+)?)\s*LG', # 70.0000 LG, 145.0000 LG 형태 (최우선) r'(\d+(?:\.\d+)?)\s*MM\s*LG', # 70MM LG 형태 r'L\s*(\d+(?:\.\d+)?)\s*MM', r'LENGTH\s*(\d+(?:\.\d+)?)\s*MM', r'(\d+(?:\.\d+)?)\s*MM\s*LONG', r'X\s*(\d+(?:\.\d+)?)\s*MM', # M8 X 20MM 형태 r',\s*(\d+(?:\.\d+)?)\s*LG', # ", 145.0000 LG" 형태 (PSV, LT 볼트용) r',\s*(\d+(?:\.\d+)?)\s+(?:CK|PSV|LT)', # ", 140 CK" 형태 (PSV 볼트용) r'(\d+(?:\.\d+)?)\s*MM(?:\s|,|$)', # 75MM 형태 (단독) r'(\d+(?:\.\d+)?)\s*mm(?:\s|,|$)' # 75mm 형태 (단독) ] for pattern in length_patterns: match = re.search(pattern, desc_upper) if match: length_value = match.group(1) # 소수점 제거 (145.0000 → 145) if '.' in length_value and length_value.endswith('.0000'): length_value = length_value.split('.')[0] elif '.' in length_value and all(c == '0' for c in length_value.split('.')[1]): length_value = length_value.split('.')[0] dimensions["length"] = f"{length_value}mm" break # 지름 정보 (이미 main_nom에 있지만 확인) diameter_patterns = [ r'D\s*(\d+(?:\.\d+)?)\s*MM', r'DIA\s*(\d+(?:\.\d+)?)\s*MM' ] for pattern in diameter_patterns: match = re.search(pattern, desc_upper) if match: dimensions["diameter"] = f"{match.group(1)}mm" break # 치수 설명 조합 (분수 사용) desc_parts = [nominal_size_fraction] if dimensions["length"]: desc_parts.append(f"L{dimensions['length']}") dimensions["dimension_description"] = " ".join(desc_parts) return dimensions def classify_bolt_grade(description: str, thread_result: Dict) -> Dict: """볼트 등급 및 강도 분류""" desc_upper = description.upper() thread_standard = thread_result.get('standard', 'UNKNOWN') if thread_standard == "METRIC": # 미터 나사 등급 (8.8, 10.9, 12.9) grade_patterns = [r'(\d+\.\d+)', r'CLASS\s*(\d+\.\d+)', r'등급\s*(\d+\.\d+)'] for pattern in grade_patterns: match = re.search(pattern, desc_upper) if match: grade = match.group(1) grade_info = BOLT_GRADES["METRIC"].get(grade, {}) return { "grade": f"Grade {grade}", "tensile_strength": grade_info.get("tensile_strength", ""), "yield_strength": grade_info.get("yield_strength", ""), "confidence": 0.9, "standard": "METRIC" } elif thread_standard == "INCH": # 인치 나사 등급 (A307, A325, A490) astm_patterns = [r'ASTM\s*(A\d+)', r'(A\d+)'] for pattern in astm_patterns: match = re.search(pattern, desc_upper) if match: grade = match.group(1) grade_info = BOLT_GRADES["INCH"].get(grade, {}) return { "grade": f"ASTM {grade}", "tensile_strength": grade_info.get("tensile_strength", ""), "yield_strength": grade_info.get("yield_strength", ""), "confidence": 0.9, "standard": "INCH" } return { "grade": "UNKNOWN", "tensile_strength": "", "yield_strength": "", "confidence": 0.0, "standard": "" } def calculate_bolt_confidence(confidence_scores: Dict) -> float: """볼트 분류 전체 신뢰도 계산 (개선된 버전)""" # 기본 점수들 material_conf = confidence_scores.get("material", 0) fastener_type_conf = confidence_scores.get("fastener_type", 0) thread_conf = confidence_scores.get("thread", 0) grade_conf = confidence_scores.get("grade", 0) # 체결재 카테고리 신뢰도 (BOLT인지 확실한가?) fastener_category_conf = confidence_scores.get("fastener_category", 0) # 볼트 확신도 보너스 bolt_certainty_bonus = 0.0 # 1. 체결재 카테고리가 BOLT이고 신뢰도가 높으면 보너스 if fastener_category_conf >= 0.8: bolt_certainty_bonus += 0.2 # 2. 피팅 타입이 명확하게 인식되면 보너스 if fastener_type_conf >= 0.8: bolt_certainty_bonus += 0.1 # 3. ASTM 볼트 재질이 인식되면 큰 보너스 if material_conf >= 0.8: bolt_certainty_bonus += 0.2 elif material_conf >= 0.5: bolt_certainty_bonus += 0.1 # 기본 가중 평균 (재질 비중을 낮추고 체결재 타입 비중 증가) weights = { "fastener_category": 0.3, # 새로 추가 "material": 0.1, # 낮춤 (0.2 -> 0.1) "fastener_type": 0.4, # 유지 "thread": 0.15, # 낮춤 (0.3 -> 0.15) "grade": 0.05 # 낮춤 (0.1 -> 0.05) } weighted_sum = sum( confidence_scores.get(key, 0) * weight for key, weight in weights.items() ) # 최종 신뢰도 = 기본 가중평균 + 보너스 final_confidence = weighted_sum + bolt_certainty_bonus # 최대값 1.0으로 제한 return round(min(final_confidence, 1.0), 2) # ========== 특수 기능들 ========== def extract_bolt_additional_requirements(description: str) -> str: """볼트 설명에서 추가요구사항 추출 (표면처리, 특수 요구사항 등)""" desc_upper = description.upper() additional_reqs = [] # 표면처리 패턴들 surface_treatments = { 'ELEC.GALV': '전기아연도금', 'ELEC GALV': '전기아연도금', 'GALVANIZED': '아연도금', 'GALV': '아연도금', 'HOT DIP GALV': '용융아연도금', 'HDG': '용융아연도금', 'ZINC PLATED': '아연도금', 'ZINC': '아연도금', 'STAINLESS': '스테인리스', 'SS': '스테인리스', 'PASSIVATED': '부동태화', 'ANODIZED': '아노다이징', 'BLACK OXIDE': '흑색산화', 'PHOSPHATE': '인산처리', 'DACROMET': '다크로메트', 'GEOMET': '지오메트' } # 특수 요구사항 패턴들 special_requirements = { 'HEAVY HEX': '중육각', 'FULL THREAD': '전나사', 'PARTIAL THREAD': '부분나사', 'FINE THREAD': '세나사', 'COARSE THREAD': '조나사', 'LEFT HAND': '좌나사', 'RIGHT HAND': '우나사', 'SOCKET HEAD': '소켓헤드', 'BUTTON HEAD': '버튼헤드', 'FLAT HEAD': '평머리', 'PAN HEAD': '팬헤드', 'TRUSS HEAD': '트러스헤드', 'WASHER FACE': '와셔면', 'SERRATED': '톱니형', 'LOCK': '잠금', 'SPRING': '스프링', 'WAVE': '웨이브' } # 표면처리 확인 for pattern, korean in surface_treatments.items(): if pattern in desc_upper: additional_reqs.append(korean) # 특수 요구사항 확인 for pattern, korean in special_requirements.items(): if pattern in desc_upper: additional_reqs.append(korean) # 중복 제거 및 정렬 additional_reqs = list(set(additional_reqs)) return ', '.join(additional_reqs) if additional_reqs else '' def get_bolt_purchase_info(bolt_result: Dict) -> Dict: """볼트 구매 정보 생성""" fastener_category = bolt_result["fastener_category"]["category"] fastener_type = bolt_result["fastener_type"]["type"] thread_standard = bolt_result["thread_specification"]["standard"] grade = bolt_result["grade_strength"]["grade"] # 공급업체 타입 결정 if grade in ["Grade 10.9", "Grade 12.9", "ASTM A325", "ASTM A490"]: supplier_type = "고강도 볼트 전문업체" elif thread_standard == "METRIC": supplier_type = "미터 볼트 업체" elif fastener_type in ["SOCKET_HEAD_CAP", "SET_SCREW"]: supplier_type = "정밀 볼트 업체" else: supplier_type = "일반 볼트 업체" # 납기 추정 if grade in ["Grade 12.9", "ASTM A490"]: lead_time = "4-6주 (고강도 특수품)" elif fastener_type in ["STUD_BOLT", "U_BOLT"]: lead_time = "2-4주 (제작품)" else: lead_time = "1-2주 (재고품)" # 구매 단위 if fastener_category == "WASHER": purchase_unit = "EA (개별)" elif fastener_type == "STUD_BOLT": purchase_unit = "SET (세트)" else: purchase_unit = "EA 또는 BOX" return { "supplier_type": supplier_type, "lead_time_estimate": lead_time, "purchase_category": f"{fastener_type} {thread_standard}", "purchase_unit": purchase_unit, "grade_note": f"{grade} {bolt_result['grade_strength']['tensile_strength']}", "thread_note": f"{thread_standard} {bolt_result['thread_specification']['size']}", "applications": bolt_result["fastener_type"]["applications"] } def is_high_strength_bolt(bolt_result: Dict) -> bool: """고강도 볼트 여부 판단""" grade = bolt_result.get("grade_strength", {}).get("grade", "") high_strength_grades = ["Grade 10.9", "Grade 12.9", "ASTM A325", "ASTM A490"] return any(g in grade for g in high_strength_grades) def is_stainless_bolt(bolt_result: Dict) -> bool: """스테인리스 볼트 여부 판단""" material_type = bolt_result.get("material", {}).get("material_type", "") return material_type == "STAINLESS_STEEL"