Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 서포트 카테고리 UI 개선: 좌우 스크롤, 헤더/본문 동기화, 가운데 정렬 - 동일 항목 합산 기능 구현 (Type + Size + Grade 기준) - 헤더 구조 변경: 압력/스케줄 제거, 구매수량 단일화, User Requirements 추가 - 우레탄 블럭슈 두께 정보(40t, 27t) Material Grade에 포함 - 서포트 수량 계산 수정: 취합된 숫자 그대로 표시 (4의 배수 계산 제거) - 서포트 분류 로직 개선: CLAMP, U-BOLT, URETHANE BLOCK SHOE 등 정확한 분류 - 백엔드 서포트 분류기에 User Requirements 추출 기능 추가 - 엑셀 내보내기에 서포트 카테고리 처리 로직 추가
1252 lines
45 KiB
Python
1252 lines
45 KiB
Python
"""
|
|
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', ''),
|
|
"bolts_per_flange": dimensions_result.get('bolts_per_flange', 1)
|
|
},
|
|
|
|
"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
|
|
|
|
# 플랜지당 볼트 세트 수 추출 (예: (8), (4))
|
|
bolts_per_flange = 1 # 기본값
|
|
flange_bolt_pattern = re.search(r'\((\d+)\)', description)
|
|
if flange_bolt_pattern:
|
|
bolts_per_flange = int(flange_bolt_pattern.group(1))
|
|
|
|
dimensions = {
|
|
"nominal_size": actual_bolt_size, # 실제 볼트 사이즈
|
|
"nominal_size_fraction": nominal_size_fraction, # 분수 변환값
|
|
"length": "",
|
|
"diameter": "",
|
|
"dimension_description": nominal_size_fraction, # 분수로 표시
|
|
"bolts_per_flange": bolts_per_flange # 플랜지당 볼트 세트 수
|
|
}
|
|
|
|
# 길이 정보 추출 (개선된 패턴)
|
|
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'PSV\s+(\d+(?:\.\d+)?)', # PSV 140 형태 (PSV 볼트 전용)
|
|
r'(\d+(?:\.\d+)?)\s+PSV', # 140 PSV 형태 (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"
|