🎯 주요 성과: - 8개 주요 자재군 완전 분류 시스템 구축 - 재질 분류 엔진 + 개별 자재별 특화 분류 - 스풀 관리 시스템 (파이프 절단 계획용) - 실제 BOM 데이터 기반 설계 및 테스트 📁 새로 추가된 자재 분류 시스템: - app/services/pipe_classifier.py (파이프 + 스풀 관리) - app/services/fitting_classifier.py (피팅 10가지 타입) - app/services/flange_classifier.py (플랜지 SPECIAL/STANDARD) - app/services/valve_classifier.py (밸브 단조/주조 구분) - app/services/gasket_classifier.py (가스켓 8가지 타입) - app/services/bolt_classifier.py (볼트/너트/와셔 통합) - app/services/instrument_classifier.py (계기류 기본) 🔧 분류 성능: - PIPE: 제조방법, 끝가공, 스케줄, 절단계획 - FITTING: 타입, 연결방식, 압력등급, 제작방법 - FLANGE: SPECIAL(10종)/STANDARD(6종), 면가공 - VALVE: 9가지 타입, 단조/주조 구분, 작동방식 - GASKET: 8가지 타입, 재질별, 온도/압력 범위 - BOLT: 체결재 3종, 나사규격, 강도등급 📊 기술적 특징: - 정규표현식 기반 패턴 매칭 엔진 - 신뢰도 점수 시스템 (0.0-1.0) - 증거 기반 분류 추적 (evidence tracking) - 모듈화 구조로 재사용성 극대화 - 실제 DAT_FILE + DESCRIPTION 패턴 분석 🎯 분류 커버리지: - 재질: ASTM/ASME 표준 + 특수합금 (INCONEL, TITANIUM) - 제작방법: FORGED, CAST, SEAMLESS, WELDED 자동 판단 - 압력등급: 150LB ~ 9000LB 전 범위 - 연결방식: BW, SW, THD, FL 등 모든 방식 - 사이즈: 1/8" ~ 48" 전 범위 💾 데이터 통합: - 기존 materials 테이블과 완전 호환 - 프로젝트/도면 정보 자동 연결 - 스풀 정보 사용자 입력 대기 (파이프만) - 구매 정보 자동 생성 (공급업체, 납기) 🧪 테스트 완료: - 각 시스템별 10+ 테스트 케이스 - 실제 BOM 데이터 기반 검증 - 예외 상황 처리 테스트 - 신뢰도 검증 완료 Version: v2.0 (Major Release) Date: 2024-07-15 Author: hyungiahn Breaking Changes: 새로운 분류 시스템 추가 (기존 호환성 유지) Next Phase: files.py 통합 및 웹 인터페이스 연동
594 lines
21 KiB
Python
594 lines
21 KiB
Python
"""
|
|
BOLT 분류 시스템
|
|
볼트, 너트, 와셔, 스터드 등 체결용 부품 분류
|
|
"""
|
|
|
|
import re
|
|
from typing import Dict, List, Optional
|
|
from .material_classifier import classify_material
|
|
|
|
# ========== 볼트 타입별 분류 ==========
|
|
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"],
|
|
"description_keywords": ["STUD BOLT", "STUD", "스터드볼트", "전나사"],
|
|
"characteristics": "양끝 나사 스터드",
|
|
"applications": "플랜지 체결용",
|
|
"head_type": "NONE"
|
|
},
|
|
|
|
"FLANGE_BOLT": {
|
|
"dat_file_patterns": ["FLG_BOLT", "FLANGE_BOLT"],
|
|
"description_keywords": ["FLANGE BOLT", "플랜지볼트"],
|
|
"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) -> Dict:
|
|
"""
|
|
완전한 BOLT 분류
|
|
|
|
Args:
|
|
dat_file: DAT_FILE 필드
|
|
description: DESCRIPTION 필드
|
|
main_nom: MAIN_NOM 필드 (나사 사이즈)
|
|
|
|
Returns:
|
|
완전한 볼트 분류 결과
|
|
"""
|
|
|
|
# 1. 재질 분류 (공통 모듈 사용)
|
|
material_result = classify_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", "볼트", "나사", "스크류"]
|
|
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"]
|
|
}
|
|
|
|
# 기본값: BOLT
|
|
return {
|
|
"category": "BOLT",
|
|
"confidence": 0.6,
|
|
"evidence": ["DEFAULT_BOLT"]
|
|
}
|
|
|
|
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'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:
|
|
"""볼트 분류 전체 신뢰도 계산"""
|
|
|
|
scores = [score for score in confidence_scores.values() if score > 0]
|
|
|
|
if not scores:
|
|
return 0.0
|
|
|
|
# 가중 평균
|
|
weights = {
|
|
"material": 0.2,
|
|
"fastener_type": 0.4,
|
|
"thread": 0.3,
|
|
"grade": 0.1
|
|
}
|
|
|
|
weighted_sum = sum(
|
|
confidence_scores.get(key, 0) * weight
|
|
for key, weight in weights.items()
|
|
)
|
|
|
|
return round(weighted_sum, 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"
|