diff --git a/backend/app/services/bolt_classifier.py b/backend/app/services/bolt_classifier.py new file mode 100644 index 0000000..6128d49 --- /dev/null +++ b/backend/app/services/bolt_classifier.py @@ -0,0 +1,593 @@ +""" +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" diff --git a/backend/app/services/gasket_classifier.py b/backend/app/services/gasket_classifier.py new file mode 100644 index 0000000..ca9f0d8 --- /dev/null +++ b/backend/app/services/gasket_classifier.py @@ -0,0 +1,552 @@ +""" +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) -> 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) + }, + + # 가스켓 분류 정보 + "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 classify_gasket_material(description: str) -> Dict: + """가스켓 전용 재질 분류""" + + desc_upper = description.upper() + + # 가스켓 전용 재질 확인 + for material_type, material_data in GASKET_MATERIALS.items(): + for keyword in material_data["keywords"]: + if keyword in desc_upper: + return { + "material": material_type, + "characteristics": material_data["characteristics"], + "temperature_range": material_data["temperature_range"], + "confidence": 0.9, + "matched_keyword": keyword, + "applications": material_data["applications"] + } + + # 일반 재질 키워드 확인 + 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) diff --git a/backend/app/services/instrument_classifier.py b/backend/app/services/instrument_classifier.py new file mode 100644 index 0000000..d95394d --- /dev/null +++ b/backend/app/services/instrument_classifier.py @@ -0,0 +1,190 @@ +""" +INSTRUMENT 분류 시스템 (간단 버전) +완제품 구매용 기본 분류만 +""" + +import re +from typing import Dict, List, Optional +from .material_classifier import classify_material + +# ========== 기본 계기 타입 ========== +INSTRUMENT_TYPES = { + "PRESSURE_GAUGE": { + "dat_file_patterns": ["PG_", "PRESS_G"], + "description_keywords": ["PRESSURE GAUGE", "압력계", "PG"], + "characteristics": "압력 측정용 게이지" + }, + "TEMPERATURE_GAUGE": { + "dat_file_patterns": ["TG_", "TEMP_G"], + "description_keywords": ["TEMPERATURE GAUGE", "온도계", "TG", "THERMOMETER"], + "characteristics": "온도 측정용 게이지" + }, + "FLOW_METER": { + "dat_file_patterns": ["FM_", "FLOW_"], + "description_keywords": ["FLOW METER", "유량계", "FM"], + "characteristics": "유량 측정용" + }, + "LEVEL_GAUGE": { + "dat_file_patterns": ["LG_", "LEVEL_"], + "description_keywords": ["LEVEL GAUGE", "액위계", "LG", "SIGHT GLASS"], + "characteristics": "액위 측정용" + }, + "TRANSMITTER": { + "dat_file_patterns": ["PT_", "TT_", "FT_", "LT_"], + "description_keywords": ["TRANSMITTER", "트랜스미터", "4-20MA"], + "characteristics": "신호 전송용" + }, + "INDICATOR": { + "dat_file_patterns": ["PI_", "TI_", "FI_", "LI_"], + "description_keywords": ["INDICATOR", "지시계", "DISPLAY"], + "characteristics": "표시용" + }, + "SPECIAL_INSTRUMENT": { + "dat_file_patterns": ["INST_", "SPEC_"], + "description_keywords": ["THERMOWELL", "ORIFICE PLATE", "MANOMETER", "ROTAMETER"], + "characteristics": "특수 계기류" + } +} + +def classify_instrument(dat_file: str, description: str, main_nom: str) -> Dict: + """ + 간단한 INSTRUMENT 분류 + + Args: + dat_file: DAT_FILE 필드 + description: DESCRIPTION 필드 + main_nom: MAIN_NOM 필드 (연결 사이즈) + + Returns: + 간단한 계기 분류 결과 + """ + + # 1. 재질 분류 (공통 모듈) + material_result = classify_material(description) + + # 2. 계기 타입 분류 + instrument_type_result = classify_instrument_type(dat_file, description) + + # 3. 측정 범위 추출 (있다면) + measurement_range = extract_measurement_range(description) + + # 4. 최종 결과 + return { + "category": "INSTRUMENT", + + # 재질 정보 + "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) + }, + + # 계기 정보 + "instrument_type": { + "type": instrument_type_result.get('type', 'UNKNOWN'), + "characteristics": instrument_type_result.get('characteristics', ''), + "confidence": instrument_type_result.get('confidence', 0.0) + }, + + "measurement_info": { + "range": measurement_range.get('range', ''), + "unit": measurement_range.get('unit', ''), + "signal_type": measurement_range.get('signal_type', '') + }, + + "size_info": { + "connection_size": main_nom, + "size_description": main_nom + }, + + "purchase_info": { + "category": "완제품 구매", + "supplier_type": "계기 전문업체", + "lead_time": "2-4주", + "note": "사양서 확인 후 주문" + }, + + # 간단한 신뢰도 + "overall_confidence": calculate_simple_confidence([ + material_result.get('confidence', 0), + instrument_type_result.get('confidence', 0) + ]) + } + +def classify_instrument_type(dat_file: str, description: str) -> Dict: + """계기 타입 분류""" + + dat_upper = dat_file.upper() + desc_upper = description.upper() + + # DAT_FILE 패턴 확인 + for inst_type, type_data in INSTRUMENT_TYPES.items(): + for pattern in type_data["dat_file_patterns"]: + if pattern in dat_upper: + return { + "type": inst_type, + "characteristics": type_data["characteristics"], + "confidence": 0.9, + "evidence": [f"DAT_PATTERN: {pattern}"] + } + + # DESCRIPTION 키워드 확인 + for inst_type, type_data in INSTRUMENT_TYPES.items(): + for keyword in type_data["description_keywords"]: + if keyword in desc_upper: + return { + "type": inst_type, + "characteristics": type_data["characteristics"], + "confidence": 0.8, + "evidence": [f"KEYWORD: {keyword}"] + } + + return { + "type": "UNKNOWN", + "characteristics": "분류되지 않은 계기", + "confidence": 0.0, + "evidence": ["NO_INSTRUMENT_TYPE_FOUND"] + } + +def extract_measurement_range(description: str) -> Dict: + """측정 범위 추출 (간단히)""" + + desc_upper = description.upper() + + # 압력 범위 + pressure_match = re.search(r'(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*(PSI|BAR|KPA)', desc_upper) + if pressure_match: + return { + "range": f"{pressure_match.group(1)}-{pressure_match.group(2)}", + "unit": pressure_match.group(3), + "signal_type": "PRESSURE" + } + + # 온도 범위 + temp_match = re.search(r'(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*(°?C|°?F)', desc_upper) + if temp_match: + return { + "range": f"{temp_match.group(1)}-{temp_match.group(2)}", + "unit": temp_match.group(3), + "signal_type": "TEMPERATURE" + } + + # 신호 타입 + if "4-20MA" in desc_upper or "4-20 MA" in desc_upper: + return { + "range": "4-20mA", + "unit": "mA", + "signal_type": "ANALOG" + } + + return { + "range": "", + "unit": "", + "signal_type": "" + } + +def calculate_simple_confidence(scores: List[float]) -> float: + """간단한 신뢰도 계산""" + valid_scores = [s for s in scores if s > 0] + return round(sum(valid_scores) / len(valid_scores), 2) if valid_scores else 0.0 diff --git a/backend/app/services/test_bolt_classifier.py b/backend/app/services/test_bolt_classifier.py new file mode 100644 index 0000000..4dc8a93 --- /dev/null +++ b/backend/app/services/test_bolt_classifier.py @@ -0,0 +1,143 @@ +""" +BOLT 분류 테스트 +""" + +from .bolt_classifier import ( + classify_bolt, + get_bolt_purchase_info, + is_high_strength_bolt, + is_stainless_bolt +) + +def test_bolt_classification(): + """BOLT 분류 테스트""" + + test_cases = [ + { + "name": "육각 볼트 (미터)", + "dat_file": "BOLT_HEX_M12", + "description": "HEX BOLT, M12 X 50MM, GRADE 8.8, ZINC PLATED", + "main_nom": "M12" + }, + { + "name": "소켓 헤드 캡 스크류", + "dat_file": "SHCS_M8", + "description": "SOCKET HEAD CAP SCREW, M8 X 25MM, SS316", + "main_nom": "M8" + }, + { + "name": "스터드 볼트", + "dat_file": "STUD_M16", + "description": "STUD BOLT, M16 X 100MM, ASTM A193 B7", + "main_nom": "M16" + }, + { + "name": "플랜지 볼트", + "dat_file": "FLG_BOLT_M20", + "description": "FLANGE BOLT, M20 X 80MM, GRADE 10.9", + "main_nom": "M20" + }, + { + "name": "인치 볼트", + "dat_file": "BOLT_HEX_1/2", + "description": "HEX BOLT, 1/2-13 UNC X 2 INCH, ASTM A325", + "main_nom": "1/2\"" + }, + { + "name": "육각 너트", + "dat_file": "NUT_HEX_M12", + "description": "HEX NUT, M12, GRADE 8, ZINC PLATED", + "main_nom": "M12" + }, + { + "name": "헤비 너트", + "dat_file": "HEAVY_NUT_M16", + "description": "HEAVY HEX NUT, M16, SS316", + "main_nom": "M16" + }, + { + "name": "평 와셔", + "dat_file": "WASH_FLAT_M12", + "description": "FLAT WASHER, M12, STAINLESS STEEL", + "main_nom": "M12" + }, + { + "name": "스프링 와셔", + "dat_file": "SPRING_WASH_M10", + "description": "SPRING WASHER, M10, CARBON STEEL", + "main_nom": "M10" + }, + { + "name": "U볼트", + "dat_file": "U_BOLT_M8", + "description": "U-BOLT, M8 X 50MM, GALVANIZED", + "main_nom": "M8" + } + ] + + print("🔩 BOLT 분류 테스트 시작\n") + print("=" * 80) + + for i, test in enumerate(test_cases, 1): + print(f"\n테스트 {i}: {test['name']}") + print("-" * 60) + + result = classify_bolt( + test["dat_file"], + test["description"], + test["main_nom"] + ) + + purchase_info = get_bolt_purchase_info(result) + + print(f"📋 입력:") + print(f" DAT_FILE: {test['dat_file']}") + print(f" DESCRIPTION: {test['description']}") + print(f" SIZE: {result['dimensions']['dimension_description']}") + + print(f"\n🔩 분류 결과:") + print(f" 카테고리: {result['fastener_category']['category']}") + print(f" 타입: {result['fastener_type']['type']}") + print(f" 특성: {result['fastener_type']['characteristics']}") + print(f" 나사규격: {result['thread_specification']['standard']} {result['thread_specification']['size']}") + if result['thread_specification']['pitch']: + print(f" 피치: {result['thread_specification']['pitch']}") + print(f" 치수: {result['dimensions']['dimension_description']}") + print(f" 등급: {result['grade_strength']['grade']}") + if result['grade_strength']['tensile_strength']: + print(f" 인장강도: {result['grade_strength']['tensile_strength']}") + + # 특수 조건 표시 + conditions = [] + if is_high_strength_bolt(result): + conditions.append("💪 고강도") + if is_stainless_bolt(result): + conditions.append("✨ 스테인리스") + if conditions: + print(f" 특수조건: {' '.join(conditions)}") + + print(f"\n📊 신뢰도:") + print(f" 전체신뢰도: {result['overall_confidence']}") + print(f" 재질: {result['material']['confidence']}") + print(f" 타입: {result['fastener_type']['confidence']}") + print(f" 나사규격: {result['thread_specification']['confidence']}") + print(f" 등급: {result['grade_strength']['confidence']}") + + print(f"\n🛒 구매 정보:") + print(f" 공급업체: {purchase_info['supplier_type']}") + print(f" 예상납기: {purchase_info['lead_time_estimate']}") + print(f" 구매단위: {purchase_info['purchase_unit']}") + print(f" 구매카테고리: {purchase_info['purchase_category']}") + + print(f"\n💾 저장될 데이터:") + print(f" FASTENER_CATEGORY: {result['fastener_category']['category']}") + print(f" FASTENER_TYPE: {result['fastener_type']['type']}") + print(f" THREAD_STANDARD: {result['thread_specification']['standard']}") + print(f" THREAD_SIZE: {result['thread_specification']['size']}") + print(f" GRADE: {result['grade_strength']['grade']}") + + if i < len(test_cases): + print("\n" + "=" * 80) + +if __name__ == "__main__": + test_bolt_classification() diff --git a/backend/app/services/test_gasket_classifier.py b/backend/app/services/test_gasket_classifier.py new file mode 100644 index 0000000..375e7e2 --- /dev/null +++ b/backend/app/services/test_gasket_classifier.py @@ -0,0 +1,127 @@ +""" +GASKET 분류 테스트 +""" + +from .gasket_classifier import ( + classify_gasket, + get_gasket_purchase_info, + is_high_temperature_gasket, + is_high_pressure_gasket +) + +def test_gasket_classification(): + """GASKET 분류 테스트""" + + test_cases = [ + { + "name": "스파이럴 와운드 가스켓", + "dat_file": "SWG_150", + "description": "SPIRAL WOUND GASKET, GRAPHITE FILLER, SS316 WINDING, 150LB", + "main_nom": "4\"" + }, + { + "name": "링 조인트 가스켓", + "dat_file": "RTJ_600", + "description": "RING JOINT GASKET, RTJ, SS316, 600LB", + "main_nom": "6\"" + }, + { + "name": "풀 페이스 가스켓", + "dat_file": "FF_150", + "description": "FULL FACE GASKET, RUBBER, 150LB", + "main_nom": "8\"" + }, + { + "name": "레이즈드 페이스 가스켓", + "dat_file": "RF_300", + "description": "RAISED FACE GASKET, PTFE, 300LB, -200°C TO 260°C", + "main_nom": "3\"" + }, + { + "name": "오링", + "dat_file": "OR_VITON", + "description": "O-RING, VITON, ID 50MM, THK 3MM", + "main_nom": "50mm" + }, + { + "name": "시트 가스켓", + "dat_file": "SHEET_150", + "description": "SHEET GASKET, GRAPHITE, 150LB, MAX 650°C", + "main_nom": "10\"" + }, + { + "name": "캄프로파일 가스켓", + "dat_file": "KAMM_600", + "description": "KAMMPROFILE GASKET, GRAPHITE FACING, SS304 CORE, 600LB", + "main_nom": "12\"" + }, + { + "name": "특수 가스켓", + "dat_file": "CUSTOM_SPEC", + "description": "CUSTOM GASKET, SPECIAL SHAPE, PTFE, -50°C TO 200°C", + "main_nom": "특수" + } + ] + + print("🔧 GASKET 분류 테스트 시작\n") + print("=" * 80) + + for i, test in enumerate(test_cases, 1): + print(f"\n테스트 {i}: {test['name']}") + print("-" * 60) + + result = classify_gasket( + test["dat_file"], + test["description"], + test["main_nom"] + ) + + purchase_info = get_gasket_purchase_info(result) + + print(f"📋 입력:") + print(f" DAT_FILE: {test['dat_file']}") + print(f" DESCRIPTION: {test['description']}") + print(f" SIZE: {result['size_info']['size_description']}") + + print(f"\n🔧 분류 결과:") + print(f" 가스켓타입: {result['gasket_type']['type']}") + print(f" 특성: {result['gasket_type']['characteristics']}") + print(f" 가스켓재질: {result['gasket_material']['material']}") + print(f" 재질특성: {result['gasket_material']['characteristics']}") + print(f" 압력등급: {result['pressure_rating']['rating']}") + print(f" 온도범위: {result['temperature_info']['range']}") + print(f" 용도: {result['gasket_type']['applications']}") + + # 특수 조건 표시 + conditions = [] + if is_high_temperature_gasket(result): + conditions.append("🔥 고온용") + if is_high_pressure_gasket(result): + conditions.append("💪 고압용") + if conditions: + print(f" 특수조건: {' '.join(conditions)}") + + print(f"\n📊 신뢰도:") + print(f" 전체신뢰도: {result['overall_confidence']}") + print(f" 가스켓타입: {result['gasket_type']['confidence']}") + print(f" 가스켓재질: {result['gasket_material']['confidence']}") + print(f" 압력등급: {result['pressure_rating']['confidence']}") + + print(f"\n🛒 구매 정보:") + print(f" 공급업체: {purchase_info['supplier_type']}") + print(f" 예상납기: {purchase_info['lead_time_estimate']}") + print(f" 구매단위: {purchase_info['purchase_unit']}") + print(f" 구매카테고리: {purchase_info['purchase_category']}") + + print(f"\n💾 저장될 데이터:") + print(f" GASKET_TYPE: {result['gasket_type']['type']}") + print(f" GASKET_MATERIAL: {result['gasket_material']['material']}") + print(f" PRESSURE_RATING: {result['pressure_rating']['rating']}") + print(f" SIZE_INFO: {result['size_info']['size_description']}") + print(f" TEMPERATURE_RANGE: {result['temperature_info']['range']}") + + if i < len(test_cases): + print("\n" + "=" * 80) + +if __name__ == "__main__": + test_gasket_classification() diff --git a/backend/app/services/test_instrument_classifier.py b/backend/app/services/test_instrument_classifier.py new file mode 100644 index 0000000..375b357 --- /dev/null +++ b/backend/app/services/test_instrument_classifier.py @@ -0,0 +1,48 @@ +""" +INSTRUMENT 간단 테스트 +""" + +from .instrument_classifier import classify_instrument + +def test_instrument_classification(): + """간단한 계기류 테스트""" + + test_cases = [ + { + "name": "압력 게이지", + "dat_file": "PG_001", + "description": "PRESSURE GAUGE, 0-100 PSI, 1/4 NPT", + "main_nom": "1/4\"" + }, + { + "name": "온도 트랜스미터", + "dat_file": "TT_001", + "description": "TEMPERATURE TRANSMITTER, 4-20mA, 0-200°C", + "main_nom": "1/2\"" + }, + { + "name": "유량계", + "dat_file": "FM_001", + "description": "FLOW METER, MAGNETIC TYPE", + "main_nom": "3\"" + } + ] + + print("🔧 INSTRUMENT 간단 테스트\n") + + for i, test in enumerate(test_cases, 1): + result = classify_instrument( + test["dat_file"], + test["description"], + test["main_nom"] + ) + + print(f"테스트 {i}: {test['name']}") + print(f" 계기타입: {result['instrument_type']['type']}") + print(f" 측정범위: {result['measurement_info']['range']} {result['measurement_info']['unit']}") + print(f" 연결사이즈: {result['size_info']['connection_size']}") + print(f" 구매정보: {result['purchase_info']['category']}") + print() + +if __name__ == "__main__": + test_instrument_classification() diff --git a/backend/app/services/test_valve_classifier.py b/backend/app/services/test_valve_classifier.py new file mode 100644 index 0000000..baae847 --- /dev/null +++ b/backend/app/services/test_valve_classifier.py @@ -0,0 +1,130 @@ +""" +VALVE 분류 테스트 +""" + +from .valve_classifier import classify_valve, get_valve_purchase_info, is_forged_valve + +def test_valve_classification(): + """VALVE 분류 테스트""" + + test_cases = [ + { + "name": "게이트 밸브 (주조)", + "dat_file": "GATE_FL_150", + "description": "GATE VALVE, FLANGED, 150LB, WCB, OS&Y", + "main_nom": "6\"" + }, + { + "name": "볼 밸브 (주조)", + "dat_file": "BALL_FL_300", + "description": "BALL VALVE, FULL PORT, FLANGED, 300LB, WCB", + "main_nom": "4\"" + }, + { + "name": "볼 밸브 (단조)", + "dat_file": "BALL_SW_1500", + "description": "BALL VALVE, FORGED, SW, 1500LB, A105", + "main_nom": "1\"" + }, + { + "name": "글로브 밸브 (단조)", + "dat_file": "GLOBE_SW_800", + "description": "GLOBE VALVE, FORGED, SW, 800LB, A182 F316", + "main_nom": "2\"" + }, + { + "name": "체크 밸브 (주조)", + "dat_file": "CHK_FL_150", + "description": "CHECK VALVE, SWING TYPE, FLANGED, 150LB, WCB", + "main_nom": "8\"" + }, + { + "name": "체크 밸브 (단조)", + "dat_file": "CHK_SW_3000", + "description": "CHECK VALVE, LIFT TYPE, SW, 3000LB, A105", + "main_nom": "1\"" + }, + { + "name": "니들 밸브 (단조)", + "dat_file": "NEEDLE_THD_6000", + "description": "NEEDLE VALVE, FORGED, THD, 6000LB, SS316", + "main_nom": "1/2\"" + }, + { + "name": "버터플라이 밸브", + "dat_file": "BUTTERFLY_WAF_150", + "description": "BUTTERFLY VALVE, WAFER TYPE, 150LB, GEAR OPERATED", + "main_nom": "12\"" + }, + { + "name": "릴리프 밸브", + "dat_file": "RELIEF_FL_150", + "description": "RELIEF VALVE, SET PRESSURE 150 PSI, FLANGED, 150LB", + "main_nom": "3\"" + }, + { + "name": "솔레노이드 밸브", + "dat_file": "SOLENOID_THD", + "description": "SOLENOID VALVE, 2-WAY, 24VDC, 1/4 NPT", + "main_nom": "1/4\"" + } + ] + + print("🔧 VALVE 분류 테스트 시작\n") + print("=" * 80) + + for i, test in enumerate(test_cases, 1): + print(f"\n테스트 {i}: {test['name']}") + print("-" * 60) + + result = classify_valve( + test["dat_file"], + test["description"], + test["main_nom"] + ) + + purchase_info = get_valve_purchase_info(result) + + print(f"📋 입력:") + print(f" DAT_FILE: {test['dat_file']}") + print(f" DESCRIPTION: {test['description']}") + print(f" SIZE: {result['size_info']['size_description']}") + + print(f"\n🔧 분류 결과:") + print(f" 재질: {result['material']['standard']} | {result['material']['grade']}") + print(f" 밸브타입: {result['valve_type']['type']}") + print(f" 특성: {result['valve_type']['characteristics']}") + print(f" 연결방식: {result['connection_method']['method']}") + print(f" 압력등급: {result['pressure_rating']['rating']}") + print(f" 작동방식: {result['actuation']['method']}") + print(f" 제작방법: {result['manufacturing']['method']} ({'🔨 단조' if is_forged_valve(result) else '🏭 주조'})") + + if result['special_features']: + print(f" 특수기능: {', '.join(result['special_features'])}") + + print(f"\n📊 신뢰도:") + print(f" 전체신뢰도: {result['overall_confidence']}") + print(f" 재질: {result['material']['confidence']}") + print(f" 밸브타입: {result['valve_type']['confidence']}") + print(f" 연결방식: {result['connection_method']['confidence']}") + print(f" 압력등급: {result['pressure_rating']['confidence']}") + + print(f"\n🛒 구매 정보:") + print(f" 공급업체: {purchase_info['supplier_type']}") + print(f" 예상납기: {purchase_info['lead_time_estimate']}") + print(f" 구매카테고리: {purchase_info['purchase_category']}") + if purchase_info['special_requirements']: + print(f" 특수요구사항: {', '.join(purchase_info['special_requirements'])}") + + print(f"\n💾 저장될 데이터:") + print(f" VALVE_TYPE: {result['valve_type']['type']}") + print(f" CONNECTION: {result['connection_method']['method']}") + print(f" PRESSURE_RATING: {result['pressure_rating']['rating']}") + print(f" MANUFACTURING: {result['manufacturing']['method']}") + print(f" ACTUATION: {result['actuation']['method']}") + + if i < len(test_cases): + print("\n" + "=" * 80) + +if __name__ == "__main__": + test_valve_classification() diff --git a/backend/app/services/valve_classifier.py b/backend/app/services/valve_classifier.py new file mode 100644 index 0000000..e332e96 --- /dev/null +++ b/backend/app/services/valve_classifier.py @@ -0,0 +1,717 @@ +""" +VALVE 분류 시스템 +주조 밸브 + 단조 밸브 구분 포함 +""" + +import re +from typing import Dict, List, Optional +from .material_classifier import classify_material, get_manufacturing_method_from_material + +# ========== 밸브 타입별 분류 ========== +VALVE_TYPES = { + "GATE_VALVE": { + "dat_file_patterns": ["GATE_", "GV_"], + "description_keywords": ["GATE VALVE", "GATE", "게이트"], + "characteristics": "완전 개폐용, 직선 유로", + "typical_connections": ["FLANGED", "SOCKET_WELD", "BUTT_WELD"], + "pressure_range": "150LB ~ 2500LB", + "special_features": ["OS&Y", "RS", "NRS"] + }, + + "BALL_VALVE": { + "dat_file_patterns": ["BALL_", "BV_"], + "description_keywords": ["BALL VALVE", "BALL", "볼밸브"], + "characteristics": "빠른 개폐, 낮은 압력손실", + "typical_connections": ["FLANGED", "THREADED", "SOCKET_WELD"], + "pressure_range": "150LB ~ 6000LB", + "special_features": ["FULL_PORT", "REDUCED_PORT", "3_WAY", "4_WAY"] + }, + + "GLOBE_VALVE": { + "dat_file_patterns": ["GLOBE_", "GLV_"], + "description_keywords": ["GLOBE VALVE", "GLOBE", "글로브"], + "characteristics": "유량 조절용, 정밀 제어", + "typical_connections": ["FLANGED", "SOCKET_WELD", "BUTT_WELD"], + "pressure_range": "150LB ~ 2500LB", + "special_features": ["ANGLE_TYPE", "Y_TYPE"] + }, + + "CHECK_VALVE": { + "dat_file_patterns": ["CHK_", "CHECK_", "CV_"], + "description_keywords": ["CHECK VALVE", "CHECK", "체크", "역지"], + "characteristics": "역류 방지용", + "typical_connections": ["FLANGED", "SOCKET_WELD", "WAFER"], + "pressure_range": "150LB ~ 2500LB", + "special_features": ["SWING_TYPE", "LIFT_TYPE", "DUAL_PLATE", "PISTON_TYPE"] + }, + + "BUTTERFLY_VALVE": { + "dat_file_patterns": ["BUTTERFLY_", "BFV_"], + "description_keywords": ["BUTTERFLY VALVE", "BUTTERFLY", "버터플라이"], + "characteristics": "대구경용, 경량", + "typical_connections": ["WAFER", "LUG", "FLANGED"], + "pressure_range": "150LB ~ 600LB", + "special_features": ["GEAR_OPERATED", "LEVER_OPERATED"] + }, + + "NEEDLE_VALVE": { + "dat_file_patterns": ["NEEDLE_", "NV_"], + "description_keywords": ["NEEDLE VALVE", "NEEDLE", "니들"], + "characteristics": "정밀 유량 조절용", + "typical_connections": ["THREADED", "SOCKET_WELD"], + "pressure_range": "800LB ~ 6000LB", + "special_features": ["FINE_ADJUSTMENT"], + "typically_forged": True + }, + + "RELIEF_VALVE": { + "dat_file_patterns": ["RELIEF_", "RV_", "PSV_"], + "description_keywords": ["RELIEF VALVE", "PRESSURE RELIEF", "SAFETY VALVE", "릴리프"], + "characteristics": "안전 압력 방출용", + "typical_connections": ["FLANGED", "THREADED"], + "pressure_range": "150LB ~ 2500LB", + "special_features": ["SET_PRESSURE", "PILOT_OPERATED"] + }, + + "SOLENOID_VALVE": { + "dat_file_patterns": ["SOLENOID_", "SOL_"], + "description_keywords": ["SOLENOID VALVE", "SOLENOID", "솔레노이드"], + "characteristics": "전기 제어용", + "typical_connections": ["THREADED"], + "pressure_range": "150LB ~ 600LB", + "special_features": ["2_WAY", "3_WAY", "NC", "NO"] + }, + + "PLUG_VALVE": { + "dat_file_patterns": ["PLUG_", "PV_"], + "description_keywords": ["PLUG VALVE", "PLUG", "플러그"], + "characteristics": "다방향 제어용", + "typical_connections": ["FLANGED", "THREADED"], + "pressure_range": "150LB ~ 600LB", + "special_features": ["LUBRICATED", "NON_LUBRICATED"] + } +} + +# ========== 연결 방식별 분류 ========== +VALVE_CONNECTIONS = { + "FLANGED": { + "codes": ["FL", "FLANGED", "플랜지"], + "dat_patterns": ["_FL_"], + "size_range": "1\" ~ 48\"", + "pressure_range": "150LB ~ 2500LB", + "manufacturing": "CAST", + "confidence": 0.95 + }, + "THREADED": { + "codes": ["THD", "THRD", "NPT", "THREADED", "나사"], + "dat_patterns": ["_THD_", "_TR_"], + "size_range": "1/4\" ~ 4\"", + "pressure_range": "150LB ~ 6000LB", + "manufacturing": "FORGED_OR_CAST", + "confidence": 0.95 + }, + "SOCKET_WELD": { + "codes": ["SW", "SOCKET WELD", "소켓웰드"], + "dat_patterns": ["_SW_"], + "size_range": "1/4\" ~ 4\"", + "pressure_range": "800LB ~ 9000LB", + "manufacturing": "FORGED", + "confidence": 0.95 + }, + "BUTT_WELD": { + "codes": ["BW", "BUTT WELD", "맞대기용접"], + "dat_patterns": ["_BW_"], + "size_range": "1/2\" ~ 48\"", + "pressure_range": "150LB ~ 2500LB", + "manufacturing": "CAST_OR_FORGED", + "confidence": 0.95 + }, + "WAFER": { + "codes": ["WAFER", "WAFER TYPE", "웨이퍼"], + "dat_patterns": ["_WAF_"], + "size_range": "2\" ~ 48\"", + "pressure_range": "150LB ~ 600LB", + "manufacturing": "CAST", + "confidence": 0.9 + }, + "LUG": { + "codes": ["LUG", "LUG TYPE", "러그"], + "dat_patterns": ["_LUG_"], + "size_range": "2\" ~ 48\"", + "pressure_range": "150LB ~ 600LB", + "manufacturing": "CAST", + "confidence": 0.9 + } +} + +# ========== 작동 방식별 분류 ========== +VALVE_ACTUATION = { + "MANUAL": { + "keywords": ["MANUAL", "HAND WHEEL", "LEVER", "수동"], + "characteristics": "수동 조작", + "applications": "일반 개폐용" + }, + "GEAR_OPERATED": { + "keywords": ["GEAR OPERATED", "GEAR", "기어"], + "characteristics": "기어 구동", + "applications": "대구경 수동 조작" + }, + "PNEUMATIC": { + "keywords": ["PNEUMATIC", "AIR OPERATED", "공압"], + "characteristics": "공압 구동", + "applications": "자동 제어" + }, + "ELECTRIC": { + "keywords": ["ELECTRIC", "MOTOR OPERATED", "전동"], + "characteristics": "전동 구동", + "applications": "원격 제어" + }, + "SOLENOID": { + "keywords": ["SOLENOID", "솔레노이드"], + "characteristics": "전자 밸브", + "applications": "빠른 전기 제어" + } +} + +# ========== 압력 등급별 분류 ========== +VALVE_PRESSURE_RATINGS = { + "patterns": [ + r"(\d+)LB", + r"CLASS\s*(\d+)", + r"CL\s*(\d+)", + r"(\d+)#", + r"(\d+)\s*WOG" # Water Oil Gas + ], + "standard_ratings": { + "150LB": {"max_pressure": "285 PSI", "typical_manufacturing": "CAST"}, + "300LB": {"max_pressure": "740 PSI", "typical_manufacturing": "CAST"}, + "600LB": {"max_pressure": "1480 PSI", "typical_manufacturing": "CAST_OR_FORGED"}, + "800LB": {"max_pressure": "2000 PSI", "typical_manufacturing": "FORGED"}, + "900LB": {"max_pressure": "2220 PSI", "typical_manufacturing": "CAST_OR_FORGED"}, + "1500LB": {"max_pressure": "3705 PSI", "typical_manufacturing": "FORGED"}, + "2500LB": {"max_pressure": "6170 PSI", "typical_manufacturing": "FORGED"}, + "3000LB": {"max_pressure": "7400 PSI", "typical_manufacturing": "FORGED"}, + "6000LB": {"max_pressure": "14800 PSI", "typical_manufacturing": "FORGED"}, + "9000LB": {"max_pressure": "22200 PSI", "typical_manufacturing": "FORGED"} + } +} + +def classify_valve(dat_file: str, description: str, main_nom: str) -> Dict: + """ + 완전한 VALVE 분류 + + Args: + dat_file: DAT_FILE 필드 + description: DESCRIPTION 필드 + main_nom: MAIN_NOM 필드 (밸브 사이즈) + + Returns: + 완전한 밸브 분류 결과 + """ + + # 1. 재질 분류 (공통 모듈 사용) + material_result = classify_material(description) + + # 2. 밸브 타입 분류 + valve_type_result = classify_valve_type(dat_file, description) + + # 3. 연결 방식 분류 + connection_result = classify_valve_connection(dat_file, description) + + # 4. 압력 등급 분류 + pressure_result = classify_valve_pressure_rating(dat_file, description) + + # 5. 작동 방식 분류 + actuation_result = classify_valve_actuation(description) + + # 6. 제작 방법 결정 (주조 vs 단조) + manufacturing_result = determine_valve_manufacturing( + material_result, valve_type_result, connection_result, + pressure_result, main_nom + ) + + # 7. 특수 기능 추출 + special_features = extract_valve_special_features(description, valve_type_result) + + # 8. 최종 결과 조합 + return { + "category": "VALVE", + + # 재질 정보 (공통 모듈) + "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) + }, + + # 밸브 분류 정보 + "valve_type": { + "type": valve_type_result.get('type', 'UNKNOWN'), + "characteristics": valve_type_result.get('characteristics', ''), + "confidence": valve_type_result.get('confidence', 0.0), + "evidence": valve_type_result.get('evidence', []) + }, + + "connection_method": { + "method": connection_result.get('method', 'UNKNOWN'), + "confidence": connection_result.get('confidence', 0.0), + "size_range": connection_result.get('size_range', ''), + "pressure_range": connection_result.get('pressure_range', '') + }, + + "pressure_rating": { + "rating": pressure_result.get('rating', 'UNKNOWN'), + "confidence": pressure_result.get('confidence', 0.0), + "max_pressure": pressure_result.get('max_pressure', ''), + "typical_manufacturing": pressure_result.get('typical_manufacturing', '') + }, + + "actuation": { + "method": actuation_result.get('method', 'MANUAL'), + "characteristics": actuation_result.get('characteristics', ''), + "confidence": actuation_result.get('confidence', 0.6) + }, + + "manufacturing": { + "method": manufacturing_result.get('method', 'UNKNOWN'), + "confidence": manufacturing_result.get('confidence', 0.0), + "evidence": manufacturing_result.get('evidence', []), + "characteristics": manufacturing_result.get('characteristics', '') + }, + + "special_features": special_features, + + "size_info": { + "valve_size": main_nom, + "size_description": main_nom + }, + + # 전체 신뢰도 + "overall_confidence": calculate_valve_confidence({ + "material": material_result.get('confidence', 0), + "valve_type": valve_type_result.get('confidence', 0), + "connection": connection_result.get('confidence', 0), + "pressure": pressure_result.get('confidence', 0) + }) + } + +def classify_valve_type(dat_file: str, description: str) -> Dict: + """밸브 타입 분류""" + + dat_upper = dat_file.upper() + desc_upper = description.upper() + + # 1. DAT_FILE 패턴으로 1차 분류 (가장 신뢰도 높음) + for valve_type, type_data in VALVE_TYPES.items(): + for pattern in type_data["dat_file_patterns"]: + if pattern in dat_upper: + return { + "type": valve_type, + "characteristics": type_data["characteristics"], + "confidence": 0.95, + "evidence": [f"DAT_FILE_PATTERN: {pattern}"], + "typical_connections": type_data["typical_connections"], + "special_features": type_data.get("special_features", []) + } + + # 2. DESCRIPTION 키워드로 2차 분류 + for valve_type, type_data in VALVE_TYPES.items(): + for keyword in type_data["description_keywords"]: + if keyword in desc_upper: + return { + "type": valve_type, + "characteristics": type_data["characteristics"], + "confidence": 0.85, + "evidence": [f"DESCRIPTION_KEYWORD: {keyword}"], + "typical_connections": type_data["typical_connections"], + "special_features": type_data.get("special_features", []) + } + + # 3. 분류 실패 + return { + "type": "UNKNOWN", + "characteristics": "", + "confidence": 0.0, + "evidence": ["NO_VALVE_TYPE_IDENTIFIED"], + "typical_connections": [], + "special_features": [] + } + +def classify_valve_connection(dat_file: str, description: str) -> Dict: + """밸브 연결 방식 분류""" + + dat_upper = dat_file.upper() + desc_upper = description.upper() + combined_text = f"{dat_upper} {desc_upper}" + + # 1. DAT_FILE 패턴 우선 확인 + for connection_type, conn_data in VALVE_CONNECTIONS.items(): + for pattern in conn_data["dat_patterns"]: + if pattern in dat_upper: + return { + "method": connection_type, + "confidence": 0.95, + "matched_pattern": pattern, + "source": "DAT_FILE_PATTERN", + "size_range": conn_data["size_range"], + "pressure_range": conn_data["pressure_range"], + "typical_manufacturing": conn_data["manufacturing"] + } + + # 2. 키워드 확인 + for connection_type, conn_data in VALVE_CONNECTIONS.items(): + for code in conn_data["codes"]: + if code in combined_text: + return { + "method": connection_type, + "confidence": conn_data["confidence"], + "matched_code": code, + "source": "KEYWORD_MATCH", + "size_range": conn_data["size_range"], + "pressure_range": conn_data["pressure_range"], + "typical_manufacturing": conn_data["manufacturing"] + } + + return { + "method": "UNKNOWN", + "confidence": 0.0, + "matched_code": "", + "source": "NO_CONNECTION_METHOD_FOUND" + } + +def classify_valve_pressure_rating(dat_file: str, description: str) -> Dict: + """밸브 압력 등급 분류""" + + combined_text = f"{dat_file} {description}".upper() + + # 패턴 매칭으로 압력 등급 추출 + for pattern in VALVE_PRESSURE_RATINGS["patterns"]: + match = re.search(pattern, combined_text) + if match: + rating_num = match.group(1) + + # WOG 처리 (Water Oil Gas) + if "WOG" in pattern: + rating = f"{rating_num}WOG" + # WOG를 LB로 변환 (대략적) + if int(rating_num) <= 600: + equivalent_lb = "150LB" + elif int(rating_num) <= 1000: + equivalent_lb = "300LB" + else: + equivalent_lb = "600LB" + + return { + "rating": f"{rating} ({equivalent_lb} 상당)", + "confidence": 0.8, + "matched_pattern": pattern, + "max_pressure": f"{rating_num} PSI", + "typical_manufacturing": "CAST_OR_FORGED" + } + else: + rating = f"{rating_num}LB" + + # 표준 등급 정보 확인 + rating_info = VALVE_PRESSURE_RATINGS["standard_ratings"].get(rating, {}) + + if rating_info: + confidence = 0.95 + else: + confidence = 0.8 + rating_info = { + "max_pressure": "확인 필요", + "typical_manufacturing": "UNKNOWN" + } + + return { + "rating": rating, + "confidence": confidence, + "matched_pattern": pattern, + "matched_value": rating_num, + "max_pressure": rating_info.get("max_pressure", ""), + "typical_manufacturing": rating_info.get("typical_manufacturing", "") + } + + return { + "rating": "UNKNOWN", + "confidence": 0.0, + "matched_pattern": "", + "max_pressure": "", + "typical_manufacturing": "" + } + +def classify_valve_actuation(description: str) -> Dict: + """밸브 작동 방식 분류""" + + desc_upper = description.upper() + + # 키워드 기반 작동 방식 분류 + for actuation_type, act_data in VALVE_ACTUATION.items(): + for keyword in act_data["keywords"]: + if keyword in desc_upper: + return { + "method": actuation_type, + "characteristics": act_data["characteristics"], + "confidence": 0.9, + "matched_keyword": keyword, + "applications": act_data["applications"] + } + + # 기본값: MANUAL + return { + "method": "MANUAL", + "characteristics": "수동 조작 (기본값)", + "confidence": 0.6, + "matched_keyword": "DEFAULT", + "applications": "일반 수동 조작" + } + +def determine_valve_manufacturing(material_result: Dict, valve_type_result: Dict, + connection_result: Dict, pressure_result: Dict, + main_nom: str) -> Dict: + """밸브 제작 방법 결정 (주조 vs 단조)""" + + evidence = [] + + # 1. 재질 기반 제작방법 (가장 확실) + material_manufacturing = get_manufacturing_method_from_material(material_result) + if material_manufacturing in ["FORGED", "CAST"]: + evidence.append(f"MATERIAL_STANDARD: {material_result.get('standard')}") + + characteristics = { + "FORGED": "단조품 - 고강도, 소구경, 고압용", + "CAST": "주조품 - 대구경, 복잡형상, 중저압용" + }.get(material_manufacturing, "") + + return { + "method": material_manufacturing, + "confidence": 0.9, + "evidence": evidence, + "characteristics": characteristics + } + + # 2. 단조 밸브 조건 확인 + forged_indicators = 0 + + # 연결방식이 소켓웰드 + connection_method = connection_result.get('method', '') + if connection_method == "SOCKET_WELD": + forged_indicators += 2 + evidence.append(f"SOCKET_WELD_CONNECTION") + + # 고압 등급 + pressure_rating = pressure_result.get('rating', '') + high_pressure = ["800LB", "1500LB", "2500LB", "3000LB", "6000LB", "9000LB"] + if any(pressure in pressure_rating for pressure in high_pressure): + forged_indicators += 2 + evidence.append(f"HIGH_PRESSURE: {pressure_rating}") + + # 소구경 + try: + size_num = float(re.findall(r'(\d+(?:\.\d+)?)', main_nom)[0]) + if size_num <= 4.0: + forged_indicators += 1 + evidence.append(f"SMALL_SIZE: {main_nom}") + except: + pass + + # 니들 밸브는 일반적으로 단조 + valve_type = valve_type_result.get('type', '') + if valve_type == "NEEDLE_VALVE": + forged_indicators += 2 + evidence.append("NEEDLE_VALVE_TYPICALLY_FORGED") + + # 단조 결정 + if forged_indicators >= 3: + return { + "method": "FORGED", + "confidence": 0.85, + "evidence": evidence, + "characteristics": "단조품 - 고압, 소구경용" + } + + # 3. 압력등급별 일반적 제작방법 + pressure_manufacturing = pressure_result.get('typical_manufacturing', '') + if pressure_manufacturing: + if pressure_manufacturing == "FORGED": + evidence.append(f"PRESSURE_BASED: {pressure_rating}") + return { + "method": "FORGED", + "confidence": 0.75, + "evidence": evidence, + "characteristics": "고압용 단조품" + } + elif pressure_manufacturing == "CAST": + evidence.append(f"PRESSURE_BASED: {pressure_rating}") + return { + "method": "CAST", + "confidence": 0.75, + "evidence": evidence, + "characteristics": "저중압용 주조품" + } + + # 4. 연결방식별 일반적 제작방법 + connection_manufacturing = connection_result.get('typical_manufacturing', '') + if connection_manufacturing: + evidence.append(f"CONNECTION_BASED: {connection_method}") + + if connection_manufacturing == "FORGED": + return { + "method": "FORGED", + "confidence": 0.7, + "evidence": evidence, + "characteristics": "소구경 단조품" + } + elif connection_manufacturing == "CAST": + return { + "method": "CAST", + "confidence": 0.7, + "evidence": evidence, + "characteristics": "대구경 주조품" + } + + # 5. 기본 추정 (사이즈 기반) + try: + size_num = float(re.findall(r'(\d+(?:\.\d+)?)', main_nom)[0]) + if size_num <= 2.0: + return { + "method": "FORGED", + "confidence": 0.6, + "evidence": ["SIZE_BASED_SMALL"], + "characteristics": "소구경 - 일반적으로 단조품" + } + else: + return { + "method": "CAST", + "confidence": 0.6, + "evidence": ["SIZE_BASED_LARGE"], + "characteristics": "대구경 - 일반적으로 주조품" + } + except: + pass + + return { + "method": "UNKNOWN", + "confidence": 0.0, + "evidence": ["INSUFFICIENT_MANUFACTURING_INFO"], + "characteristics": "" + } + +def extract_valve_special_features(description: str, valve_type_result: Dict) -> List[str]: + """밸브 특수 기능 추출""" + + desc_upper = description.upper() + features = [] + + # 밸브 타입별 특수 기능 + valve_special_features = valve_type_result.get('special_features', []) + for feature in valve_special_features: + # 기능별 키워드 매핑 + feature_keywords = { + "OS&Y": ["OS&Y", "OUTSIDE SCREW"], + "FULL_PORT": ["FULL PORT", "FULL BORE"], + "REDUCED_PORT": ["REDUCED PORT", "REDUCED BORE"], + "3_WAY": ["3 WAY", "3-WAY", "THREE WAY"], + "SWING_TYPE": ["SWING", "SWING TYPE"], + "LIFT_TYPE": ["LIFT", "LIFT TYPE"], + "GEAR_OPERATED": ["GEAR OPERATED", "GEAR"], + "SET_PRESSURE": ["SET PRESSURE", "SET @"] + } + + keywords = feature_keywords.get(feature, [feature]) + for keyword in keywords: + if keyword in desc_upper: + features.append(feature) + break + + # 일반적인 특수 기능들 + general_features = { + "FIRE_SAFE": ["FIRE SAFE", "FIRE-SAFE"], + "ANTI_STATIC": ["ANTI STATIC", "ANTI-STATIC"], + "BLOW_OUT_PROOF": ["BLOW OUT PROOF", "BOP"], + "EXTENDED_STEM": ["EXTENDED STEM", "EXT STEM"], + "CRYOGENIC": ["CRYOGENIC", "CRYO"], + "HIGH_TEMPERATURE": ["HIGH TEMP", "HT"] + } + + for feature, keywords in general_features.items(): + for keyword in keywords: + if keyword in desc_upper: + features.append(feature) + break + + return list(set(features)) # 중복 제거 + +def calculate_valve_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, + "valve_type": 0.4, + "connection": 0.25, + "pressure": 0.15 + } + + weighted_sum = sum( + confidence_scores.get(key, 0) * weight + for key, weight in weights.items() + ) + + return round(weighted_sum, 2) + +# ========== 특수 기능들 ========== + +def is_forged_valve(valve_result: Dict) -> bool: + """단조 밸브 여부 판단""" + return valve_result.get("manufacturing", {}).get("method") == "FORGED" + +def is_high_pressure_valve(valve_result: Dict) -> bool: + """고압 밸브 여부 판단""" + pressure_rating = valve_result.get("pressure_rating", {}).get("rating", "") + high_pressure_ratings = ["800LB", "1500LB", "2500LB", "3000LB", "6000LB", "9000LB"] + return any(pressure in pressure_rating for pressure in high_pressure_ratings) + +def get_valve_purchase_info(valve_result: Dict) -> Dict: + """밸브 구매 정보 생성""" + + valve_type = valve_result["valve_type"]["type"] + connection = valve_result["connection_method"]["method"] + pressure = valve_result["pressure_rating"]["rating"] + manufacturing = valve_result["manufacturing"]["method"] + actuation = valve_result["actuation"]["method"] + + # 공급업체 타입 결정 + if manufacturing == "FORGED": + supplier_type = "단조 밸브 전문업체" + elif valve_type == "BUTTERFLY_VALVE": + supplier_type = "버터플라이 밸브 전문업체" + elif actuation in ["PNEUMATIC", "ELECTRIC"]: + supplier_type = "자동 밸브 전문업체" + else: + supplier_type = "일반 밸브 업체" + + # 납기 추정 + if manufacturing == "FORGED" and is_high_pressure_valve(valve_result): + lead_time = "8-12주 (단조 고압용)" + elif actuation in ["PNEUMATIC", "ELECTRIC"]: + lead_time = "6-10주 (자동 밸브)" + elif manufacturing == "FORGED": + lead_time = "6-8주 (단조품)" + else: + lead_time = "4-8주 (일반품)" + + return { + "supplier_type": supplier_type, + "lead_time_estimate": lead_time, + "purchase_category": f"{valve_type} {connection} {pressure}", + "manufacturing_note": valve_result["manufacturing"]["characteristics"], + "actuation_note": valve_result["actuation"]["characteristics"], + "special_requirements": valve_result["special_features"] + }