Files
TK-BOM-Project/backend/app/services/gasket_classifier.py
Hyungi Ahn 3dd301cb57 볼트 분류 개선 및 업로드 성능 최적화
- 볼트 길이 추출 로직 개선: '70.0000 LG' 형태 인식 추가
- 재질 중복 표시 수정: 'ASTM A193 ASTM A193 B7' → 'B7'
- A193/A194 등급 추출 로직 개선: 'GR B7/2H' 형태 지원
- bolt_details 테이블에 pressure_rating 컬럼 추가
- 볼트 분류기 오분류 방지: 플랜지/피팅이 볼트로 분류되지 않도록 수정
- 업로드 성능 개선: 키워드 기반 빠른 분류기 선택 로직 추가
- 분류 키워드 대폭 확장: 피팅/파이프/플랜지 키워드 추가
2025-07-18 12:48:24 +09:00

617 lines
22 KiB
Python

"""
GASKET 분류 시스템
플랜지용 가스켓 및 씰링 제품 분류
"""
import re
from typing import Dict, List, Optional
from .material_classifier import classify_material
# ========== 가스켓 타입별 분류 ==========
GASKET_TYPES = {
"SPIRAL_WOUND": {
"dat_file_patterns": ["SWG_", "SPIRAL_"],
"description_keywords": ["SPIRAL WOUND", "SPIRAL", "스파이럴", "SWG"],
"characteristics": "금속 스트립과 필러의 나선형 조합",
"pressure_range": "150LB ~ 2500LB",
"temperature_range": "-200°C ~ 800°C",
"applications": "고온고압, 일반 산업용"
},
"RING_JOINT": {
"dat_file_patterns": ["RTJ_", "RJ_", "RING_"],
"description_keywords": ["RING JOINT", "RTJ", "RING TYPE JOINT", "링조인트"],
"characteristics": "금속 링 형태의 고압용 가스켓",
"pressure_range": "600LB ~ 2500LB",
"temperature_range": "-100°C ~ 650°C",
"applications": "고압 플랜지 전용"
},
"FULL_FACE": {
"dat_file_patterns": ["FF_", "FULL_"],
"description_keywords": ["FULL FACE", "FF", "풀페이스"],
"characteristics": "플랜지 전면 커버 가스켓",
"pressure_range": "150LB ~ 300LB",
"temperature_range": "-50°C ~ 400°C",
"applications": "평면 플랜지용"
},
"RAISED_FACE": {
"dat_file_patterns": ["RF_", "RAISED_"],
"description_keywords": ["RAISED FACE", "RF", "레이즈드"],
"characteristics": "볼록한 면 전용 가스켓",
"pressure_range": "150LB ~ 600LB",
"temperature_range": "-50°C ~ 450°C",
"applications": "일반 볼록면 플랜지용"
},
"O_RING": {
"dat_file_patterns": ["OR_", "ORING_"],
"description_keywords": ["O-RING", "O RING", "ORING", "오링"],
"characteristics": "원형 단면의 씰링 링",
"pressure_range": "저압 ~ 고압 (재질별)",
"temperature_range": "-60°C ~ 300°C (재질별)",
"applications": "홈 씰링, 회전축 씰링"
},
"SHEET_GASKET": {
"dat_file_patterns": ["SHEET_", "SHT_"],
"description_keywords": ["SHEET GASKET", "SHEET", "시트"],
"characteristics": "판 형태의 가스켓",
"pressure_range": "150LB ~ 600LB",
"temperature_range": "-50°C ~ 500°C",
"applications": "일반 플랜지, 맨홀"
},
"KAMMPROFILE": {
"dat_file_patterns": ["KAMM_", "KP_"],
"description_keywords": ["KAMMPROFILE", "KAMM", "캄프로파일"],
"characteristics": "파형 금속에 소프트 코팅",
"pressure_range": "150LB ~ 1500LB",
"temperature_range": "-200°C ~ 700°C",
"applications": "고온고압, 화학공정"
},
"CUSTOM_GASKET": {
"dat_file_patterns": ["CUSTOM_", "SPEC_"],
"description_keywords": ["CUSTOM", "SPECIAL", "특주", "맞춤"],
"characteristics": "특수 제작 가스켓",
"pressure_range": "요구사항별",
"temperature_range": "요구사항별",
"applications": "특수 형상, 특수 조건"
}
}
# ========== 가스켓 재질별 분류 ==========
GASKET_MATERIALS = {
"GRAPHITE": {
"keywords": ["GRAPHITE", "그라파이트", "흑연"],
"characteristics": "고온 내성, 화학 안정성",
"temperature_range": "-200°C ~ 650°C",
"applications": "고온 스팀, 화학공정"
},
"PTFE": {
"keywords": ["PTFE", "TEFLON", "테프론"],
"characteristics": "화학 내성, 낮은 마찰",
"temperature_range": "-200°C ~ 260°C",
"applications": "화학공정, 식품용"
},
"VITON": {
"keywords": ["VITON", "FKM", "바이톤"],
"characteristics": "유류 내성, 고온 내성",
"temperature_range": "-20°C ~ 200°C",
"applications": "유류, 고온 가스"
},
"EPDM": {
"keywords": ["EPDM", "이피디엠"],
"characteristics": "일반 고무, 스팀 내성",
"temperature_range": "-50°C ~ 150°C",
"applications": "스팀, 일반용"
},
"NBR": {
"keywords": ["NBR", "NITRILE", "니트릴"],
"characteristics": "유류 내성",
"temperature_range": "-30°C ~ 100°C",
"applications": "유압, 윤활유"
},
"METAL": {
"keywords": ["METAL", "SS", "STAINLESS", "금속"],
"characteristics": "고온고압 내성",
"temperature_range": "-200°C ~ 800°C",
"applications": "극한 조건"
},
"COMPOSITE": {
"keywords": ["COMPOSITE", "복합재", "FIBER"],
"characteristics": "다층 구조",
"temperature_range": "재질별 상이",
"applications": "특수 조건"
}
}
# ========== 압력 등급별 분류 ==========
GASKET_PRESSURE_RATINGS = {
"patterns": [
r"(\d+)LB",
r"CLASS\s*(\d+)",
r"CL\s*(\d+)",
r"(\d+)#"
],
"standard_ratings": {
"150LB": {"max_pressure": "285 PSI", "typical_gasket": "SHEET, SPIRAL_WOUND"},
"300LB": {"max_pressure": "740 PSI", "typical_gasket": "SPIRAL_WOUND, SHEET"},
"600LB": {"max_pressure": "1480 PSI", "typical_gasket": "SPIRAL_WOUND, RTJ"},
"900LB": {"max_pressure": "2220 PSI", "typical_gasket": "SPIRAL_WOUND, RTJ"},
"1500LB": {"max_pressure": "3705 PSI", "typical_gasket": "RTJ, SPIRAL_WOUND"},
"2500LB": {"max_pressure": "6170 PSI", "typical_gasket": "RTJ"}
}
}
# ========== 사이즈 표기법 ==========
GASKET_SIZE_PATTERNS = {
"flange_size": r"(\d+(?:\.\d+)?)\s*[\"\'']?\s*(?:INCH|IN|인치)?",
"inner_diameter": r"ID\s*(\d+(?:\.\d+)?)",
"outer_diameter": r"OD\s*(\d+(?:\.\d+)?)",
"thickness": r"THK?\s*(\d+(?:\.\d+)?)\s*MM"
}
def classify_gasket(dat_file: str, description: str, main_nom: str, length: float = None) -> Dict:
"""
완전한 GASKET 분류
Args:
dat_file: DAT_FILE 필드
description: DESCRIPTION 필드
main_nom: MAIN_NOM 필드 (플랜지 사이즈)
Returns:
완전한 가스켓 분류 결과
"""
# 1. 재질 분류 (공통 모듈 + 가스켓 전용)
material_result = classify_material(description)
gasket_material_result = classify_gasket_material(description)
# 2. 가스켓 타입 분류
gasket_type_result = classify_gasket_type(dat_file, description)
# 3. 압력 등급 분류
pressure_result = classify_gasket_pressure_rating(dat_file, description)
# 4. 사이즈 정보 추출
size_result = extract_gasket_size_info(main_nom, description)
# 5. 온도 범위 추출
temperature_result = extract_temperature_range(description, gasket_type_result, gasket_material_result)
# 6. 최종 결과 조합
return {
"category": "GASKET",
# 재질 정보 (공통 + 가스켓 전용)
"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)
},
"gasket_material": {
"material": gasket_material_result.get('material', 'UNKNOWN'),
"characteristics": gasket_material_result.get('characteristics', ''),
"temperature_range": gasket_material_result.get('temperature_range', ''),
"confidence": gasket_material_result.get('confidence', 0.0),
"swg_details": gasket_material_result.get('swg_details', {})
},
# 가스켓 분류 정보
"gasket_type": {
"type": gasket_type_result.get('type', 'UNKNOWN'),
"characteristics": gasket_type_result.get('characteristics', ''),
"confidence": gasket_type_result.get('confidence', 0.0),
"evidence": gasket_type_result.get('evidence', []),
"pressure_range": gasket_type_result.get('pressure_range', ''),
"applications": gasket_type_result.get('applications', '')
},
"pressure_rating": {
"rating": pressure_result.get('rating', 'UNKNOWN'),
"confidence": pressure_result.get('confidence', 0.0),
"max_pressure": pressure_result.get('max_pressure', ''),
"typical_gasket": pressure_result.get('typical_gasket', '')
},
"size_info": {
"flange_size": size_result.get('flange_size', main_nom),
"inner_diameter": size_result.get('inner_diameter', ''),
"outer_diameter": size_result.get('outer_diameter', ''),
"thickness": size_result.get('thickness', ''),
"size_description": size_result.get('size_description', main_nom)
},
"temperature_info": {
"range": temperature_result.get('range', ''),
"max_temp": temperature_result.get('max_temp', ''),
"min_temp": temperature_result.get('min_temp', ''),
"confidence": temperature_result.get('confidence', 0.0)
},
# 전체 신뢰도
"overall_confidence": calculate_gasket_confidence({
"gasket_type": gasket_type_result.get('confidence', 0),
"gasket_material": gasket_material_result.get('confidence', 0),
"pressure": pressure_result.get('confidence', 0),
"size": size_result.get('confidence', 0.8) # 기본 신뢰도
})
}
def classify_gasket_type(dat_file: str, description: str) -> Dict:
"""가스켓 타입 분류"""
dat_upper = dat_file.upper()
desc_upper = description.upper()
# 1. DAT_FILE 패턴으로 1차 분류
for gasket_type, type_data in GASKET_TYPES.items():
for pattern in type_data["dat_file_patterns"]:
if pattern in dat_upper:
return {
"type": gasket_type,
"characteristics": type_data["characteristics"],
"confidence": 0.95,
"evidence": [f"DAT_FILE_PATTERN: {pattern}"],
"pressure_range": type_data["pressure_range"],
"temperature_range": type_data["temperature_range"],
"applications": type_data["applications"]
}
# 2. DESCRIPTION 키워드로 2차 분류
for gasket_type, type_data in GASKET_TYPES.items():
for keyword in type_data["description_keywords"]:
if keyword in desc_upper:
return {
"type": gasket_type,
"characteristics": type_data["characteristics"],
"confidence": 0.85,
"evidence": [f"DESCRIPTION_KEYWORD: {keyword}"],
"pressure_range": type_data["pressure_range"],
"temperature_range": type_data["temperature_range"],
"applications": type_data["applications"]
}
# 3. 분류 실패
return {
"type": "UNKNOWN",
"characteristics": "",
"confidence": 0.0,
"evidence": ["NO_GASKET_TYPE_IDENTIFIED"],
"pressure_range": "",
"temperature_range": "",
"applications": ""
}
def parse_swg_details(description: str) -> Dict:
"""SWG (Spiral Wound Gasket) 상세 정보 파싱"""
desc_upper = description.upper()
result = {
"face_type": "UNKNOWN",
"outer_ring": "UNKNOWN",
"filler": "UNKNOWN",
"inner_ring": "UNKNOWN",
"thickness": None,
"detailed_construction": "",
"confidence": 0.0
}
# H/F/I/O 패턴 파싱 (Head/Face/Inner/Outer)
hfio_pattern = r'H/F/I/O|HFIO'
if re.search(hfio_pattern, desc_upper):
result["face_type"] = "H/F/I/O"
result["confidence"] += 0.3
# 재질 구성 파싱 (SS304/GRAPHITE/CS/CS)
# H/F/I/O 다음에 나오는 재질 구성을 찾음
material_pattern = r'H/F/I/O\s+([A-Z0-9]+)/([A-Z]+)/([A-Z0-9]+)/([A-Z0-9]+)'
material_match = re.search(material_pattern, desc_upper)
if material_match:
result["outer_ring"] = material_match.group(1) # SS304
result["filler"] = material_match.group(2) # GRAPHITE
result["inner_ring"] = material_match.group(3) # CS
# 네 번째는 보통 outer ring 반복
result["detailed_construction"] = f"{material_match.group(1)}/{material_match.group(2)}/{material_match.group(3)}/{material_match.group(4)}"
result["confidence"] += 0.4
else:
# H/F/I/O 없이 재질만 있는 경우
material_pattern_simple = r'([A-Z0-9]+)/([A-Z]+)/([A-Z0-9]+)/([A-Z0-9]+)'
material_match = re.search(material_pattern_simple, desc_upper)
if material_match:
result["outer_ring"] = material_match.group(1)
result["filler"] = material_match.group(2)
result["inner_ring"] = material_match.group(3)
result["detailed_construction"] = f"{material_match.group(1)}/{material_match.group(2)}/{material_match.group(3)}/{material_match.group(4)}"
result["confidence"] += 0.3
# 두께 파싱 (4.5mm)
thickness_pattern = r'(\d+(?:\.\d+)?)\s*MM'
thickness_match = re.search(thickness_pattern, desc_upper)
if thickness_match:
result["thickness"] = float(thickness_match.group(1))
result["confidence"] += 0.3
return result
def classify_gasket_material(description: str) -> Dict:
"""가스켓 전용 재질 분류 (SWG 상세 정보 포함)"""
desc_upper = description.upper()
# SWG 상세 정보 파싱
swg_details = None
if "SWG" in desc_upper or "SPIRAL WOUND" in desc_upper:
swg_details = parse_swg_details(description)
# 기본 가스켓 재질 확인
for material_type, material_data in GASKET_MATERIALS.items():
for keyword in material_data["keywords"]:
if keyword in desc_upper:
result = {
"material": material_type,
"characteristics": material_data["characteristics"],
"temperature_range": material_data["temperature_range"],
"confidence": 0.9,
"matched_keyword": keyword,
"applications": material_data["applications"]
}
# SWG 상세 정보 추가
if swg_details and swg_details["confidence"] > 0:
result["swg_details"] = swg_details
result["confidence"] = min(0.95, result["confidence"] + swg_details["confidence"] * 0.1)
return result
# 일반 재질 키워드 확인
if any(keyword in desc_upper for keyword in ["RUBBER", "고무"]):
return {
"material": "RUBBER",
"characteristics": "일반 고무계",
"temperature_range": "-50°C ~ 100°C",
"confidence": 0.7,
"matched_keyword": "RUBBER",
"applications": "일반용"
}
return {
"material": "UNKNOWN",
"characteristics": "",
"temperature_range": "",
"confidence": 0.0,
"matched_keyword": "",
"applications": ""
}
def classify_gasket_pressure_rating(dat_file: str, description: str) -> Dict:
"""가스켓 압력 등급 분류"""
combined_text = f"{dat_file} {description}".upper()
# 패턴 매칭으로 압력 등급 추출
for pattern in GASKET_PRESSURE_RATINGS["patterns"]:
match = re.search(pattern, combined_text)
if match:
rating_num = match.group(1)
rating = f"{rating_num}LB"
# 표준 등급 정보 확인
rating_info = GASKET_PRESSURE_RATINGS["standard_ratings"].get(rating, {})
if rating_info:
confidence = 0.95
else:
confidence = 0.8
rating_info = {
"max_pressure": "확인 필요",
"typical_gasket": "확인 필요"
}
return {
"rating": rating,
"confidence": confidence,
"matched_pattern": pattern,
"matched_value": rating_num,
"max_pressure": rating_info.get("max_pressure", ""),
"typical_gasket": rating_info.get("typical_gasket", "")
}
return {
"rating": "UNKNOWN",
"confidence": 0.0,
"matched_pattern": "",
"max_pressure": "",
"typical_gasket": ""
}
def extract_gasket_size_info(main_nom: str, description: str) -> Dict:
"""가스켓 사이즈 정보 추출"""
desc_upper = description.upper()
size_info = {
"flange_size": main_nom,
"inner_diameter": "",
"outer_diameter": "",
"thickness": "",
"size_description": main_nom,
"confidence": 0.8
}
# 내경(ID) 추출
id_match = re.search(GASKET_SIZE_PATTERNS["inner_diameter"], desc_upper)
if id_match:
size_info["inner_diameter"] = f"{id_match.group(1)}mm"
# 외경(OD) 추출
od_match = re.search(GASKET_SIZE_PATTERNS["outer_diameter"], desc_upper)
if od_match:
size_info["outer_diameter"] = f"{od_match.group(1)}mm"
# 두께(THK) 추출
thk_match = re.search(GASKET_SIZE_PATTERNS["thickness"], desc_upper)
if thk_match:
size_info["thickness"] = f"{thk_match.group(1)}mm"
# 사이즈 설명 조합
size_parts = [main_nom]
if size_info["inner_diameter"] and size_info["outer_diameter"]:
size_parts.append(f"ID{size_info['inner_diameter']}")
size_parts.append(f"OD{size_info['outer_diameter']}")
if size_info["thickness"]:
size_parts.append(f"THK{size_info['thickness']}")
size_info["size_description"] = " ".join(size_parts)
return size_info
def extract_temperature_range(description: str, gasket_type_result: Dict,
gasket_material_result: Dict) -> Dict:
"""온도 범위 정보 추출"""
desc_upper = description.upper()
# DESCRIPTION에서 직접 온도 추출
temp_patterns = [
r'(\-?\d+(?:\.\d+)?)\s*°?C\s*~\s*(\-?\d+(?:\.\d+)?)\s*°?C',
r'(\-?\d+(?:\.\d+)?)\s*TO\s*(\-?\d+(?:\.\d+)?)\s*°?C',
r'MAX\s*(\-?\d+(?:\.\d+)?)\s*°?C',
r'MIN\s*(\-?\d+(?:\.\d+)?)\s*°?C'
]
for pattern in temp_patterns:
match = re.search(pattern, desc_upper)
if match:
if len(match.groups()) == 2: # 범위
return {
"range": f"{match.group(1)}°C ~ {match.group(2)}°C",
"min_temp": f"{match.group(1)}°C",
"max_temp": f"{match.group(2)}°C",
"confidence": 0.95,
"source": "DESCRIPTION_RANGE"
}
else: # 단일 온도
temp_value = match.group(1)
if "MAX" in pattern:
return {
"range": f"~ {temp_value}°C",
"max_temp": f"{temp_value}°C",
"confidence": 0.9,
"source": "DESCRIPTION_MAX"
}
# 가스켓 재질 기반 온도 범위
material_temp = gasket_material_result.get('temperature_range', '')
if material_temp:
return {
"range": material_temp,
"confidence": 0.8,
"source": "MATERIAL_BASED"
}
# 가스켓 타입 기반 온도 범위
type_temp = gasket_type_result.get('temperature_range', '')
if type_temp:
return {
"range": type_temp,
"confidence": 0.7,
"source": "TYPE_BASED"
}
return {
"range": "",
"confidence": 0.0,
"source": "NO_TEMPERATURE_INFO"
}
def calculate_gasket_confidence(confidence_scores: Dict) -> float:
"""가스켓 분류 전체 신뢰도 계산"""
scores = [score for score in confidence_scores.values() if score > 0]
if not scores:
return 0.0
# 가중 평균
weights = {
"gasket_type": 0.4,
"gasket_material": 0.3,
"pressure": 0.2,
"size": 0.1
}
weighted_sum = sum(
confidence_scores.get(key, 0) * weight
for key, weight in weights.items()
)
return round(weighted_sum, 2)
# ========== 특수 기능들 ==========
def get_gasket_purchase_info(gasket_result: Dict) -> Dict:
"""가스켓 구매 정보 생성"""
gasket_type = gasket_result["gasket_type"]["type"]
gasket_material = gasket_result["gasket_material"]["material"]
pressure = gasket_result["pressure_rating"]["rating"]
# 공급업체 타입 결정
if gasket_type == "CUSTOM_GASKET":
supplier_type = "특수 가스켓 제작업체"
elif gasket_material in ["GRAPHITE", "PTFE"]:
supplier_type = "고급 씰링 전문업체"
elif gasket_type in ["SPIRAL_WOUND", "RING_JOINT"]:
supplier_type = "산업용 가스켓 전문업체"
else:
supplier_type = "일반 가스켓 업체"
# 납기 추정
if gasket_type == "CUSTOM_GASKET":
lead_time = "4-8주 (특수 제작)"
elif gasket_type in ["SPIRAL_WOUND", "KAMMPROFILE"]:
lead_time = "2-4주 (제작품)"
else:
lead_time = "1-2주 (재고품)"
# 구매 단위
if gasket_type == "O_RING":
purchase_unit = "EA (개별)"
elif gasket_type == "SHEET_GASKET":
purchase_unit = "SHEET (시트)"
else:
purchase_unit = "SET (세트)"
return {
"supplier_type": supplier_type,
"lead_time_estimate": lead_time,
"purchase_category": f"{gasket_type} {pressure}",
"purchase_unit": purchase_unit,
"material_note": gasket_result["gasket_material"]["characteristics"],
"temperature_note": gasket_result["temperature_info"]["range"],
"applications": gasket_result["gasket_type"]["applications"]
}
def is_high_temperature_gasket(gasket_result: Dict) -> bool:
"""고온용 가스켓 여부 판단"""
temp_range = gasket_result.get("temperature_info", {}).get("range", "")
return any(indicator in temp_range for indicator in ["500°C", "600°C", "700°C", "800°C"])
def is_high_pressure_gasket(gasket_result: Dict) -> bool:
"""고압용 가스켓 여부 판단"""
pressure_rating = gasket_result.get("pressure_rating", {}).get("rating", "")
high_pressure_ratings = ["600LB", "900LB", "1500LB", "2500LB"]
return any(pressure in pressure_rating for pressure in high_pressure_ratings)