""" PIPE 분류 전용 모듈 재질 분류 + 파이프 특화 분류 """ import re from typing import Dict, List, Optional from .material_classifier import classify_material, get_manufacturing_method_from_material # ========== PIPE 제조 방법별 분류 ========== PIPE_MANUFACTURING = { "SEAMLESS": { "keywords": ["SEAMLESS", "SMLS", "심리스", "무계목"], "confidence": 0.95, "characteristics": "이음새 없는 고품질 파이프" }, "WELDED": { "keywords": ["WELDED", "WLD", "ERW", "SAW", "용접", "전기저항용접"], "confidence": 0.95, "characteristics": "용접으로 제조된 파이프" }, "CAST": { "keywords": ["CAST", "주조"], "confidence": 0.85, "characteristics": "주조로 제조된 파이프" } } # ========== PIPE 끝 가공별 분류 ========== PIPE_END_PREP = { "BOTH_ENDS_BEVELED": { "codes": ["BBE", "BOE", "BOTH END", "BOTH BEVELED", "양쪽개선"], "cutting_note": "양쪽 개선", "machining_required": True, "confidence": 0.95 }, "ONE_END_BEVELED": { "codes": ["BE", "BEV", "PBE", "PIPE BEVELED END", "POE"], "cutting_note": "한쪽 개선", "machining_required": True, "confidence": 0.95 }, "NO_BEVEL": { "codes": ["PE", "PLAIN END", "PPE", "평단", "무개선"], "cutting_note": "무 개선", "machining_required": False, "confidence": 0.95 }, "THREADED": { "codes": ["TOE", "THE", "THREADED", "나사", "스레드"], "cutting_note": "나사 가공", "machining_required": True, "confidence": 0.90 } } # ========== 구매용 파이프 분류 (끝단 가공 제외) ========== def get_purchase_pipe_description(description: str) -> str: """구매용 파이프 설명 - 끝단 가공 정보 제거""" # 모든 끝단 가공 코드들을 수집 end_prep_codes = [] for prep_data in PIPE_END_PREP.values(): end_prep_codes.extend(prep_data["codes"]) # 설명에서 끝단 가공 코드 제거 clean_description = description.upper() # 끝단 가공 코드들을 길이 순으로 정렬 (긴 것부터 처리) end_prep_codes.sort(key=len, reverse=True) for code in end_prep_codes: # 단어 경계를 고려하여 제거 (부분 매칭 방지) pattern = r'\b' + re.escape(code) + r'\b' clean_description = re.sub(pattern, '', clean_description, flags=re.IGNORECASE) # 끝단 가공 관련 패턴들 추가 제거 # BOE-POE, POE-TOE 같은 조합 패턴들 end_prep_patterns = [ r'\b[A-Z]{2,3}E-[A-Z]{2,3}E\b', # BOE-POE, POE-TOE 등 r'\b[A-Z]{2,3}E-[A-Z]{2,3}\b', # BOE-TO, POE-TO 등 r'\b[A-Z]{2,3}-[A-Z]{2,3}E\b', # BO-POE, PO-TOE 등 r'\b[A-Z]{2,3}-[A-Z]{2,3}\b', # BO-PO, PO-TO 등 ] for pattern in end_prep_patterns: clean_description = re.sub(pattern, '', clean_description, flags=re.IGNORECASE) # 남은 하이픈과 공백 정리 clean_description = re.sub(r'\s*-\s*', ' ', clean_description) # 하이픈 제거 clean_description = re.sub(r'\s+', ' ', clean_description).strip() # 연속 공백 정리 return clean_description def extract_end_preparation_info(description: str) -> Dict: """파이프 설명에서 끝단 가공 정보 추출""" desc_upper = description.upper() # 끝단 가공 코드 찾기 for prep_type, prep_data in PIPE_END_PREP.items(): for code in prep_data["codes"]: if code in desc_upper: return { "end_preparation_type": prep_type, "end_preparation_code": code, "machining_required": prep_data["machining_required"], "cutting_note": prep_data["cutting_note"], "confidence": prep_data["confidence"], "matched_pattern": code, "original_description": description, "clean_description": get_purchase_pipe_description(description) } # 기본값: PBE (양쪽 무개선) return { "end_preparation_type": "NO_BEVEL", # PBE로 매핑될 예정 "end_preparation_code": "PBE", "machining_required": False, "cutting_note": "양쪽 무개선 (기본값)", "confidence": 0.5, "matched_pattern": "DEFAULT", "original_description": description, "clean_description": get_purchase_pipe_description(description) } # ========== PIPE 스케줄별 분류 ========== PIPE_SCHEDULE = { "patterns": [ r"SCH\s*(\d+)", r"SCHEDULE\s*(\d+)", r"스케줄\s*(\d+)" ], "common_schedules": ["10", "20", "40", "80", "120", "160"], "wall_thickness_patterns": [ r"(\d+(?:\.\d+)?)\s*mm\s*THK", r"(\d+(?:\.\d+)?)\s*THK" ] } def classify_pipe_for_purchase(dat_file: str, description: str, main_nom: str, length: Optional[float] = None) -> Dict: """구매용 파이프 분류 - 끝단 가공 정보 제외""" # 끝단 가공 정보 제거한 설명으로 분류 clean_description = get_purchase_pipe_description(description) # 기본 파이프 분류 수행 result = classify_pipe(dat_file, clean_description, main_nom, length) # 구매용임을 표시 result["purchase_classification"] = True result["original_description"] = description result["clean_description"] = clean_description return result def classify_pipe(dat_file: str, description: str, main_nom: str, length: Optional[float] = None) -> Dict: """ 완전한 PIPE 분류 Args: dat_file: DAT_FILE 필드 description: DESCRIPTION 필드 main_nom: MAIN_NOM 필드 (사이즈) length: LENGTH 필드 (절단 치수) Returns: 완전한 파이프 분류 결과 """ desc_upper = description.upper() # 1. 명칭 우선 확인 (다른 자재 타입 키워드가 있으면 파이프가 아님) other_material_keywords = [ 'FLG', 'FLANGE', '플랜지', # 플랜지 'ELBOW', 'ELL', 'TEE', 'REDUCER', 'CAP', 'COUPLING', '엘보', '티', '리듀서', # 피팅 'VALVE', 'BALL', 'GATE', 'GLOBE', 'CHECK', '밸브', # 밸브 'BOLT', 'STUD', 'NUT', 'SCREW', '볼트', '너트', # 볼트 'GASKET', 'GASK', '가스켓', # 가스켓 'GAUGE', 'SENSOR', 'TRANSMITTER', 'INSTRUMENT', '계기' # 계기 ] for keyword in other_material_keywords: if keyword in desc_upper: return { "category": "UNKNOWN", "overall_confidence": 0.0, "reason": f"다른 자재 키워드 발견: {keyword}" } # 2. 파이프 키워드 확인 pipe_keywords = ['PIPE', 'TUBE', '파이프', '배관', 'SMLS', 'SEAMLESS'] has_pipe_keyword = any(keyword in desc_upper for keyword in pipe_keywords) # 파이프 재질 확인 (A106, A333, A312, A53) pipe_materials = ['A106', 'A333', 'A312', 'A53'] has_pipe_material = any(material in desc_upper for material in pipe_materials) # 파이프 키워드도 없고 파이프 재질도 없으면 UNKNOWN if not has_pipe_keyword and not has_pipe_material: return { "category": "UNKNOWN", "overall_confidence": 0.0, "reason": "파이프 키워드 및 재질 없음" } # 3. 재질 분류 (공통 모듈 사용) material_result = classify_material(description) # 2. 제조 방법 분류 manufacturing_result = classify_pipe_manufacturing(description, material_result) # 3. 끝 가공 분류 end_prep_result = classify_pipe_end_preparation(description) # 4. 스케줄 분류 schedule_result = classify_pipe_schedule(description) # 5. 길이(절단 치수) 처리 length_info = extract_pipe_length_info(length, description) # 6. 최종 결과 조합 return { "category": "PIPE", # 재질 정보 (공통 모듈) "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) }, # 파이프 특화 정보 "manufacturing": { "method": manufacturing_result.get('method', 'UNKNOWN'), "confidence": manufacturing_result.get('confidence', 0.0), "evidence": manufacturing_result.get('evidence', []) }, "end_preparation": { "type": end_prep_result.get('type', 'UNKNOWN'), "cutting_note": end_prep_result.get('cutting_note', ''), "machining_required": end_prep_result.get('machining_required', False), "confidence": end_prep_result.get('confidence', 0.0) }, "schedule": { "schedule": schedule_result.get('schedule', 'UNKNOWN'), "wall_thickness": schedule_result.get('wall_thickness', ''), "confidence": schedule_result.get('confidence', 0.0) }, "length_info": length_info, "size_info": { "nominal_size": main_nom, "length_mm": length_info.get('length_mm') }, # 전체 신뢰도 "overall_confidence": calculate_pipe_confidence({ "material": material_result.get('confidence', 0), "manufacturing": manufacturing_result.get('confidence', 0), "end_prep": end_prep_result.get('confidence', 0), "schedule": schedule_result.get('confidence', 0) }) } def classify_pipe_manufacturing(description: str, material_result: Dict) -> Dict: """파이프 제조 방법 분류""" desc_upper = description.upper() # 1. DESCRIPTION 키워드 우선 확인 for method, method_data in PIPE_MANUFACTURING.items(): for keyword in method_data["keywords"]: if keyword in desc_upper: return { "method": method, "confidence": method_data["confidence"], "evidence": [f"KEYWORD: {keyword}"], "characteristics": method_data["characteristics"] } # 2. 재질 규격으로 추정 material_manufacturing = get_manufacturing_method_from_material(material_result) if material_manufacturing in ["SEAMLESS", "WELDED"]: return { "method": material_manufacturing, "confidence": 0.8, "evidence": [f"MATERIAL_STANDARD: {material_result.get('standard')}"], "characteristics": PIPE_MANUFACTURING.get(material_manufacturing, {}).get("characteristics", "") } # 3. 기본값 return { "method": "UNKNOWN", "confidence": 0.0, "evidence": ["NO_MANUFACTURING_INFO"] } def classify_pipe_end_preparation(description: str) -> Dict: """파이프 끝 가공 분류""" desc_upper = description.upper() # 우선순위: 양쪽 > 한쪽 > 무개선 for prep_type, prep_data in PIPE_END_PREP.items(): for code in prep_data["codes"]: if code in desc_upper: return { "type": prep_type, "cutting_note": prep_data["cutting_note"], "machining_required": prep_data["machining_required"], "confidence": prep_data["confidence"], "matched_code": code } # 기본값: 무개선 return { "type": "NO_BEVEL", "cutting_note": "무 개선 (기본값)", "machining_required": False, "confidence": 0.5, "matched_code": "DEFAULT" } def classify_pipe_schedule(description: str) -> Dict: """파이프 스케줄 분류""" desc_upper = description.upper() # 1. 스케줄 패턴 확인 for pattern in PIPE_SCHEDULE["patterns"]: match = re.search(pattern, desc_upper) if match: schedule_num = match.group(1) return { "schedule": f"SCH {schedule_num}", "schedule_number": schedule_num, "confidence": 0.95, "matched_pattern": pattern } # 2. 두께 패턴 확인 for pattern in PIPE_SCHEDULE["wall_thickness_patterns"]: match = re.search(pattern, desc_upper) if match: thickness = match.group(1) return { "schedule": f"{thickness}mm THK", "wall_thickness": f"{thickness}mm", "confidence": 0.9, "matched_pattern": pattern } # 3. 기본값 return { "schedule": "UNKNOWN", "confidence": 0.0 } def extract_pipe_length_info(length: Optional[float], description: str) -> Dict: """파이프 길이(절단 치수) 정보 추출""" length_info = { "length_mm": None, "source": None, "confidence": 0.0, "note": "" } # 1. LENGTH 필드에서 추출 (우선) if length and length > 0: length_info.update({ "length_mm": round(length, 1), "source": "LENGTH_FIELD", "confidence": 0.95, "note": f"도면 명기 길이: {length}mm" }) # 2. DESCRIPTION에서 백업 추출 else: desc_length = extract_length_from_description(description) if desc_length: length_info.update({ "length_mm": desc_length, "source": "DESCRIPTION_PARSED", "confidence": 0.8, "note": f"설명란에서 추출: {desc_length}mm" }) else: length_info.update({ "source": "NO_LENGTH_INFO", "confidence": 0.0, "note": "길이 정보 없음 - 도면 확인 필요" }) return length_info def extract_length_from_description(description: str) -> Optional[float]: """DESCRIPTION에서 길이 정보 추출""" length_patterns = [ r'(\d+(?:\.\d+)?)\s*mm', r'(\d+(?:\.\d+)?)\s*M', r'(\d+(?:\.\d+)?)\s*LG', r'L\s*=\s*(\d+(?:\.\d+)?)', r'길이\s*(\d+(?:\.\d+)?)' ] for pattern in length_patterns: match = re.search(pattern, description.upper()) if match: return float(match.group(1)) return None def calculate_pipe_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.4, "manufacturing": 0.25, "end_prep": 0.2, "schedule": 0.15 } weighted_sum = sum( confidence_scores.get(key, 0) * weight for key, weight in weights.items() ) return round(weighted_sum, 2) def generate_pipe_cutting_plan(pipe_data: Dict) -> Dict: """파이프 절단 계획 생성""" cutting_plan = { "material_spec": f"{pipe_data['material']['grade']} {pipe_data['schedule']['schedule']}", "length_mm": pipe_data['length_info']['length_mm'], "end_preparation": pipe_data['end_preparation']['cutting_note'], "machining_required": pipe_data['end_preparation']['machining_required'] } # 절단 지시서 생성 if cutting_plan["length_mm"]: cutting_plan["cutting_instruction"] = f""" 재질: {cutting_plan['material_spec']} 절단길이: {cutting_plan['length_mm']}mm 끝가공: {cutting_plan['end_preparation']} 가공여부: {'베벨가공 필요' if cutting_plan['machining_required'] else '직각절단만'} """.strip() else: cutting_plan["cutting_instruction"] = "도면 확인 후 길이 정보 입력 필요" return cutting_plan