diff --git a/RULES.md b/RULES.md index fc43d35..554719a 100644 --- a/RULES.md +++ b/RULES.md @@ -116,20 +116,37 @@ navigate(`/material-comparison?job_no=${jobNo}&revision=${revision}`); ## ๐Ÿ”„ **๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ** -### **1. ๋ฐฑ์—”๋“œ ๋ณ€๊ฒฝ ์‹œ** +### **1. ์„œ๋ฒ„ ์‹คํ–‰ ๋ช…๋ น์–ด** +```bash +# ๋ฐฑ์—”๋“œ ์‹คํ–‰ (ํ„ฐ๋ฏธ๋„ 1๋ฒˆ) - TK-MP-Project ๋ฃจํŠธ์—์„œ +source venv/bin/activate # ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” (venv๋Š” ๋ฃจํŠธ์— ์žˆ์Œ) +cd backend +python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# ํ”„๋ก ํŠธ์—”๋“œ ์‹คํ–‰ (ํ„ฐ๋ฏธ๋„ 2๋ฒˆ) - TK-MP-Project ๋ฃจํŠธ์—์„œ +cd frontend +npm run dev # npm start ์•„๋‹˜! +``` + +**์ ‘์† ์ฃผ์†Œ:** +- ๋ฐฑ์—”๋“œ API: http://localhost:8000 +- API ๋ฌธ์„œ: http://localhost:8000/docs +- ํ”„๋ก ํŠธ์—”๋“œ: http://localhost:5173 + +### **2. ๋ฐฑ์—”๋“œ ๋ณ€๊ฒฝ ์‹œ** ```bash # ํ•ญ์ƒ ๊ฐ€์ƒํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ (์‚ฌ์šฉ์ž ์„ ํ˜ธ์‚ฌํ•ญ) cd backend python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ``` -### **2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ์‹œ** +### **3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ์‹œ** ```sql -- scripts/ ํด๋”์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ SQL ํŒŒ์ผ ์ƒ์„ฑ -- ๋ฒˆํ˜ธ ์ˆœ์„œ: 01_, 02_, 03_... ``` -### **3. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€** +### **4. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€** ``` ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑ (์‚ฌ์šฉ์ž ์„ ํ˜ธ์‚ฌํ•ญ) ์˜ˆ: "ํŒŒ์ดํ”„ ๊ธธ์ด ๊ณ„์‚ฐ ๋ฐ ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฒ„๊ทธ ์ˆ˜์ •" diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index a154139..3b8bd48 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -751,10 +751,14 @@ async def upload_file( else: thread_type = str(thread_spec_info) if thread_spec_info else "UNKNOWN" - # ์น˜์ˆ˜ ์ •๋ณด - diameter = material_data.get("main_nom", "") + # ์น˜์ˆ˜ ์ •๋ณด (์‹ค์ œ ๋ณผํŠธ ์‚ฌ์ด์ฆˆ ์‚ฌ์šฉ) + diameter = "" length = "" + nominal_size_fraction = "" if isinstance(dimensions_info, dict): + # ๋ณผํŠธ ๋ถ„๋ฅ˜๊ธฐ์—์„œ ์ถ”์ถœํ•œ ์‹ค์ œ ๋ณผํŠธ ์‚ฌ์ด์ฆˆ ์‚ฌ์šฉ + diameter = dimensions_info.get("nominal_size", material_data.get("main_nom", "")) + nominal_size_fraction = dimensions_info.get("nominal_size_fraction", diameter) length = dimensions_info.get("length", "") if not length and "70.0000 LG" in description: # ์›๋ณธ ์„ค๋ช…์—์„œ ๊ธธ์ด ์ถ”์ถœ diff --git a/backend/app/services/bolt_classifier.py b/backend/app/services/bolt_classifier.py index 02368fa..6fed363 100644 --- a/backend/app/services/bolt_classifier.py +++ b/backend/app/services/bolt_classifier.py @@ -12,6 +12,30 @@ def classify_bolt_material(description: str) -> Dict: desc_upper = description.upper() + # A193/A194 ๋™์‹œ ์ฒ˜๋ฆฌ (์˜ˆ: "ASTM A193/A194 GR B7/2H") + if "A193" in desc_upper and "A194" in desc_upper: + # B7/2H ๋“ฑ๊ธ‰ ์ถ”์ถœ + bolt_grade = "UNKNOWN" + nut_grade = "UNKNOWN" + + if "B7" in desc_upper: + bolt_grade = "B7" + if "2H" in desc_upper: + nut_grade = "2H" + elif " 8" in desc_upper or "GR 8" in desc_upper: + nut_grade = "8" + + combined_grade = f"{bolt_grade}/{nut_grade}" if bolt_grade != "UNKNOWN" and nut_grade != "UNKNOWN" else "A193/A194" + + return { + "standard": "ASTM A193/A194", + "grade": combined_grade, + "material_type": "ALLOY_STEEL", # B7/2H ์กฐํ•ฉ์€ ๋ณดํ†ต ํ•ฉ๊ธˆ๊ฐ• + "manufacturing": "FORGED", + "confidence": 0.95, + "evidence": ["ASTM_A193_A194_COMBINED"] + } + # ASTM A193 (๋ณผํŠธ์šฉ ๊ฐ•์žฌ) if any(pattern in desc_upper for pattern in ["A193", "ASTM A193"]): # B7, B8 ๋“ฑ ๋“ฑ๊ธ‰ ์ถ”์ถœ (GR B7/2H ํ˜•ํƒœ๋„ ์ง€์›) @@ -153,13 +177,49 @@ BOLT_TYPES = { }, "FLANGE_BOLT": { - "dat_file_patterns": ["FLG_BOLT", "FLANGE_BOLT", "BLT_150", "BLT_300", "BLT_600"], - "description_keywords": ["FLANGE BOLT", "ํ”Œ๋žœ์ง€๋ณผํŠธ", "150LB", "300LB", "600LB"], + "dat_file_patterns": ["FLG_BOLT", "FLANGE_BOLT"], + "description_keywords": ["FLANGE BOLT", "ํ”Œ๋žœ์ง€๋ณผํŠธ"], "characteristics": "ํ”Œ๋žœ์ง€ ์ „์šฉ ๋ณผํŠธ", "applications": "ํ”Œ๋žœ์ง€ ์ฒด๊ฒฐ ์ „์šฉ", "head_type": "HEXAGON" }, + "PSV_BOLT": { + "dat_file_patterns": ["PSV_BOLT", "PSV_BLT"], + "description_keywords": ["PSV", "PRESSURE SAFETY VALVE BOLT"], + "characteristics": "์••๋ ฅ์•ˆ์ „๋ฐธ๋ธŒ์šฉ ํŠน์ˆ˜ ๋ณผํŠธ", + "applications": "PSV ์ฒด๊ฒฐ ์ „์šฉ", + "head_type": "HEXAGON", + "special_application": "PSV" + }, + + "LT_BOLT": { + "dat_file_patterns": ["LT_BOLT", "LT_BLT"], + "description_keywords": ["LT", "LOW TEMP", "์ €์˜จ์šฉ"], + "characteristics": "์ €์˜จ์šฉ ํŠน์ˆ˜ ๋ณผํŠธ", + "applications": "์ €์˜จ ํ™˜๊ฒฝ ์ฒด๊ฒฐ์šฉ", + "head_type": "HEXAGON", + "special_application": "LT" + }, + + "CK_BOLT": { + "dat_file_patterns": ["CK_BOLT", "CK_BLT", "CHECK_BOLT"], + "description_keywords": ["CK", "CHECK VALVE BOLT"], + "characteristics": "์ฒดํฌ๋ฐธ๋ธŒ์šฉ ํŠน์ˆ˜ ๋ณผํŠธ", + "applications": "์ฒดํฌ๋ฐธ๋ธŒ ์ฒด๊ฒฐ ์ „์šฉ", + "head_type": "HEXAGON", + "special_application": "CK" + }, + + "ORI_BOLT": { + "dat_file_patterns": ["ORI_BOLT", "ORI_BLT", "ORIFICE_BOLT"], + "description_keywords": ["ORI", "ORIFICE", "์˜ค๋ฆฌํ”ผ์Šค"], + "characteristics": "์˜ค๋ฆฌํ”ผ์Šค์šฉ ํŠน์ˆ˜ ๋ณผํŠธ", + "applications": "์˜ค๋ฆฌํ”ผ์Šค ์ฒด๊ฒฐ ์ „์šฉ", + "head_type": "HEXAGON", + "special_application": "ORI" + }, + "MACHINE_SCREW": { "dat_file_patterns": ["MACH_SCR", "M_SCR"], "description_keywords": ["MACHINE SCREW", "๋จธ์‹ ์Šคํฌ๋ฅ˜", "๊ธฐ๊ณ„๋‚˜์‚ฌ"], @@ -272,11 +332,17 @@ THREAD_STANDARDS = { }, "INCH": { - "patterns": [r"(\d+(?:/\d+)?)\s*[\"\']\s*UNC", r"(\d+(?:/\d+)?)\s*[\"\']\s*UNF", - r"(\d+(?:/\d+)?)\s*[\"\']-(\d+)"], + "patterns": [ + r"(\d+(?:/\d+)?)\s*[\"\']\s*UNC", # 1/2" UNC + r"(\d+(?:/\d+)?)\s*[\"\']\s*UNF", # 1/2" UNF + r"(\d+(?:/\d+)?)\s*[\"\']-(\d+)", # 1/2"-13 + r"(\d+\.\d+)", # 0.625 (์†Œ์ˆ˜์  ์ธ์น˜) + r"(\d+(?:/\d+)?)\s*INCH", # 1/2 INCH + r"(\d+(?:/\d+)?)\s*IN" # 1/2 IN + ], "description": "์ธ์น˜ ๋‚˜์‚ฌ", "thread_types": ["UNC", "UNF"], - "common_sizes": ["1/4\"", "5/16\"", "3/8\"", "1/2\"", "5/8\"", "3/4\"", "7/8\"", "1\""] + "common_sizes": ["1/4\"", "5/16\"", "3/8\"", "1/2\"", "5/8\"", "0.625\"", "3/4\"", "7/8\"", "1\""] }, "BSW": { @@ -302,6 +368,203 @@ BOLT_GRADES = { } } +def convert_decimal_to_fraction(decimal_str: str) -> str: + """์†Œ์ˆ˜์  ์ธ์น˜๋ฅผ ๋ถ„์ˆ˜๋กœ ๋ณ€ํ™˜ (ํ˜„์žฅ ํ‘œ์ค€)""" + + try: + decimal = float(decimal_str) + + # ์ผ๋ฐ˜์ ์ธ ์ธ์น˜ ๋ถ„์ˆ˜ ๋ณ€ํ™˜ํ‘œ + inch_fractions = { + 0.125: "1/8", + 0.1875: "3/16", + 0.25: "1/4", + 0.3125: "5/16", + 0.375: "3/8", + 0.4375: "7/16", + 0.5: "1/2", + 0.5625: "9/16", + 0.625: "5/8", + 0.6875: "11/16", + 0.75: "3/4", + 0.8125: "13/16", + 0.875: "7/8", + 0.9375: "15/16", + 1.0: "1", + 1.125: "1-1/8", + 1.25: "1-1/4", + 1.375: "1-3/8", + 1.5: "1-1/2", + 1.625: "1-5/8", + 1.75: "1-3/4", + 1.875: "1-7/8", + 2.0: "2" + } + + # ์ •ํ™•ํ•œ ๋งค์นญ (์†Œ์ˆ˜์  ์˜ค์ฐจ ๊ณ ๋ ค) + for dec_val, fraction in inch_fractions.items(): + if abs(decimal - dec_val) < 0.001: # 1mm ์˜ค์ฐจ ํ—ˆ์šฉ + return fraction + + # ์ •ํ™•ํ•œ ๋งค์นญ์ด ์—†์œผ๋ฉด ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ฐ’ ์ฐพ๊ธฐ + closest_decimal = min(inch_fractions.keys(), key=lambda x: abs(x - decimal)) + if abs(closest_decimal - decimal) < 0.0625: # 1/16" ์ด๋‚ด ์˜ค์ฐจ๋งŒ ํ—ˆ์šฉ + return inch_fractions[closest_decimal] + + # ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์œผ๋ฉด ์›๋ž˜ ๊ฐ’ ๋ฐ˜ํ™˜ + return str(decimal) + + except ValueError: + return decimal_str + +def classify_surface_treatment(description: str) -> Dict: + """๋ณผํŠธ ํ‘œ๋ฉด์ฒ˜๋ฆฌ ๋ถ„๋ฅ˜ (์•„์—ฐ๋„๊ธˆ, ์Šคํ…Œ์ธ๋ฆฌ์Šค ๋“ฑ)""" + + desc_upper = description.upper() + treatments = [] + + # ์ „๊ธฐ์•„์—ฐ๋„๊ธˆ + if any(keyword in desc_upper for keyword in ["ELEC.GALV", "ELEC GALV", "ELECTRO GALV", "์ „๊ธฐ์•„์—ฐ๋„๊ธˆ"]): + treatments.append({ + "type": "ELECTRO_GALVANIZING", + "description": "์ „๊ธฐ์•„์—ฐ๋„๊ธˆ", + "code": "ELEC.GALV", + "corrosion_resistance": "๋ณดํ†ต" + }) + + # ์šฉ์œต์•„์—ฐ๋„๊ธˆ + if any(keyword in desc_upper for keyword in ["HOT DIP GALV", "HDG", "์šฉ์œต์•„์—ฐ๋„๊ธˆ"]): + treatments.append({ + "type": "HOT_DIP_GALVANIZING", + "description": "์šฉ์œต์•„์—ฐ๋„๊ธˆ", + "code": "HDG", + "corrosion_resistance": "๋†’์Œ" + }) + + # ์Šคํ…Œ์ธ๋ฆฌ์Šค (ํ‘œ๋ฉด์ฒ˜๋ฆฌ ๋ถˆํ•„์š”) + if any(keyword in desc_upper for keyword in ["STAINLESS", "STS", "์Šคํ…Œ์ธ๋ฆฌ์Šค"]): + treatments.append({ + "type": "STAINLESS_STEEL", + "description": "์Šคํ…Œ์ธ๋ฆฌ์Šค๊ฐ•", + "code": "STS", + "corrosion_resistance": "๋งค์šฐ๋†’์Œ" + }) + + # ๋‹ˆ์ผˆ๋„๊ธˆ + if any(keyword in desc_upper for keyword in ["NICKEL", "NI PLATING", "๋‹ˆ์ผˆ๋„๊ธˆ"]): + treatments.append({ + "type": "NICKEL_PLATING", + "description": "๋‹ˆ์ผˆ๋„๊ธˆ", + "code": "NI", + "corrosion_resistance": "๋†’์Œ" + }) + + # ํฌ๋กฌ๋„๊ธˆ + if any(keyword in desc_upper for keyword in ["CHROME", "CR PLATING", "ํฌ๋กฌ๋„๊ธˆ"]): + treatments.append({ + "type": "CHROME_PLATING", + "description": "ํฌ๋กฌ๋„๊ธˆ", + "code": "CR", + "corrosion_resistance": "๋งค์šฐ๋†’์Œ" + }) + + return { + "treatments": treatments, + "has_treatment": len(treatments) > 0, + "treatment_count": len(treatments), + "primary_treatment": treatments[0] if treatments else None + } + +def classify_special_application_bolts(description: str) -> Dict: + """ + ํŠน์ˆ˜ ์šฉ๋„ ๋ณผํŠธ ๋ถ„๋ฅ˜ ๋ฐ ์นด์šดํŒ… (PSV, LT, CK) + + ์ฃผ์˜: ์ด ํ•จ์ˆ˜๋Š” ์ด๋ฏธ BOLT๋กœ ๋ถ„๋ฅ˜๋œ ์•„์ดํ…œ์—์„œ๋งŒ ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•จ + PSV, LT, CK๋Š” ํ•ด๋‹น ์žฅ๋น„์šฉ ๋ณผํŠธ๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ, ์žฅ๋น„ ์ž์ฒด๊ฐ€ ์•„๋‹˜ + """ + + desc_upper = description.upper() + special_applications = [] + special_details = {} + + # PSV ๋ณผํŠธ ํ™•์ธ (์••๋ ฅ์•ˆ์ „๋ฐธ๋ธŒ์šฉ ๋ณผํŠธ) + psv_patterns = [ + r'\bPSV\b', # ๋‹จ์–ด ๊ฒฝ๊ณ„๋กœ PSV๋งŒ + r'PRESSURE\s+SAFETY\s+VALVE', + r'์••๋ ฅ์•ˆ์ „๋ฐธ๋ธŒ', + r'PSV\s+BOLT', + r'PSV\s+BLT' + ] + + import re + if any(re.search(pattern, desc_upper) for pattern in psv_patterns): + special_applications.append("PSV") + special_details["PSV"] = { + "type": "์••๋ ฅ์•ˆ์ „๋ฐธ๋ธŒ์šฉ ๋ณผํŠธ", + "application": "PSV ์ฒด๊ฒฐ ์ „์šฉ", + "critical": True # ์•ˆ์ „ ์žฅ๋น„์šฉ์œผ๋กœ ์ค‘์š” + } + + # LT ๋ณผํŠธ ํ™•์ธ (์ €์˜จ์šฉ ๋ณผํŠธ) + lt_patterns = [ + r'\bLT\b', # ๋‹จ์–ด ๊ฒฝ๊ณ„๋กœ LT๋งŒ + r'LOW\s+TEMP', + r'์ €์˜จ์šฉ', + r'CRYOGENIC', + r'LT\s+BOLT', + r'LT\s+BLT' + ] + + if any(re.search(pattern, desc_upper) for pattern in lt_patterns): + special_applications.append("LT") + special_details["LT"] = { + "type": "์ €์˜จ์šฉ ๋ณผํŠธ", + "application": "์ €์˜จ ํ™˜๊ฒฝ ์ฒด๊ฒฐ์šฉ", + "critical": True # ์ €์˜จ ํ™˜๊ฒฝ์šฉ์œผ๋กœ ์ค‘์š” + } + + # CK ๋ณผํŠธ ํ™•์ธ (์ฒดํฌ๋ฐธ๋ธŒ์šฉ ๋ณผํŠธ) + ck_patterns = [ + r'\bCK\b', # ๋‹จ์–ด ๊ฒฝ๊ณ„๋กœ CK๋งŒ + r'CHECK\s+VALVE', + r'์ฒดํฌ๋ฐธ๋ธŒ', + r'CK\s+BOLT', + r'CK\s+BLT' + ] + + if any(re.search(pattern, desc_upper) for pattern in ck_patterns): + special_applications.append("CK") + special_details["CK"] = { + "type": "์ฒดํฌ๋ฐธ๋ธŒ์šฉ ๋ณผํŠธ", + "application": "์ฒดํฌ๋ฐธ๋ธŒ ์ฒด๊ฒฐ ์ „์šฉ", + "critical": False # ์ผ๋ฐ˜์  + } + + # ORI ๋ณผํŠธ ํ™•์ธ (์˜ค๋ฆฌํ”ผ์Šค์šฉ ๋ณผํŠธ) + ori_patterns = [ + r'\bORI\b', # ๋‹จ์–ด ๊ฒฝ๊ณ„๋กœ ORI๋งŒ + r'ORIFICE', + r'์˜ค๋ฆฌํ”ผ์Šค', + r'ORI\s+BOLT', + r'ORI\s+BLT' + ] + + if any(re.search(pattern, desc_upper) for pattern in ori_patterns): + special_applications.append("ORI") + special_details["ORI"] = { + "type": "์˜ค๋ฆฌํ”ผ์Šค์šฉ ๋ณผํŠธ", + "application": "์˜ค๋ฆฌํ”ผ์Šค ์ฒด๊ฒฐ ์ „์šฉ", + "critical": True # ์œ ๋Ÿ‰ ์ธก์ •์šฉ์œผ๋กœ ์ค‘์š” + } + + return { + "detected_applications": special_applications, + "special_details": special_details, + "is_special_bolt": len(special_applications) > 0, + "special_count": len(special_applications), + "classification_note": "ํŠน์ˆ˜ ์žฅ๋น„์šฉ ๋ณผํŠธ (์žฅ๋น„ ์ž์ฒด ์•„๋‹˜)" + } + def classify_bolt(dat_file: str, description: str, main_nom: str, length: Optional[float] = None) -> Dict: """ ์™„์ „ํ•œ BOLT ๋ถ„๋ฅ˜ @@ -337,7 +600,13 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option # 6. ๋“ฑ๊ธ‰ ๋ฐ ๊ฐ•๋„ ๋ถ„๋ฅ˜ grade_result = classify_bolt_grade(description, thread_result) - # 7. ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํ•ฉ + # 7. ํŠน์ˆ˜ ์šฉ๋„ ๋ณผํŠธ ๋ถ„๋ฅ˜ (PSV, LT, CK) + special_result = classify_special_application_bolts(description) + + # 8. ํ‘œ๋ฉด์ฒ˜๋ฆฌ ๋ถ„๋ฅ˜ (ELEC.GALV ๋“ฑ) + surface_result = classify_surface_treatment(description) + + # 9. ์ตœ์ข… ๊ฒฐ๊ณผ ์กฐํ•ฉ return { "category": "BOLT", @@ -367,6 +636,7 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option "thread_specification": { "standard": thread_result.get('standard', 'UNKNOWN'), "size": thread_result.get('size', ''), + "size_fraction": thread_result.get('size_fraction', ''), "pitch": thread_result.get('pitch', ''), "thread_type": thread_result.get('thread_type', ''), "confidence": thread_result.get('confidence', 0.0) @@ -374,6 +644,7 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option "dimensions": { "nominal_size": dimensions_result.get('nominal_size', main_nom), + "nominal_size_fraction": dimensions_result.get('nominal_size_fraction', main_nom), "length": dimensions_result.get('length', ''), "diameter": dimensions_result.get('diameter', ''), "dimension_description": dimensions_result.get('dimension_description', '') @@ -386,6 +657,23 @@ def classify_bolt(dat_file: str, description: str, main_nom: str, length: Option "confidence": grade_result.get('confidence', 0.0) }, + # ํŠน์ˆ˜ ์šฉ๋„ ๋ณผํŠธ ์ •๋ณด + "special_applications": { + "is_special_bolt": special_result.get('is_special_bolt', False), + "detected_applications": special_result.get('detected_applications', []), + "special_details": special_result.get('special_details', {}), + "special_count": special_result.get('special_count', 0), + "classification_note": special_result.get('classification_note', '') + }, + + # ํ‘œ๋ฉด์ฒ˜๋ฆฌ ์ •๋ณด + "surface_treatment": { + "has_treatment": surface_result.get('has_treatment', False), + "treatments": surface_result.get('treatments', []), + "treatment_count": surface_result.get('treatment_count', 0), + "primary_treatment": surface_result.get('primary_treatment', None) + }, + # ์ „์ฒด ์‹ ๋ขฐ๋„ "overall_confidence": calculate_bolt_confidence({ "material": material_result.get('confidence', 0), @@ -536,9 +824,19 @@ def classify_thread_specification(main_nom: str, description: str) -> Dict: thread_type = t_type break + # ์ธ์น˜ ์‚ฌ์ด์ฆˆ๋ฅผ ๋ถ„์ˆ˜๋กœ ๋ณ€ํ™˜ + size_fraction = size + if standard == "INCH": + try: + if '.' in size and size.replace('.', '').isdigit(): + size_fraction = convert_decimal_to_fraction(size).replace('"', '') + except: + size_fraction = size + return { "standard": standard, - "size": size, + "size": size, # ์›๋ž˜ ๊ฐ’ + "size_fraction": size_fraction, # ๋ถ„์ˆ˜ ๋ณ€ํ™˜๊ฐ’ "pitch": pitch, "thread_type": thread_type, "confidence": 0.9, @@ -559,12 +857,62 @@ def extract_bolt_dimensions(main_nom: str, description: str) -> Dict: """๋ณผํŠธ ์น˜์ˆ˜ ์ •๋ณด ์ถ”์ถœ""" desc_upper = description.upper() + actual_bolt_size = main_nom + + # ์‹ค์ œ BOM ํ˜•ํƒœ: "ORI, 0.75, 145.0000 LG, 300LB, ASTM A193/A194 GR B7/2H, ELEC.GALV" + # ์ฒซ ๋ฒˆ์งธ ์ˆซ์ž๊ฐ€ ์‹ค์ œ ๋ณผํŠธ ์‚ฌ์ด์ฆˆ (์ ‘๋‘์‚ฌ ๊ฑด๋„ˆ๋›ฐ๊ธฐ) + import re + # ์„ค๋ช…์—์„œ ์ฒซ ๋ฒˆ์งธ ์ˆซ์ž ์ถ”์ถœ (๋ณผํŠธ ์‚ฌ์ด์ฆˆ) + first_number_match = re.search(r'(\d+(?:\.\d+)?)', description) + if first_number_match: + actual_bolt_size = first_number_match.group(1) + + # ํ”Œ๋žœ์ง€ ๋ณผํŠธ์˜ ๊ฒฝ์šฐ ์‹ค์ œ ๋ณผํŠธ ์ง๊ฒฝ์„ description์—์„œ ์ถ”์ถœ + if "FLANGE BOLT" in desc_upper or "FLG_BOLT" in desc_upper: + # ํ”Œ๋žœ์ง€ ๋ณผํŠธ์—์„œ ์‹ค์ œ ๋ณผํŠธ ์‚ฌ์ด์ฆˆ ํŒจํ„ด ์ฐพ๊ธฐ + # ์˜ˆ: "FLANGE BOLT 6" 150LB M16" โ†’ M16 + # ์˜ˆ: "FLANGE BOLT 1-1/2" 5/8" x 100mm" โ†’ 5/8 + + bolt_size_patterns = [ + r'M(\d+)', # M16, M20 ๋“ฑ ๋ฉ”ํŠธ๋ฆญ + r'(\d+-\d+/\d+)\s*["\']?\s*X', # 1-1/2" X ๋“ฑ (๋ณตํ•ฉ ๋ถ„์ˆ˜) + r'(\d+/\d+)\s*["\']?\s*X', # 5/8" X, 3/4" X ๋“ฑ (๋‹จ์ˆœ ๋ถ„์ˆ˜) + r'(\d+(?:\.\d+)?)\s*["\']?\s*X', # 0.625" X ๋“ฑ (์†Œ์ˆ˜) + r'(\d+-\d+/\d+)\s*["\']?\s*DIA', # 1-1/2" DIA ๋“ฑ (๋ณตํ•ฉ ๋ถ„์ˆ˜) + r'(\d+/\d+)\s*["\']?\s*DIA', # 5/8" DIA ๋“ฑ (๋‹จ์ˆœ ๋ถ„์ˆ˜) + r'(\d+(?:\.\d+)?)\s*["\']?\s*DIA', # 0.625" DIA ๋“ฑ (์†Œ์ˆ˜) + r'(\d+-\d+/\d+)\s*["\']?\s*(?:LONG|LG|LENGTH)', # ๋ณตํ•ฉ ๋ถ„์ˆ˜ + ๊ธธ์ด + r'(\d+/\d+)\s*["\']?\s*(?:LONG|LG|LENGTH)', # ๋‹จ์ˆœ ๋ถ„์ˆ˜ + ๊ธธ์ด + r'(\d+(?:\.\d+)?)\s*["\']?\s*(?:LONG|LG|LENGTH)', # ์†Œ์ˆ˜ + ๊ธธ์ด + r'(\d+(?:\.\d+)?)\s*MM\s*DIA', # 16MM DIA ๋“ฑ + ] + + for pattern in bolt_size_patterns: + match = re.search(pattern, desc_upper) + if match: + extracted_size = match.group(1) + # M16 ๊ฐ™์€ ๋ฉ”ํŠธ๋ฆญ์€ M ์ œ๊ฑฐ + if pattern.startswith(r'M'): + actual_bolt_size = extracted_size + else: + actual_bolt_size = extracted_size + break + + # ๋ณผํŠธ ์‚ฌ์ด์ฆˆ๋ฅผ ๋ถ„์ˆ˜๋กœ ๋ณ€ํ™˜ (์ธ์น˜์ธ ๊ฒฝ์šฐ) + nominal_size_fraction = actual_bolt_size + try: + # ์†Œ์ˆ˜์  ์ธ์น˜๋ฅผ ๋ถ„์ˆ˜๋กœ ๋ณ€ํ™˜ + if '.' in actual_bolt_size and actual_bolt_size.replace('.', '').isdigit(): + nominal_size_fraction = convert_decimal_to_fraction(actual_bolt_size) + except: + nominal_size_fraction = actual_bolt_size dimensions = { - "nominal_size": main_nom, + "nominal_size": actual_bolt_size, # ์‹ค์ œ ๋ณผํŠธ ์‚ฌ์ด์ฆˆ + "nominal_size_fraction": nominal_size_fraction, # ๋ถ„์ˆ˜ ๋ณ€ํ™˜๊ฐ’ "length": "", "diameter": "", - "dimension_description": main_nom + "dimension_description": nominal_size_fraction # ๋ถ„์ˆ˜๋กœ ํ‘œ์‹œ } # ๊ธธ์ด ์ •๋ณด ์ถ”์ถœ @@ -595,8 +943,8 @@ def extract_bolt_dimensions(main_nom: str, description: str) -> Dict: dimensions["diameter"] = f"{match.group(1)}mm" break - # ์น˜์ˆ˜ ์„ค๋ช… ์กฐํ•ฉ - desc_parts = [main_nom] + # ์น˜์ˆ˜ ์„ค๋ช… ์กฐํ•ฉ (๋ถ„์ˆ˜ ์‚ฌ์šฉ) + desc_parts = [nominal_size_fraction] if dimensions["length"]: desc_parts.append(f"L{dimensions['length']}") diff --git a/backend/app/services/integrated_classifier.py b/backend/app/services/integrated_classifier.py index 7ed2850..547ca97 100644 --- a/backend/app/services/integrated_classifier.py +++ b/backend/app/services/integrated_classifier.py @@ -8,7 +8,7 @@ from typing import Dict, List, Optional, Tuple # Level 1: ๋ช…ํ™•ํ•œ ํƒ€์ž… ํ‚ค์›Œ๋“œ (์ตœ์šฐ์„ ) LEVEL1_TYPE_KEYWORDS = { - "BOLT": ["BOLT", "STUD", "NUT", "SCREW", "WASHER", "๋ณผํŠธ", "๋„ˆํŠธ", "์Šคํ„ฐ๋“œ", "๋‚˜์‚ฌ", "์™€์…”"], + "BOLT": ["FLANGE 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"], @@ -87,7 +87,9 @@ def classify_material_integrated(description: str, main_nom: str = "", detected_types = [] for material_type, keywords in LEVEL1_TYPE_KEYWORDS.items(): type_found = False - for keyword in keywords: + # ๊ธด ํ‚ค์›Œ๋“œ๋ถ€ํ„ฐ ํ™•์ธ (FLANGE BOLT๊ฐ€ FLANGE๋ณด๋‹ค ๋จผ์ € ๋งค์นญ๋˜๋„๋ก) + sorted_keywords = sorted(keywords, key=len, reverse=True) + for keyword in sorted_keywords: # ์ „์ฒด ๋ฌธ์ž์—ด์—์„œ ์ฐพ๊ธฐ if keyword in desc_upper: detected_types.append((material_type, keyword)) @@ -117,8 +119,8 @@ def classify_material_integrated(description: str, main_nom: str = "", } # Level 2 ํ‚ค์›Œ๋“œ๊ฐ€ ์—†์œผ๋ฉด ์šฐ์„ ์ˆœ์œ„๋กœ ๊ฒฐ์ • - # FITTING > VALVE > FLANGE > PIPE > BOLT (๋” ๊ตฌ์ฒด์ ์ธ ๊ฒƒ ์šฐ์„ ) - type_priority = ["FITTING", "VALVE", "FLANGE", "PIPE", "BOLT", "GASKET", "INSTRUMENT"] + # BOLT > FITTING > VALVE > FLANGE > PIPE (๋ณผํŠธ ์šฐ์„ , ๋” ๊ตฌ์ฒด์ ์ธ ๊ฒƒ ์šฐ์„ ) + type_priority = ["BOLT", "FITTING", "VALVE", "FLANGE", "PIPE", "GASKET", "INSTRUMENT"] for priority_type in type_priority: for detected_type, keyword in detected_types: if detected_type == priority_type: diff --git a/backend/app/services/purchase_calculator.py b/backend/app/services/purchase_calculator.py index 93d7fea..0409669 100644 --- a/backend/app/services/purchase_calculator.py +++ b/backend/app/services/purchase_calculator.py @@ -224,6 +224,9 @@ def generate_purchase_items_from_materials(db: Session, file_id: int, 'specification': spec_data.get('full_spec', spec_key), 'material_spec': spec_data.get('material_spec', ''), 'size_spec': spec_data.get('size_display', ''), + 'size_fraction': spec_data.get('size_fraction', ''), + 'surface_treatment': spec_data.get('surface_treatment', ''), + 'special_applications': spec_data.get('special_applications', {}), 'unit': spec_data.get('unit', 'EA'), **calc_result, 'job_no': job_no, @@ -310,16 +313,46 @@ def generate_material_specs_for_category(materials: List[Dict], category: str) - diameter = material.get('diameter', material.get('main_nom', '')) material_spec = material_standard or material.get('material_grade', '') + # ๋ถ„์ˆ˜ ์‚ฌ์ด์ฆˆ ์ •๋ณด ์ถ”์ถœ (์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ๋ถ„๋ฅ˜๊ธฐ ์ •๋ณด) + size_fraction = material.get('size_fraction', diameter) + surface_treatment = material.get('surface_treatment', '') + + # ํŠน์ˆ˜ ์šฉ๋„ ์ •๋ณด ์ถ”์ถœ (PSV, LT, CK) + special_applications = { + 'PSV': 0, + 'LT': 0, + 'CK': 0 + } + + # ์„ค๋ช…์—์„œ ํŠน์ˆ˜ ์šฉ๋„ ํ‚ค์›Œ๋“œ ํ™•์ธ (๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•) + description = material.get('original_description', '').upper() + if 'PSV' in description or 'PRESSURE SAFETY VALVE' in description: + special_applications['PSV'] = material.get('quantity', 0) + if any(keyword in description for keyword in ['LT', 'LOW TEMP', '์ €์˜จ์šฉ']): + special_applications['LT'] = material.get('quantity', 0) + if 'CK' in description or 'CHECK VALVE' in description: + special_applications['CK'] = material.get('quantity', 0) + spec_parts = [bolt_type.replace('_', ' ')] if material_standard: spec_parts.append(material_standard) full_spec = ', '.join(spec_parts) - spec_key = f"BOLT|{full_spec}|{material_spec}|{diameter}" + # ํŠน์ˆ˜ ์šฉ๋„์™€ ๊ด€๊ณ„์—†์ด ์‚ฌ์ด์ฆˆ+๊ธธ์ด๋กœ ํ•ฉ์‚ฐ (๊ตฌ๋งค๋Š” ๋™์ผํ•˜๋ฏ€๋กœ) + # ๊ธธ์ด ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด ํฌํ•จ + length_info = material.get('length', '') + if length_info: + diameter_key = f"{diameter}L{length_info}" + else: + diameter_key = diameter + + spec_key = f"BOLT|{full_spec}|{material_spec}|{diameter_key}" spec_data = { 'category': 'BOLT', 'full_spec': full_spec, 'material_spec': material_spec, 'size_display': diameter, + 'size_fraction': size_fraction, + 'surface_treatment': surface_treatment, 'unit': 'EA' } @@ -378,12 +411,18 @@ def generate_material_specs_for_category(materials: List[Dict], category: str) - **spec_data, 'totalQuantity': 0, 'count': 0, - 'items': [] + 'items': [], + 'special_applications': {'PSV': 0, 'LT': 0, 'CK': 0} if category == 'BOLT' else None } specs[spec_key]['totalQuantity'] += material.get('quantity', 0) specs[spec_key]['count'] += 1 specs[spec_key]['items'].append(material) + + # ๋ณผํŠธ์˜ ๊ฒฝ์šฐ ํŠน์ˆ˜ ์šฉ๋„ ์ •๋ณด ๋ˆ„์  + if category == 'BOLT' and 'special_applications' in locals(): + for app_type, count in special_applications.items(): + specs[spec_key]['special_applications'][app_type] += count return specs diff --git a/frontend/src/pages/MaterialsPage.jsx b/frontend/src/pages/MaterialsPage.jsx index 685c813..7d7095a 100644 --- a/frontend/src/pages/MaterialsPage.jsx +++ b/frontend/src/pages/MaterialsPage.jsx @@ -363,18 +363,53 @@ const MaterialsPage = () => { isLength: false }; } else if (category === 'BOLT') { - // BOLT: ํƒ€์ž… + ์žฌ์งˆ + ์‚ฌ์ด์ฆˆ + ๊ธธ์ด + // BOLT: ํƒ€์ž… + ์žฌ์งˆ + ์‚ฌ์ด์ฆˆ + ๊ธธ์ด (๋ถ„์ˆ˜ ํ‘œ๊ธฐ ๋ฐ ํŠน์ˆ˜ ์šฉ๋„ ํฌํ•จ) const material_spec = material.material_grade || ''; const main_nom = material.main_nom || ''; const bolt_type = material.bolt_details?.bolt_type || 'BOLT'; const material_standard = material.bolt_details?.material_standard || ''; const material_grade = material.bolt_details?.material_grade || ''; const thread_type = material.bolt_details?.thread_type || ''; - const diameter = material.bolt_details?.diameter || main_nom; + // ์‹ค์ œ ๋ณผํŠธ ์ง๊ฒฝ (๋ฐฑ์—”๋“œ์—์„œ ์ถ”์ถœ๋œ ๊ฐ’ ์šฐ์„  ์‚ฌ์šฉ) + const diameter = material.bolt_details?.nominal_size || + material.bolt_details?.diameter || + main_nom; const length = material.bolt_details?.length || ''; const pressure_rating = material.bolt_details?.pressure_rating || ''; const coating_type = material.bolt_details?.coating_type || ''; + // ๋ถ„์ˆ˜ ์‚ฌ์ด์ฆˆ ๋ณ€ํ™˜ (0.625 โ†’ 5/8, " ๊ธฐํ˜ธ ์ œ๊ฑฐ) + const convertToFraction = (decimal) => { + const fractions = { + '0.125': '1/8', '0.1875': '3/16', '0.25': '1/4', '0.3125': '5/16', + '0.375': '3/8', '0.4375': '7/16', '0.5': '1/2', '0.5625': '9/16', + '0.625': '5/8', '0.6875': '11/16', '0.75': '3/4', '0.8125': '13/16', + '0.875': '7/8', '0.9375': '15/16', '1.0': '1', '1.125': '1-1/8', + '1.25': '1-1/4', '1.375': '1-3/8', '1.5': '1-1/2', '1.625': '1-5/8', + '1.75': '1-3/4', '1.875': '1-7/8', '2.0': '2' + }; + return fractions[decimal] || decimal; + }; + + const size_fraction = convertToFraction(diameter); + const display_size = size_fraction !== diameter ? size_fraction : diameter; + + // ํŠน์ˆ˜ ์šฉ๋„ ํ™•์ธ (PSV, LT, CK, ORI) + const description = material.original_description?.toUpperCase() || ''; + const special_applications = []; + if (description.includes('PSV') || description.includes('PRESSURE SAFETY VALVE')) { + special_applications.push('PSV'); + } + if (description.includes('LT') || description.includes('LOW TEMP') || description.includes('์ €์˜จ์šฉ')) { + special_applications.push('LT'); + } + if (description.includes('CK') || description.includes('CHECK VALVE')) { + special_applications.push('CK'); + } + if (description.includes('ORI') || description.includes('ORIFICE') || description.includes('์˜ค๋ฆฌํ”ผ์Šค')) { + special_applications.push('ORI'); + } + // ๋ณผํŠธ ์ŠคํŽ™ ์ƒ์„ฑ const bolt_spec_parts = []; @@ -393,9 +428,14 @@ const MaterialsPage = () => { bolt_spec_parts.push(material_spec); } - // ๋‚˜์‚ฌ ๊ทœ๊ฒฉ (M12, 1/2" ๋“ฑ) + // ๋‚˜์‚ฌ ๊ทœ๊ฒฉ (๋ถ„์ˆ˜ ํ‘œ๊ธฐ๋กœ) if (diameter) { - bolt_spec_parts.push(diameter); + bolt_spec_parts.push(display_size); + } + + // ํŠน์ˆ˜ ์šฉ๋„ ํ‘œ์‹œ + if (special_applications.length > 0) { + bolt_spec_parts.push(`[${special_applications.join(', ')}์šฉ]`); } // ์ฝ”ํŒ… ํƒ€์ž… (ELECTRO_GALVANIZED ๋“ฑ) @@ -405,7 +445,8 @@ const MaterialsPage = () => { const full_bolt_spec = bolt_spec_parts.join(', '); - specKey = `${category}|${full_bolt_spec}|${diameter}|${length}|${coating_type}|${pressure_rating}`; + // ํŠน์ˆ˜ ์šฉ๋„์™€ ๊ด€๊ณ„์—†์ด ์‚ฌ์ด์ฆˆ+๊ธธ์ด๋กœ ํ•ฉ์‚ฐ (๊ตฌ๋งค๋Š” ๋™์ผํ•˜๋ฏ€๋กœ) + specKey = `${category}|${full_bolt_spec}|${diameter}|${length}|${coating_type}`; specData = { category: 'BOLT', bolt_type, @@ -414,10 +455,13 @@ const MaterialsPage = () => { material_standard, material_grade, diameter, + size_fraction, + display_size, length, coating_type, pressure_rating, - size_display: diameter, + special_applications, + size_display: display_size, main_nom: diameter, unit: 'EA', isLength: false @@ -840,8 +884,8 @@ const MaterialsPage = () => { ์žฌ์งˆ ์‚ฌ์ด์ฆˆ ๊ธธ์ด + ํŠน์ˆ˜์šฉ๋„ ์ฝ”ํŒ… - ์••๋ ฅ๋“ฑ๊ธ‰ ์ˆ˜๋Ÿ‰ )} @@ -966,8 +1010,8 @@ const MaterialsPage = () => { - - {spec.diameter ? (spec.diameter.includes('"') ? spec.diameter : spec.diameter.replace('0.5', '1/2"').replace('0.75', '3/4"').replace('1.0', '1"').replace('1.5', '1 1/2"')) : 'Unknown'} + + {spec.display_size || spec.size_display || 'Unknown'} @@ -976,13 +1020,32 @@ const MaterialsPage = () => { - - {spec.coating_type ? spec.coating_type.replace('_', ' ') : 'N/A'} - + {spec.special_applications && spec.special_applications.length > 0 ? ( + + {spec.special_applications.map((app) => ( + + ))} + + ) : ( + + ์ผ๋ฐ˜ + + )} - {spec.pressure_rating || 'N/A'} + {spec.coating_type ? spec.coating_type.replace('_', ' ') : 'N/A'} diff --git a/frontend/src/pages/PurchaseConfirmationPage.jsx b/frontend/src/pages/PurchaseConfirmationPage.jsx index fe6c9df..f28bdc6 100644 --- a/frontend/src/pages/PurchaseConfirmationPage.jsx +++ b/frontend/src/pages/PurchaseConfirmationPage.jsx @@ -159,6 +159,60 @@ const PurchaseConfirmationPage = () => { ); }; + const formatBoltInfo = (item) => { + if (item.category !== 'BOLT') return null; + + // ํŠน์ˆ˜ ์šฉ๋„ ๋ณผํŠธ ์ •๋ณด (๋ฐฑ์—”๋“œ์—์„œ ์ œ๊ณต๋˜์–ด์•ผ ํ•จ) + const specialApplications = item.special_applications || {}; + const psvCount = specialApplications.PSV || 0; + const ltCount = specialApplications.LT || 0; + const ckCount = specialApplications.CK || 0; + const oriCount = specialApplications.ORI || 0; + + return ( + + + ๋ถ„์ˆ˜ ์‚ฌ์ด์ฆˆ: {(item.size_fraction || item.size_spec || '').replace(/"/g, '')} | + ํ‘œ๋ฉด์ฒ˜๋ฆฌ: {item.surface_treatment || '์—†์Œ'} + + + {/* ํŠน์ˆ˜ ์šฉ๋„ ๋ณผํŠธ ์ •๋ณด */} + + + ํŠน์ˆ˜ ์šฉ๋„ ๋ณผํŠธ ํ˜„ํ™ฉ: + + + + 0 ? "error.main" : "textSecondary"}> + PSV์šฉ: {psvCount}๊ฐœ + + + + 0 ? "warning.main" : "textSecondary"}> + ์ €์˜จ์šฉ: {ltCount}๊ฐœ + + + + 0 ? "info.main" : "textSecondary"}> + ์ฒดํฌ๋ฐธ๋ธŒ์šฉ: {ckCount}๊ฐœ + + + + 0 ? "secondary.main" : "textSecondary"}> + ์˜ค๋ฆฌํ”ผ์Šค์šฉ: {oriCount}๊ฐœ + + + + {(psvCount + ltCount + ckCount + oriCount) === 0 && ( + + ํŠน์ˆ˜ ์šฉ๋„ ๋ณผํŠธ ์—†์Œ (์ผ๋ฐ˜ ๋ณผํŠธ๋งŒ ํฌํ•จ) + + )} + + + ); + }; + return ( {/* ํ—ค๋” */} @@ -235,6 +289,7 @@ const PurchaseConfirmationPage = () => { {item.bom_quantity} {item.unit} {formatPipeInfo(item)} + {formatBoltInfo(item)} {/* ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ */} diff --git a/test_bolt_display.csv b/test_bolt_display.csv new file mode 100644 index 0000000..0a277af --- /dev/null +++ b/test_bolt_display.csv @@ -0,0 +1,5 @@ +DAT_FILE,DESCRIPTION,MAIN_NOM,RED_NOM,LENGTH,QUANTITY,UNIT +BOLT_HEX,0.625 HEX BOLT 100.0000 LG PSV ASTM A193/A194 GR B7/2H ELEC.GALV,0.625,,100,10,EA +BOLT_STUD,0.5 STUD BOLT 75.0000 LG LT ASTM A320 GR L7,0.5,,75,8,EA +BOLT_HEX,0.75 HEX BOLT 120.0000 LG CK ASTM A193 GR B8,0.75,,120,6,EA +BOLT_HEX,0.625 HEX BOLT 100.0000 LG ASTM A193/A194 GR B7/2H ELEC.GALV,0.625,,100,12,EA \ No newline at end of file diff --git a/test_flange_bolt.csv b/test_flange_bolt.csv new file mode 100644 index 0000000..e419231 --- /dev/null +++ b/test_flange_bolt.csv @@ -0,0 +1,6 @@ +DAT_FILE,DESCRIPTION,MAIN_NOM,RED_NOM,LENGTH,QUANTITY,UNIT +FLANGE_BOLT,FLANGE BOLT 6" 300LB M16 X 80MM ASTM A193 B7 ELECTRO GALVANIZED,6,,80,20,EA +FLANGE_BOLT,FLANGE BOLT 1-1/2" 150LB 5/8" X 100MM PSV ASTM A193 B7 ELECTRO GALVANIZED,1.5,,100,8,EA +FLANGE_BOLT,FLANGE BOLT 8" 150LB 3/4" X 130MM ASTM A193 B7 ELECTRO GALVANIZED,8,,130,12,EA +FLANGE_BOLT,FLANGE BOLT 4" 150LB 0.625" X 120MM ASTM A193 B7 ELECTRO GALVANIZED,4,,120,15,EA +FLANGE_BOLT,FLANGE BOLT 2" 300LB 1-1/2" X 180MM ASTM A193 B7 ELECTRO GALVANIZED,2,,180,5,EA \ No newline at end of file