""" 전체 재질명 추출기 원본 설명에서 완전한 재질명을 추출하여 축약되지 않은 형태로 제공 """ import re from typing import Optional, Dict def extract_full_material_grade(description: str) -> str: """ 원본 설명에서 전체 재질명 추출 Args: description: 원본 자재 설명 Returns: 전체 재질명 (예: "ASTM A312 TP304", "ASTM A106 GR B") """ if not description: return "" desc_upper = description.upper().strip() # 1. ASTM 규격 패턴들 (가장 구체적인 것부터) astm_patterns = [ # ASTM A312 TP304, ASTM A312 TP316L 등 r'ASTM\s+A\d{3,4}[A-Z]*\s+TP\d+[A-Z]*', # ASTM A182 F304, ASTM A182 F316L 등 r'ASTM\s+A\d{3,4}[A-Z]*\s+F\d+[A-Z]*', # ASTM A403 WP304, ASTM A234 WPB 등 r'ASTM\s+A\d{3,4}[A-Z]*\s+WP[A-Z0-9]+', # ASTM A351 CF8M, ASTM A216 WCB 등 r'ASTM\s+A\d{3,4}[A-Z]*\s+[A-Z]{2,4}[0-9]*[A-Z]*', # ASTM A106 GR B, ASTM A105 등 - GR 포함 r'ASTM\s+A\d{3,4}[A-Z]*\s+GR\s+[A-Z0-9]+', r'ASTM\s+A\d{3,4}[A-Z]*\s+GRADE\s+[A-Z0-9]+', # ASTM A106 B (GR 없이 바로 등급) - 단일 문자 등급 r'ASTM\s+A\d{3,4}[A-Z]*\s+[A-Z](?=\s|$)', # ASTM A105, ASTM A234 등 (등급 없는 경우) r'ASTM\s+A\d{3,4}[A-Z]*(?!\s+[A-Z0-9])', # 2자리 ASTM 규격도 지원 (A10, A36 등) r'ASTM\s+A\d{2}(?:\s+GR\s+[A-Z0-9]+)?', ] for pattern in astm_patterns: match = re.search(pattern, desc_upper) if match: full_grade = match.group(0).strip() # 추가 정보가 있는지 확인 (PBE, BBE 등은 제외) end_pos = match.end() remaining = desc_upper[end_pos:].strip() # 끝단 가공 정보는 제외 end_prep_codes = ['PBE', 'BBE', 'POE', 'BOE', 'TOE'] for code in end_prep_codes: remaining = re.sub(rf'\b{code}\b', '', remaining).strip() # 남은 재질 관련 정보가 있으면 추가 additional_info = [] if remaining: # 일반적인 재질 추가 정보 패턴 additional_patterns = [ r'\bH\b', # H (고온용) r'\bL\b', # L (저탄소) r'\bN\b', # N (질소 첨가) r'\bS\b', # S (황 첨가) r'\bMOD\b', # MOD (개량형) ] for add_pattern in additional_patterns: if re.search(add_pattern, remaining): additional_info.append(re.search(add_pattern, remaining).group(0)) if additional_info: full_grade += ' ' + ' '.join(additional_info) return full_grade # 2. ASME 규격 패턴들 asme_patterns = [ r'ASME\s+SA\d+[A-Z]*\s+TP\d+[A-Z]*(?:\s+[A-Z]+)*', r'ASME\s+SA\d+[A-Z]*\s+GR\s+[A-Z0-9]+(?:\s+[A-Z]+)*', r'ASME\s+SA\d+[A-Z]*\s+F\d+[A-Z]*(?:\s+[A-Z]+)*', r'ASME\s+SA\d+[A-Z]*(?!\s+[A-Z0-9])', ] for pattern in asme_patterns: match = re.search(pattern, desc_upper) if match: return match.group(0).strip() # 3. KS 규격 패턴들 ks_patterns = [ r'KS\s+D\d+[A-Z]*\s+[A-Z0-9]+(?:\s+[A-Z]+)*', r'KS\s+D\d+[A-Z]*(?!\s+[A-Z0-9])', ] for pattern in ks_patterns: match = re.search(pattern, desc_upper) if match: return match.group(0).strip() # 4. JIS 규격 패턴들 jis_patterns = [ r'JIS\s+[A-Z]\d+[A-Z]*\s+[A-Z0-9]+(?:\s+[A-Z]+)*', r'JIS\s+[A-Z]\d+[A-Z]*(?!\s+[A-Z0-9])', ] for pattern in jis_patterns: match = re.search(pattern, desc_upper) if match: return match.group(0).strip() # 5. 특수 재질 패턴들 special_patterns = [ # Inconel, Hastelloy 등 r'INCONEL\s+\d+[A-Z]*', r'HASTELLOY\s+[A-Z]\d*[A-Z]*', r'MONEL\s+\d+[A-Z]*', # Titanium r'TITANIUM\s+GRADE\s+\d+[A-Z]*', r'TI\s+GR\s*\d+[A-Z]*', # 듀플렉스 스테인리스 r'DUPLEX\s+\d+[A-Z]*', r'SUPER\s+DUPLEX\s+\d+[A-Z]*', ] for pattern in special_patterns: match = re.search(pattern, desc_upper) if match: return match.group(0).strip() # 6. 일반 스테인리스 패턴들 (숫자만) stainless_patterns = [ r'\b(304L?|316L?|321|347|310|317L?|904L?|254SMO)\b', r'\bSS\s*(304L?|316L?|321|347|310|317L?|904L?|254SMO)\b', r'\bSUS\s*(304L?|316L?|321|347|310|317L?|904L?|254SMO)\b', ] for pattern in stainless_patterns: match = re.search(pattern, desc_upper) if match: grade = match.group(1) if match.groups() else match.group(0) if grade.startswith(('SS', 'SUS')): return grade else: return f"SS{grade}" # 7. 탄소강 패턴들 carbon_patterns = [ r'\bSM\d+[A-Z]*\b', # SM400, SM490 등 r'\bSS\d+[A-Z]*\b', # SS400, SS490 등 (스테인리스와 구분) r'\bS\d+C\b', # S45C, S50C 등 ] for pattern in carbon_patterns: match = re.search(pattern, desc_upper) if match: return match.group(0).strip() # 8. 기존 material_grade가 있으면 그대로 반환 # (분류기에서 이미 처리된 경우) return "" def update_full_material_grades(db, batch_size: int = 1000) -> Dict: """ 기존 자재들의 full_material_grade 업데이트 Args: db: 데이터베이스 세션 batch_size: 배치 처리 크기 Returns: 업데이트 결과 통계 """ from sqlalchemy import text try: # 전체 자재 수 조회 count_query = text("SELECT COUNT(*) FROM materials WHERE full_material_grade IS NULL OR full_material_grade = ''") total_count = db.execute(count_query).scalar() print(f"📊 업데이트 대상 자재: {total_count}개") updated_count = 0 processed_count = 0 # 배치 단위로 처리 offset = 0 while offset < total_count: # 배치 조회 select_query = text(""" SELECT id, original_description, material_grade FROM materials WHERE full_material_grade IS NULL OR full_material_grade = '' ORDER BY id LIMIT :limit OFFSET :offset """) results = db.execute(select_query, {"limit": batch_size, "offset": offset}).fetchall() if not results: break # 배치 업데이트 for material_id, original_description, current_grade in results: full_grade = extract_full_material_grade(original_description) # 전체 재질명이 추출되지 않으면 기존 grade 사용 if not full_grade and current_grade: full_grade = current_grade if full_grade: update_query = text(""" UPDATE materials SET full_material_grade = :full_grade WHERE id = :material_id """) db.execute(update_query, { "full_grade": full_grade, "material_id": material_id }) updated_count += 1 processed_count += 1 # 배치 커밋 db.commit() offset += batch_size print(f"📈 진행률: {processed_count}/{total_count} ({processed_count/total_count*100:.1f}%)") return { "total_processed": processed_count, "updated_count": updated_count, "success": True } except Exception as e: db.rollback() print(f"❌ 업데이트 실패: {str(e)}") return { "total_processed": 0, "updated_count": 0, "success": False, "error": str(e) }