""" 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() # 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"] } # 기본 재질 분류기 호출 (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", "BLT_150", "BLT_300", "BLT_600"], "description_keywords": ["FLANGE BOLT", "플랜지볼트", "150LB", "300LB", "600LB"], "characteristics": "플랜지 전용 볼트", "applications": "플랜지 체결 전용", "head_type": "HEXAGON" }, "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", r"(\d+(?:/\d+)?)\s*[\"\']\s*UNF", r"(\d+(?:/\d+)?)\s*[\"\']-(\d+)"], "description": "인치 나사", "thread_types": ["UNC", "UNF"], "common_sizes": ["1/4\"", "5/16\"", "3/8\"", "1/2\"", "5/8\"", "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 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. 최종 결과 조합 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', ''), "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), "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) }, # 전체 신뢰도 "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 return { "standard": standard, "size": size, "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() dimensions = { "nominal_size": main_nom, "length": "", "diameter": "", "dimension_description": main_nom } # 길이 정보 추출 length_patterns = [ r'(\d+(?:\.\d+)?)\s*LG', # 70.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 형태 ] for pattern in length_patterns: match = re.search(pattern, desc_upper) if match: dimensions["length"] = f"{match.group(1)}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 = [main_nom] 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 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"