자재 분류 시스템 개선 및 통합 분류기 구현

- 통합 분류기 구현으로 키워드 우선순위 체계 적용
- HEX.PLUG → FITTING 분류 수정 (기존 VALVE 오분류 해결)
- 플랜지/밸브가 볼트로 오분류되는 문제 해결 (A193, A194 재질 키워드 우선순위 적용)
- 피팅 재질(A234, A403, A420) 기반 분류 추가
- 니플 길이 정보 보존 로직 개선
- 파이프 끝단 가공 정보를 구매 단계에서 제외
- PostgreSQL 사용으로 RULES.md 업데이트
- 상호 배타적 키워드 시스템 구현 (Level 1 키워드 우선)
This commit is contained in:
Hyungi Ahn
2025-07-23 14:38:49 +09:00
parent 0d31d8b3fc
commit 9e5250a8f9
9 changed files with 327 additions and 121 deletions

View File

@@ -202,15 +202,20 @@ def classify_fitting(dat_file: str, description: str, main_nom: str,
desc_upper = description.upper()
dat_upper = dat_file.upper()
# 1. 명칭 우선 확인 (피팅 키워드가 있으면 피팅)
fitting_keywords = ['ELBOW', 'ELL', 'TEE', 'REDUCER', 'RED', 'CAP', 'NIPPLE', 'SWAGE', 'OLET', 'COUPLING', 'PLUG', 'SOCKLET', 'SOCKET', '엘보', '', '리듀서', '', '니플', '스웨지', '올렛', '커플링', 'SOCK-O-LET', 'WELD-O-LET', 'SOCKOLET', 'WELDOLET']
is_fitting = any(keyword in desc_upper or keyword in dat_upper for keyword in fitting_keywords)
# 1. 피팅 키워드 확인 (재질만 있어도 통합 분류기가 이미 피팅으로 분류했으므로 진행)
fitting_keywords = ['ELBOW', 'ELL', 'TEE', 'REDUCER', 'RED', 'CAP', 'NIPPLE', 'SWAGE', 'OLET', 'COUPLING', 'PLUG', 'SOCKLET', 'SOCKET', '엘보', '', '리듀서', '', '니플', '스웨지', '올렛', '커플링', '플러그', 'SOCK-O-LET', 'WELD-O-LET', 'SOCKOLET', 'WELDOLET']
has_fitting_keyword = any(keyword in desc_upper or keyword in dat_upper for keyword in fitting_keywords)
if not is_fitting:
# 피팅 재질 확인 (A234, A403, A420)
fitting_materials = ['A234', 'A403', 'A420']
has_fitting_material = any(material in desc_upper for material in fitting_materials)
# 피팅 키워드도 없고 피팅 재질도 없으면 UNKNOWN
if not has_fitting_keyword and not has_fitting_material:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": "피팅 키워드 없음"
"reason": "피팅 키워드 및 재질 없음"
}
# 2. 재질 분류 (공통 모듈 사용)

View File

@@ -181,15 +181,20 @@ def classify_flange(dat_file: str, description: str, main_nom: str,
desc_upper = description.upper()
dat_upper = dat_file.upper()
# 1. 명칭 우선 확인 (플랜지 키워드가 있으면 플랜지)
# 1. 플랜지 키워드 확인 (재질만 있어도 통합 분류기가 이미 플랜지로 분류했으므로 진행)
flange_keywords = ['FLG', 'FLANGE', '플랜지', 'ORIFICE', 'SPECTACLE', 'PADDLE', 'SPACER']
is_flange = any(keyword in desc_upper or keyword in dat_upper for keyword in flange_keywords)
has_flange_keyword = any(keyword in desc_upper or keyword in dat_upper for keyword in flange_keywords)
if not is_flange:
# 플랜지 재질 확인 (A182, A350, A105 - 범용이지만 플랜지에 많이 사용)
flange_materials = ['A182', 'A350', 'A105']
has_flange_material = any(material in desc_upper for material in flange_materials)
# 플랜지 키워드도 없고 플랜지 재질도 없으면 UNKNOWN
if not has_flange_keyword and not has_flange_material:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": "플랜지 키워드 없음"
"reason": "플랜지 키워드 및 재질 없음"
}
# 2. 재질 분류 (공통 모듈 사용)

