Files
TK-BOM-Project/backend/app/services/support_classifier.py
hyungi 6b6360ecd5
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 서포트 카테고리 전면 개선
- 서포트 카테고리 UI 개선: 좌우 스크롤, 헤더/본문 동기화, 가운데 정렬
- 동일 항목 합산 기능 구현 (Type + Size + Grade 기준)
- 헤더 구조 변경: 압력/스케줄 제거, 구매수량 단일화, User Requirements 추가
- 우레탄 블럭슈 두께 정보(40t, 27t) Material Grade에 포함
- 서포트 수량 계산 수정: 취합된 숫자 그대로 표시 (4의 배수 계산 제거)
- 서포트 분류 로직 개선: CLAMP, U-BOLT, URETHANE BLOCK SHOE 등 정확한 분류
- 백엔드 서포트 분류기에 User Requirements 추출 기능 추가
- 엑셀 내보내기에 서포트 카테고리 처리 로직 추가
2025-10-17 07:59:35 +09:00

330 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
SUPPORT 분류 시스템
배관 지지재, 우레탄 블록, 클램프 등 지지 부품 분류
"""
import re
from typing import Dict, List, Optional
from .material_classifier import classify_material
# ========== 서포트 타입별 분류 ==========
SUPPORT_TYPES = {
"URETHANE_BLOCK": {
"dat_file_patterns": ["URETHANE", "BLOCK", "SHOE"],
"description_keywords": ["URETHANE BLOCK", "BLOCK SHOE", "우레탄 블록", "우레탄", "URETHANE"],
"characteristics": "우레탄 블록 슈",
"applications": "배관 지지, 진동 흡수",
"material_type": "URETHANE"
},
"CLAMP": {
"dat_file_patterns": ["CLAMP", "CL-"],
"description_keywords": ["CLAMP", "클램프", "CL-1", "CL-2", "CL-3"],
"characteristics": "배관 클램프",
"applications": "배관 고정, 지지",
"material_type": "STEEL"
},
"HANGER": {
"dat_file_patterns": ["HANGER", "HANG", "SUPP"],
"description_keywords": ["HANGER", "SUPPORT", "행거", "서포트", "PIPE HANGER"],
"characteristics": "배관 행거",
"applications": "배관 매달기, 지지",
"material_type": "STEEL"
},
"SPRING_HANGER": {
"dat_file_patterns": ["SPRING", "SPR_"],
"description_keywords": ["SPRING HANGER", "SPRING", "스프링", "스프링 행거"],
"characteristics": "스프링 행거",
"applications": "가변 하중 지지",
"material_type": "STEEL"
},
"GUIDE": {
"dat_file_patterns": ["GUIDE", "GD_"],
"description_keywords": ["GUIDE", "가이드", "PIPE GUIDE"],
"characteristics": "배관 가이드",
"applications": "배관 방향 제어",
"material_type": "STEEL"
},
"ANCHOR": {
"dat_file_patterns": ["ANCHOR", "ANCH"],
"description_keywords": ["ANCHOR", "앵커", "PIPE ANCHOR"],
"characteristics": "배관 앵커",
"applications": "배관 고정점",
"material_type": "STEEL"
}
}
# ========== 하중 등급 분류 ==========
LOAD_RATINGS = {
"LIGHT": {
"patterns": [r"(\d+)T", r"(\d+)TON"],
"range": (0, 5), # 5톤 이하
"description": "경하중용"
},
"MEDIUM": {
"patterns": [r"(\d+)T", r"(\d+)TON"],
"range": (5, 20), # 5-20톤
"description": "중하중용"
},
"HEAVY": {
"patterns": [r"(\d+)T", r"(\d+)TON"],
"range": (20, 100), # 20-100톤
"description": "중하중용"
}
}
def classify_support(dat_file: str, description: str, main_nom: str,
length: Optional[float] = None) -> Dict:
"""
SUPPORT 분류 메인 함수
Args:
dat_file: DAT 파일명
description: 자재 설명
main_nom: 주 사이즈
length: 길이 (옵션)
Returns:
분류 결과 딕셔너리
"""
dat_upper = dat_file.upper()
desc_upper = description.upper()
combined_text = f"{dat_upper} {desc_upper}"
# 1. 서포트 타입 분류
support_type_result = classify_support_type(dat_file, description)
# 2. 재질 분류 (공통 모듈 사용)
material_result = classify_material(description)
# 3. 하중 등급 분류
load_result = classify_load_rating(description)
# 4. 사이즈 정보 추출
size_result = extract_support_size(description, main_nom)
# 5. 사용자 요구사항 추출
user_requirements = extract_support_user_requirements(description)
# 6. 우레탄 블럭슈 두께 정보 추출 및 Material Grade 보강
enhanced_material_grade = material_result.get('grade', 'UNKNOWN')
if support_type_result.get("support_type") == "URETHANE_BLOCK":
# 두께 정보 추출 (40t, 27t 등)
thickness_match = re.search(r'(\d+)\s*[tT](?![oO])', description.upper())
if thickness_match:
thickness = f"{thickness_match.group(1)}t"
if enhanced_material_grade == 'UNKNOWN' or not enhanced_material_grade:
enhanced_material_grade = thickness
elif thickness not in enhanced_material_grade:
enhanced_material_grade = f"{enhanced_material_grade} {thickness}"
# 7. 최종 결과 조합
return {
"category": "SUPPORT",
# 서포트 특화 정보
"support_type": support_type_result.get("support_type", "UNKNOWN"),
"support_subtype": support_type_result.get("subtype", ""),
"load_rating": load_result.get("load_rating", ""),
"load_capacity": load_result.get("capacity", ""),
# 재질 정보 (공통 모듈) - 우레탄 블럭슈 두께 정보 포함
"material": {
"standard": material_result.get('standard', 'UNKNOWN'),
"grade": enhanced_material_grade,
"material_type": material_result.get('material_type', 'UNKNOWN'),
"confidence": material_result.get('confidence', 0.0)
},
# 사이즈 정보
"size_info": size_result,
# 사용자 요구사항
"user_requirements": user_requirements,
# 전체 신뢰도
"overall_confidence": calculate_support_confidence({
"type": support_type_result.get('confidence', 0),
"material": material_result.get('confidence', 0),
"load": load_result.get('confidence', 0),
"size": size_result.get('confidence', 0)
}),
# 증거
"evidence": [
f"SUPPORT_TYPE: {support_type_result.get('support_type', 'UNKNOWN')}",
f"MATERIAL: {material_result.get('standard', 'UNKNOWN')}",
f"LOAD: {load_result.get('load_rating', 'UNKNOWN')}"
]
}
def classify_support_type(dat_file: str, description: str) -> Dict:
"""서포트 타입 분류"""
dat_upper = dat_file.upper()
desc_upper = description.upper()
combined_text = f"{dat_upper} {desc_upper}"
for support_type, type_data in SUPPORT_TYPES.items():
# DAT 파일 패턴 확인
for pattern in type_data["dat_file_patterns"]:
if pattern in dat_upper:
return {
"support_type": support_type,
"subtype": type_data["characteristics"],
"applications": type_data["applications"],
"confidence": 0.95,
"evidence": [f"DAT_PATTERN: {pattern}"]
}
# 설명 키워드 확인
for keyword in type_data["description_keywords"]:
if keyword in desc_upper:
return {
"support_type": support_type,
"subtype": type_data["characteristics"],
"applications": type_data["applications"],
"confidence": 0.9,
"evidence": [f"DESC_KEYWORD: {keyword}"]
}
return {
"support_type": "UNKNOWN",
"subtype": "",
"applications": "",
"confidence": 0.0,
"evidence": ["NO_SUPPORT_TYPE_FOUND"]
}
def extract_support_user_requirements(description: str) -> List[str]:
"""서포트 사용자 요구사항 추출"""
desc_upper = description.upper()
requirements = []
# 표면처리 관련
if 'GALV' in desc_upper or 'GALVANIZED' in desc_upper:
requirements.append('GALVANIZED')
if 'HDG' in desc_upper or 'HOT DIP' in desc_upper:
requirements.append('HOT DIP GALVANIZED')
if 'PAINT' in desc_upper or 'PAINTED' in desc_upper:
requirements.append('PAINTED')
# 재질 관련
if 'SS' in desc_upper or 'STAINLESS' in desc_upper:
requirements.append('STAINLESS STEEL')
if 'CARBON' in desc_upper:
requirements.append('CARBON STEEL')
# 특수 요구사항
if 'FIRE SAFE' in desc_upper:
requirements.append('FIRE SAFE')
if 'SEISMIC' in desc_upper or '내진' in desc_upper:
requirements.append('SEISMIC')
return requirements
def classify_load_rating(description: str) -> Dict:
"""하중 등급 분류"""
desc_upper = description.upper()
# 하중 패턴 찾기 (40T, 50TON 등)
for rating, rating_data in LOAD_RATINGS.items():
for pattern in rating_data["patterns"]:
match = re.search(pattern, desc_upper)
if match:
capacity = int(match.group(1))
min_load, max_load = rating_data["range"]
if min_load <= capacity <= max_load:
return {
"load_rating": rating,
"capacity": f"{capacity}T",
"description": rating_data["description"],
"confidence": 0.9,
"evidence": [f"LOAD_PATTERN: {match.group(0)}"]
}
# 특정 하중 값이 있지만 등급을 모르는 경우
load_match = re.search(r'(\d+)\s*[T톤]', desc_upper)
if load_match:
capacity = int(load_match.group(1))
return {
"load_rating": "CUSTOM",
"capacity": f"{capacity}T",
"description": f"{capacity}톤 하중",
"confidence": 0.7,
"evidence": [f"CUSTOM_LOAD: {load_match.group(0)}"]
}
return {
"load_rating": "UNKNOWN",
"capacity": "",
"description": "",
"confidence": 0.0,
"evidence": ["NO_LOAD_RATING_FOUND"]
}
def extract_support_size(description: str, main_nom: str) -> Dict:
"""서포트 사이즈 정보 추출"""
desc_upper = description.upper()
# 파이프 사이즈 (서포트가 지지하는 파이프 크기)
pipe_size = main_nom if main_nom else ""
# 서포트 자체 치수 (길이x폭x높이 등)
dimension_patterns = [
r'(\d+)\s*[X×]\s*(\d+)\s*[X×]\s*(\d+)', # 100x50x20
r'(\d+)\s*[X×]\s*(\d+)', # 100x50
r'L\s*(\d+)', # L100 (길이)
r'W\s*(\d+)', # W50 (폭)
r'H\s*(\d+)' # H20 (높이)
]
dimensions = {}
for pattern in dimension_patterns:
match = re.search(pattern, desc_upper)
if match:
if len(match.groups()) == 3:
dimensions = {
"length": f"{match.group(1)}mm",
"width": f"{match.group(2)}mm",
"height": f"{match.group(3)}mm"
}
elif len(match.groups()) == 2:
dimensions = {
"length": f"{match.group(1)}mm",
"width": f"{match.group(2)}mm"
}
break
return {
"pipe_size": pipe_size,
"dimensions": dimensions,
"confidence": 0.8 if dimensions else 0.3
}
def calculate_support_confidence(confidence_scores: Dict) -> float:
"""서포트 분류 전체 신뢰도 계산"""
weights = {
"type": 0.4, # 타입이 가장 중요
"material": 0.2, # 재질
"load": 0.2, # 하중
"size": 0.2 # 사이즈
}
weighted_sum = sum(
confidence_scores.get(key, 0) * weight
for key, weight in weights.items()
)
return round(weighted_sum, 2)