diff --git a/RULES.md b/RULES.md index 3630c07..fc43d35 100644 --- a/RULES.md +++ b/RULES.md @@ -8,7 +8,7 @@ ``` Frontend: React.js + Material-UI + Vite + React Router DOM Backend: FastAPI + SQLAlchemy + Python -Database: SQLite (개발) / PostgreSQL (운영) +Database: PostgreSQL (운영 및 개발) 기타: Axios, XLSX (SheetJS), file-saver ``` diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index 7854879..a154139 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -13,6 +13,7 @@ import json from ..database import get_db from app.services.material_classifier import classify_material +from app.services.integrated_classifier import classify_material_integrated, should_exclude_material from app.services.bolt_classifier import classify_bolt from app.services.flange_classifier import classify_flange from app.services.fitting_classifier import classify_fitting @@ -316,110 +317,50 @@ async def upload_file( main_nom = material_data.get("main_nom") red_nom = material_data.get("red_nom") - classification_results = [] - try: - # EXCLUDE 분류기 우선 호출 (제외 대상 먼저 걸러냄) - from app.services.exclude_classifier import classify_exclude - exclude_result = classify_exclude("", description, main_nom or "") - print(f"EXCLUDE 분류 결과: {exclude_result.get('category', 'UNKNOWN')} (신뢰도: {exclude_result.get('overall_confidence', 0)})") - - # EXCLUDE가 높은 신뢰도로 제외 대상이라고 하면 바로 사용 - if exclude_result.get("overall_confidence", 0) >= 0.8: - classification_result = exclude_result - else: - # 키워드 기반 빠른 분류기 선택 (성능 개선) - classification_results = [] - - # 키워드 기반으로 우선 분류기 결정 - desc_lower = description.lower() - primary_classifiers = [] - - # 볼트 관련 키워드 - if any(keyword in desc_lower for keyword in ['bolt', 'stud', 'nut', 'screw', 'washer', '볼트', '너트', 'a193', 'a194']): - primary_classifiers.append(('bolt', classify_bolt)) - - # 파이프 관련 키워드 (확장) - pipe_keywords = [ - 'pipe', 'tube', 'smls', '파이프', '배관', - 'a106', 'a333', 'a312', 'a53', 'seamless', 'sch', 'schedule', - 'boe', 'poe', 'bbe', 'pbe' # end preparation - ] - if any(keyword in desc_lower for keyword in pipe_keywords): - primary_classifiers.append(('pipe', classify_pipe)) - - # 피팅 관련 키워드 (확장) - fitting_keywords = [ - 'elbow', 'ell', 'tee', 'reducer', 'red', 'cap', 'coupling', 'nipple', 'swage', 'olet', - '엘보', '티', '리듀서', '캡', '니플', '커플링', - '90l_', '45l_', 'socket', 'sw', 'equal', 'reducing', 'concentric', 'eccentric', - 'sockolet', 'weldolet', 'threadolet', 'socklet', 'plug' - ] - if any(keyword in desc_lower for keyword in fitting_keywords): - primary_classifiers.append(('fitting', classify_fitting)) - - # 플랜지 관련 키워드 (확장) - flange_keywords = [ - 'flg', 'flange', '플랜지', 'weld neck', 'blind', 'slip on', 'socket weld', - 'threaded', 'lap joint', 'orifice', 'spectacle', 'paddle', 'spacer', - 'wn', 'so', 'bl', 'sw', 'thd', 'lj', 'rf', 'ff', 'rtj', - 'raised face', 'flat face', 'ring joint' - ] - if any(keyword in desc_lower for keyword in flange_keywords): - primary_classifiers.append(('flange', classify_flange)) - - # 밸브 관련 키워드 - if any(keyword in desc_lower for keyword in ['valve', 'gate', 'ball', 'globe', 'check', '밸브']): - primary_classifiers.append(('valve', classify_valve)) - - # 가스켓 관련 키워드 - if any(keyword in desc_lower for keyword in ['gasket', 'gask', '가스켓', 'swg', 'spiral']): - primary_classifiers.append(('gasket', classify_gasket)) - - # 계기 관련 키워드 - if any(keyword in desc_lower for keyword in ['gauge', 'transmitter', 'sensor', 'thermometer', '계기', '게이지']): - primary_classifiers.append(('instrument', classify_instrument)) - - # 우선 분류기만 실행 (1-2개) - if primary_classifiers: - for name, classifier in primary_classifiers: - try: - if name in ['fitting', 'flange']: - result = classifier("", description, main_nom or "", red_nom) - elif name == 'pipe': - result = classifier("", description, main_nom or "", length_value) - else: - result = classifier("", description, main_nom or "") - classification_results.append(result) - except Exception as e: - print(f"분류기 {name} 오류: {e}") - continue - - # 우선 분류기로 결과가 없으면 모든 분류기 실행 - if not classification_results or max(r.get('overall_confidence', 0) for r in classification_results) < 0.3: - # 볼트는 항상 확인 (매우 일반적) - if not any('bolt' in str(r) for r in primary_classifiers): - bolt_result = classify_bolt("", description, main_nom or "") - classification_results.append(bolt_result) - - # 가장 높은 신뢰도의 결과 선택 (UNKNOWN 제외) - valid_results = [r for r in classification_results if r.get('category') != 'UNKNOWN' and r.get('overall_confidence', 0) > 0] - - if valid_results: - classification_result = max(valid_results, key=lambda x: x.get('overall_confidence', 0)) - print(f"최종 선택: {classification_result.get('category')} (신뢰도: {classification_result.get('overall_confidence', 0)})") - else: - # 모든 분류기가 UNKNOWN이면 가장 높은 신뢰도의 UNKNOWN 선택 - classification_result = max(classification_results, key=lambda x: x.get('overall_confidence', 0)) - print(f"모든 분류기 실패, 최고 신뢰도 UNKNOWN 선택: (신뢰도: {classification_result.get('overall_confidence', 0)})") - - except Exception as e: - print(f"분류기 실행 중 오류 발생: {e}") - # 기본 분류 결과 생성 + # 1. 통합 분류기로 자재 타입 결정 + integrated_result = classify_material_integrated(description, main_nom or "", red_nom or "", length_value) + print(f"[분류] {description}") + print(f"통합 분류 결과: {integrated_result.get('category', 'UNKNOWN')} (신뢰도: {integrated_result.get('confidence', 0)}, Level: {integrated_result.get('classification_level', 'NONE')})") + + # 2. 제외 대상 확인 + if should_exclude_material(description): classification_result = { - "category": "UNKNOWN", - "overall_confidence": 0.0, - "reason": f"분류기 오류: {str(e)}" + "category": "EXCLUDE", + "overall_confidence": 0.95, + "reason": "제외 대상 자재" } + else: + # 3. 타입별 상세 분류기 실행 + material_type = integrated_result.get('category', 'UNKNOWN') + + if material_type == "PIPE": + classification_result = classify_pipe("", description, main_nom or "", length_value) + elif material_type == "FITTING": + classification_result = classify_fitting("", description, main_nom or "", red_nom) + elif material_type == "FLANGE": + classification_result = classify_flange("", description, main_nom or "", red_nom) + elif material_type == "VALVE": + classification_result = classify_valve("", description, main_nom or "") + elif material_type == "BOLT": + classification_result = classify_bolt("", description, main_nom or "") + elif material_type == "GASKET": + classification_result = classify_gasket("", description, main_nom or "") + elif material_type == "INSTRUMENT": + classification_result = classify_instrument("", description, main_nom or "") + else: + # UNKNOWN 처리 + classification_result = { + "category": "UNKNOWN", + "overall_confidence": integrated_result.get('confidence', 0.0), + "reason": f"분류 불가: {integrated_result.get('evidence', [])}" + } + + # 통합 분류기의 신뢰도가 더 낮으면 조정 + if integrated_result.get('confidence', 0) < 0.5: + classification_result['overall_confidence'] = min( + classification_result.get('overall_confidence', 1.0), + integrated_result.get('confidence', 0.0) + 0.2 + ) print(f"최종 분류 결과: {classification_result.get('category', 'UNKNOWN')}") diff --git a/backend/app/services/fitting_classifier.py b/backend/app/services/fitting_classifier.py index 873dbe4..3647e11 100644 --- a/backend/app/services/fitting_classifier.py +++ b/backend/app/services/fitting_classifier.py @@ -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. 재질 분류 (공통 모듈 사용) diff --git a/backend/app/services/flange_classifier.py b/backend/app/services/flange_classifier.py index 67964c6..579bde4 100644 --- a/backend/app/services/flange_classifier.py +++ b/backend/app/services/flange_classifier.py @@ -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. 재질 분류 (공통 모듈 사용) diff --git a/backend/app/services/integrated_classifier.py b/backend/app/services/integrated_classifier.py new file mode 100644 index 0000000..7ed2850 --- /dev/null +++ b/backend/app/services/integrated_classifier.py @@ -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) \ No newline at end of file diff --git a/backend/app/services/material_classifier.py b/backend/app/services/material_classifier.py index 804a6fb..3e36d80 100644 --- a/backend/app/services/material_classifier.py +++ b/backend/app/services/material_classifier.py @@ -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): diff --git a/backend/app/services/pipe_classifier.py b/backend/app/services/pipe_classifier.py index 5b5bbc2..f2661a8 100644 --- a/backend/app/services/pipe_classifier.py +++ b/backend/app/services/pipe_classifier.py @@ -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. 재질 분류 (공통 모듈 사용) diff --git a/backend/app/services/valve_classifier.py b/backend/app/services/valve_classifier.py index d09e9d2..7b800ff 100644 --- a/backend/app/services/valve_classifier.py +++ b/backend/app/services/valve_classifier.py @@ -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. 재질 분류 (공통 모듈 사용) diff --git a/backend/debug_step_by_step.py b/backend/debug_step_by_step.py new file mode 100644 index 0000000..e9392db --- /dev/null +++ b/backend/debug_step_by_step.py @@ -0,0 +1,41 @@ +from app.services.integrated_classifier import LEVEL1_TYPE_KEYWORDS + +test = "NIPPLE, SMLS, SCH 80, ASTM A106 GR B PBE" +print(f"테스트: {test}") + +desc_upper = test.upper() +desc_parts = [part.strip() for part in desc_upper.split(',')] + +print(f"대문자 변환: {desc_upper}") +print(f"쉼표 분리: {desc_parts}") + +# 단계별 디버깅 +detected_types = [] +for material_type, keywords in LEVEL1_TYPE_KEYWORDS.items(): + type_found = False + for keyword in keywords: + # 전체 문자열에서 찾기 + if keyword in desc_upper: + print(f"✓ {material_type}: '{keyword}' 전체 문자열에서 발견") + detected_types.append((material_type, keyword)) + type_found = True + break + # 각 부분에서도 정확히 매칭되는지 확인 + for part in desc_parts: + if keyword == part or keyword in part: + print(f"✓ {material_type}: '{keyword}' 부분 '{part}'에서 발견") + detected_types.append((material_type, keyword)) + type_found = True + break + if type_found: + break + +print(f"\n감지된 타입들: {detected_types}") +print(f"감지된 타입 개수: {len(detected_types)}") + +if len(detected_types) == 1: + print(f"단일 타입 확정: {detected_types[0][0]}") +elif len(detected_types) > 1: + print(f"복수 타입 감지: {detected_types}") +else: + print("Level 1 키워드 없음 - 재질 기반 분류로 이동") \ No newline at end of file