Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 서포트 카테고리 UI 개선: 좌우 스크롤, 헤더/본문 동기화, 가운데 정렬 - 동일 항목 합산 기능 구현 (Type + Size + Grade 기준) - 헤더 구조 변경: 압력/스케줄 제거, 구매수량 단일화, User Requirements 추가 - 우레탄 블럭슈 두께 정보(40t, 27t) Material Grade에 포함 - 서포트 수량 계산 수정: 취합된 숫자 그대로 표시 (4의 배수 계산 제거) - 서포트 분류 로직 개선: CLAMP, U-BOLT, URETHANE BLOCK SHOE 등 정확한 분류 - 백엔드 서포트 분류기에 User Requirements 추출 기능 추가 - 엑셀 내보내기에 서포트 카테고리 처리 로직 추가
330 lines
11 KiB
Python
330 lines
11 KiB
Python
"""
|
||
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)
|