View File

@@ -0,0 +1,200 @@
"""
통합 자재 분류 시스템
메모리에 정의된 키워드 우선순위 체계를 적용
"""
import re
from typing import Dict, List, Optional, Tuple
# Level 1: 명확한 타입 키워드 (최우선)
LEVEL1_TYPE_KEYWORDS = {
"BOLT": ["BOLT", "STUD", "NUT", "SCREW", "WASHER", "볼트", "너트", "스터드", "나사", "와셔"],
"VALVE": ["VALVE", "GATE", "BALL", "GLOBE", "CHECK", "BUTTERFLY", "NEEDLE", "RELIEF", "밸브", "게이트", "", "글로브", "체크", "버터플라이", "니들", "릴리프"],
"FLANGE": ["FLG", "FLANGE", "플랜지", "프랜지", "ORIFICE", "SPECTACLE", "PADDLE", "SPACER", "BLIND"],
"PIPE": ["PIPE", "TUBE", "파이프", "배관", "SMLS", "SEAMLESS"],
"FITTING": ["ELBOW", "ELL", "TEE", "REDUCER", "RED", "CAP", "COUPLING", "NIPPLE", "SWAGE", "OLET", "PLUG", "엘보", "", "리듀서", "", "니플", "커플링", "플러그", "CONC", "ECC", "SOCK-O-LET", "WELD-O-LET", "SOCKOLET", "WELDOLET", "THREADOLET"],
"GASKET": ["GASKET", "GASK", "가스켓", "SWG", "SPIRAL"],
"INSTRUMENT": ["GAUGE", "TRANSMITTER", "SENSOR", "THERMOMETER", "계기", "게이지", "트랜스미터", "센서"]
}
# Level 2: 서브타입 키워드 (구체화)
LEVEL2_SUBTYPE_KEYWORDS = {
"VALVE": {
"GATE": ["GATE VALVE", "GATE", "게이트 밸브"],
"BALL": ["BALL VALVE", "BALL", "볼 밸브"],
"GLOBE": ["GLOBE VALVE", "GLOBE", "글로브 밸브"],
"CHECK": ["CHECK VALVE", "CHECK", "체크 밸브", "역지 밸브"]
},
"FLANGE": {
"WELD_NECK": ["WELD NECK", "WN", "웰드넥"],
"SLIP_ON": ["SLIP ON", "SO", "슬립온"],
"BLIND": ["BLIND", "BL", "막음", "차단"],
"SOCKET_WELD": ["SOCKET WELD", "소켓웰드"]
},
"BOLT": {
"HEX_BOLT": ["HEX BOLT", "HEXAGON", "육각 볼트"],
"STUD_BOLT": ["STUD BOLT", "STUD", "스터드 볼트"]
}
}
# Level 3: 연결/압력 키워드 (공용)
LEVEL3_CONNECTION_KEYWORDS = {
"SW": ["SW", "SOCKET WELD", "소켓웰드"],
"THD": ["THD", "THREADED", "NPT", "나사"],
"FL": ["FL", "FLANGED", "플랜지형"],
"BW": ["BW", "BUTT WELD", "맞대기용접"]
}
LEVEL3_PRESSURE_KEYWORDS = ["150LB", "300LB", "600LB", "900LB", "1500LB", "2500LB", "3000LB", "6000LB"]
# Level 4: 재질 키워드 (최후 판단)
LEVEL4_MATERIAL_KEYWORDS = {
"PIPE": ["A106", "A333", "A312", "A53"],
"FITTING": ["A234", "A403", "A420"],
"FLANGE": ["A182", "A350"], # A105 제거 (범용 재질로 이동)
"VALVE": ["A216", "A217", "A351", "A352"],
"BOLT": ["A193", "A194", "A320", "A325", "A490"]
}
# 범용 재질 (여러 타입에 사용 가능)
GENERIC_MATERIALS = {
"A105": ["VALVE", "FLANGE", "FITTING"], # 우선순위 순서
"316": ["VALVE", "FLANGE", "FITTING", "PIPE", "BOLT"],
"304": ["VALVE", "FLANGE", "FITTING", "PIPE", "BOLT"]
}
def classify_material_integrated(description: str, main_nom: str = "",
red_nom: str = "", length: float = None) -> Dict:
"""
통합 자재 분류 함수
Args:
description: 자재 설명
main_nom: 주 사이즈
red_nom: 축소 사이즈 (플랜지/피팅용)
length: 길이 (파이프용)
Returns:
분류 결과 딕셔너리
"""
desc_upper = description.upper()
# 쉼표로 구분된 각 부분을 별도로 체크 (예: "NIPPLE, SMLS, SCH 80")
desc_parts = [part.strip() for part in desc_upper.split(',')]
# 1단계: Level 1 키워드로 타입 식별
detected_types = []
for material_type, keywords in LEVEL1_TYPE_KEYWORDS.items():
type_found = False
for keyword in keywords:
# 전체 문자열에서 찾기
if keyword in desc_upper:
detected_types.append((material_type, keyword))
type_found = True
break
# 각 부분에서도 정확히 매칭되는지 확인
for part in desc_parts:
if keyword == part or keyword in part:
detected_types.append((material_type, keyword))
type_found = True
break
if type_found:
break
# 2단계: 복수 타입 감지 시 Level 2로 구체화
if len(detected_types) > 1:
# Level 2 키워드로 우선순위 결정
for material_type, subtype_dict in LEVEL2_SUBTYPE_KEYWORDS.items():
for subtype, keywords in subtype_dict.items():
for keyword in keywords:
if keyword in desc_upper:
return {
"category": material_type,
"confidence": 0.95,
"evidence": [f"L1_KEYWORD: {detected_types}", f"L2_KEYWORD: {keyword}"],
"classification_level": "LEVEL2"
}
# Level 2 키워드가 없으면 우선순위로 결정
# FITTING > VALVE > FLANGE > PIPE > BOLT (더 구체적인 것 우선)
type_priority = ["FITTING", "VALVE", "FLANGE", "PIPE", "BOLT", "GASKET", "INSTRUMENT"]
for priority_type in type_priority:
for detected_type, keyword in detected_types:
if detected_type == priority_type:
return {
"category": priority_type,
"confidence": 0.85,
"evidence": [f"L1_MULTI_TYPE: {detected_types}", f"PRIORITY: {priority_type}"],
"classification_level": "LEVEL1_PRIORITY"
}
# 3단계: 단일 타입 확정 또는 Level 3/4로 판단
if len(detected_types) == 1:
material_type = detected_types[0][0]
return {
"category": material_type,
"confidence": 0.9,
"evidence": [f"L1_KEYWORD: {detected_types[0][1]}"],
"classification_level": "LEVEL1"
}
# 4단계: Level 1 없으면 재질 기반 분류
if not detected_types:
# 전용 재질 확인
for material_type, materials in LEVEL4_MATERIAL_KEYWORDS.items():
for material in materials:
if material in desc_upper:
# 볼트 재질(A193, A194)은 다른 키워드가 있는지 확인
if material_type == "BOLT":
# 다른 타입 키워드가 있으면 볼트로 분류하지 않음
other_type_found = False
for other_type, keywords in LEVEL1_TYPE_KEYWORDS.items():
if other_type != "BOLT":
for keyword in keywords:
if keyword in desc_upper:
other_type_found = True
break
if other_type_found:
break
if other_type_found:
continue # 볼트로 분류하지 않음
return {
"category": material_type,
"confidence": 0.35, # 재질만으로 분류 시 낮은 신뢰도
"evidence": [f"L4_MATERIAL: {material}"],
"classification_level": "LEVEL4"
}
# 범용 재질 확인
for material, priority_types in GENERIC_MATERIALS.items():
if material in desc_upper:
# 우선순위에 따라 타입 결정
return {
"category": priority_types[0], # 첫 번째 우선순위
"confidence": 0.3,
"evidence": [f"GENERIC_MATERIAL: {material}"],
"classification_level": "LEVEL4_GENERIC"
}
# 분류 실패
return {
"category": "UNKNOWN",
"confidence": 0.0,
"evidence": ["NO_CLASSIFICATION_POSSIBLE"],
"classification_level": "NONE"
}
def should_exclude_material(description: str) -> bool:
"""
제외 대상 자재인지 확인
"""
exclude_keywords = [
"DUMMY", "RESERVED", "SPARE", "DELETED", "CANCELED",
"더미", "예비", "삭제", "취소", "예약"
]
desc_upper = description.upper()
return any(keyword in desc_upper for keyword in exclude_keywords)

