Files
TK-BOM-Project/backend/app/services/material_grade_extractor.py
Hyungi Ahn 50570e4624
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 사용자 요구사항 기능 완전 구현 및 전체 카테고리 추가
- 사용자 요구사항 저장/로드/엑셀 내보내기 기능 완전 구현
- 백엔드 API 수정: Request Body 방식으로 변경
- 데이터베이스 스키마: material_id 컬럼 추가
- 프론트엔드 상태 관리 개선: 저장 후 자동 리로드
- 입력 필드 연결 문제 해결: 누락된 onChange 핸들러 추가
- NewMaterialsPage에 '전체' 카테고리 버튼 추가 (기본 선택)
- Docker 환경 개선: 프론트엔드 볼륨 마운트 및 포트 수정
- UI 개선: 벌레 이모지 제거, 디버그 코드 정리
2025-09-30 08:55:20 +09:00

255 lines
8.6 KiB
Python

"""
전체 재질명 추출기
원본 설명에서 완전한 재질명을 추출하여 축약되지 않은 형태로 제공
"""
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 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)
}