feat(tkeg): tkeg BOM 자재관리 서비스 초기 세팅 (api + web + docker-compose)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
263
tkeg/api/app/services/material_grade_extractor.py
Normal file
263
tkeg/api/app/services/material_grade_extractor.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
전체 재질명 추출기
|
||||
원본 설명에서 완전한 재질명을 추출하여 축약되지 않은 형태로 제공
|
||||
"""
|
||||
|
||||
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 = [
|
||||
# A320 L7, A325, A490 등 단독 규격 (ASTM 없이)
|
||||
r'\bA320\s+L[0-9]+\b', # A320 L7
|
||||
r'\bA325\b', # A325
|
||||
r'\bA490\b', # A490
|
||||
# ASTM A193/A194 GR B7/2H (볼트용 조합 패턴) - 최우선
|
||||
r'ASTM\s+A193/A194\s+GR\s+[A-Z0-9/]+',
|
||||
r'ASTM\s+A193/A194\s+[A-Z0-9/]+',
|
||||
# ASTM A320/A194M GR B8/8 (저온용 볼트 조합 패턴)
|
||||
r'ASTM\s+A320[M]?/A194[M]?\s+GR\s+[A-Z0-9/]+',
|
||||
r'ASTM\s+A320[M]?/A194[M]?\s+[A-Z0-9/]+',
|
||||
# 단독 A193/A194 패턴 (ASTM 없이)
|
||||
r'\bA193/A194\s+GR\s+[A-Z0-9/]+\b',
|
||||
r'\bA193/A194\s+[A-Z0-9/]+\b',
|
||||
# 단독 A320/A194M 패턴 (ASTM 없이)
|
||||
r'\bA320[M]?/A194[M]?\s+GR\s+[A-Z0-9/]+\b',
|
||||
r'\bA320[M]?/A194[M]?\s+[A-Z0-9/]+\b',
|
||||
# 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)
|
||||
}
|
||||
Reference in New Issue
Block a user