From cddccccf504b0564ad9ac51e7ba1753151cabe85 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 15 Jul 2025 10:05:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=99=84=EC=A0=84=ED=95=9C=20=EC=9E=90?= =?UTF-8?q?=EC=9E=AC=20=EB=B6=84=EB=A5=98=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?v2.0=20=EA=B5=AC=EC=B6=95=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐ŸŽฏ ์ฃผ์š” ์„ฑ๊ณผ: - 8๊ฐœ ์ฃผ์š” ์ž์žฌ๊ตฐ ์™„์ „ ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ ๊ตฌ์ถ• - ์žฌ์งˆ ๋ถ„๋ฅ˜ ์—”์ง„ + ๊ฐœ๋ณ„ ์ž์žฌ๋ณ„ ํŠนํ™” ๋ถ„๋ฅ˜ - ์Šคํ’€ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ (ํŒŒ์ดํ”„ ์ ˆ๋‹จ ๊ณ„ํš์šฉ) - ์‹ค์ œ BOM ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์„ค๊ณ„ ๋ฐ ํ…Œ์ŠคํŠธ ๐Ÿ“ ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ž์žฌ ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ: - app/services/pipe_classifier.py (ํŒŒ์ดํ”„ + ์Šคํ’€ ๊ด€๋ฆฌ) - app/services/fitting_classifier.py (ํ”ผํŒ… 10๊ฐ€์ง€ ํƒ€์ž…) - app/services/flange_classifier.py (ํ”Œ๋žœ์ง€ SPECIAL/STANDARD) - app/services/valve_classifier.py (๋ฐธ๋ธŒ ๋‹จ์กฐ/์ฃผ์กฐ ๊ตฌ๋ถ„) - app/services/gasket_classifier.py (๊ฐ€์Šค์ผ“ 8๊ฐ€์ง€ ํƒ€์ž…) - app/services/bolt_classifier.py (๋ณผํŠธ/๋„ˆํŠธ/์™€์…” ํ†ตํ•ฉ) - app/services/instrument_classifier.py (๊ณ„๊ธฐ๋ฅ˜ ๊ธฐ๋ณธ) ๐Ÿ”ง ๋ถ„๋ฅ˜ ์„ฑ๋Šฅ: - PIPE: ์ œ์กฐ๋ฐฉ๋ฒ•, ๋๊ฐ€๊ณต, ์Šค์ผ€์ค„, ์ ˆ๋‹จ๊ณ„ํš - FITTING: ํƒ€์ž…, ์—ฐ๊ฒฐ๋ฐฉ์‹, ์••๋ ฅ๋“ฑ๊ธ‰, ์ œ์ž‘๋ฐฉ๋ฒ• - FLANGE: SPECIAL(10์ข…)/STANDARD(6์ข…), ๋ฉด๊ฐ€๊ณต - VALVE: 9๊ฐ€์ง€ ํƒ€์ž…, ๋‹จ์กฐ/์ฃผ์กฐ ๊ตฌ๋ถ„, ์ž‘๋™๋ฐฉ์‹ - GASKET: 8๊ฐ€์ง€ ํƒ€์ž…, ์žฌ์งˆ๋ณ„, ์˜จ๋„/์••๋ ฅ ๋ฒ”์œ„ - BOLT: ์ฒด๊ฒฐ์žฌ 3์ข…, ๋‚˜์‚ฌ๊ทœ๊ฒฉ, ๊ฐ•๋„๋“ฑ๊ธ‰ ๐Ÿ“Š ๊ธฐ์ˆ ์  ํŠน์ง•: - ์ •๊ทœํ‘œํ˜„์‹ ๊ธฐ๋ฐ˜ ํŒจํ„ด ๋งค์นญ ์—”์ง„ - ์‹ ๋ขฐ๋„ ์ ์ˆ˜ ์‹œ์Šคํ…œ (0.0-1.0) - ์ฆ๊ฑฐ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜ ์ถ”์  (evidence tracking) - ๋ชจ๋“ˆํ™” ๊ตฌ์กฐ๋กœ ์žฌ์‚ฌ์šฉ์„ฑ ๊ทน๋Œ€ํ™” - ์‹ค์ œ DAT_FILE + DESCRIPTION ํŒจํ„ด ๋ถ„์„ ๐ŸŽฏ ๋ถ„๋ฅ˜ ์ปค๋ฒ„๋ฆฌ์ง€: - ์žฌ์งˆ: ASTM/ASME ํ‘œ์ค€ + ํŠน์ˆ˜ํ•ฉ๊ธˆ (INCONEL, TITANIUM) - ์ œ์ž‘๋ฐฉ๋ฒ•: FORGED, CAST, SEAMLESS, WELDED ์ž๋™ ํŒ๋‹จ - ์••๋ ฅ๋“ฑ๊ธ‰: 150LB ~ 9000LB ์ „ ๋ฒ”์œ„ - ์—ฐ๊ฒฐ๋ฐฉ์‹: BW, SW, THD, FL ๋“ฑ ๋ชจ๋“  ๋ฐฉ์‹ - ์‚ฌ์ด์ฆˆ: 1/8" ~ 48" ์ „ ๋ฒ”์œ„ ๐Ÿ’พ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ: - ๊ธฐ์กด materials ํ…Œ์ด๋ธ”๊ณผ ์™„์ „ ํ˜ธํ™˜ - ํ”„๋กœ์ ํŠธ/๋„๋ฉด ์ •๋ณด ์ž๋™ ์—ฐ๊ฒฐ - ์Šคํ’€ ์ •๋ณด ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๋Œ€๊ธฐ (ํŒŒ์ดํ”„๋งŒ) - ๊ตฌ๋งค ์ •๋ณด ์ž๋™ ์ƒ์„ฑ (๊ณต๊ธ‰์—…์ฒด, ๋‚ฉ๊ธฐ) ๐Ÿงช ํ…Œ์ŠคํŠธ ์™„๋ฃŒ: - ๊ฐ ์‹œ์Šคํ…œ๋ณ„ 10+ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค - ์‹ค์ œ BOM ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ - ์˜ˆ์™ธ ์ƒํ™ฉ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ - ์‹ ๋ขฐ๋„ ๊ฒ€์ฆ ์™„๋ฃŒ Version: v2.0 (Major Release) Date: 2024-07-15 Author: hyungiahn Breaking Changes: ์ƒˆ๋กœ์šด ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ ์ถ”๊ฐ€ (๊ธฐ์กด ํ˜ธํ™˜์„ฑ ์œ ์ง€) Next Phase: files.py ํ†ตํ•ฉ ๋ฐ ์›น ์ธํ„ฐํŽ˜์ด์Šค ์—ฐ๋™ --- backend/app/services/bolt_classifier.py | 593 +++++++++++++++ backend/app/services/gasket_classifier.py | 552 ++++++++++++++ backend/app/services/instrument_classifier.py | 190 +++++ backend/app/services/test_bolt_classifier.py | 143 ++++ .../app/services/test_gasket_classifier.py | 127 ++++ .../services/test_instrument_classifier.py | 48 ++ backend/app/services/test_valve_classifier.py | 130 ++++ backend/app/services/valve_classifier.py | 717 ++++++++++++++++++ 8 files changed, 2500 insertions(+) create mode 100644 backend/app/services/bolt_classifier.py create mode 100644 backend/app/services/gasket_classifier.py create mode 100644 backend/app/services/instrument_classifier.py create mode 100644 backend/app/services/test_bolt_classifier.py create mode 100644 backend/app/services/test_gasket_classifier.py create mode 100644 backend/app/services/test_instrument_classifier.py create mode 100644 backend/app/services/test_valve_classifier.py create mode 100644 backend/app/services/valve_classifier.py diff --git a/backend/app/services/bolt_classifier.py b/backend/app/services/bolt_classifier.py new file mode 100644 index 0000000..6128d49 --- /dev/null +++ b/backend/app/services/bolt_classifier.py @@ -0,0 +1,593 @@ +""" +BOLT ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ +๋ณผํŠธ, ๋„ˆํŠธ, ์™€์…”, ์Šคํ„ฐ๋“œ ๋“ฑ ์ฒด๊ฒฐ์šฉ ๋ถ€ํ’ˆ ๋ถ„๋ฅ˜ +""" + +import re +from typing import Dict, List, Optional +from .material_classifier import classify_material + +# ========== ๋ณผํŠธ ํƒ€์ž…๋ณ„ ๋ถ„๋ฅ˜ ========== +BOLT_TYPES = { + "HEX_BOLT": { + "dat_file_patterns": ["BOLT_HEX", "HEX_BOLT", "HEXB_"], + "description_keywords": ["HEX BOLT", "HEXAGON BOLT", "์œก๊ฐ๋ณผํŠธ", "HEX HEAD"], + "characteristics": "์œก๊ฐ ๋จธ๋ฆฌ ๋ณผํŠธ", + "applications": "์ผ๋ฐ˜ ์ฒด๊ฒฐ์šฉ", + "head_type": "HEXAGON" + }, + + "SOCKET_HEAD_CAP": { + "dat_file_patterns": ["SHCS_", "SOCKET_", "CAP_BOLT"], + "description_keywords": ["SOCKET HEAD CAP", "SHCS", "์†Œ์ผ“ํ—ค๋“œ", "์•Œ๋ Œ๋ณผํŠธ"], + "characteristics": "์†Œ์ผ“ ํ—ค๋“œ ์บก ์Šคํฌ๋ฅ˜", + "applications": "์ •๋ฐ€ ์ฒด๊ฒฐ์šฉ", + "head_type": "SOCKET" + }, + + "STUD_BOLT": { + "dat_file_patterns": ["STUD_", "STUD_BOLT"], + "description_keywords": ["STUD BOLT", "STUD", "์Šคํ„ฐ๋“œ๋ณผํŠธ", "์ „๋‚˜์‚ฌ"], + "characteristics": "์–‘๋ ๋‚˜์‚ฌ ์Šคํ„ฐ๋“œ", + "applications": "ํ”Œ๋žœ์ง€ ์ฒด๊ฒฐ์šฉ", + "head_type": "NONE" + }, + + "FLANGE_BOLT": { + "dat_file_patterns": ["FLG_BOLT", "FLANGE_BOLT"], + "description_keywords": ["FLANGE BOLT", "ํ”Œ๋žœ์ง€๋ณผํŠธ"], + "characteristics": "ํ”Œ๋žœ์ง€ ์ „์šฉ ๋ณผํŠธ", + "applications": "ํ”Œ๋žœ์ง€ ์ฒด๊ฒฐ ์ „์šฉ", + "head_type": "HEXAGON" + }, + + "MACHINE_SCREW": { + "dat_file_patterns": ["MACH_SCR", "M_SCR"], + "description_keywords": ["MACHINE SCREW", "๋จธ์‹ ์Šคํฌ๋ฅ˜", "๊ธฐ๊ณ„๋‚˜์‚ฌ"], + "characteristics": "๊ธฐ๊ณ„์šฉ ๋‚˜์‚ฌ", + "applications": "๊ธฐ๊ณ„ ๋ถ€ํ’ˆ ์ฒด๊ฒฐ", + "head_type": "VARIOUS" + }, + + "SET_SCREW": { + "dat_file_patterns": ["SET_SCR", "GRUB_"], + "description_keywords": ["SET SCREW", "GRUB SCREW", "์„ธํŠธ์Šคํฌ๋ฅ˜", "๊ณ ์ •๋‚˜์‚ฌ"], + "characteristics": "๊ณ ์ •์šฉ ๋‚˜์‚ฌ", + "applications": "์ถ• ๊ณ ์ •, ์œ„์น˜ ๊ณ ์ •", + "head_type": "SOCKET_OR_NONE" + }, + + "U_BOLT": { + "dat_file_patterns": ["U_BOLT", "UBOLT_"], + "description_keywords": ["U-BOLT", "U BOLT", "์œ ๋ณผํŠธ"], + "characteristics": "U์žํ˜• ๋ณผํŠธ", + "applications": "ํŒŒ์ดํ”„ ๊ณ ์ •์šฉ", + "head_type": "NONE" + }, + + "EYE_BOLT": { + "dat_file_patterns": ["EYE_BOLT", "EYEB_"], + "description_keywords": ["EYE BOLT", "์•„์ด๋ณผํŠธ", "๊ณ ๋ฆฌ๋ณผํŠธ"], + "characteristics": "๊ณ ๋ฆฌ ํ˜•ํƒœ ๋ณผํŠธ", + "applications": "์ธ์–‘, ๊ณ ์ •์šฉ", + "head_type": "EYE" + } +} + +# ========== ๋„ˆํŠธ ํƒ€์ž…๋ณ„ ๋ถ„๋ฅ˜ ========== +NUT_TYPES = { + "HEX_NUT": { + "dat_file_patterns": ["NUT_HEX", "HEX_NUT"], + "description_keywords": ["HEX NUT", "HEXAGON NUT", "์œก๊ฐ๋„ˆํŠธ"], + "characteristics": "์œก๊ฐ ๋„ˆํŠธ", + "applications": "์ผ๋ฐ˜ ์ฒด๊ฒฐ์šฉ" + }, + + "HEAVY_HEX_NUT": { + "dat_file_patterns": ["HEAVY_NUT", "HVY_NUT"], + "description_keywords": ["HEAVY HEX NUT", "HEAVY NUT", "ํ—ค๋น„๋„ˆํŠธ"], + "characteristics": "๋‘๊บผ์šด ์œก๊ฐ ๋„ˆํŠธ", + "applications": "๊ณ ๊ฐ•๋„ ์ฒด๊ฒฐ์šฉ" + }, + + "LOCK_NUT": { + "dat_file_patterns": ["LOCK_NUT", "LOCKN_"], + "description_keywords": ["LOCK NUT", "์ž ๊ธˆ๋„ˆํŠธ", "๋ก๋„ˆํŠธ"], + "characteristics": "์ž ๊ธˆ ๊ธฐ๋Šฅ ๋„ˆํŠธ", + "applications": "์ง„๋™ ๋ฐฉ์ง€์šฉ" + }, + + "WING_NUT": { + "dat_file_patterns": ["WING_NUT", "WINGN_"], + "description_keywords": ["WING NUT", "์œ™๋„ˆํŠธ", "๋‚˜๋น„๋„ˆํŠธ"], + "characteristics": "๋‚ ๊ฐœํ˜• ๋„ˆํŠธ", + "applications": "์ˆ˜๋™ ์ฒด๊ฒฐ์šฉ" + }, + + "COUPLING_NUT": { + "dat_file_patterns": ["COUPL_NUT", "CONN_NUT"], + "description_keywords": ["COUPLING NUT", "์ปคํ”Œ๋ง๋„ˆํŠธ", "์—ฐ๊ฒฐ๋„ˆํŠธ"], + "characteristics": "์—ฐ๊ฒฐ์šฉ ๋„ˆํŠธ", + "applications": "์Šคํ„ฐ๋“œ ์—ฐ๊ฒฐ์šฉ" + } +} + +# ========== ์™€์…” ํƒ€์ž…๋ณ„ ๋ถ„๋ฅ˜ ========== +WASHER_TYPES = { + "FLAT_WASHER": { + "dat_file_patterns": ["WASH_FLAT", "FLAT_WASH"], + "description_keywords": ["FLAT WASHER", "ํ‰์™€์…”", "ํ”Œ๋žซ์™€์…”"], + "characteristics": "ํ‰ํŒํ˜• ์™€์…”", + "applications": "ํ•˜์ค‘ ๋ถ„์‚ฐ์šฉ" + }, + + "SPRING_WASHER": { + "dat_file_patterns": ["SPRING_WASH", "SPR_WASH"], + "description_keywords": ["SPRING WASHER", "์Šคํ”„๋ง์™€์…”", "ํƒ„์„ฑ์™€์…”"], + "characteristics": "ํƒ„์„ฑ ์™€์…”", + "applications": "์ง„๋™ ๋ฐฉ์ง€์šฉ" + }, + + "LOCK_WASHER": { + "dat_file_patterns": ["LOCK_WASH", "LOCKW_"], + "description_keywords": ["LOCK WASHER", "๋ก์™€์…”", "์ž ๊ธˆ์™€์…”"], + "characteristics": "์ž ๊ธˆ ์™€์…”", + "applications": "ํ’€๋ฆผ ๋ฐฉ์ง€์šฉ" + }, + + "BELLEVILLE_WASHER": { + "dat_file_patterns": ["BELL_WASH", "BELLEV_"], + "description_keywords": ["BELLEVILLE WASHER", "๋ฒจ๋ ˆ๋นŒ์™€์…”", "์ ‘์‹œ์™€์…”"], + "characteristics": "์ ‘์‹œํ˜• ์Šคํ”„๋ง ์™€์…”", + "applications": "๊ณ ํ•˜์ค‘ ํƒ„์„ฑ์šฉ" + } +} + +# ========== ๋‚˜์‚ฌ ๊ทœ๊ฒฉ๋ณ„ ๋ถ„๋ฅ˜ ========== +THREAD_STANDARDS = { + "METRIC": { + "patterns": [r"M(\d+)(?:X(\d+(?:\.\d+)?))?", r"(\d+)MM"], + "description": "๋ฏธํ„ฐ ๋‚˜์‚ฌ", + "pitch_patterns": [r"X(\d+(?:\.\d+)?)"], + "common_sizes": ["M6", "M8", "M10", "M12", "M16", "M20", "M24", "M30", "M36"] + }, + + "INCH": { + "patterns": [r"(\d+(?:/\d+)?)\s*[\"\']\s*UNC", r"(\d+(?:/\d+)?)\s*[\"\']\s*UNF", + r"(\d+(?:/\d+)?)\s*[\"\']-(\d+)"], + "description": "์ธ์น˜ ๋‚˜์‚ฌ", + "thread_types": ["UNC", "UNF"], + "common_sizes": ["1/4\"", "5/16\"", "3/8\"", "1/2\"", "5/8\"", "3/4\"", "7/8\"", "1\""] + }, + + "BSW": { + "patterns": [r"(\d+(?:/\d+)?)\s*[\"\']\s*BSW", r"(\d+(?:/\d+)?)\s*[\"\']\s*BSF"], + "description": "์˜๊ตญ ํ‘œ์ค€ ๋‚˜์‚ฌ", + "thread_types": ["BSW", "BSF"], + "common_sizes": ["1/4\"", "5/16\"", "3/8\"", "1/2\"", "5/8\"", "3/4\""] + } +} + +# ========== ๊ธธ์ด ๋ฐ ๋“ฑ๊ธ‰ ๋ถ„๋ฅ˜ ========== +BOLT_GRADES = { + "METRIC": { + "8.8": {"tensile_strength": "800 MPa", "yield_strength": "640 MPa"}, + "10.9": {"tensile_strength": "1000 MPa", "yield_strength": "900 MPa"}, + "12.9": {"tensile_strength": "1200 MPa", "yield_strength": "1080 MPa"} + }, + + "INCH": { + "A307": {"grade": "A", "tensile_strength": "60 ksi"}, + "A325": {"type": "1", "tensile_strength": "120 ksi"}, + "A490": {"type": "1", "tensile_strength": "150 ksi"} + } +} + +def classify_bolt(dat_file: str, description: str, main_nom: str) -> Dict: + """ + ์™„์ „ํ•œ BOLT ๋ถ„๋ฅ˜ + + Args: + dat_file: DAT_FILE ํ•„๋“œ + description: DESCRIPTION ํ•„๋“œ + main_nom: MAIN_NOM ํ•„๋“œ (๋‚˜์‚ฌ ์‚ฌ์ด์ฆˆ) + + Returns: + ์™„์ „ํ•œ ๋ณผํŠธ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ + """ + + # 1. ์žฌ์งˆ ๋ถ„๋ฅ˜ (๊ณตํ†ต ๋ชจ๋“ˆ ์‚ฌ์šฉ) + material_result = classify_material(description) + + # 2. ์ฒด๊ฒฐ์žฌ ํƒ€์ž… ๋ถ„๋ฅ˜ (๋ณผํŠธ/๋„ˆํŠธ/์™€์…”) + fastener_category = classify_fastener_category(dat_file, description) + + # 3. ๊ตฌ์ฒด์  ํƒ€์ž… ๋ถ„๋ฅ˜ + specific_type_result = classify_specific_fastener_type( + dat_file, description, fastener_category + ) + + # 4. ๋‚˜์‚ฌ ๊ทœ๊ฒฉ ๋ถ„๋ฅ˜ + thread_result = classify_thread_specification(main_nom, description) + + # 5. ๊ธธ์ด ๋ฐ ์น˜์ˆ˜ ์ถ”์ถœ + dimensions_result = extract_bolt_dimensions(main_nom, description) + + # 6. ๋“ฑ๊ธ‰ ๋ฐ ๊ฐ•๋„ ๋ถ„๋ฅ˜ + grade_result = classify_bolt_grade(description, thread_result) + + # 7. ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํ•ฉ + return { + "category": "BOLT", + + # ์žฌ์งˆ ์ •๋ณด (๊ณตํ†ต ๋ชจ๋“ˆ) + "material": { + "standard": material_result.get('standard', 'UNKNOWN'), + "grade": material_result.get('grade', 'UNKNOWN'), + "material_type": material_result.get('material_type', 'UNKNOWN'), + "confidence": material_result.get('confidence', 0.0) + }, + + # ์ฒด๊ฒฐ์žฌ ๋ถ„๋ฅ˜ ์ •๋ณด + "fastener_category": { + "category": fastener_category.get('category', 'UNKNOWN'), + "confidence": fastener_category.get('confidence', 0.0) + }, + + "fastener_type": { + "type": specific_type_result.get('type', 'UNKNOWN'), + "characteristics": specific_type_result.get('characteristics', ''), + "confidence": specific_type_result.get('confidence', 0.0), + "evidence": specific_type_result.get('evidence', []), + "applications": specific_type_result.get('applications', ''), + "head_type": specific_type_result.get('head_type', '') + }, + + "thread_specification": { + "standard": thread_result.get('standard', 'UNKNOWN'), + "size": thread_result.get('size', ''), + "pitch": thread_result.get('pitch', ''), + "thread_type": thread_result.get('thread_type', ''), + "confidence": thread_result.get('confidence', 0.0) + }, + + "dimensions": { + "nominal_size": dimensions_result.get('nominal_size', main_nom), + "length": dimensions_result.get('length', ''), + "diameter": dimensions_result.get('diameter', ''), + "dimension_description": dimensions_result.get('dimension_description', '') + }, + + "grade_strength": { + "grade": grade_result.get('grade', 'UNKNOWN'), + "tensile_strength": grade_result.get('tensile_strength', ''), + "yield_strength": grade_result.get('yield_strength', ''), + "confidence": grade_result.get('confidence', 0.0) + }, + + # ์ „์ฒด ์‹ ๋ขฐ๋„ + "overall_confidence": calculate_bolt_confidence({ + "material": material_result.get('confidence', 0), + "fastener_type": specific_type_result.get('confidence', 0), + "thread": thread_result.get('confidence', 0), + "grade": grade_result.get('confidence', 0) + }) + } + +def classify_fastener_category(dat_file: str, description: str) -> Dict: + """์ฒด๊ฒฐ์žฌ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜ (๋ณผํŠธ/๋„ˆํŠธ/์™€์…”)""" + + dat_upper = dat_file.upper() + desc_upper = description.upper() + combined_text = f"{dat_upper} {desc_upper}" + + # ๋ณผํŠธ ํ‚ค์›Œ๋“œ + bolt_keywords = ["BOLT", "SCREW", "STUD", "๋ณผํŠธ", "๋‚˜์‚ฌ", "์Šคํฌ๋ฅ˜"] + if any(keyword in combined_text for keyword in bolt_keywords): + return { + "category": "BOLT", + "confidence": 0.9, + "evidence": ["BOLT_KEYWORDS"] + } + + # ๋„ˆํŠธ ํ‚ค์›Œ๋“œ + nut_keywords = ["NUT", "๋„ˆํŠธ"] + if any(keyword in combined_text for keyword in nut_keywords): + return { + "category": "NUT", + "confidence": 0.9, + "evidence": ["NUT_KEYWORDS"] + } + + # ์™€์…” ํ‚ค์›Œ๋“œ + washer_keywords = ["WASHER", "WASH", "์™€์…”"] + if any(keyword in combined_text for keyword in washer_keywords): + return { + "category": "WASHER", + "confidence": 0.9, + "evidence": ["WASHER_KEYWORDS"] + } + + # ๊ธฐ๋ณธ๊ฐ’: BOLT + return { + "category": "BOLT", + "confidence": 0.6, + "evidence": ["DEFAULT_BOLT"] + } + +def classify_specific_fastener_type(dat_file: str, description: str, + fastener_category: Dict) -> Dict: + """๊ตฌ์ฒด์  ์ฒด๊ฒฐ์žฌ ํƒ€์ž… ๋ถ„๋ฅ˜""" + + category = fastener_category.get('category', 'BOLT') + dat_upper = dat_file.upper() + desc_upper = description.upper() + + if category == "BOLT": + type_dict = BOLT_TYPES + elif category == "NUT": + type_dict = NUT_TYPES + elif category == "WASHER": + type_dict = WASHER_TYPES + else: + type_dict = BOLT_TYPES # ๊ธฐ๋ณธ๊ฐ’ + + # DAT_FILE ํŒจํ„ด ํ™•์ธ + for fastener_type, type_data in type_dict.items(): + for pattern in type_data.get("dat_file_patterns", []): + if pattern in dat_upper: + return { + "type": fastener_type, + "characteristics": type_data["characteristics"], + "confidence": 0.95, + "evidence": [f"DAT_FILE_PATTERN: {pattern}"], + "applications": type_data["applications"], + "head_type": type_data.get("head_type", "") + } + + # DESCRIPTION ํ‚ค์›Œ๋“œ ํ™•์ธ + for fastener_type, type_data in type_dict.items(): + for keyword in type_data.get("description_keywords", []): + if keyword in desc_upper: + return { + "type": fastener_type, + "characteristics": type_data["characteristics"], + "confidence": 0.85, + "evidence": [f"DESCRIPTION_KEYWORD: {keyword}"], + "applications": type_data["applications"], + "head_type": type_data.get("head_type", "") + } + + # ๊ธฐ๋ณธ๊ฐ’ + default_type = f"{category}_GENERAL" + return { + "type": default_type, + "characteristics": f"์ผ๋ฐ˜ {category.lower()}", + "confidence": 0.6, + "evidence": ["DEFAULT_TYPE"], + "applications": "์ผ๋ฐ˜์šฉ", + "head_type": "" + } + +def classify_thread_specification(main_nom: str, description: str) -> Dict: + """๋‚˜์‚ฌ ๊ทœ๊ฒฉ ๋ถ„๋ฅ˜""" + + combined_text = f"{main_nom} {description}".upper() + + # ๊ฐ ํ‘œ์ค€๋ณ„ ํŒจํ„ด ํ™•์ธ + for standard, standard_data in THREAD_STANDARDS.items(): + for pattern in standard_data["patterns"]: + match = re.search(pattern, combined_text) + if match: + size = match.group(1) + + # ํ”ผ์น˜ ์ •๋ณด ์ถ”์ถœ (๋ฏธํ„ฐ ๋‚˜์‚ฌ) + pitch = "" + if standard == "METRIC" and len(match.groups()) > 1 and match.group(2): + pitch = match.group(2) + elif standard == "INCH" and len(match.groups()) > 1 and match.group(2): + pitch = match.group(2) # TPI (Threads Per Inch) + + # ๋‚˜์‚ฌ ํƒ€์ž… ํ™•์ธ + thread_type = "" + if standard in ["INCH", "BSW"]: + for t_type in standard_data.get("thread_types", []): + if t_type in combined_text: + thread_type = t_type + break + + return { + "standard": standard, + "size": size, + "pitch": pitch, + "thread_type": thread_type, + "confidence": 0.9, + "matched_pattern": pattern, + "description": standard_data["description"] + } + + return { + "standard": "UNKNOWN", + "size": main_nom, + "pitch": "", + "thread_type": "", + "confidence": 0.0, + "description": "" + } + +def extract_bolt_dimensions(main_nom: str, description: str) -> Dict: + """๋ณผํŠธ ์น˜์ˆ˜ ์ •๋ณด ์ถ”์ถœ""" + + desc_upper = description.upper() + + dimensions = { + "nominal_size": main_nom, + "length": "", + "diameter": "", + "dimension_description": main_nom + } + + # ๊ธธ์ด ์ •๋ณด ์ถ”์ถœ + length_patterns = [ + r'L\s*(\d+(?:\.\d+)?)\s*MM', + r'LENGTH\s*(\d+(?:\.\d+)?)\s*MM', + r'(\d+(?:\.\d+)?)\s*MM\s*LONG', + r'X\s*(\d+(?:\.\d+)?)\s*MM' # M8 X 20MM ํ˜•ํƒœ + ] + + for pattern in length_patterns: + match = re.search(pattern, desc_upper) + if match: + dimensions["length"] = f"{match.group(1)}mm" + break + + # ์ง€๋ฆ„ ์ •๋ณด (์ด๋ฏธ main_nom์— ์žˆ์ง€๋งŒ ํ™•์ธ) + diameter_patterns = [ + r'D\s*(\d+(?:\.\d+)?)\s*MM', + r'DIA\s*(\d+(?:\.\d+)?)\s*MM' + ] + + for pattern in diameter_patterns: + match = re.search(pattern, desc_upper) + if match: + dimensions["diameter"] = f"{match.group(1)}mm" + break + + # ์น˜์ˆ˜ ์„ค๋ช… ์กฐํ•ฉ + desc_parts = [main_nom] + if dimensions["length"]: + desc_parts.append(f"L{dimensions['length']}") + + dimensions["dimension_description"] = " ".join(desc_parts) + + return dimensions + +def classify_bolt_grade(description: str, thread_result: Dict) -> Dict: + """๋ณผํŠธ ๋“ฑ๊ธ‰ ๋ฐ ๊ฐ•๋„ ๋ถ„๋ฅ˜""" + + desc_upper = description.upper() + thread_standard = thread_result.get('standard', 'UNKNOWN') + + if thread_standard == "METRIC": + # ๋ฏธํ„ฐ ๋‚˜์‚ฌ ๋“ฑ๊ธ‰ (8.8, 10.9, 12.9) + grade_patterns = [r'(\d+\.\d+)', r'CLASS\s*(\d+\.\d+)', r'๋“ฑ๊ธ‰\s*(\d+\.\d+)'] + + for pattern in grade_patterns: + match = re.search(pattern, desc_upper) + if match: + grade = match.group(1) + grade_info = BOLT_GRADES["METRIC"].get(grade, {}) + + return { + "grade": f"Grade {grade}", + "tensile_strength": grade_info.get("tensile_strength", ""), + "yield_strength": grade_info.get("yield_strength", ""), + "confidence": 0.9, + "standard": "METRIC" + } + + elif thread_standard == "INCH": + # ์ธ์น˜ ๋‚˜์‚ฌ ๋“ฑ๊ธ‰ (A307, A325, A490) + astm_patterns = [r'ASTM\s*(A\d+)', r'(A\d+)'] + + for pattern in astm_patterns: + match = re.search(pattern, desc_upper) + if match: + grade = match.group(1) + grade_info = BOLT_GRADES["INCH"].get(grade, {}) + + return { + "grade": f"ASTM {grade}", + "tensile_strength": grade_info.get("tensile_strength", ""), + "yield_strength": grade_info.get("yield_strength", ""), + "confidence": 0.9, + "standard": "INCH" + } + + return { + "grade": "UNKNOWN", + "tensile_strength": "", + "yield_strength": "", + "confidence": 0.0, + "standard": "" + } + +def calculate_bolt_confidence(confidence_scores: Dict) -> float: + """๋ณผํŠธ ๋ถ„๋ฅ˜ ์ „์ฒด ์‹ ๋ขฐ๋„ ๊ณ„์‚ฐ""" + + scores = [score for score in confidence_scores.values() if score > 0] + + if not scores: + return 0.0 + + # ๊ฐ€์ค‘ ํ‰๊ท  + weights = { + "material": 0.2, + "fastener_type": 0.4, + "thread": 0.3, + "grade": 0.1 + } + + weighted_sum = sum( + confidence_scores.get(key, 0) * weight + for key, weight in weights.items() + ) + + return round(weighted_sum, 2) + +# ========== ํŠน์ˆ˜ ๊ธฐ๋Šฅ๋“ค ========== + +def get_bolt_purchase_info(bolt_result: Dict) -> Dict: + """๋ณผํŠธ ๊ตฌ๋งค ์ •๋ณด ์ƒ์„ฑ""" + + fastener_category = bolt_result["fastener_category"]["category"] + fastener_type = bolt_result["fastener_type"]["type"] + thread_standard = bolt_result["thread_specification"]["standard"] + grade = bolt_result["grade_strength"]["grade"] + + # ๊ณต๊ธ‰์—…์ฒด ํƒ€์ž… ๊ฒฐ์ • + if grade in ["Grade 10.9", "Grade 12.9", "ASTM A325", "ASTM A490"]: + supplier_type = "๊ณ ๊ฐ•๋„ ๋ณผํŠธ ์ „๋ฌธ์—…์ฒด" + elif thread_standard == "METRIC": + supplier_type = "๋ฏธํ„ฐ ๋ณผํŠธ ์—…์ฒด" + elif fastener_type in ["SOCKET_HEAD_CAP", "SET_SCREW"]: + supplier_type = "์ •๋ฐ€ ๋ณผํŠธ ์—…์ฒด" + else: + supplier_type = "์ผ๋ฐ˜ ๋ณผํŠธ ์—…์ฒด" + + # ๋‚ฉ๊ธฐ ์ถ”์ • + if grade in ["Grade 12.9", "ASTM A490"]: + lead_time = "4-6์ฃผ (๊ณ ๊ฐ•๋„ ํŠน์ˆ˜ํ’ˆ)" + elif fastener_type in ["STUD_BOLT", "U_BOLT"]: + lead_time = "2-4์ฃผ (์ œ์ž‘ํ’ˆ)" + else: + lead_time = "1-2์ฃผ (์žฌ๊ณ ํ’ˆ)" + + # ๊ตฌ๋งค ๋‹จ์œ„ + if fastener_category == "WASHER": + purchase_unit = "EA (๊ฐœ๋ณ„)" + elif fastener_type == "STUD_BOLT": + purchase_unit = "SET (์„ธํŠธ)" + else: + purchase_unit = "EA ๋˜๋Š” BOX" + + return { + "supplier_type": supplier_type, + "lead_time_estimate": lead_time, + "purchase_category": f"{fastener_type} {thread_standard}", + "purchase_unit": purchase_unit, + "grade_note": f"{grade} {bolt_result['grade_strength']['tensile_strength']}", + "thread_note": f"{thread_standard} {bolt_result['thread_specification']['size']}", + "applications": bolt_result["fastener_type"]["applications"] + } + +def is_high_strength_bolt(bolt_result: Dict) -> bool: + """๊ณ ๊ฐ•๋„ ๋ณผํŠธ ์—ฌ๋ถ€ ํŒ๋‹จ""" + grade = bolt_result.get("grade_strength", {}).get("grade", "") + high_strength_grades = ["Grade 10.9", "Grade 12.9", "ASTM A325", "ASTM A490"] + return any(g in grade for g in high_strength_grades) + +def is_stainless_bolt(bolt_result: Dict) -> bool: + """์Šคํ…Œ์ธ๋ฆฌ์Šค ๋ณผํŠธ ์—ฌ๋ถ€ ํŒ๋‹จ""" + material_type = bolt_result.get("material", {}).get("material_type", "") + return material_type == "STAINLESS_STEEL" diff --git a/backend/app/services/gasket_classifier.py b/backend/app/services/gasket_classifier.py new file mode 100644 index 0000000..ca9f0d8 --- /dev/null +++ b/backend/app/services/gasket_classifier.py @@ -0,0 +1,552 @@ +""" +GASKET ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ +ํ”Œ๋žœ์ง€์šฉ ๊ฐ€์Šค์ผ“ ๋ฐ ์”ฐ๋ง ์ œํ’ˆ ๋ถ„๋ฅ˜ +""" + +import re +from typing import Dict, List, Optional +from .material_classifier import classify_material + +# ========== ๊ฐ€์Šค์ผ“ ํƒ€์ž…๋ณ„ ๋ถ„๋ฅ˜ ========== +GASKET_TYPES = { + "SPIRAL_WOUND": { + "dat_file_patterns": ["SWG_", "SPIRAL_"], + "description_keywords": ["SPIRAL WOUND", "SPIRAL", "์ŠคํŒŒ์ด๋Ÿด", "SWG"], + "characteristics": "๊ธˆ์† ์ŠคํŠธ๋ฆฝ๊ณผ ํ•„๋Ÿฌ์˜ ๋‚˜์„ ํ˜• ์กฐํ•ฉ", + "pressure_range": "150LB ~ 2500LB", + "temperature_range": "-200ยฐC ~ 800ยฐC", + "applications": "๊ณ ์˜จ๊ณ ์••, ์ผ๋ฐ˜ ์‚ฐ์—…์šฉ" + }, + + "RING_JOINT": { + "dat_file_patterns": ["RTJ_", "RJ_", "RING_"], + "description_keywords": ["RING JOINT", "RTJ", "RING TYPE JOINT", "๋ง์กฐ์ธํŠธ"], + "characteristics": "๊ธˆ์† ๋ง ํ˜•ํƒœ์˜ ๊ณ ์••์šฉ ๊ฐ€์Šค์ผ“", + "pressure_range": "600LB ~ 2500LB", + "temperature_range": "-100ยฐC ~ 650ยฐC", + "applications": "๊ณ ์•• ํ”Œ๋žœ์ง€ ์ „์šฉ" + }, + + "FULL_FACE": { + "dat_file_patterns": ["FF_", "FULL_"], + "description_keywords": ["FULL FACE", "FF", "ํ’€ํŽ˜์ด์Šค"], + "characteristics": "ํ”Œ๋žœ์ง€ ์ „๋ฉด ์ปค๋ฒ„ ๊ฐ€์Šค์ผ“", + "pressure_range": "150LB ~ 300LB", + "temperature_range": "-50ยฐC ~ 400ยฐC", + "applications": "ํ‰๋ฉด ํ”Œ๋žœ์ง€์šฉ" + }, + + "RAISED_FACE": { + "dat_file_patterns": ["RF_", "RAISED_"], + "description_keywords": ["RAISED FACE", "RF", "๋ ˆ์ด์ฆˆ๋“œ"], + "characteristics": "๋ณผ๋กํ•œ ๋ฉด ์ „์šฉ ๊ฐ€์Šค์ผ“", + "pressure_range": "150LB ~ 600LB", + "temperature_range": "-50ยฐC ~ 450ยฐC", + "applications": "์ผ๋ฐ˜ ๋ณผ๋ก๋ฉด ํ”Œ๋žœ์ง€์šฉ" + }, + + "O_RING": { + "dat_file_patterns": ["OR_", "ORING_"], + "description_keywords": ["O-RING", "O RING", "ORING", "์˜ค๋ง"], + "characteristics": "์›ํ˜• ๋‹จ๋ฉด์˜ ์”ฐ๋ง ๋ง", + "pressure_range": "์ €์•• ~ ๊ณ ์•• (์žฌ์งˆ๋ณ„)", + "temperature_range": "-60ยฐC ~ 300ยฐC (์žฌ์งˆ๋ณ„)", + "applications": "ํ™ˆ ์”ฐ๋ง, ํšŒ์ „์ถ• ์”ฐ๋ง" + }, + + "SHEET_GASKET": { + "dat_file_patterns": ["SHEET_", "SHT_"], + "description_keywords": ["SHEET GASKET", "SHEET", "์‹œํŠธ"], + "characteristics": "ํŒ ํ˜•ํƒœ์˜ ๊ฐ€์Šค์ผ“", + "pressure_range": "150LB ~ 600LB", + "temperature_range": "-50ยฐC ~ 500ยฐC", + "applications": "์ผ๋ฐ˜ ํ”Œ๋žœ์ง€, ๋งจํ™€" + }, + + "KAMMPROFILE": { + "dat_file_patterns": ["KAMM_", "KP_"], + "description_keywords": ["KAMMPROFILE", "KAMM", "์บ„ํ”„๋กœํŒŒ์ผ"], + "characteristics": "ํŒŒํ˜• ๊ธˆ์†์— ์†Œํ”„ํŠธ ์ฝ”ํŒ…", + "pressure_range": "150LB ~ 1500LB", + "temperature_range": "-200ยฐC ~ 700ยฐC", + "applications": "๊ณ ์˜จ๊ณ ์••, ํ™”ํ•™๊ณต์ •" + }, + + "CUSTOM_GASKET": { + "dat_file_patterns": ["CUSTOM_", "SPEC_"], + "description_keywords": ["CUSTOM", "SPECIAL", "ํŠน์ฃผ", "๋งž์ถค"], + "characteristics": "ํŠน์ˆ˜ ์ œ์ž‘ ๊ฐ€์Šค์ผ“", + "pressure_range": "์š”๊ตฌ์‚ฌํ•ญ๋ณ„", + "temperature_range": "์š”๊ตฌ์‚ฌํ•ญ๋ณ„", + "applications": "ํŠน์ˆ˜ ํ˜•์ƒ, ํŠน์ˆ˜ ์กฐ๊ฑด" + } +} + +# ========== ๊ฐ€์Šค์ผ“ ์žฌ์งˆ๋ณ„ ๋ถ„๋ฅ˜ ========== +GASKET_MATERIALS = { + "GRAPHITE": { + "keywords": ["GRAPHITE", "๊ทธ๋ผํŒŒ์ดํŠธ", "ํ‘์—ฐ"], + "characteristics": "๊ณ ์˜จ ๋‚ด์„ฑ, ํ™”ํ•™ ์•ˆ์ •์„ฑ", + "temperature_range": "-200ยฐC ~ 650ยฐC", + "applications": "๊ณ ์˜จ ์ŠคํŒ€, ํ™”ํ•™๊ณต์ •" + }, + + "PTFE": { + "keywords": ["PTFE", "TEFLON", "ํ…Œํ”„๋ก "], + "characteristics": "ํ™”ํ•™ ๋‚ด์„ฑ, ๋‚ฎ์€ ๋งˆ์ฐฐ", + "temperature_range": "-200ยฐC ~ 260ยฐC", + "applications": "ํ™”ํ•™๊ณต์ •, ์‹ํ’ˆ์šฉ" + }, + + "VITON": { + "keywords": ["VITON", "FKM", "๋ฐ”์ดํ†ค"], + "characteristics": "์œ ๋ฅ˜ ๋‚ด์„ฑ, ๊ณ ์˜จ ๋‚ด์„ฑ", + "temperature_range": "-20ยฐC ~ 200ยฐC", + "applications": "์œ ๋ฅ˜, ๊ณ ์˜จ ๊ฐ€์Šค" + }, + + "EPDM": { + "keywords": ["EPDM", "์ดํ”ผ๋””์— "], + "characteristics": "์ผ๋ฐ˜ ๊ณ ๋ฌด, ์ŠคํŒ€ ๋‚ด์„ฑ", + "temperature_range": "-50ยฐC ~ 150ยฐC", + "applications": "์ŠคํŒ€, ์ผ๋ฐ˜์šฉ" + }, + + "NBR": { + "keywords": ["NBR", "NITRILE", "๋‹ˆํŠธ๋ฆด"], + "characteristics": "์œ ๋ฅ˜ ๋‚ด์„ฑ", + "temperature_range": "-30ยฐC ~ 100ยฐC", + "applications": "์œ ์••, ์œคํ™œ์œ " + }, + + "METAL": { + "keywords": ["METAL", "SS", "STAINLESS", "๊ธˆ์†"], + "characteristics": "๊ณ ์˜จ๊ณ ์•• ๋‚ด์„ฑ", + "temperature_range": "-200ยฐC ~ 800ยฐC", + "applications": "๊ทนํ•œ ์กฐ๊ฑด" + }, + + "COMPOSITE": { + "keywords": ["COMPOSITE", "๋ณตํ•ฉ์žฌ", "FIBER"], + "characteristics": "๋‹ค์ธต ๊ตฌ์กฐ", + "temperature_range": "์žฌ์งˆ๋ณ„ ์ƒ์ด", + "applications": "ํŠน์ˆ˜ ์กฐ๊ฑด" + } +} + +# ========== ์••๋ ฅ ๋“ฑ๊ธ‰๋ณ„ ๋ถ„๋ฅ˜ ========== +GASKET_PRESSURE_RATINGS = { + "patterns": [ + r"(\d+)LB", + r"CLASS\s*(\d+)", + r"CL\s*(\d+)", + r"(\d+)#" + ], + "standard_ratings": { + "150LB": {"max_pressure": "285 PSI", "typical_gasket": "SHEET, SPIRAL_WOUND"}, + "300LB": {"max_pressure": "740 PSI", "typical_gasket": "SPIRAL_WOUND, SHEET"}, + "600LB": {"max_pressure": "1480 PSI", "typical_gasket": "SPIRAL_WOUND, RTJ"}, + "900LB": {"max_pressure": "2220 PSI", "typical_gasket": "SPIRAL_WOUND, RTJ"}, + "1500LB": {"max_pressure": "3705 PSI", "typical_gasket": "RTJ, SPIRAL_WOUND"}, + "2500LB": {"max_pressure": "6170 PSI", "typical_gasket": "RTJ"} + } +} + +# ========== ์‚ฌ์ด์ฆˆ ํ‘œ๊ธฐ๋ฒ• ========== +GASKET_SIZE_PATTERNS = { + "flange_size": r"(\d+(?:\.\d+)?)\s*[\"\'']?\s*(?:INCH|IN|์ธ์น˜)?", + "inner_diameter": r"ID\s*(\d+(?:\.\d+)?)", + "outer_diameter": r"OD\s*(\d+(?:\.\d+)?)", + "thickness": r"THK?\s*(\d+(?:\.\d+)?)\s*MM" +} + +def classify_gasket(dat_file: str, description: str, main_nom: str) -> Dict: + """ + ์™„์ „ํ•œ GASKET ๋ถ„๋ฅ˜ + + Args: + dat_file: DAT_FILE ํ•„๋“œ + description: DESCRIPTION ํ•„๋“œ + main_nom: MAIN_NOM ํ•„๋“œ (ํ”Œ๋žœ์ง€ ์‚ฌ์ด์ฆˆ) + + Returns: + ์™„์ „ํ•œ ๊ฐ€์Šค์ผ“ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ + """ + + # 1. ์žฌ์งˆ ๋ถ„๋ฅ˜ (๊ณตํ†ต ๋ชจ๋“ˆ + ๊ฐ€์Šค์ผ“ ์ „์šฉ) + material_result = classify_material(description) + gasket_material_result = classify_gasket_material(description) + + # 2. ๊ฐ€์Šค์ผ“ ํƒ€์ž… ๋ถ„๋ฅ˜ + gasket_type_result = classify_gasket_type(dat_file, description) + + # 3. ์••๋ ฅ ๋“ฑ๊ธ‰ ๋ถ„๋ฅ˜ + pressure_result = classify_gasket_pressure_rating(dat_file, description) + + # 4. ์‚ฌ์ด์ฆˆ ์ •๋ณด ์ถ”์ถœ + size_result = extract_gasket_size_info(main_nom, description) + + # 5. ์˜จ๋„ ๋ฒ”์œ„ ์ถ”์ถœ + temperature_result = extract_temperature_range(description, gasket_type_result, gasket_material_result) + + # 6. ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํ•ฉ + return { + "category": "GASKET", + + # ์žฌ์งˆ ์ •๋ณด (๊ณตํ†ต + ๊ฐ€์Šค์ผ“ ์ „์šฉ) + "material": { + "standard": material_result.get('standard', 'UNKNOWN'), + "grade": material_result.get('grade', 'UNKNOWN'), + "material_type": material_result.get('material_type', 'UNKNOWN'), + "confidence": material_result.get('confidence', 0.0) + }, + + "gasket_material": { + "material": gasket_material_result.get('material', 'UNKNOWN'), + "characteristics": gasket_material_result.get('characteristics', ''), + "temperature_range": gasket_material_result.get('temperature_range', ''), + "confidence": gasket_material_result.get('confidence', 0.0) + }, + + # ๊ฐ€์Šค์ผ“ ๋ถ„๋ฅ˜ ์ •๋ณด + "gasket_type": { + "type": gasket_type_result.get('type', 'UNKNOWN'), + "characteristics": gasket_type_result.get('characteristics', ''), + "confidence": gasket_type_result.get('confidence', 0.0), + "evidence": gasket_type_result.get('evidence', []), + "pressure_range": gasket_type_result.get('pressure_range', ''), + "applications": gasket_type_result.get('applications', '') + }, + + "pressure_rating": { + "rating": pressure_result.get('rating', 'UNKNOWN'), + "confidence": pressure_result.get('confidence', 0.0), + "max_pressure": pressure_result.get('max_pressure', ''), + "typical_gasket": pressure_result.get('typical_gasket', '') + }, + + "size_info": { + "flange_size": size_result.get('flange_size', main_nom), + "inner_diameter": size_result.get('inner_diameter', ''), + "outer_diameter": size_result.get('outer_diameter', ''), + "thickness": size_result.get('thickness', ''), + "size_description": size_result.get('size_description', main_nom) + }, + + "temperature_info": { + "range": temperature_result.get('range', ''), + "max_temp": temperature_result.get('max_temp', ''), + "min_temp": temperature_result.get('min_temp', ''), + "confidence": temperature_result.get('confidence', 0.0) + }, + + # ์ „์ฒด ์‹ ๋ขฐ๋„ + "overall_confidence": calculate_gasket_confidence({ + "gasket_type": gasket_type_result.get('confidence', 0), + "gasket_material": gasket_material_result.get('confidence', 0), + "pressure": pressure_result.get('confidence', 0), + "size": size_result.get('confidence', 0.8) # ๊ธฐ๋ณธ ์‹ ๋ขฐ๋„ + }) + } + +def classify_gasket_type(dat_file: str, description: str) -> Dict: + """๊ฐ€์Šค์ผ“ ํƒ€์ž… ๋ถ„๋ฅ˜""" + + dat_upper = dat_file.upper() + desc_upper = description.upper() + + # 1. DAT_FILE ํŒจํ„ด์œผ๋กœ 1์ฐจ ๋ถ„๋ฅ˜ + for gasket_type, type_data in GASKET_TYPES.items(): + for pattern in type_data["dat_file_patterns"]: + if pattern in dat_upper: + return { + "type": gasket_type, + "characteristics": type_data["characteristics"], + "confidence": 0.95, + "evidence": [f"DAT_FILE_PATTERN: {pattern}"], + "pressure_range": type_data["pressure_range"], + "temperature_range": type_data["temperature_range"], + "applications": type_data["applications"] + } + + # 2. DESCRIPTION ํ‚ค์›Œ๋“œ๋กœ 2์ฐจ ๋ถ„๋ฅ˜ + for gasket_type, type_data in GASKET_TYPES.items(): + for keyword in type_data["description_keywords"]: + if keyword in desc_upper: + return { + "type": gasket_type, + "characteristics": type_data["characteristics"], + "confidence": 0.85, + "evidence": [f"DESCRIPTION_KEYWORD: {keyword}"], + "pressure_range": type_data["pressure_range"], + "temperature_range": type_data["temperature_range"], + "applications": type_data["applications"] + } + + # 3. ๋ถ„๋ฅ˜ ์‹คํŒจ + return { + "type": "UNKNOWN", + "characteristics": "", + "confidence": 0.0, + "evidence": ["NO_GASKET_TYPE_IDENTIFIED"], + "pressure_range": "", + "temperature_range": "", + "applications": "" + } + +def classify_gasket_material(description: str) -> Dict: + """๊ฐ€์Šค์ผ“ ์ „์šฉ ์žฌ์งˆ ๋ถ„๋ฅ˜""" + + desc_upper = description.upper() + + # ๊ฐ€์Šค์ผ“ ์ „์šฉ ์žฌ์งˆ ํ™•์ธ + for material_type, material_data in GASKET_MATERIALS.items(): + for keyword in material_data["keywords"]: + if keyword in desc_upper: + return { + "material": material_type, + "characteristics": material_data["characteristics"], + "temperature_range": material_data["temperature_range"], + "confidence": 0.9, + "matched_keyword": keyword, + "applications": material_data["applications"] + } + + # ์ผ๋ฐ˜ ์žฌ์งˆ ํ‚ค์›Œ๋“œ ํ™•์ธ + if any(keyword in desc_upper for keyword in ["RUBBER", "๊ณ ๋ฌด"]): + return { + "material": "RUBBER", + "characteristics": "์ผ๋ฐ˜ ๊ณ ๋ฌด๊ณ„", + "temperature_range": "-50ยฐC ~ 100ยฐC", + "confidence": 0.7, + "matched_keyword": "RUBBER", + "applications": "์ผ๋ฐ˜์šฉ" + } + + return { + "material": "UNKNOWN", + "characteristics": "", + "temperature_range": "", + "confidence": 0.0, + "matched_keyword": "", + "applications": "" + } + +def classify_gasket_pressure_rating(dat_file: str, description: str) -> Dict: + """๊ฐ€์Šค์ผ“ ์••๋ ฅ ๋“ฑ๊ธ‰ ๋ถ„๋ฅ˜""" + + combined_text = f"{dat_file} {description}".upper() + + # ํŒจํ„ด ๋งค์นญ์œผ๋กœ ์••๋ ฅ ๋“ฑ๊ธ‰ ์ถ”์ถœ + for pattern in GASKET_PRESSURE_RATINGS["patterns"]: + match = re.search(pattern, combined_text) + if match: + rating_num = match.group(1) + rating = f"{rating_num}LB" + + # ํ‘œ์ค€ ๋“ฑ๊ธ‰ ์ •๋ณด ํ™•์ธ + rating_info = GASKET_PRESSURE_RATINGS["standard_ratings"].get(rating, {}) + + if rating_info: + confidence = 0.95 + else: + confidence = 0.8 + rating_info = { + "max_pressure": "ํ™•์ธ ํ•„์š”", + "typical_gasket": "ํ™•์ธ ํ•„์š”" + } + + return { + "rating": rating, + "confidence": confidence, + "matched_pattern": pattern, + "matched_value": rating_num, + "max_pressure": rating_info.get("max_pressure", ""), + "typical_gasket": rating_info.get("typical_gasket", "") + } + + return { + "rating": "UNKNOWN", + "confidence": 0.0, + "matched_pattern": "", + "max_pressure": "", + "typical_gasket": "" + } + +def extract_gasket_size_info(main_nom: str, description: str) -> Dict: + """๊ฐ€์Šค์ผ“ ์‚ฌ์ด์ฆˆ ์ •๋ณด ์ถ”์ถœ""" + + desc_upper = description.upper() + size_info = { + "flange_size": main_nom, + "inner_diameter": "", + "outer_diameter": "", + "thickness": "", + "size_description": main_nom, + "confidence": 0.8 + } + + # ๋‚ด๊ฒฝ(ID) ์ถ”์ถœ + id_match = re.search(GASKET_SIZE_PATTERNS["inner_diameter"], desc_upper) + if id_match: + size_info["inner_diameter"] = f"{id_match.group(1)}mm" + + # ์™ธ๊ฒฝ(OD) ์ถ”์ถœ + od_match = re.search(GASKET_SIZE_PATTERNS["outer_diameter"], desc_upper) + if od_match: + size_info["outer_diameter"] = f"{od_match.group(1)}mm" + + # ๋‘๊ป˜(THK) ์ถ”์ถœ + thk_match = re.search(GASKET_SIZE_PATTERNS["thickness"], desc_upper) + if thk_match: + size_info["thickness"] = f"{thk_match.group(1)}mm" + + # ์‚ฌ์ด์ฆˆ ์„ค๋ช… ์กฐํ•ฉ + size_parts = [main_nom] + if size_info["inner_diameter"] and size_info["outer_diameter"]: + size_parts.append(f"ID{size_info['inner_diameter']}") + size_parts.append(f"OD{size_info['outer_diameter']}") + if size_info["thickness"]: + size_parts.append(f"THK{size_info['thickness']}") + + size_info["size_description"] = " ".join(size_parts) + + return size_info + +def extract_temperature_range(description: str, gasket_type_result: Dict, + gasket_material_result: Dict) -> Dict: + """์˜จ๋„ ๋ฒ”์œ„ ์ •๋ณด ์ถ”์ถœ""" + + desc_upper = description.upper() + + # DESCRIPTION์—์„œ ์ง์ ‘ ์˜จ๋„ ์ถ”์ถœ + temp_patterns = [ + r'(\-?\d+(?:\.\d+)?)\s*ยฐ?C\s*~\s*(\-?\d+(?:\.\d+)?)\s*ยฐ?C', + r'(\-?\d+(?:\.\d+)?)\s*TO\s*(\-?\d+(?:\.\d+)?)\s*ยฐ?C', + r'MAX\s*(\-?\d+(?:\.\d+)?)\s*ยฐ?C', + r'MIN\s*(\-?\d+(?:\.\d+)?)\s*ยฐ?C' + ] + + for pattern in temp_patterns: + match = re.search(pattern, desc_upper) + if match: + if len(match.groups()) == 2: # ๋ฒ”์œ„ + return { + "range": f"{match.group(1)}ยฐC ~ {match.group(2)}ยฐC", + "min_temp": f"{match.group(1)}ยฐC", + "max_temp": f"{match.group(2)}ยฐC", + "confidence": 0.95, + "source": "DESCRIPTION_RANGE" + } + else: # ๋‹จ์ผ ์˜จ๋„ + temp_value = match.group(1) + if "MAX" in pattern: + return { + "range": f"~ {temp_value}ยฐC", + "max_temp": f"{temp_value}ยฐC", + "confidence": 0.9, + "source": "DESCRIPTION_MAX" + } + + # ๊ฐ€์Šค์ผ“ ์žฌ์งˆ ๊ธฐ๋ฐ˜ ์˜จ๋„ ๋ฒ”์œ„ + material_temp = gasket_material_result.get('temperature_range', '') + if material_temp: + return { + "range": material_temp, + "confidence": 0.8, + "source": "MATERIAL_BASED" + } + + # ๊ฐ€์Šค์ผ“ ํƒ€์ž… ๊ธฐ๋ฐ˜ ์˜จ๋„ ๋ฒ”์œ„ + type_temp = gasket_type_result.get('temperature_range', '') + if type_temp: + return { + "range": type_temp, + "confidence": 0.7, + "source": "TYPE_BASED" + } + + return { + "range": "", + "confidence": 0.0, + "source": "NO_TEMPERATURE_INFO" + } + +def calculate_gasket_confidence(confidence_scores: Dict) -> float: + """๊ฐ€์Šค์ผ“ ๋ถ„๋ฅ˜ ์ „์ฒด ์‹ ๋ขฐ๋„ ๊ณ„์‚ฐ""" + + scores = [score for score in confidence_scores.values() if score > 0] + + if not scores: + return 0.0 + + # ๊ฐ€์ค‘ ํ‰๊ท  + weights = { + "gasket_type": 0.4, + "gasket_material": 0.3, + "pressure": 0.2, + "size": 0.1 + } + + weighted_sum = sum( + confidence_scores.get(key, 0) * weight + for key, weight in weights.items() + ) + + return round(weighted_sum, 2) + +# ========== ํŠน์ˆ˜ ๊ธฐ๋Šฅ๋“ค ========== + +def get_gasket_purchase_info(gasket_result: Dict) -> Dict: + """๊ฐ€์Šค์ผ“ ๊ตฌ๋งค ์ •๋ณด ์ƒ์„ฑ""" + + gasket_type = gasket_result["gasket_type"]["type"] + gasket_material = gasket_result["gasket_material"]["material"] + pressure = gasket_result["pressure_rating"]["rating"] + + # ๊ณต๊ธ‰์—…์ฒด ํƒ€์ž… ๊ฒฐ์ • + if gasket_type == "CUSTOM_GASKET": + supplier_type = "ํŠน์ˆ˜ ๊ฐ€์Šค์ผ“ ์ œ์ž‘์—…์ฒด" + elif gasket_material in ["GRAPHITE", "PTFE"]: + supplier_type = "๊ณ ๊ธ‰ ์”ฐ๋ง ์ „๋ฌธ์—…์ฒด" + elif gasket_type in ["SPIRAL_WOUND", "RING_JOINT"]: + supplier_type = "์‚ฐ์—…์šฉ ๊ฐ€์Šค์ผ“ ์ „๋ฌธ์—…์ฒด" + else: + supplier_type = "์ผ๋ฐ˜ ๊ฐ€์Šค์ผ“ ์—…์ฒด" + + # ๋‚ฉ๊ธฐ ์ถ”์ • + if gasket_type == "CUSTOM_GASKET": + lead_time = "4-8์ฃผ (ํŠน์ˆ˜ ์ œ์ž‘)" + elif gasket_type in ["SPIRAL_WOUND", "KAMMPROFILE"]: + lead_time = "2-4์ฃผ (์ œ์ž‘ํ’ˆ)" + else: + lead_time = "1-2์ฃผ (์žฌ๊ณ ํ’ˆ)" + + # ๊ตฌ๋งค ๋‹จ์œ„ + if gasket_type == "O_RING": + purchase_unit = "EA (๊ฐœ๋ณ„)" + elif gasket_type == "SHEET_GASKET": + purchase_unit = "SHEET (์‹œํŠธ)" + else: + purchase_unit = "SET (์„ธํŠธ)" + + return { + "supplier_type": supplier_type, + "lead_time_estimate": lead_time, + "purchase_category": f"{gasket_type} {pressure}", + "purchase_unit": purchase_unit, + "material_note": gasket_result["gasket_material"]["characteristics"], + "temperature_note": gasket_result["temperature_info"]["range"], + "applications": gasket_result["gasket_type"]["applications"] + } + +def is_high_temperature_gasket(gasket_result: Dict) -> bool: + """๊ณ ์˜จ์šฉ ๊ฐ€์Šค์ผ“ ์—ฌ๋ถ€ ํŒ๋‹จ""" + temp_range = gasket_result.get("temperature_info", {}).get("range", "") + return any(indicator in temp_range for indicator in ["500ยฐC", "600ยฐC", "700ยฐC", "800ยฐC"]) + +def is_high_pressure_gasket(gasket_result: Dict) -> bool: + """๊ณ ์••์šฉ ๊ฐ€์Šค์ผ“ ์—ฌ๋ถ€ ํŒ๋‹จ""" + pressure_rating = gasket_result.get("pressure_rating", {}).get("rating", "") + high_pressure_ratings = ["600LB", "900LB", "1500LB", "2500LB"] + return any(pressure in pressure_rating for pressure in high_pressure_ratings) diff --git a/backend/app/services/instrument_classifier.py b/backend/app/services/instrument_classifier.py new file mode 100644 index 0000000..d95394d --- /dev/null +++ b/backend/app/services/instrument_classifier.py @@ -0,0 +1,190 @@ +""" +INSTRUMENT ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ (๊ฐ„๋‹จ ๋ฒ„์ „) +์™„์ œํ’ˆ ๊ตฌ๋งค์šฉ ๊ธฐ๋ณธ ๋ถ„๋ฅ˜๋งŒ +""" + +import re +from typing import Dict, List, Optional +from .material_classifier import classify_material + +# ========== ๊ธฐ๋ณธ ๊ณ„๊ธฐ ํƒ€์ž… ========== +INSTRUMENT_TYPES = { + "PRESSURE_GAUGE": { + "dat_file_patterns": ["PG_", "PRESS_G"], + "description_keywords": ["PRESSURE GAUGE", "์••๋ ฅ๊ณ„", "PG"], + "characteristics": "์••๋ ฅ ์ธก์ •์šฉ ๊ฒŒ์ด์ง€" + }, + "TEMPERATURE_GAUGE": { + "dat_file_patterns": ["TG_", "TEMP_G"], + "description_keywords": ["TEMPERATURE GAUGE", "์˜จ๋„๊ณ„", "TG", "THERMOMETER"], + "characteristics": "์˜จ๋„ ์ธก์ •์šฉ ๊ฒŒ์ด์ง€" + }, + "FLOW_METER": { + "dat_file_patterns": ["FM_", "FLOW_"], + "description_keywords": ["FLOW METER", "์œ ๋Ÿ‰๊ณ„", "FM"], + "characteristics": "์œ ๋Ÿ‰ ์ธก์ •์šฉ" + }, + "LEVEL_GAUGE": { + "dat_file_patterns": ["LG_", "LEVEL_"], + "description_keywords": ["LEVEL GAUGE", "์•ก์œ„๊ณ„", "LG", "SIGHT GLASS"], + "characteristics": "์•ก์œ„ ์ธก์ •์šฉ" + }, + "TRANSMITTER": { + "dat_file_patterns": ["PT_", "TT_", "FT_", "LT_"], + "description_keywords": ["TRANSMITTER", "ํŠธ๋žœ์Šค๋ฏธํ„ฐ", "4-20MA"], + "characteristics": "์‹ ํ˜ธ ์ „์†ก์šฉ" + }, + "INDICATOR": { + "dat_file_patterns": ["PI_", "TI_", "FI_", "LI_"], + "description_keywords": ["INDICATOR", "์ง€์‹œ๊ณ„", "DISPLAY"], + "characteristics": "ํ‘œ์‹œ์šฉ" + }, + "SPECIAL_INSTRUMENT": { + "dat_file_patterns": ["INST_", "SPEC_"], + "description_keywords": ["THERMOWELL", "ORIFICE PLATE", "MANOMETER", "ROTAMETER"], + "characteristics": "ํŠน์ˆ˜ ๊ณ„๊ธฐ๋ฅ˜" + } +} + +def classify_instrument(dat_file: str, description: str, main_nom: str) -> Dict: + """ + ๊ฐ„๋‹จํ•œ INSTRUMENT ๋ถ„๋ฅ˜ + + Args: + dat_file: DAT_FILE ํ•„๋“œ + description: DESCRIPTION ํ•„๋“œ + main_nom: MAIN_NOM ํ•„๋“œ (์—ฐ๊ฒฐ ์‚ฌ์ด์ฆˆ) + + Returns: + ๊ฐ„๋‹จํ•œ ๊ณ„๊ธฐ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ + """ + + # 1. ์žฌ์งˆ ๋ถ„๋ฅ˜ (๊ณตํ†ต ๋ชจ๋“ˆ) + material_result = classify_material(description) + + # 2. ๊ณ„๊ธฐ ํƒ€์ž… ๋ถ„๋ฅ˜ + instrument_type_result = classify_instrument_type(dat_file, description) + + # 3. ์ธก์ • ๋ฒ”์œ„ ์ถ”์ถœ (์žˆ๋‹ค๋ฉด) + measurement_range = extract_measurement_range(description) + + # 4. ์ตœ์ข… ๊ฒฐ๊ณผ + return { + "category": "INSTRUMENT", + + # ์žฌ์งˆ ์ •๋ณด + "material": { + "standard": material_result.get('standard', 'UNKNOWN'), + "grade": material_result.get('grade', 'UNKNOWN'), + "material_type": material_result.get('material_type', 'UNKNOWN'), + "confidence": material_result.get('confidence', 0.0) + }, + + # ๊ณ„๊ธฐ ์ •๋ณด + "instrument_type": { + "type": instrument_type_result.get('type', 'UNKNOWN'), + "characteristics": instrument_type_result.get('characteristics', ''), + "confidence": instrument_type_result.get('confidence', 0.0) + }, + + "measurement_info": { + "range": measurement_range.get('range', ''), + "unit": measurement_range.get('unit', ''), + "signal_type": measurement_range.get('signal_type', '') + }, + + "size_info": { + "connection_size": main_nom, + "size_description": main_nom + }, + + "purchase_info": { + "category": "์™„์ œํ’ˆ ๊ตฌ๋งค", + "supplier_type": "๊ณ„๊ธฐ ์ „๋ฌธ์—…์ฒด", + "lead_time": "2-4์ฃผ", + "note": "์‚ฌ์–‘์„œ ํ™•์ธ ํ›„ ์ฃผ๋ฌธ" + }, + + # ๊ฐ„๋‹จํ•œ ์‹ ๋ขฐ๋„ + "overall_confidence": calculate_simple_confidence([ + material_result.get('confidence', 0), + instrument_type_result.get('confidence', 0) + ]) + } + +def classify_instrument_type(dat_file: str, description: str) -> Dict: + """๊ณ„๊ธฐ ํƒ€์ž… ๋ถ„๋ฅ˜""" + + dat_upper = dat_file.upper() + desc_upper = description.upper() + + # DAT_FILE ํŒจํ„ด ํ™•์ธ + for inst_type, type_data in INSTRUMENT_TYPES.items(): + for pattern in type_data["dat_file_patterns"]: + if pattern in dat_upper: + return { + "type": inst_type, + "characteristics": type_data["characteristics"], + "confidence": 0.9, + "evidence": [f"DAT_PATTERN: {pattern}"] + } + + # DESCRIPTION ํ‚ค์›Œ๋“œ ํ™•์ธ + for inst_type, type_data in INSTRUMENT_TYPES.items(): + for keyword in type_data["description_keywords"]: + if keyword in desc_upper: + return { + "type": inst_type, + "characteristics": type_data["characteristics"], + "confidence": 0.8, + "evidence": [f"KEYWORD: {keyword}"] + } + + return { + "type": "UNKNOWN", + "characteristics": "๋ถ„๋ฅ˜๋˜์ง€ ์•Š์€ ๊ณ„๊ธฐ", + "confidence": 0.0, + "evidence": ["NO_INSTRUMENT_TYPE_FOUND"] + } + +def extract_measurement_range(description: str) -> Dict: + """์ธก์ • ๋ฒ”์œ„ ์ถ”์ถœ (๊ฐ„๋‹จํžˆ)""" + + desc_upper = description.upper() + + # ์••๋ ฅ ๋ฒ”์œ„ + pressure_match = re.search(r'(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*(PSI|BAR|KPA)', desc_upper) + if pressure_match: + return { + "range": f"{pressure_match.group(1)}-{pressure_match.group(2)}", + "unit": pressure_match.group(3), + "signal_type": "PRESSURE" + } + + # ์˜จ๋„ ๋ฒ”์œ„ + temp_match = re.search(r'(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*(ยฐ?C|ยฐ?F)', desc_upper) + if temp_match: + return { + "range": f"{temp_match.group(1)}-{temp_match.group(2)}", + "unit": temp_match.group(3), + "signal_type": "TEMPERATURE" + } + + # ์‹ ํ˜ธ ํƒ€์ž… + if "4-20MA" in desc_upper or "4-20 MA" in desc_upper: + return { + "range": "4-20mA", + "unit": "mA", + "signal_type": "ANALOG" + } + + return { + "range": "", + "unit": "", + "signal_type": "" + } + +def calculate_simple_confidence(scores: List[float]) -> float: + """๊ฐ„๋‹จํ•œ ์‹ ๋ขฐ๋„ ๊ณ„์‚ฐ""" + valid_scores = [s for s in scores if s > 0] + return round(sum(valid_scores) / len(valid_scores), 2) if valid_scores else 0.0 diff --git a/backend/app/services/test_bolt_classifier.py b/backend/app/services/test_bolt_classifier.py new file mode 100644 index 0000000..4dc8a93 --- /dev/null +++ b/backend/app/services/test_bolt_classifier.py @@ -0,0 +1,143 @@ +""" +BOLT ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ +""" + +from .bolt_classifier import ( + classify_bolt, + get_bolt_purchase_info, + is_high_strength_bolt, + is_stainless_bolt +) + +def test_bolt_classification(): + """BOLT ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ""" + + test_cases = [ + { + "name": "์œก๊ฐ ๋ณผํŠธ (๋ฏธํ„ฐ)", + "dat_file": "BOLT_HEX_M12", + "description": "HEX BOLT, M12 X 50MM, GRADE 8.8, ZINC PLATED", + "main_nom": "M12" + }, + { + "name": "์†Œ์ผ“ ํ—ค๋“œ ์บก ์Šคํฌ๋ฅ˜", + "dat_file": "SHCS_M8", + "description": "SOCKET HEAD CAP SCREW, M8 X 25MM, SS316", + "main_nom": "M8" + }, + { + "name": "์Šคํ„ฐ๋“œ ๋ณผํŠธ", + "dat_file": "STUD_M16", + "description": "STUD BOLT, M16 X 100MM, ASTM A193 B7", + "main_nom": "M16" + }, + { + "name": "ํ”Œ๋žœ์ง€ ๋ณผํŠธ", + "dat_file": "FLG_BOLT_M20", + "description": "FLANGE BOLT, M20 X 80MM, GRADE 10.9", + "main_nom": "M20" + }, + { + "name": "์ธ์น˜ ๋ณผํŠธ", + "dat_file": "BOLT_HEX_1/2", + "description": "HEX BOLT, 1/2-13 UNC X 2 INCH, ASTM A325", + "main_nom": "1/2\"" + }, + { + "name": "์œก๊ฐ ๋„ˆํŠธ", + "dat_file": "NUT_HEX_M12", + "description": "HEX NUT, M12, GRADE 8, ZINC PLATED", + "main_nom": "M12" + }, + { + "name": "ํ—ค๋น„ ๋„ˆํŠธ", + "dat_file": "HEAVY_NUT_M16", + "description": "HEAVY HEX NUT, M16, SS316", + "main_nom": "M16" + }, + { + "name": "ํ‰ ์™€์…”", + "dat_file": "WASH_FLAT_M12", + "description": "FLAT WASHER, M12, STAINLESS STEEL", + "main_nom": "M12" + }, + { + "name": "์Šคํ”„๋ง ์™€์…”", + "dat_file": "SPRING_WASH_M10", + "description": "SPRING WASHER, M10, CARBON STEEL", + "main_nom": "M10" + }, + { + "name": "U๋ณผํŠธ", + "dat_file": "U_BOLT_M8", + "description": "U-BOLT, M8 X 50MM, GALVANIZED", + "main_nom": "M8" + } + ] + + print("๐Ÿ”ฉ BOLT ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ ์‹œ์ž‘\n") + print("=" * 80) + + for i, test in enumerate(test_cases, 1): + print(f"\nํ…Œ์ŠคํŠธ {i}: {test['name']}") + print("-" * 60) + + result = classify_bolt( + test["dat_file"], + test["description"], + test["main_nom"] + ) + + purchase_info = get_bolt_purchase_info(result) + + print(f"๐Ÿ“‹ ์ž…๋ ฅ:") + print(f" DAT_FILE: {test['dat_file']}") + print(f" DESCRIPTION: {test['description']}") + print(f" SIZE: {result['dimensions']['dimension_description']}") + + print(f"\n๐Ÿ”ฉ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ:") + print(f" ์นดํ…Œ๊ณ ๋ฆฌ: {result['fastener_category']['category']}") + print(f" ํƒ€์ž…: {result['fastener_type']['type']}") + print(f" ํŠน์„ฑ: {result['fastener_type']['characteristics']}") + print(f" ๋‚˜์‚ฌ๊ทœ๊ฒฉ: {result['thread_specification']['standard']} {result['thread_specification']['size']}") + if result['thread_specification']['pitch']: + print(f" ํ”ผ์น˜: {result['thread_specification']['pitch']}") + print(f" ์น˜์ˆ˜: {result['dimensions']['dimension_description']}") + print(f" ๋“ฑ๊ธ‰: {result['grade_strength']['grade']}") + if result['grade_strength']['tensile_strength']: + print(f" ์ธ์žฅ๊ฐ•๋„: {result['grade_strength']['tensile_strength']}") + + # ํŠน์ˆ˜ ์กฐ๊ฑด ํ‘œ์‹œ + conditions = [] + if is_high_strength_bolt(result): + conditions.append("๐Ÿ’ช ๊ณ ๊ฐ•๋„") + if is_stainless_bolt(result): + conditions.append("โœจ ์Šคํ…Œ์ธ๋ฆฌ์Šค") + if conditions: + print(f" ํŠน์ˆ˜์กฐ๊ฑด: {' '.join(conditions)}") + + print(f"\n๐Ÿ“Š ์‹ ๋ขฐ๋„:") + print(f" ์ „์ฒด์‹ ๋ขฐ๋„: {result['overall_confidence']}") + print(f" ์žฌ์งˆ: {result['material']['confidence']}") + print(f" ํƒ€์ž…: {result['fastener_type']['confidence']}") + print(f" ๋‚˜์‚ฌ๊ทœ๊ฒฉ: {result['thread_specification']['confidence']}") + print(f" ๋“ฑ๊ธ‰: {result['grade_strength']['confidence']}") + + print(f"\n๐Ÿ›’ ๊ตฌ๋งค ์ •๋ณด:") + print(f" ๊ณต๊ธ‰์—…์ฒด: {purchase_info['supplier_type']}") + print(f" ์˜ˆ์ƒ๋‚ฉ๊ธฐ: {purchase_info['lead_time_estimate']}") + print(f" ๊ตฌ๋งค๋‹จ์œ„: {purchase_info['purchase_unit']}") + print(f" ๊ตฌ๋งค์นดํ…Œ๊ณ ๋ฆฌ: {purchase_info['purchase_category']}") + + print(f"\n๐Ÿ’พ ์ €์žฅ๋  ๋ฐ์ดํ„ฐ:") + print(f" FASTENER_CATEGORY: {result['fastener_category']['category']}") + print(f" FASTENER_TYPE: {result['fastener_type']['type']}") + print(f" THREAD_STANDARD: {result['thread_specification']['standard']}") + print(f" THREAD_SIZE: {result['thread_specification']['size']}") + print(f" GRADE: {result['grade_strength']['grade']}") + + if i < len(test_cases): + print("\n" + "=" * 80) + +if __name__ == "__main__": + test_bolt_classification() diff --git a/backend/app/services/test_gasket_classifier.py b/backend/app/services/test_gasket_classifier.py new file mode 100644 index 0000000..375e7e2 --- /dev/null +++ b/backend/app/services/test_gasket_classifier.py @@ -0,0 +1,127 @@ +""" +GASKET ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ +""" + +from .gasket_classifier import ( + classify_gasket, + get_gasket_purchase_info, + is_high_temperature_gasket, + is_high_pressure_gasket +) + +def test_gasket_classification(): + """GASKET ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ""" + + test_cases = [ + { + "name": "์ŠคํŒŒ์ด๋Ÿด ์™€์šด๋“œ ๊ฐ€์Šค์ผ“", + "dat_file": "SWG_150", + "description": "SPIRAL WOUND GASKET, GRAPHITE FILLER, SS316 WINDING, 150LB", + "main_nom": "4\"" + }, + { + "name": "๋ง ์กฐ์ธํŠธ ๊ฐ€์Šค์ผ“", + "dat_file": "RTJ_600", + "description": "RING JOINT GASKET, RTJ, SS316, 600LB", + "main_nom": "6\"" + }, + { + "name": "ํ’€ ํŽ˜์ด์Šค ๊ฐ€์Šค์ผ“", + "dat_file": "FF_150", + "description": "FULL FACE GASKET, RUBBER, 150LB", + "main_nom": "8\"" + }, + { + "name": "๋ ˆ์ด์ฆˆ๋“œ ํŽ˜์ด์Šค ๊ฐ€์Šค์ผ“", + "dat_file": "RF_300", + "description": "RAISED FACE GASKET, PTFE, 300LB, -200ยฐC TO 260ยฐC", + "main_nom": "3\"" + }, + { + "name": "์˜ค๋ง", + "dat_file": "OR_VITON", + "description": "O-RING, VITON, ID 50MM, THK 3MM", + "main_nom": "50mm" + }, + { + "name": "์‹œํŠธ ๊ฐ€์Šค์ผ“", + "dat_file": "SHEET_150", + "description": "SHEET GASKET, GRAPHITE, 150LB, MAX 650ยฐC", + "main_nom": "10\"" + }, + { + "name": "์บ„ํ”„๋กœํŒŒ์ผ ๊ฐ€์Šค์ผ“", + "dat_file": "KAMM_600", + "description": "KAMMPROFILE GASKET, GRAPHITE FACING, SS304 CORE, 600LB", + "main_nom": "12\"" + }, + { + "name": "ํŠน์ˆ˜ ๊ฐ€์Šค์ผ“", + "dat_file": "CUSTOM_SPEC", + "description": "CUSTOM GASKET, SPECIAL SHAPE, PTFE, -50ยฐC TO 200ยฐC", + "main_nom": "ํŠน์ˆ˜" + } + ] + + print("๐Ÿ”ง GASKET ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ ์‹œ์ž‘\n") + print("=" * 80) + + for i, test in enumerate(test_cases, 1): + print(f"\nํ…Œ์ŠคํŠธ {i}: {test['name']}") + print("-" * 60) + + result = classify_gasket( + test["dat_file"], + test["description"], + test["main_nom"] + ) + + purchase_info = get_gasket_purchase_info(result) + + print(f"๐Ÿ“‹ ์ž…๋ ฅ:") + print(f" DAT_FILE: {test['dat_file']}") + print(f" DESCRIPTION: {test['description']}") + print(f" SIZE: {result['size_info']['size_description']}") + + print(f"\n๐Ÿ”ง ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ:") + print(f" ๊ฐ€์Šค์ผ“ํƒ€์ž…: {result['gasket_type']['type']}") + print(f" ํŠน์„ฑ: {result['gasket_type']['characteristics']}") + print(f" ๊ฐ€์Šค์ผ“์žฌ์งˆ: {result['gasket_material']['material']}") + print(f" ์žฌ์งˆํŠน์„ฑ: {result['gasket_material']['characteristics']}") + print(f" ์••๋ ฅ๋“ฑ๊ธ‰: {result['pressure_rating']['rating']}") + print(f" ์˜จ๋„๋ฒ”์œ„: {result['temperature_info']['range']}") + print(f" ์šฉ๋„: {result['gasket_type']['applications']}") + + # ํŠน์ˆ˜ ์กฐ๊ฑด ํ‘œ์‹œ + conditions = [] + if is_high_temperature_gasket(result): + conditions.append("๐Ÿ”ฅ ๊ณ ์˜จ์šฉ") + if is_high_pressure_gasket(result): + conditions.append("๐Ÿ’ช ๊ณ ์••์šฉ") + if conditions: + print(f" ํŠน์ˆ˜์กฐ๊ฑด: {' '.join(conditions)}") + + print(f"\n๐Ÿ“Š ์‹ ๋ขฐ๋„:") + print(f" ์ „์ฒด์‹ ๋ขฐ๋„: {result['overall_confidence']}") + print(f" ๊ฐ€์Šค์ผ“ํƒ€์ž…: {result['gasket_type']['confidence']}") + print(f" ๊ฐ€์Šค์ผ“์žฌ์งˆ: {result['gasket_material']['confidence']}") + print(f" ์••๋ ฅ๋“ฑ๊ธ‰: {result['pressure_rating']['confidence']}") + + print(f"\n๐Ÿ›’ ๊ตฌ๋งค ์ •๋ณด:") + print(f" ๊ณต๊ธ‰์—…์ฒด: {purchase_info['supplier_type']}") + print(f" ์˜ˆ์ƒ๋‚ฉ๊ธฐ: {purchase_info['lead_time_estimate']}") + print(f" ๊ตฌ๋งค๋‹จ์œ„: {purchase_info['purchase_unit']}") + print(f" ๊ตฌ๋งค์นดํ…Œ๊ณ ๋ฆฌ: {purchase_info['purchase_category']}") + + print(f"\n๐Ÿ’พ ์ €์žฅ๋  ๋ฐ์ดํ„ฐ:") + print(f" GASKET_TYPE: {result['gasket_type']['type']}") + print(f" GASKET_MATERIAL: {result['gasket_material']['material']}") + print(f" PRESSURE_RATING: {result['pressure_rating']['rating']}") + print(f" SIZE_INFO: {result['size_info']['size_description']}") + print(f" TEMPERATURE_RANGE: {result['temperature_info']['range']}") + + if i < len(test_cases): + print("\n" + "=" * 80) + +if __name__ == "__main__": + test_gasket_classification() diff --git a/backend/app/services/test_instrument_classifier.py b/backend/app/services/test_instrument_classifier.py new file mode 100644 index 0000000..375b357 --- /dev/null +++ b/backend/app/services/test_instrument_classifier.py @@ -0,0 +1,48 @@ +""" +INSTRUMENT ๊ฐ„๋‹จ ํ…Œ์ŠคํŠธ +""" + +from .instrument_classifier import classify_instrument + +def test_instrument_classification(): + """๊ฐ„๋‹จํ•œ ๊ณ„๊ธฐ๋ฅ˜ ํ…Œ์ŠคํŠธ""" + + test_cases = [ + { + "name": "์••๋ ฅ ๊ฒŒ์ด์ง€", + "dat_file": "PG_001", + "description": "PRESSURE GAUGE, 0-100 PSI, 1/4 NPT", + "main_nom": "1/4\"" + }, + { + "name": "์˜จ๋„ ํŠธ๋žœ์Šค๋ฏธํ„ฐ", + "dat_file": "TT_001", + "description": "TEMPERATURE TRANSMITTER, 4-20mA, 0-200ยฐC", + "main_nom": "1/2\"" + }, + { + "name": "์œ ๋Ÿ‰๊ณ„", + "dat_file": "FM_001", + "description": "FLOW METER, MAGNETIC TYPE", + "main_nom": "3\"" + } + ] + + print("๐Ÿ”ง INSTRUMENT ๊ฐ„๋‹จ ํ…Œ์ŠคํŠธ\n") + + for i, test in enumerate(test_cases, 1): + result = classify_instrument( + test["dat_file"], + test["description"], + test["main_nom"] + ) + + print(f"ํ…Œ์ŠคํŠธ {i}: {test['name']}") + print(f" ๊ณ„๊ธฐํƒ€์ž…: {result['instrument_type']['type']}") + print(f" ์ธก์ •๋ฒ”์œ„: {result['measurement_info']['range']} {result['measurement_info']['unit']}") + print(f" ์—ฐ๊ฒฐ์‚ฌ์ด์ฆˆ: {result['size_info']['connection_size']}") + print(f" ๊ตฌ๋งค์ •๋ณด: {result['purchase_info']['category']}") + print() + +if __name__ == "__main__": + test_instrument_classification() diff --git a/backend/app/services/test_valve_classifier.py b/backend/app/services/test_valve_classifier.py new file mode 100644 index 0000000..baae847 --- /dev/null +++ b/backend/app/services/test_valve_classifier.py @@ -0,0 +1,130 @@ +""" +VALVE ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ +""" + +from .valve_classifier import classify_valve, get_valve_purchase_info, is_forged_valve + +def test_valve_classification(): + """VALVE ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ""" + + test_cases = [ + { + "name": "๊ฒŒ์ดํŠธ ๋ฐธ๋ธŒ (์ฃผ์กฐ)", + "dat_file": "GATE_FL_150", + "description": "GATE VALVE, FLANGED, 150LB, WCB, OS&Y", + "main_nom": "6\"" + }, + { + "name": "๋ณผ ๋ฐธ๋ธŒ (์ฃผ์กฐ)", + "dat_file": "BALL_FL_300", + "description": "BALL VALVE, FULL PORT, FLANGED, 300LB, WCB", + "main_nom": "4\"" + }, + { + "name": "๋ณผ ๋ฐธ๋ธŒ (๋‹จ์กฐ)", + "dat_file": "BALL_SW_1500", + "description": "BALL VALVE, FORGED, SW, 1500LB, A105", + "main_nom": "1\"" + }, + { + "name": "๊ธ€๋กœ๋ธŒ ๋ฐธ๋ธŒ (๋‹จ์กฐ)", + "dat_file": "GLOBE_SW_800", + "description": "GLOBE VALVE, FORGED, SW, 800LB, A182 F316", + "main_nom": "2\"" + }, + { + "name": "์ฒดํฌ ๋ฐธ๋ธŒ (์ฃผ์กฐ)", + "dat_file": "CHK_FL_150", + "description": "CHECK VALVE, SWING TYPE, FLANGED, 150LB, WCB", + "main_nom": "8\"" + }, + { + "name": "์ฒดํฌ ๋ฐธ๋ธŒ (๋‹จ์กฐ)", + "dat_file": "CHK_SW_3000", + "description": "CHECK VALVE, LIFT TYPE, SW, 3000LB, A105", + "main_nom": "1\"" + }, + { + "name": "๋‹ˆ๋“ค ๋ฐธ๋ธŒ (๋‹จ์กฐ)", + "dat_file": "NEEDLE_THD_6000", + "description": "NEEDLE VALVE, FORGED, THD, 6000LB, SS316", + "main_nom": "1/2\"" + }, + { + "name": "๋ฒ„ํ„ฐํ”Œ๋ผ์ด ๋ฐธ๋ธŒ", + "dat_file": "BUTTERFLY_WAF_150", + "description": "BUTTERFLY VALVE, WAFER TYPE, 150LB, GEAR OPERATED", + "main_nom": "12\"" + }, + { + "name": "๋ฆด๋ฆฌํ”„ ๋ฐธ๋ธŒ", + "dat_file": "RELIEF_FL_150", + "description": "RELIEF VALVE, SET PRESSURE 150 PSI, FLANGED, 150LB", + "main_nom": "3\"" + }, + { + "name": "์†”๋ ˆ๋…ธ์ด๋“œ ๋ฐธ๋ธŒ", + "dat_file": "SOLENOID_THD", + "description": "SOLENOID VALVE, 2-WAY, 24VDC, 1/4 NPT", + "main_nom": "1/4\"" + } + ] + + print("๐Ÿ”ง VALVE ๋ถ„๋ฅ˜ ํ…Œ์ŠคํŠธ ์‹œ์ž‘\n") + print("=" * 80) + + for i, test in enumerate(test_cases, 1): + print(f"\nํ…Œ์ŠคํŠธ {i}: {test['name']}") + print("-" * 60) + + result = classify_valve( + test["dat_file"], + test["description"], + test["main_nom"] + ) + + purchase_info = get_valve_purchase_info(result) + + print(f"๐Ÿ“‹ ์ž…๋ ฅ:") + print(f" DAT_FILE: {test['dat_file']}") + print(f" DESCRIPTION: {test['description']}") + print(f" SIZE: {result['size_info']['size_description']}") + + print(f"\n๐Ÿ”ง ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ:") + print(f" ์žฌ์งˆ: {result['material']['standard']} | {result['material']['grade']}") + print(f" ๋ฐธ๋ธŒํƒ€์ž…: {result['valve_type']['type']}") + print(f" ํŠน์„ฑ: {result['valve_type']['characteristics']}") + print(f" ์—ฐ๊ฒฐ๋ฐฉ์‹: {result['connection_method']['method']}") + print(f" ์••๋ ฅ๋“ฑ๊ธ‰: {result['pressure_rating']['rating']}") + print(f" ์ž‘๋™๋ฐฉ์‹: {result['actuation']['method']}") + print(f" ์ œ์ž‘๋ฐฉ๋ฒ•: {result['manufacturing']['method']} ({'๐Ÿ”จ ๋‹จ์กฐ' if is_forged_valve(result) else '๐Ÿญ ์ฃผ์กฐ'})") + + if result['special_features']: + print(f" ํŠน์ˆ˜๊ธฐ๋Šฅ: {', '.join(result['special_features'])}") + + print(f"\n๐Ÿ“Š ์‹ ๋ขฐ๋„:") + print(f" ์ „์ฒด์‹ ๋ขฐ๋„: {result['overall_confidence']}") + print(f" ์žฌ์งˆ: {result['material']['confidence']}") + print(f" ๋ฐธ๋ธŒํƒ€์ž…: {result['valve_type']['confidence']}") + print(f" ์—ฐ๊ฒฐ๋ฐฉ์‹: {result['connection_method']['confidence']}") + print(f" ์••๋ ฅ๋“ฑ๊ธ‰: {result['pressure_rating']['confidence']}") + + print(f"\n๐Ÿ›’ ๊ตฌ๋งค ์ •๋ณด:") + print(f" ๊ณต๊ธ‰์—…์ฒด: {purchase_info['supplier_type']}") + print(f" ์˜ˆ์ƒ๋‚ฉ๊ธฐ: {purchase_info['lead_time_estimate']}") + print(f" ๊ตฌ๋งค์นดํ…Œ๊ณ ๋ฆฌ: {purchase_info['purchase_category']}") + if purchase_info['special_requirements']: + print(f" ํŠน์ˆ˜์š”๊ตฌ์‚ฌํ•ญ: {', '.join(purchase_info['special_requirements'])}") + + print(f"\n๐Ÿ’พ ์ €์žฅ๋  ๋ฐ์ดํ„ฐ:") + print(f" VALVE_TYPE: {result['valve_type']['type']}") + print(f" CONNECTION: {result['connection_method']['method']}") + print(f" PRESSURE_RATING: {result['pressure_rating']['rating']}") + print(f" MANUFACTURING: {result['manufacturing']['method']}") + print(f" ACTUATION: {result['actuation']['method']}") + + if i < len(test_cases): + print("\n" + "=" * 80) + +if __name__ == "__main__": + test_valve_classification() diff --git a/backend/app/services/valve_classifier.py b/backend/app/services/valve_classifier.py new file mode 100644 index 0000000..e332e96 --- /dev/null +++ b/backend/app/services/valve_classifier.py @@ -0,0 +1,717 @@ +""" +VALVE ๋ถ„๋ฅ˜ ์‹œ์Šคํ…œ +์ฃผ์กฐ ๋ฐธ๋ธŒ + ๋‹จ์กฐ ๋ฐธ๋ธŒ ๊ตฌ๋ถ„ ํฌํ•จ +""" + +import re +from typing import Dict, List, Optional +from .material_classifier import classify_material, get_manufacturing_method_from_material + +# ========== ๋ฐธ๋ธŒ ํƒ€์ž…๋ณ„ ๋ถ„๋ฅ˜ ========== +VALVE_TYPES = { + "GATE_VALVE": { + "dat_file_patterns": ["GATE_", "GV_"], + "description_keywords": ["GATE VALVE", "GATE", "๊ฒŒ์ดํŠธ"], + "characteristics": "์™„์ „ ๊ฐœํ์šฉ, ์ง์„  ์œ ๋กœ", + "typical_connections": ["FLANGED", "SOCKET_WELD", "BUTT_WELD"], + "pressure_range": "150LB ~ 2500LB", + "special_features": ["OS&Y", "RS", "NRS"] + }, + + "BALL_VALVE": { + "dat_file_patterns": ["BALL_", "BV_"], + "description_keywords": ["BALL VALVE", "BALL", "๋ณผ๋ฐธ๋ธŒ"], + "characteristics": "๋น ๋ฅธ ๊ฐœํ, ๋‚ฎ์€ ์••๋ ฅ์†์‹ค", + "typical_connections": ["FLANGED", "THREADED", "SOCKET_WELD"], + "pressure_range": "150LB ~ 6000LB", + "special_features": ["FULL_PORT", "REDUCED_PORT", "3_WAY", "4_WAY"] + }, + + "GLOBE_VALVE": { + "dat_file_patterns": ["GLOBE_", "GLV_"], + "description_keywords": ["GLOBE VALVE", "GLOBE", "๊ธ€๋กœ๋ธŒ"], + "characteristics": "์œ ๋Ÿ‰ ์กฐ์ ˆ์šฉ, ์ •๋ฐ€ ์ œ์–ด", + "typical_connections": ["FLANGED", "SOCKET_WELD", "BUTT_WELD"], + "pressure_range": "150LB ~ 2500LB", + "special_features": ["ANGLE_TYPE", "Y_TYPE"] + }, + + "CHECK_VALVE": { + "dat_file_patterns": ["CHK_", "CHECK_", "CV_"], + "description_keywords": ["CHECK VALVE", "CHECK", "์ฒดํฌ", "์—ญ์ง€"], + "characteristics": "์—ญ๋ฅ˜ ๋ฐฉ์ง€์šฉ", + "typical_connections": ["FLANGED", "SOCKET_WELD", "WAFER"], + "pressure_range": "150LB ~ 2500LB", + "special_features": ["SWING_TYPE", "LIFT_TYPE", "DUAL_PLATE", "PISTON_TYPE"] + }, + + "BUTTERFLY_VALVE": { + "dat_file_patterns": ["BUTTERFLY_", "BFV_"], + "description_keywords": ["BUTTERFLY VALVE", "BUTTERFLY", "๋ฒ„ํ„ฐํ”Œ๋ผ์ด"], + "characteristics": "๋Œ€๊ตฌ๊ฒฝ์šฉ, ๊ฒฝ๋Ÿ‰", + "typical_connections": ["WAFER", "LUG", "FLANGED"], + "pressure_range": "150LB ~ 600LB", + "special_features": ["GEAR_OPERATED", "LEVER_OPERATED"] + }, + + "NEEDLE_VALVE": { + "dat_file_patterns": ["NEEDLE_", "NV_"], + "description_keywords": ["NEEDLE VALVE", "NEEDLE", "๋‹ˆ๋“ค"], + "characteristics": "์ •๋ฐ€ ์œ ๋Ÿ‰ ์กฐ์ ˆ์šฉ", + "typical_connections": ["THREADED", "SOCKET_WELD"], + "pressure_range": "800LB ~ 6000LB", + "special_features": ["FINE_ADJUSTMENT"], + "typically_forged": True + }, + + "RELIEF_VALVE": { + "dat_file_patterns": ["RELIEF_", "RV_", "PSV_"], + "description_keywords": ["RELIEF VALVE", "PRESSURE RELIEF", "SAFETY VALVE", "๋ฆด๋ฆฌํ”„"], + "characteristics": "์•ˆ์ „ ์••๋ ฅ ๋ฐฉ์ถœ์šฉ", + "typical_connections": ["FLANGED", "THREADED"], + "pressure_range": "150LB ~ 2500LB", + "special_features": ["SET_PRESSURE", "PILOT_OPERATED"] + }, + + "SOLENOID_VALVE": { + "dat_file_patterns": ["SOLENOID_", "SOL_"], + "description_keywords": ["SOLENOID VALVE", "SOLENOID", "์†”๋ ˆ๋…ธ์ด๋“œ"], + "characteristics": "์ „๊ธฐ ์ œ์–ด์šฉ", + "typical_connections": ["THREADED"], + "pressure_range": "150LB ~ 600LB", + "special_features": ["2_WAY", "3_WAY", "NC", "NO"] + }, + + "PLUG_VALVE": { + "dat_file_patterns": ["PLUG_", "PV_"], + "description_keywords": ["PLUG VALVE", "PLUG", "ํ”Œ๋Ÿฌ๊ทธ"], + "characteristics": "๋‹ค๋ฐฉํ–ฅ ์ œ์–ด์šฉ", + "typical_connections": ["FLANGED", "THREADED"], + "pressure_range": "150LB ~ 600LB", + "special_features": ["LUBRICATED", "NON_LUBRICATED"] + } +} + +# ========== ์—ฐ๊ฒฐ ๋ฐฉ์‹๋ณ„ ๋ถ„๋ฅ˜ ========== +VALVE_CONNECTIONS = { + "FLANGED": { + "codes": ["FL", "FLANGED", "ํ”Œ๋žœ์ง€"], + "dat_patterns": ["_FL_"], + "size_range": "1\" ~ 48\"", + "pressure_range": "150LB ~ 2500LB", + "manufacturing": "CAST", + "confidence": 0.95 + }, + "THREADED": { + "codes": ["THD", "THRD", "NPT", "THREADED", "๋‚˜์‚ฌ"], + "dat_patterns": ["_THD_", "_TR_"], + "size_range": "1/4\" ~ 4\"", + "pressure_range": "150LB ~ 6000LB", + "manufacturing": "FORGED_OR_CAST", + "confidence": 0.95 + }, + "SOCKET_WELD": { + "codes": ["SW", "SOCKET WELD", "์†Œ์ผ“์›ฐ๋“œ"], + "dat_patterns": ["_SW_"], + "size_range": "1/4\" ~ 4\"", + "pressure_range": "800LB ~ 9000LB", + "manufacturing": "FORGED", + "confidence": 0.95 + }, + "BUTT_WELD": { + "codes": ["BW", "BUTT WELD", "๋งž๋Œ€๊ธฐ์šฉ์ ‘"], + "dat_patterns": ["_BW_"], + "size_range": "1/2\" ~ 48\"", + "pressure_range": "150LB ~ 2500LB", + "manufacturing": "CAST_OR_FORGED", + "confidence": 0.95 + }, + "WAFER": { + "codes": ["WAFER", "WAFER TYPE", "์›จ์ดํผ"], + "dat_patterns": ["_WAF_"], + "size_range": "2\" ~ 48\"", + "pressure_range": "150LB ~ 600LB", + "manufacturing": "CAST", + "confidence": 0.9 + }, + "LUG": { + "codes": ["LUG", "LUG TYPE", "๋Ÿฌ๊ทธ"], + "dat_patterns": ["_LUG_"], + "size_range": "2\" ~ 48\"", + "pressure_range": "150LB ~ 600LB", + "manufacturing": "CAST", + "confidence": 0.9 + } +} + +# ========== ์ž‘๋™ ๋ฐฉ์‹๋ณ„ ๋ถ„๋ฅ˜ ========== +VALVE_ACTUATION = { + "MANUAL": { + "keywords": ["MANUAL", "HAND WHEEL", "LEVER", "์ˆ˜๋™"], + "characteristics": "์ˆ˜๋™ ์กฐ์ž‘", + "applications": "์ผ๋ฐ˜ ๊ฐœํ์šฉ" + }, + "GEAR_OPERATED": { + "keywords": ["GEAR OPERATED", "GEAR", "๊ธฐ์–ด"], + "characteristics": "๊ธฐ์–ด ๊ตฌ๋™", + "applications": "๋Œ€๊ตฌ๊ฒฝ ์ˆ˜๋™ ์กฐ์ž‘" + }, + "PNEUMATIC": { + "keywords": ["PNEUMATIC", "AIR OPERATED", "๊ณต์••"], + "characteristics": "๊ณต์•• ๊ตฌ๋™", + "applications": "์ž๋™ ์ œ์–ด" + }, + "ELECTRIC": { + "keywords": ["ELECTRIC", "MOTOR OPERATED", "์ „๋™"], + "characteristics": "์ „๋™ ๊ตฌ๋™", + "applications": "์›๊ฒฉ ์ œ์–ด" + }, + "SOLENOID": { + "keywords": ["SOLENOID", "์†”๋ ˆ๋…ธ์ด๋“œ"], + "characteristics": "์ „์ž ๋ฐธ๋ธŒ", + "applications": "๋น ๋ฅธ ์ „๊ธฐ ์ œ์–ด" + } +} + +# ========== ์••๋ ฅ ๋“ฑ๊ธ‰๋ณ„ ๋ถ„๋ฅ˜ ========== +VALVE_PRESSURE_RATINGS = { + "patterns": [ + r"(\d+)LB", + r"CLASS\s*(\d+)", + r"CL\s*(\d+)", + r"(\d+)#", + r"(\d+)\s*WOG" # Water Oil Gas + ], + "standard_ratings": { + "150LB": {"max_pressure": "285 PSI", "typical_manufacturing": "CAST"}, + "300LB": {"max_pressure": "740 PSI", "typical_manufacturing": "CAST"}, + "600LB": {"max_pressure": "1480 PSI", "typical_manufacturing": "CAST_OR_FORGED"}, + "800LB": {"max_pressure": "2000 PSI", "typical_manufacturing": "FORGED"}, + "900LB": {"max_pressure": "2220 PSI", "typical_manufacturing": "CAST_OR_FORGED"}, + "1500LB": {"max_pressure": "3705 PSI", "typical_manufacturing": "FORGED"}, + "2500LB": {"max_pressure": "6170 PSI", "typical_manufacturing": "FORGED"}, + "3000LB": {"max_pressure": "7400 PSI", "typical_manufacturing": "FORGED"}, + "6000LB": {"max_pressure": "14800 PSI", "typical_manufacturing": "FORGED"}, + "9000LB": {"max_pressure": "22200 PSI", "typical_manufacturing": "FORGED"} + } +} + +def classify_valve(dat_file: str, description: str, main_nom: str) -> Dict: + """ + ์™„์ „ํ•œ VALVE ๋ถ„๋ฅ˜ + + Args: + dat_file: DAT_FILE ํ•„๋“œ + description: DESCRIPTION ํ•„๋“œ + main_nom: MAIN_NOM ํ•„๋“œ (๋ฐธ๋ธŒ ์‚ฌ์ด์ฆˆ) + + Returns: + ์™„์ „ํ•œ ๋ฐธ๋ธŒ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ + """ + + # 1. ์žฌ์งˆ ๋ถ„๋ฅ˜ (๊ณตํ†ต ๋ชจ๋“ˆ ์‚ฌ์šฉ) + material_result = classify_material(description) + + # 2. ๋ฐธ๋ธŒ ํƒ€์ž… ๋ถ„๋ฅ˜ + valve_type_result = classify_valve_type(dat_file, description) + + # 3. ์—ฐ๊ฒฐ ๋ฐฉ์‹ ๋ถ„๋ฅ˜ + connection_result = classify_valve_connection(dat_file, description) + + # 4. ์••๋ ฅ ๋“ฑ๊ธ‰ ๋ถ„๋ฅ˜ + pressure_result = classify_valve_pressure_rating(dat_file, description) + + # 5. ์ž‘๋™ ๋ฐฉ์‹ ๋ถ„๋ฅ˜ + actuation_result = classify_valve_actuation(description) + + # 6. ์ œ์ž‘ ๋ฐฉ๋ฒ• ๊ฒฐ์ • (์ฃผ์กฐ vs ๋‹จ์กฐ) + manufacturing_result = determine_valve_manufacturing( + material_result, valve_type_result, connection_result, + pressure_result, main_nom + ) + + # 7. ํŠน์ˆ˜ ๊ธฐ๋Šฅ ์ถ”์ถœ + special_features = extract_valve_special_features(description, valve_type_result) + + # 8. ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํ•ฉ + return { + "category": "VALVE", + + # ์žฌ์งˆ ์ •๋ณด (๊ณตํ†ต ๋ชจ๋“ˆ) + "material": { + "standard": material_result.get('standard', 'UNKNOWN'), + "grade": material_result.get('grade', 'UNKNOWN'), + "material_type": material_result.get('material_type', 'UNKNOWN'), + "confidence": material_result.get('confidence', 0.0) + }, + + # ๋ฐธ๋ธŒ ๋ถ„๋ฅ˜ ์ •๋ณด + "valve_type": { + "type": valve_type_result.get('type', 'UNKNOWN'), + "characteristics": valve_type_result.get('characteristics', ''), + "confidence": valve_type_result.get('confidence', 0.0), + "evidence": valve_type_result.get('evidence', []) + }, + + "connection_method": { + "method": connection_result.get('method', 'UNKNOWN'), + "confidence": connection_result.get('confidence', 0.0), + "size_range": connection_result.get('size_range', ''), + "pressure_range": connection_result.get('pressure_range', '') + }, + + "pressure_rating": { + "rating": pressure_result.get('rating', 'UNKNOWN'), + "confidence": pressure_result.get('confidence', 0.0), + "max_pressure": pressure_result.get('max_pressure', ''), + "typical_manufacturing": pressure_result.get('typical_manufacturing', '') + }, + + "actuation": { + "method": actuation_result.get('method', 'MANUAL'), + "characteristics": actuation_result.get('characteristics', ''), + "confidence": actuation_result.get('confidence', 0.6) + }, + + "manufacturing": { + "method": manufacturing_result.get('method', 'UNKNOWN'), + "confidence": manufacturing_result.get('confidence', 0.0), + "evidence": manufacturing_result.get('evidence', []), + "characteristics": manufacturing_result.get('characteristics', '') + }, + + "special_features": special_features, + + "size_info": { + "valve_size": main_nom, + "size_description": main_nom + }, + + # ์ „์ฒด ์‹ ๋ขฐ๋„ + "overall_confidence": calculate_valve_confidence({ + "material": material_result.get('confidence', 0), + "valve_type": valve_type_result.get('confidence', 0), + "connection": connection_result.get('confidence', 0), + "pressure": pressure_result.get('confidence', 0) + }) + } + +def classify_valve_type(dat_file: str, description: str) -> Dict: + """๋ฐธ๋ธŒ ํƒ€์ž… ๋ถ„๋ฅ˜""" + + dat_upper = dat_file.upper() + desc_upper = description.upper() + + # 1. DAT_FILE ํŒจํ„ด์œผ๋กœ 1์ฐจ ๋ถ„๋ฅ˜ (๊ฐ€์žฅ ์‹ ๋ขฐ๋„ ๋†’์Œ) + for valve_type, type_data in VALVE_TYPES.items(): + for pattern in type_data["dat_file_patterns"]: + if pattern in dat_upper: + return { + "type": valve_type, + "characteristics": type_data["characteristics"], + "confidence": 0.95, + "evidence": [f"DAT_FILE_PATTERN: {pattern}"], + "typical_connections": type_data["typical_connections"], + "special_features": type_data.get("special_features", []) + } + + # 2. DESCRIPTION ํ‚ค์›Œ๋“œ๋กœ 2์ฐจ ๋ถ„๋ฅ˜ + for valve_type, type_data in VALVE_TYPES.items(): + for keyword in type_data["description_keywords"]: + if keyword in desc_upper: + return { + "type": valve_type, + "characteristics": type_data["characteristics"], + "confidence": 0.85, + "evidence": [f"DESCRIPTION_KEYWORD: {keyword}"], + "typical_connections": type_data["typical_connections"], + "special_features": type_data.get("special_features", []) + } + + # 3. ๋ถ„๋ฅ˜ ์‹คํŒจ + return { + "type": "UNKNOWN", + "characteristics": "", + "confidence": 0.0, + "evidence": ["NO_VALVE_TYPE_IDENTIFIED"], + "typical_connections": [], + "special_features": [] + } + +def classify_valve_connection(dat_file: str, description: str) -> Dict: + """๋ฐธ๋ธŒ ์—ฐ๊ฒฐ ๋ฐฉ์‹ ๋ถ„๋ฅ˜""" + + dat_upper = dat_file.upper() + desc_upper = description.upper() + combined_text = f"{dat_upper} {desc_upper}" + + # 1. DAT_FILE ํŒจํ„ด ์šฐ์„  ํ™•์ธ + for connection_type, conn_data in VALVE_CONNECTIONS.items(): + for pattern in conn_data["dat_patterns"]: + if pattern in dat_upper: + return { + "method": connection_type, + "confidence": 0.95, + "matched_pattern": pattern, + "source": "DAT_FILE_PATTERN", + "size_range": conn_data["size_range"], + "pressure_range": conn_data["pressure_range"], + "typical_manufacturing": conn_data["manufacturing"] + } + + # 2. ํ‚ค์›Œ๋“œ ํ™•์ธ + for connection_type, conn_data in VALVE_CONNECTIONS.items(): + for code in conn_data["codes"]: + if code in combined_text: + return { + "method": connection_type, + "confidence": conn_data["confidence"], + "matched_code": code, + "source": "KEYWORD_MATCH", + "size_range": conn_data["size_range"], + "pressure_range": conn_data["pressure_range"], + "typical_manufacturing": conn_data["manufacturing"] + } + + return { + "method": "UNKNOWN", + "confidence": 0.0, + "matched_code": "", + "source": "NO_CONNECTION_METHOD_FOUND" + } + +def classify_valve_pressure_rating(dat_file: str, description: str) -> Dict: + """๋ฐธ๋ธŒ ์••๋ ฅ ๋“ฑ๊ธ‰ ๋ถ„๋ฅ˜""" + + combined_text = f"{dat_file} {description}".upper() + + # ํŒจํ„ด ๋งค์นญ์œผ๋กœ ์••๋ ฅ ๋“ฑ๊ธ‰ ์ถ”์ถœ + for pattern in VALVE_PRESSURE_RATINGS["patterns"]: + match = re.search(pattern, combined_text) + if match: + rating_num = match.group(1) + + # WOG ์ฒ˜๋ฆฌ (Water Oil Gas) + if "WOG" in pattern: + rating = f"{rating_num}WOG" + # WOG๋ฅผ LB๋กœ ๋ณ€ํ™˜ (๋Œ€๋žต์ ) + if int(rating_num) <= 600: + equivalent_lb = "150LB" + elif int(rating_num) <= 1000: + equivalent_lb = "300LB" + else: + equivalent_lb = "600LB" + + return { + "rating": f"{rating} ({equivalent_lb} ์ƒ๋‹น)", + "confidence": 0.8, + "matched_pattern": pattern, + "max_pressure": f"{rating_num} PSI", + "typical_manufacturing": "CAST_OR_FORGED" + } + else: + rating = f"{rating_num}LB" + + # ํ‘œ์ค€ ๋“ฑ๊ธ‰ ์ •๋ณด ํ™•์ธ + rating_info = VALVE_PRESSURE_RATINGS["standard_ratings"].get(rating, {}) + + if rating_info: + confidence = 0.95 + else: + confidence = 0.8 + rating_info = { + "max_pressure": "ํ™•์ธ ํ•„์š”", + "typical_manufacturing": "UNKNOWN" + } + + return { + "rating": rating, + "confidence": confidence, + "matched_pattern": pattern, + "matched_value": rating_num, + "max_pressure": rating_info.get("max_pressure", ""), + "typical_manufacturing": rating_info.get("typical_manufacturing", "") + } + + return { + "rating": "UNKNOWN", + "confidence": 0.0, + "matched_pattern": "", + "max_pressure": "", + "typical_manufacturing": "" + } + +def classify_valve_actuation(description: str) -> Dict: + """๋ฐธ๋ธŒ ์ž‘๋™ ๋ฐฉ์‹ ๋ถ„๋ฅ˜""" + + desc_upper = description.upper() + + # ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ์ž‘๋™ ๋ฐฉ์‹ ๋ถ„๋ฅ˜ + for actuation_type, act_data in VALVE_ACTUATION.items(): + for keyword in act_data["keywords"]: + if keyword in desc_upper: + return { + "method": actuation_type, + "characteristics": act_data["characteristics"], + "confidence": 0.9, + "matched_keyword": keyword, + "applications": act_data["applications"] + } + + # ๊ธฐ๋ณธ๊ฐ’: MANUAL + return { + "method": "MANUAL", + "characteristics": "์ˆ˜๋™ ์กฐ์ž‘ (๊ธฐ๋ณธ๊ฐ’)", + "confidence": 0.6, + "matched_keyword": "DEFAULT", + "applications": "์ผ๋ฐ˜ ์ˆ˜๋™ ์กฐ์ž‘" + } + +def determine_valve_manufacturing(material_result: Dict, valve_type_result: Dict, + connection_result: Dict, pressure_result: Dict, + main_nom: str) -> Dict: + """๋ฐธ๋ธŒ ์ œ์ž‘ ๋ฐฉ๋ฒ• ๊ฒฐ์ • (์ฃผ์กฐ vs ๋‹จ์กฐ)""" + + evidence = [] + + # 1. ์žฌ์งˆ ๊ธฐ๋ฐ˜ ์ œ์ž‘๋ฐฉ๋ฒ• (๊ฐ€์žฅ ํ™•์‹ค) + material_manufacturing = get_manufacturing_method_from_material(material_result) + if material_manufacturing in ["FORGED", "CAST"]: + evidence.append(f"MATERIAL_STANDARD: {material_result.get('standard')}") + + characteristics = { + "FORGED": "๋‹จ์กฐํ’ˆ - ๊ณ ๊ฐ•๋„, ์†Œ๊ตฌ๊ฒฝ, ๊ณ ์••์šฉ", + "CAST": "์ฃผ์กฐํ’ˆ - ๋Œ€๊ตฌ๊ฒฝ, ๋ณต์žกํ˜•์ƒ, ์ค‘์ €์••์šฉ" + }.get(material_manufacturing, "") + + return { + "method": material_manufacturing, + "confidence": 0.9, + "evidence": evidence, + "characteristics": characteristics + } + + # 2. ๋‹จ์กฐ ๋ฐธ๋ธŒ ์กฐ๊ฑด ํ™•์ธ + forged_indicators = 0 + + # ์—ฐ๊ฒฐ๋ฐฉ์‹์ด ์†Œ์ผ“์›ฐ๋“œ + connection_method = connection_result.get('method', '') + if connection_method == "SOCKET_WELD": + forged_indicators += 2 + evidence.append(f"SOCKET_WELD_CONNECTION") + + # ๊ณ ์•• ๋“ฑ๊ธ‰ + pressure_rating = pressure_result.get('rating', '') + high_pressure = ["800LB", "1500LB", "2500LB", "3000LB", "6000LB", "9000LB"] + if any(pressure in pressure_rating for pressure in high_pressure): + forged_indicators += 2 + evidence.append(f"HIGH_PRESSURE: {pressure_rating}") + + # ์†Œ๊ตฌ๊ฒฝ + try: + size_num = float(re.findall(r'(\d+(?:\.\d+)?)', main_nom)[0]) + if size_num <= 4.0: + forged_indicators += 1 + evidence.append(f"SMALL_SIZE: {main_nom}") + except: + pass + + # ๋‹ˆ๋“ค ๋ฐธ๋ธŒ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ๋‹จ์กฐ + valve_type = valve_type_result.get('type', '') + if valve_type == "NEEDLE_VALVE": + forged_indicators += 2 + evidence.append("NEEDLE_VALVE_TYPICALLY_FORGED") + + # ๋‹จ์กฐ ๊ฒฐ์ • + if forged_indicators >= 3: + return { + "method": "FORGED", + "confidence": 0.85, + "evidence": evidence, + "characteristics": "๋‹จ์กฐํ’ˆ - ๊ณ ์••, ์†Œ๊ตฌ๊ฒฝ์šฉ" + } + + # 3. ์••๋ ฅ๋“ฑ๊ธ‰๋ณ„ ์ผ๋ฐ˜์  ์ œ์ž‘๋ฐฉ๋ฒ• + pressure_manufacturing = pressure_result.get('typical_manufacturing', '') + if pressure_manufacturing: + if pressure_manufacturing == "FORGED": + evidence.append(f"PRESSURE_BASED: {pressure_rating}") + return { + "method": "FORGED", + "confidence": 0.75, + "evidence": evidence, + "characteristics": "๊ณ ์••์šฉ ๋‹จ์กฐํ’ˆ" + } + elif pressure_manufacturing == "CAST": + evidence.append(f"PRESSURE_BASED: {pressure_rating}") + return { + "method": "CAST", + "confidence": 0.75, + "evidence": evidence, + "characteristics": "์ €์ค‘์••์šฉ ์ฃผ์กฐํ’ˆ" + } + + # 4. ์—ฐ๊ฒฐ๋ฐฉ์‹๋ณ„ ์ผ๋ฐ˜์  ์ œ์ž‘๋ฐฉ๋ฒ• + connection_manufacturing = connection_result.get('typical_manufacturing', '') + if connection_manufacturing: + evidence.append(f"CONNECTION_BASED: {connection_method}") + + if connection_manufacturing == "FORGED": + return { + "method": "FORGED", + "confidence": 0.7, + "evidence": evidence, + "characteristics": "์†Œ๊ตฌ๊ฒฝ ๋‹จ์กฐํ’ˆ" + } + elif connection_manufacturing == "CAST": + return { + "method": "CAST", + "confidence": 0.7, + "evidence": evidence, + "characteristics": "๋Œ€๊ตฌ๊ฒฝ ์ฃผ์กฐํ’ˆ" + } + + # 5. ๊ธฐ๋ณธ ์ถ”์ • (์‚ฌ์ด์ฆˆ ๊ธฐ๋ฐ˜) + try: + size_num = float(re.findall(r'(\d+(?:\.\d+)?)', main_nom)[0]) + if size_num <= 2.0: + return { + "method": "FORGED", + "confidence": 0.6, + "evidence": ["SIZE_BASED_SMALL"], + "characteristics": "์†Œ๊ตฌ๊ฒฝ - ์ผ๋ฐ˜์ ์œผ๋กœ ๋‹จ์กฐํ’ˆ" + } + else: + return { + "method": "CAST", + "confidence": 0.6, + "evidence": ["SIZE_BASED_LARGE"], + "characteristics": "๋Œ€๊ตฌ๊ฒฝ - ์ผ๋ฐ˜์ ์œผ๋กœ ์ฃผ์กฐํ’ˆ" + } + except: + pass + + return { + "method": "UNKNOWN", + "confidence": 0.0, + "evidence": ["INSUFFICIENT_MANUFACTURING_INFO"], + "characteristics": "" + } + +def extract_valve_special_features(description: str, valve_type_result: Dict) -> List[str]: + """๋ฐธ๋ธŒ ํŠน์ˆ˜ ๊ธฐ๋Šฅ ์ถ”์ถœ""" + + desc_upper = description.upper() + features = [] + + # ๋ฐธ๋ธŒ ํƒ€์ž…๋ณ„ ํŠน์ˆ˜ ๊ธฐ๋Šฅ + valve_special_features = valve_type_result.get('special_features', []) + for feature in valve_special_features: + # ๊ธฐ๋Šฅ๋ณ„ ํ‚ค์›Œ๋“œ ๋งคํ•‘ + feature_keywords = { + "OS&Y": ["OS&Y", "OUTSIDE SCREW"], + "FULL_PORT": ["FULL PORT", "FULL BORE"], + "REDUCED_PORT": ["REDUCED PORT", "REDUCED BORE"], + "3_WAY": ["3 WAY", "3-WAY", "THREE WAY"], + "SWING_TYPE": ["SWING", "SWING TYPE"], + "LIFT_TYPE": ["LIFT", "LIFT TYPE"], + "GEAR_OPERATED": ["GEAR OPERATED", "GEAR"], + "SET_PRESSURE": ["SET PRESSURE", "SET @"] + } + + keywords = feature_keywords.get(feature, [feature]) + for keyword in keywords: + if keyword in desc_upper: + features.append(feature) + break + + # ์ผ๋ฐ˜์ ์ธ ํŠน์ˆ˜ ๊ธฐ๋Šฅ๋“ค + general_features = { + "FIRE_SAFE": ["FIRE SAFE", "FIRE-SAFE"], + "ANTI_STATIC": ["ANTI STATIC", "ANTI-STATIC"], + "BLOW_OUT_PROOF": ["BLOW OUT PROOF", "BOP"], + "EXTENDED_STEM": ["EXTENDED STEM", "EXT STEM"], + "CRYOGENIC": ["CRYOGENIC", "CRYO"], + "HIGH_TEMPERATURE": ["HIGH TEMP", "HT"] + } + + for feature, keywords in general_features.items(): + for keyword in keywords: + if keyword in desc_upper: + features.append(feature) + break + + return list(set(features)) # ์ค‘๋ณต ์ œ๊ฑฐ + +def calculate_valve_confidence(confidence_scores: Dict) -> float: + """๋ฐธ๋ธŒ ๋ถ„๋ฅ˜ ์ „์ฒด ์‹ ๋ขฐ๋„ ๊ณ„์‚ฐ""" + + scores = [score for score in confidence_scores.values() if score > 0] + + if not scores: + return 0.0 + + # ๊ฐ€์ค‘ ํ‰๊ท  + weights = { + "material": 0.2, + "valve_type": 0.4, + "connection": 0.25, + "pressure": 0.15 + } + + weighted_sum = sum( + confidence_scores.get(key, 0) * weight + for key, weight in weights.items() + ) + + return round(weighted_sum, 2) + +# ========== ํŠน์ˆ˜ ๊ธฐ๋Šฅ๋“ค ========== + +def is_forged_valve(valve_result: Dict) -> bool: + """๋‹จ์กฐ ๋ฐธ๋ธŒ ์—ฌ๋ถ€ ํŒ๋‹จ""" + return valve_result.get("manufacturing", {}).get("method") == "FORGED" + +def is_high_pressure_valve(valve_result: Dict) -> bool: + """๊ณ ์•• ๋ฐธ๋ธŒ ์—ฌ๋ถ€ ํŒ๋‹จ""" + pressure_rating = valve_result.get("pressure_rating", {}).get("rating", "") + high_pressure_ratings = ["800LB", "1500LB", "2500LB", "3000LB", "6000LB", "9000LB"] + return any(pressure in pressure_rating for pressure in high_pressure_ratings) + +def get_valve_purchase_info(valve_result: Dict) -> Dict: + """๋ฐธ๋ธŒ ๊ตฌ๋งค ์ •๋ณด ์ƒ์„ฑ""" + + valve_type = valve_result["valve_type"]["type"] + connection = valve_result["connection_method"]["method"] + pressure = valve_result["pressure_rating"]["rating"] + manufacturing = valve_result["manufacturing"]["method"] + actuation = valve_result["actuation"]["method"] + + # ๊ณต๊ธ‰์—…์ฒด ํƒ€์ž… ๊ฒฐ์ • + if manufacturing == "FORGED": + supplier_type = "๋‹จ์กฐ ๋ฐธ๋ธŒ ์ „๋ฌธ์—…์ฒด" + elif valve_type == "BUTTERFLY_VALVE": + supplier_type = "๋ฒ„ํ„ฐํ”Œ๋ผ์ด ๋ฐธ๋ธŒ ์ „๋ฌธ์—…์ฒด" + elif actuation in ["PNEUMATIC", "ELECTRIC"]: + supplier_type = "์ž๋™ ๋ฐธ๋ธŒ ์ „๋ฌธ์—…์ฒด" + else: + supplier_type = "์ผ๋ฐ˜ ๋ฐธ๋ธŒ ์—…์ฒด" + + # ๋‚ฉ๊ธฐ ์ถ”์ • + if manufacturing == "FORGED" and is_high_pressure_valve(valve_result): + lead_time = "8-12์ฃผ (๋‹จ์กฐ ๊ณ ์••์šฉ)" + elif actuation in ["PNEUMATIC", "ELECTRIC"]: + lead_time = "6-10์ฃผ (์ž๋™ ๋ฐธ๋ธŒ)" + elif manufacturing == "FORGED": + lead_time = "6-8์ฃผ (๋‹จ์กฐํ’ˆ)" + else: + lead_time = "4-8์ฃผ (์ผ๋ฐ˜ํ’ˆ)" + + return { + "supplier_type": supplier_type, + "lead_time_estimate": lead_time, + "purchase_category": f"{valve_type} {connection} {pressure}", + "manufacturing_note": valve_result["manufacturing"]["characteristics"], + "actuation_note": valve_result["actuation"]["characteristics"], + "special_requirements": valve_result["special_features"] + }