Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 사용자 요구사항 저장/로드/엑셀 내보내기 기능 완전 구현 - 백엔드 API 수정: Request Body 방식으로 변경 - 데이터베이스 스키마: material_id 컬럼 추가 - 프론트엔드 상태 관리 개선: 저장 후 자동 리로드 - 입력 필드 연결 문제 해결: 누락된 onChange 핸들러 추가 - NewMaterialsPage에 '전체' 카테고리 버튼 추가 (기본 선택) - Docker 환경 개선: 프론트엔드 볼륨 마운트 및 포트 수정 - UI 개선: 벌레 이모지 제거, 디버그 코드 정리
255 lines
8.6 KiB
Python
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)
|
|
}
|