View File

@@ -254,6 +254,10 @@ def check_generic_materials(description: str) -> Dict:
def determine_material_type(standard: str, grade: str) -> str:
"""규격과 등급으로 재질 타입 결정"""
# grade가 None이면 기본값 처리
if not grade:
grade = ""
# 스테인리스 등급
stainless_patterns = ["304", "316", "321", "347", "F304", "F316", "WP304", "CF8"]
if any(pattern in grade for pattern in stainless_patterns):

View File

@@ -98,14 +98,19 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
}
# 2. 파이프 키워드 확인
pipe_keywords = ['PIPE', 'TUBE', '파이프', '배관']
is_pipe = any(keyword in desc_upper for keyword in pipe_keywords)
pipe_keywords = ['PIPE', 'TUBE', '파이프', '배관', 'SMLS', 'SEAMLESS']
has_pipe_keyword = any(keyword in desc_upper for keyword in pipe_keywords)
if not is_pipe:
# 파이프 재질 확인 (A106, A333, A312, A53)
pipe_materials = ['A106', 'A333', 'A312', 'A53']
has_pipe_material = any(material in desc_upper for material in pipe_materials)
# 파이프 키워드도 없고 파이프 재질도 없으면 UNKNOWN
if not has_pipe_keyword and not has_pipe_material:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": "파이프 키워드 없음"
"reason": "파이프 키워드 및 재질 없음"
}
# 3. 재질 분류 (공통 모듈 사용)

