""" 통합 자재 분류 시스템 메모리에 정의된 키워드 우선순위 체계를 적용 """ import re from typing import Dict, List, Optional, Tuple from .fitting_classifier import classify_fitting # Level 1: 명확한 타입 키워드 (최우선) LEVEL1_TYPE_KEYWORDS = { "BOLT": ["FLANGE BOLT", "U-BOLT", "U 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"], "FITTING": ["ELBOW", "ELL", "TEE", "REDUCER", "RED", "CAP", "COUPLING", "NIPPLE", "SWAGE", "OLET", "PLUG", "엘보", "티", "리듀서", "캡", "니플", "커플링", "플러그", "CONC", "ECC", "SOCK-O-LET", "WELD-O-LET", "SOCKOLET", "WELDOLET", "THREADOLET"], "GASKET": ["GASKET", "GASK", "가스켓", "SWG", "SPIRAL"], "INSTRUMENT": ["GAUGE", "TRANSMITTER", "SENSOR", "THERMOMETER", "계기", "게이지", "트랜스미터", "센서"], "SUPPORT": ["URETHANE BLOCK", "URETHANE", "BLOCK SHOE", "CLAMP", "SUPPORT", "HANGER", "SPRING", "우레탄", "블록", "클램프", "서포트", "행거", "스프링"] } # Level 2: 서브타입 키워드 (구체화) LEVEL2_SUBTYPE_KEYWORDS = { "VALVE": { "GATE": ["GATE VALVE", "GATE", "게이트 밸브"], "BALL": ["BALL VALVE", "BALL", "볼 밸브"], "GLOBE": ["GLOBE VALVE", "GLOBE", "글로브 밸브"], "CHECK": ["CHECK VALVE", "CHECK", "체크 밸브", "역지 밸브"] }, "FLANGE": { "WELD_NECK": ["WELD NECK", "WN", "웰드넥"], "SLIP_ON": ["SLIP ON", "SO", "슬립온"], "BLIND": ["BLIND", "BL", "막음", "차단"], "SOCKET_WELD": ["SOCKET WELD", "소켓웰드"] }, "BOLT": { "HEX_BOLT": ["HEX BOLT", "HEXAGON", "육각 볼트"], "STUD_BOLT": ["STUD BOLT", "STUD", "스터드 볼트"], "U_BOLT": ["U-BOLT", "U BOLT", "유볼트"] }, "SUPPORT": { "URETHANE_BLOCK": ["URETHANE BLOCK", "BLOCK SHOE", "우레탄 블록"], "CLAMP": ["CLAMP", "클램프"], "HANGER": ["HANGER", "SUPPORT", "행거", "서포트"], "SPRING": ["SPRING", "스프링"] } } # Level 3: 연결/압력 키워드 (공용) LEVEL3_CONNECTION_KEYWORDS = { "SW": ["SW", "SOCKET WELD", "소켓웰드"], "THD": ["THD", "THREADED", "NPT", "나사"], "FL": ["FL", "FLANGED", "플랜지형"], "BW": ["BW", "BUTT WELD", "맞대기용접"] } LEVEL3_PRESSURE_KEYWORDS = ["150LB", "300LB", "600LB", "900LB", "1500LB", "2500LB", "3000LB", "6000LB"] # Level 4: 재질 키워드 (최후 판단) LEVEL4_MATERIAL_KEYWORDS = { "PIPE": ["A106", "A333", "A312", "A53"], "FITTING": ["A234", "A403", "A420"], "FLANGE": ["A182", "A350"], # A105 제거 (범용 재질로 이동) "VALVE": ["A216", "A217", "A351", "A352"], "BOLT": ["A193", "A194", "A320", "A325", "A490"] } # 범용 재질 (여러 타입에 사용 가능) GENERIC_MATERIALS = { "A105": ["VALVE", "FLANGE", "FITTING"], # 우선순위 순서 "316": ["VALVE", "FLANGE", "FITTING", "PIPE", "BOLT"], "304": ["VALVE", "FLANGE", "FITTING", "PIPE", "BOLT"] } def classify_material_integrated(description: str, main_nom: str = "", red_nom: str = "", length: float = None) -> Dict: """ 통합 자재 분류 함수 Args: description: 자재 설명 main_nom: 주 사이즈 red_nom: 축소 사이즈 (플랜지/피팅용) length: 길이 (파이프용) Returns: 분류 결과 딕셔너리 """ desc_upper = description.upper() # 최우선: SPECIAL 키워드 확인 (도면 업로드가 필요한 특수 자재) special_keywords = ['SPECIAL', '스페셜', 'SPEC', 'SPL'] for keyword in special_keywords: if keyword in desc_upper: return { "category": "SPECIAL", "confidence": 1.0, "evidence": [f"SPECIAL_KEYWORD: {keyword}"], "classification_level": "LEVEL0_SPECIAL", "reason": f"스페셜 키워드 발견: {keyword}" } # U-BOLT 및 관련 부품 우선 확인 (BOLT 카테고리보다 먼저) if ('U-BOLT' in desc_upper or 'U BOLT' in desc_upper or '유볼트' in desc_upper or 'URETHANE BLOCK' in desc_upper or 'BLOCK SHOE' in desc_upper or '우레탄' in desc_upper): return { "category": "U_BOLT", "confidence": 1.0, "evidence": ["U_BOLT_SYSTEM_KEYWORD"], "classification_level": "LEVEL0_U_BOLT", "reason": "U-BOLT 시스템 키워드 발견" } # 쉼표로 구분된 각 부분을 별도로 체크 (예: "NIPPLE, SMLS, SCH 80") desc_parts = [part.strip() for part in desc_upper.split(',')] # 1단계: Level 1 키워드로 타입 식별 detected_types = [] for material_type, keywords in LEVEL1_TYPE_KEYWORDS.items(): type_found = False # 긴 키워드부터 확인 (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)) type_found = True break # 각 부분에서도 정확히 매칭되는지 확인 for part in desc_parts: if keyword == part or keyword in part: detected_types.append((material_type, keyword)) type_found = True break if type_found: break # 2단계: 복수 타입 감지 시 Level 2로 구체화 if len(detected_types) > 1: # Level 2 키워드로 우선순위 결정 for material_type, subtype_dict in LEVEL2_SUBTYPE_KEYWORDS.items(): for subtype, keywords in subtype_dict.items(): for keyword in keywords: if keyword in desc_upper: return { "category": material_type, "confidence": 0.95, "evidence": [f"L1_KEYWORD: {detected_types}", f"L2_KEYWORD: {keyword}"], "classification_level": "LEVEL2" } # Level 2 키워드가 없으면 우선순위로 결정 # BOLT > SUPPORT > FITTING > VALVE > FLANGE > PIPE (볼트 우선, 더 구체적인 것 우선) type_priority = ["BOLT", "SUPPORT", "FITTING", "VALVE", "FLANGE", "PIPE", "GASKET", "INSTRUMENT"] for priority_type in type_priority: for detected_type, keyword in detected_types: if detected_type == priority_type: return { "category": priority_type, "confidence": 0.85, "evidence": [f"L1_MULTI_TYPE: {detected_types}", f"PRIORITY: {priority_type}"], "classification_level": "LEVEL1_PRIORITY" } # 3단계: 단일 타입 확정 또는 Level 3/4로 판단 if len(detected_types) == 1: material_type = detected_types[0][0] # FITTING으로 분류된 경우 상세 분류기 호출 if material_type == "FITTING": try: detailed_result = classify_fitting("", description, main_nom, red_nom, length) # 상세 분류 결과가 있으면 사용, 없으면 기본 FITTING 반환 if detailed_result and detailed_result.get("category"): return detailed_result except Exception as e: # 상세 분류 실패 시 기본 FITTING으로 처리 pass return { "category": material_type, "confidence": 0.9, "evidence": [f"L1_KEYWORD: {detected_types[0][1]}"], "classification_level": "LEVEL1" } # 4단계: Level 1 없으면 재질 기반 분류 if not detected_types: # 전용 재질 확인 for material_type, materials in LEVEL4_MATERIAL_KEYWORDS.items(): for material in materials: if material in desc_upper: # 볼트 재질(A193, A194)은 다른 키워드가 있는지 확인 if material_type == "BOLT": # 다른 타입 키워드가 있으면 볼트로 분류하지 않음 other_type_found = False for other_type, keywords in LEVEL1_TYPE_KEYWORDS.items(): if other_type != "BOLT": for keyword in keywords: if keyword in desc_upper: other_type_found = True break if other_type_found: break if other_type_found: continue # 볼트로 분류하지 않음 # FITTING으로 분류된 경우 상세 분류기 호출 if material_type == "FITTING": try: detailed_result = classify_fitting("", description, main_nom, red_nom, length) if detailed_result and detailed_result.get("category"): return detailed_result except Exception as e: pass return { "category": material_type, "confidence": 0.35, # 재질만으로 분류 시 낮은 신뢰도 "evidence": [f"L4_MATERIAL: {material}"], "classification_level": "LEVEL4" } # 범용 재질 확인 for material, priority_types in GENERIC_MATERIALS.items(): if material in desc_upper: # 우선순위에 따라 타입 결정 material_type = priority_types[0] # 첫 번째 우선순위 # FITTING으로 분류된 경우 상세 분류기 호출 if material_type == "FITTING": try: detailed_result = classify_fitting("", description, main_nom, red_nom, length) if detailed_result and detailed_result.get("category"): return detailed_result except Exception as e: pass return { "category": material_type, "confidence": 0.3, "evidence": [f"GENERIC_MATERIAL: {material}"], "classification_level": "LEVEL4_GENERIC" } # 분류 실패 return { "category": "UNKNOWN", "confidence": 0.0, "evidence": ["NO_CLASSIFICATION_POSSIBLE"], "classification_level": "NONE" } def should_exclude_material(description: str) -> bool: """ 제외 대상 자재인지 확인 """ exclude_keywords = [ "DUMMY", "RESERVED", "SPARE", "DELETED", "CANCELED", "더미", "예비", "삭제", "취소", "예약" ] desc_upper = description.upper() return any(keyword in desc_upper for keyword in exclude_keywords)