- 실제 볼트 사이즈 추출: 설명의 첫 번째 숫자를 실제 볼트 직경으로 사용 - 분수 표기 변환: 0.625 → 5/8, 0.75 → 3/4 등 현장 친화적 표기 - 특수 용도 볼트 분류: PSV(압력안전밸브), LT(저온용), CK(체크밸브), ORI(오리피스) - 표면처리 정보 추출: ELEC.GALV, HOT DIP GALV 등 코팅 정보 - 복합 재질 규격 파싱: ASTM A193/A194 GR B7/2H 정확 분류 - 특수 용도별 색상 구분: PSV 빨강, LT 주황, CK 파랑, ORI 보라 - 프론트엔드 표시 개선: 분수 사이즈, 특수 용도 현황 별도 섹션 - inch 기호 제거: 깔끔한 분수 표시로 현장 가독성 향상
This commit is contained in:
@@ -751,10 +751,14 @@ async def upload_file(
|
||||
else:
|
||||
thread_type = str(thread_spec_info) if thread_spec_info else "UNKNOWN"
|
||||
|
||||
# 치수 정보
|
||||
diameter = material_data.get("main_nom", "")
|
||||
# 치수 정보 (실제 볼트 사이즈 사용)
|
||||
diameter = ""
|
||||
length = ""
|
||||
nominal_size_fraction = ""
|
||||
if isinstance(dimensions_info, dict):
|
||||
# 볼트 분류기에서 추출한 실제 볼트 사이즈 사용
|
||||
diameter = dimensions_info.get("nominal_size", material_data.get("main_nom", ""))
|
||||
nominal_size_fraction = dimensions_info.get("nominal_size_fraction", diameter)
|
||||
length = dimensions_info.get("length", "")
|
||||
if not length and "70.0000 LG" in description:
|
||||
# 원본 설명에서 길이 추출
|
||||
|
||||
@@ -12,6 +12,30 @@ def classify_bolt_material(description: str) -> Dict:
|
||||
|
||||
desc_upper = description.upper()
|
||||
|
||||
# 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 형태도 지원)
|
||||
@@ -153,13 +177,49 @@ BOLT_TYPES = {
|
||||
},
|
||||
|
||||
"FLANGE_BOLT": {
|
||||
"dat_file_patterns": ["FLG_BOLT", "FLANGE_BOLT", "BLT_150", "BLT_300", "BLT_600"],
|
||||
"description_keywords": ["FLANGE BOLT", "플랜지볼트", "150LB", "300LB", "600LB"],
|
||||
"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", "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", "머신스크류", "기계나사"],
|
||||
@@ -272,11 +332,17 @@ THREAD_STANDARDS = {
|
||||
},
|
||||
|
||||
"INCH": {
|
||||
"patterns": [r"(\d+(?:/\d+)?)\s*[\"\']\s*UNC", r"(\d+(?:/\d+)?)\s*[\"\']\s*UNF",
|
||||
r"(\d+(?:/\d+)?)\s*[\"\']-(\d+)"],
|
||||
"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\"", "3/4\"", "7/8\"", "1\""]
|
||||
"common_sizes": ["1/4\"", "5/16\"", "3/8\"", "1/2\"", "5/8\"", "0.625\"", "3/4\"", "7/8\"", "1\""]
|
||||
},
|
||||
|
||||
"BSW": {
|
||||
@@ -302,6 +368,203 @@ BOLT_GRADES = {
|
||||
}
|
||||
}
|
||||
|
||||
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\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 분류
|
||||
@@ -337,7 +600,13 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option
|
||||
# 6. 등급 및 강도 분류
|
||||
grade_result = classify_bolt_grade(description, thread_result)
|
||||
|
||||
# 7. 최종 결과 조합
|
||||
# 7. 특수 용도 볼트 분류 (PSV, LT, CK)
|
||||
special_result = classify_special_application_bolts(description)
|
||||
|
||||
# 8. 표면처리 분류 (ELEC.GALV 등)
|
||||
surface_result = classify_surface_treatment(description)
|
||||
|
||||
# 9. 최종 결과 조합
|
||||
return {
|
||||
"category": "BOLT",
|
||||
|
||||
@@ -367,6 +636,7 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option
|
||||
"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)
|
||||
@@ -374,6 +644,7 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option
|
||||
|
||||
"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', '')
|
||||
@@ -386,6 +657,23 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option
|
||||
"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),
|
||||
@@ -536,9 +824,19 @@ def classify_thread_specification(main_nom: str, description: str) -> Dict:
|
||||
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": size, # 원래 값
|
||||
"size_fraction": size_fraction, # 분수 변환값
|
||||
"pitch": pitch,
|
||||
"thread_type": thread_type,
|
||||
"confidence": 0.9,
|
||||
@@ -559,12 +857,62 @@ 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": main_nom,
|
||||
"nominal_size": actual_bolt_size, # 실제 볼트 사이즈
|
||||
"nominal_size_fraction": nominal_size_fraction, # 분수 변환값
|
||||
"length": "",
|
||||
"diameter": "",
|
||||
"dimension_description": main_nom
|
||||
"dimension_description": nominal_size_fraction # 분수로 표시
|
||||
}
|
||||
|
||||
# 길이 정보 추출
|
||||
@@ -595,8 +943,8 @@ def extract_bolt_dimensions(main_nom: str, description: str) -> Dict:
|
||||
dimensions["diameter"] = f"{match.group(1)}mm"
|
||||
break
|
||||
|
||||
# 치수 설명 조합
|
||||
desc_parts = [main_nom]
|
||||
# 치수 설명 조합 (분수 사용)
|
||||
desc_parts = [nominal_size_fraction]
|
||||
if dimensions["length"]:
|
||||
desc_parts.append(f"L{dimensions['length']}")
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Dict, List, Optional, Tuple
|
||||
|
||||
# Level 1: 명확한 타입 키워드 (최우선)
|
||||
LEVEL1_TYPE_KEYWORDS = {
|
||||
"BOLT": ["BOLT", "STUD", "NUT", "SCREW", "WASHER", "볼트", "너트", "스터드", "나사", "와셔"],
|
||||
"BOLT": ["FLANGE BOLT", "BOLT", "STUD", "NUT", "SCREW", "WASHER", "볼트", "너트", "스터드", "나사", "와셔"],
|
||||
"VALVE": ["VALVE", "GATE", "BALL", "GLOBE", "CHECK", "BUTTERFLY", "NEEDLE", "RELIEF", "밸브", "게이트", "볼", "글로브", "체크", "버터플라이", "니들", "릴리프"],
|
||||
"FLANGE": ["FLG", "FLANGE", "플랜지", "프랜지", "ORIFICE", "SPECTACLE", "PADDLE", "SPACER", "BLIND"],
|
||||
"PIPE": ["PIPE", "TUBE", "파이프", "배관", "SMLS", "SEAMLESS"],
|
||||
@@ -87,7 +87,9 @@ def classify_material_integrated(description: str, main_nom: str = "",
|
||||
detected_types = []
|
||||
for material_type, keywords in LEVEL1_TYPE_KEYWORDS.items():
|
||||
type_found = False
|
||||
for keyword in keywords:
|
||||
# 긴 키워드부터 확인 (FLANGE BOLT가 FLANGE보다 먼저 매칭되도록)
|
||||
sorted_keywords = sorted(keywords, key=len, reverse=True)
|
||||
for keyword in sorted_keywords:
|
||||
# 전체 문자열에서 찾기
|
||||
if keyword in desc_upper:
|
||||
detected_types.append((material_type, keyword))
|
||||
@@ -117,8 +119,8 @@ def classify_material_integrated(description: str, main_nom: str = "",
|
||||
}
|
||||
|
||||
# Level 2 키워드가 없으면 우선순위로 결정
|
||||
# FITTING > VALVE > FLANGE > PIPE > BOLT (더 구체적인 것 우선)
|
||||
type_priority = ["FITTING", "VALVE", "FLANGE", "PIPE", "BOLT", "GASKET", "INSTRUMENT"]
|
||||
# BOLT > FITTING > VALVE > FLANGE > PIPE (볼트 우선, 더 구체적인 것 우선)
|
||||
type_priority = ["BOLT", "FITTING", "VALVE", "FLANGE", "PIPE", "GASKET", "INSTRUMENT"]
|
||||
for priority_type in type_priority:
|
||||
for detected_type, keyword in detected_types:
|
||||
if detected_type == priority_type:
|
||||
|
||||
@@ -224,6 +224,9 @@ def generate_purchase_items_from_materials(db: Session, file_id: int,
|
||||
'specification': spec_data.get('full_spec', spec_key),
|
||||
'material_spec': spec_data.get('material_spec', ''),
|
||||
'size_spec': spec_data.get('size_display', ''),
|
||||
'size_fraction': spec_data.get('size_fraction', ''),
|
||||
'surface_treatment': spec_data.get('surface_treatment', ''),
|
||||
'special_applications': spec_data.get('special_applications', {}),
|
||||
'unit': spec_data.get('unit', 'EA'),
|
||||
**calc_result,
|
||||
'job_no': job_no,
|
||||
@@ -310,16 +313,46 @@ def generate_material_specs_for_category(materials: List[Dict], category: str) -
|
||||
diameter = material.get('diameter', material.get('main_nom', ''))
|
||||
material_spec = material_standard or material.get('material_grade', '')
|
||||
|
||||
# 분수 사이즈 정보 추출 (새로 추가된 분류기 정보)
|
||||
size_fraction = material.get('size_fraction', diameter)
|
||||
surface_treatment = material.get('surface_treatment', '')
|
||||
|
||||
# 특수 용도 정보 추출 (PSV, LT, CK)
|
||||
special_applications = {
|
||||
'PSV': 0,
|
||||
'LT': 0,
|
||||
'CK': 0
|
||||
}
|
||||
|
||||
# 설명에서 특수 용도 키워드 확인 (간단한 방법)
|
||||
description = material.get('original_description', '').upper()
|
||||
if 'PSV' in description or 'PRESSURE SAFETY VALVE' in description:
|
||||
special_applications['PSV'] = material.get('quantity', 0)
|
||||
if any(keyword in description for keyword in ['LT', 'LOW TEMP', '저온용']):
|
||||
special_applications['LT'] = material.get('quantity', 0)
|
||||
if 'CK' in description or 'CHECK VALVE' in description:
|
||||
special_applications['CK'] = material.get('quantity', 0)
|
||||
|
||||
spec_parts = [bolt_type.replace('_', ' ')]
|
||||
if material_standard: spec_parts.append(material_standard)
|
||||
full_spec = ', '.join(spec_parts)
|
||||
|
||||
spec_key = f"BOLT|{full_spec}|{material_spec}|{diameter}"
|
||||
# 특수 용도와 관계없이 사이즈+길이로 합산 (구매는 동일하므로)
|
||||
# 길이 정보가 있으면 포함
|
||||
length_info = material.get('length', '')
|
||||
if length_info:
|
||||
diameter_key = f"{diameter}L{length_info}"
|
||||
else:
|
||||
diameter_key = diameter
|
||||
|
||||
spec_key = f"BOLT|{full_spec}|{material_spec}|{diameter_key}"
|
||||
spec_data = {
|
||||
'category': 'BOLT',
|
||||
'full_spec': full_spec,
|
||||
'material_spec': material_spec,
|
||||
'size_display': diameter,
|
||||
'size_fraction': size_fraction,
|
||||
'surface_treatment': surface_treatment,
|
||||
'unit': 'EA'
|
||||
}
|
||||
|
||||
@@ -378,12 +411,18 @@ def generate_material_specs_for_category(materials: List[Dict], category: str) -
|
||||
**spec_data,
|
||||
'totalQuantity': 0,
|
||||
'count': 0,
|
||||
'items': []
|
||||
'items': [],
|
||||
'special_applications': {'PSV': 0, 'LT': 0, 'CK': 0} if category == 'BOLT' else None
|
||||
}
|
||||
|
||||
specs[spec_key]['totalQuantity'] += material.get('quantity', 0)
|
||||
specs[spec_key]['count'] += 1
|
||||
specs[spec_key]['items'].append(material)
|
||||
|
||||
# 볼트의 경우 특수 용도 정보 누적
|
||||
if category == 'BOLT' and 'special_applications' in locals():
|
||||
for app_type, count in special_applications.items():
|
||||
specs[spec_key]['special_applications'][app_type] += count
|
||||
|
||||
return specs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user