View File

@@ -212,15 +212,20 @@ def classify_valve(dat_file: str, description: str, main_nom: str, length: float
desc_upper = description.upper()
dat_upper = dat_file.upper()
# 1. 명칭 우선 확인 (밸브 키워드가 있으면 밸브)
valve_keywords = ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', 'BUTTERFLY', 'NEEDLE', 'RELIEF', 'SOLENOID', 'PLUG', '밸브', '게이트', '', '글로브', '체크', '버터플라이', '니들', '릴리프', '솔레노이드', '플러그']
is_valve = any(keyword in desc_upper or keyword in dat_upper for keyword in valve_keywords)
# 1. 밸브 키워드 확인 (재질만 있어도 통합 분류기가 이미 밸브로 분류했으므로 진행)
valve_keywords = ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', 'BUTTERFLY', 'NEEDLE', 'RELIEF', 'SOLENOID', '밸브', '게이트', '', '글로브', '체크', '버터플라이', '니들', '릴리프', '솔레노이드']
has_valve_keyword = any(keyword in desc_upper or keyword in dat_upper for keyword in valve_keywords)
if not is_valve:
# 밸브 재질 확인 (A216, A217, A351, A352)
valve_materials = ['A216', 'A217', 'A351', 'A352']
has_valve_material = any(material in desc_upper for material in valve_materials)
# 밸브 키워드도 없고 밸브 재질도 없으면 UNKNOWN
if not has_valve_keyword and not has_valve_material:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": "밸브 키워드 없음"
"reason": "밸브 키워드 및 재질 없음"
}
# 2. 재질 분류 (공통 모듈 사용)