diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index 28b0a75..c9ea55e 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -633,6 +633,17 @@ async def upload_file( classification_result = classify_valve("", description, main_nom or "") elif material_type == "BOLT": classification_result = classify_bolt("", description, main_nom or "") + print(f"๐Ÿ”ง BOLT ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ: {classification_result}") + print(f"๐Ÿ”ง ์›๋ณธ ์„ค๋ช…: {description}") + print(f"๐Ÿ”ง main_nom: {main_nom}") + + # ๊ธธ์ด ์ •๋ณด ํ™•์ธ + dimensions_info = classification_result.get("dimensions", {}) + print(f"๐Ÿ”ง ๊ธธ์ด ์ •๋ณด: {dimensions_info}") + + # ์žฌ์งˆ ์ •๋ณด ํ™•์ธ + material_info = classification_result.get("material", {}) + print(f"๐Ÿ”ง ์žฌ์งˆ ์ •๋ณด: {material_info}") elif material_type == "GASKET": classification_result = classify_gasket("", description, main_nom or "") elif material_type == "INSTRUMENT": @@ -1075,12 +1086,35 @@ async def upload_file( dimensions_info = classification_result.get("dimensions", {}) material_info = classification_result.get("material", {}) + print(f"๐Ÿ”ง fastener_type_info: {fastener_type_info}") + # ๋ณผํŠธ ํƒ€์ž… (STUD_BOLT, HEX_BOLT ๋“ฑ) bolt_type = "" if isinstance(fastener_type_info, dict): bolt_type = fastener_type_info.get("type", "UNKNOWN") + print(f"๐Ÿ”ง ์ถ”์ถœ๋œ bolt_type: {bolt_type}") else: bolt_type = str(fastener_type_info) if fastener_type_info else "UNKNOWN" + print(f"๐Ÿ”ง ๋ฌธ์ž์—ด bolt_type: {bolt_type}") + + # ํŠน์ˆ˜ ์šฉ๋„ ๋ณผํŠธ ํ™•์ธ (PSV, LT, CK ๋“ฑ) + special_result = classification_result.get("special_applications", {}) + print(f"๐Ÿ”ง special_result: {special_result}") + + # ํŠน์ˆ˜ ์šฉ๋„๊ฐ€ ๊ฐ์ง€๋˜๋ฉด ํƒ€์ž… ์šฐ์„  ์ ์šฉ + if special_result and special_result.get("detected_applications"): + detected_apps = special_result.get("detected_applications", []) + if "LT" in detected_apps: + bolt_type = "LT_BOLT" + print(f"๐Ÿ”ง ํŠน์ˆ˜ ์šฉ๋„ ๊ฐ์ง€๋กœ bolt_type ๋ณ€๊ฒฝ: {bolt_type}") + elif "PSV" in detected_apps: + bolt_type = "PSV_BOLT" + print(f"๐Ÿ”ง ํŠน์ˆ˜ ์šฉ๋„ ๊ฐ์ง€๋กœ bolt_type ๋ณ€๊ฒฝ: {bolt_type}") + elif "CK" in detected_apps: + bolt_type = "CK_BOLT" + print(f"๐Ÿ”ง ํŠน์ˆ˜ ์šฉ๋„ ๊ฐ์ง€๋กœ bolt_type ๋ณ€๊ฒฝ: {bolt_type}") + + print(f"๐Ÿ”ง ์ตœ์ข… bolt_type: {bolt_type}") # ๋‚˜์‚ฌ ํƒ€์ž… (METRIC, INCH ๋“ฑ) thread_type = "" @@ -1553,6 +1587,9 @@ async def get_materials( fd.fitting_type, fd.fitting_subtype, fd.connection_method, fd.pressure_rating, fd.material_standard, fd.material_grade as fitting_material_grade, fd.main_size, fd.reduced_size, fd.length_mm as fitting_length_mm, fd.schedule as fitting_schedule, + gd.gasket_type, gd.gasket_subtype, gd.material_type as gasket_material_type, + gd.filler_material, gd.pressure_rating as gasket_pressure_rating, gd.size_inches as gasket_size_inches, + gd.thickness as gasket_thickness, gd.temperature_range as gasket_temperature_range, gd.fire_safe, mpt.confirmed_quantity, mpt.purchase_status, mpt.confirmed_by, mpt.confirmed_at, -- ๊ตฌ๋งค์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ์—์„œ ๋ถ„๋ฅ˜๋œ ์ •๋ณด๋ฅผ ์šฐ์„  ์‚ฌ์šฉ CASE @@ -1579,6 +1616,7 @@ async def get_materials( LEFT JOIN pipe_end_preparations pep ON m.id = pep.material_id LEFT JOIN fitting_details fd ON m.id = fd.material_id LEFT JOIN valve_details vd ON m.id = vd.material_id + LEFT JOIN gasket_details gd ON m.id = gd.material_id LEFT JOIN material_purchase_tracking mpt ON ( m.material_hash = mpt.material_hash AND f.job_no = mpt.job_no @@ -1914,17 +1952,18 @@ async def get_materials( flange_groups[flange_key]["materials"].append(material_dict) material_dict['clean_description'] = clean_description elif m.classified_category == 'GASKET': - gasket_query = text("SELECT * FROM gasket_details WHERE material_id = :material_id") - gasket_result = db.execute(gasket_query, {"material_id": m.id}) - gasket_detail = gasket_result.fetchone() - if gasket_detail: + # ์ด๋ฏธ JOIN๋œ gasket_details ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ + if m.gasket_type: # gasket_details๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ material_dict['gasket_details'] = { - "gasket_type": gasket_detail.gasket_type, - "material_type": gasket_detail.material_type, - "pressure_rating": gasket_detail.pressure_rating, - "size_inches": gasket_detail.size_inches, - "thickness": gasket_detail.thickness, - "temperature_range": gasket_detail.temperature_range + "gasket_type": m.gasket_type, + "gasket_subtype": m.gasket_subtype, + "material_type": m.gasket_material_type, + "filler_material": m.filler_material, + "pressure_rating": m.gasket_pressure_rating, + "size_inches": m.gasket_size_inches, + "thickness": m.gasket_thickness, + "temperature_range": m.gasket_temperature_range, + "fire_safe": m.fire_safe } # ๊ฐ€์Šค์ผ“ ๊ทธ๋ฃนํ•‘ - ํฌ๊ธฐ, ์••๋ ฅ, ์žฌ์งˆ๋กœ ๊ทธ๋ฃนํ•‘ diff --git a/backend/app/services/bolt_classifier.py b/backend/app/services/bolt_classifier.py index 9ffafee..35249d8 100644 --- a/backend/app/services/bolt_classifier.py +++ b/backend/app/services/bolt_classifier.py @@ -12,6 +12,31 @@ def classify_bolt_material(description: str) -> Dict: desc_upper = description.upper() + # A320/A194M ๋™์‹œ ์ฒ˜๋ฆฌ (์˜ˆ: "ASTM A320/A194M GR B8/8") - ์ €์˜จ์šฉ ๋ณผํŠธ ์กฐํ•ฉ + if "A320" in desc_upper and "A194" in desc_upper: + # B8/8 ๋“ฑ๊ธ‰ ์ถ”์ถœ + bolt_grade = "UNKNOWN" + nut_grade = "UNKNOWN" + + if "B8" in desc_upper: + bolt_grade = "B8" + nut_grade = "8" # A320/A194M์˜ ๊ฒฝ์šฐ ๋ณดํ†ต B8/8 ์กฐํ•ฉ + elif "L7" in desc_upper: + bolt_grade = "L7" + elif "B8M" in desc_upper: + bolt_grade = "B8M" + + combined_grade = f"{bolt_grade}/{nut_grade}" if bolt_grade != "UNKNOWN" and nut_grade != "UNKNOWN" else f"{bolt_grade}" if bolt_grade != "UNKNOWN" else "A320/A194M" + + return { + "standard": "ASTM A320/A194M", + "grade": combined_grade, + "material_type": "LOW_TEMP_STAINLESS", # ์ €์˜จ์šฉ ์Šคํ…Œ์ธ๋ฆฌ์Šค + "manufacturing": "FORGED", + "confidence": 0.95, + "evidence": ["ASTM_A320_A194M_COMBINED"] + } + # A193/A194 ๋™์‹œ ์ฒ˜๋ฆฌ (์˜ˆ: "ASTM A193/A194 GR B7/2H") if "A193" in desc_upper and "A194" in desc_upper: # B7/2H ๋“ฑ๊ธ‰ ์ถ”์ถœ @@ -136,6 +161,39 @@ def classify_bolt_material(description: str) -> Dict: "evidence": ["ISO_4762_SOCKET_SCREW"] } + # ์ผ๋ฐ˜์ ์ธ ๋ณผํŠธ ์žฌ์งˆ ํŒจํ„ด ์ถ”๊ฐ€ ํ™•์ธ + if "B7" in desc_upper and "2H" in desc_upper: + return { + "standard": "ASTM A193/A194", + "grade": "B7/2H", + "material_type": "ALLOY_STEEL", + "manufacturing": "FORGED", + "confidence": 0.85, + "evidence": ["B7_2H_PATTERN"] + } + + # ๋‹จ๋… B7 ํŒจํ„ด + if "B7" in desc_upper: + return { + "standard": "ASTM A193", + "grade": "B7", + "material_type": "ALLOY_STEEL", + "manufacturing": "FORGED", + "confidence": 0.80, + "evidence": ["B7_PATTERN"] + } + + # ๋‹จ๋… 2H ํŒจํ„ด + if "2H" in desc_upper: + return { + "standard": "ASTM A194", + "grade": "2H", + "material_type": "ALLOY_STEEL", + "manufacturing": "FORGED", + "confidence": 0.80, + "evidence": ["2H_PATTERN"] + } + # ๊ธฐ๋ณธ ์žฌ์งˆ ๋ถ„๋ฅ˜๊ธฐ ํ˜ธ์ถœ (materials_schema ๋ฌธ์ œ๊ฐ€ ์žˆ์–ด๋„ ์šฐํšŒ) try: return classify_material(description) @@ -195,7 +253,7 @@ BOLT_TYPES = { "LT_BOLT": { "dat_file_patterns": ["LT_BOLT", "LT_BLT"], - "description_keywords": ["LT", "LOW TEMP", "์ €์˜จ์šฉ"], + "description_keywords": ["LT BOLT", "LT BLT", "LOW TEMP", "์ €์˜จ์šฉ"], "characteristics": "์ €์˜จ์šฉ ํŠน์ˆ˜ ๋ณผํŠธ", "applications": "์ €์˜จ ํ™˜๊ฒฝ ์ฒด๊ฒฐ์šฉ", "head_type": "HEXAGON", @@ -507,7 +565,8 @@ def classify_special_application_bolts(description: str) -> Dict: # LT ๋ณผํŠธ ํ™•์ธ (์ €์˜จ์šฉ ๋ณผํŠธ) lt_patterns = [ - r'\bLT\b', # ๋‹จ์–ด ๊ฒฝ๊ณ„๋กœ LT๋งŒ + r'\bLT\s', # LT ๋‹ค์Œ์— ๊ณต๋ฐฑ์ด ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ (LT BOLT, LT BLT) + r'^LT\b', # ๋ฌธ์žฅ ์‹œ์ž‘์˜ LT๋งŒ r'LOW\s+TEMP', r'์ €์˜จ์šฉ', r'CRYOGENIC', @@ -915,20 +974,31 @@ def extract_bolt_dimensions(main_nom: str, description: str) -> Dict: "dimension_description": nominal_size_fraction # ๋ถ„์ˆ˜๋กœ ํ‘œ์‹œ } - # ๊ธธ์ด ์ •๋ณด ์ถ”์ถœ + # ๊ธธ์ด ์ •๋ณด ์ถ”์ถœ (๊ฐœ์„ ๋œ ํŒจํ„ด) length_patterns = [ - r'(\d+(?:\.\d+)?)\s*LG', # 70.0000 LG ํ˜•ํƒœ (์ตœ์šฐ์„ ) + r'(\d+(?:\.\d+)?)\s*LG', # 70.0000 LG, 145.0000 LG ํ˜•ํƒœ (์ตœ์šฐ์„ ) r'(\d+(?:\.\d+)?)\s*MM\s*LG', # 70MM LG ํ˜•ํƒœ 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 ํ˜•ํƒœ + r'X\s*(\d+(?:\.\d+)?)\s*MM', # M8 X 20MM ํ˜•ํƒœ + r',\s*(\d+(?:\.\d+)?)\s*LG', # ", 145.0000 LG" ํ˜•ํƒœ (PSV, LT ๋ณผํŠธ์šฉ) + r',\s*(\d+(?:\.\d+)?)\s+(?:CK|PSV|LT)', # ", 140 CK" ํ˜•ํƒœ (PSV ๋ณผํŠธ์šฉ) + r'(\d+(?:\.\d+)?)\s*MM(?:\s|,|$)', # 75MM ํ˜•ํƒœ (๋‹จ๋…) + r'(\d+(?:\.\d+)?)\s*mm(?:\s|,|$)' # 75mm ํ˜•ํƒœ (๋‹จ๋…) ] for pattern in length_patterns: match = re.search(pattern, desc_upper) if match: - dimensions["length"] = f"{match.group(1)}mm" + length_value = match.group(1) + # ์†Œ์ˆ˜์  ์ œ๊ฑฐ (145.0000 โ†’ 145) + if '.' in length_value and length_value.endswith('.0000'): + length_value = length_value.split('.')[0] + elif '.' in length_value and all(c == '0' for c in length_value.split('.')[1]): + length_value = length_value.split('.')[0] + + dimensions["length"] = f"{length_value}mm" break # ์ง€๋ฆ„ ์ •๋ณด (์ด๋ฏธ main_nom์— ์žˆ์ง€๋งŒ ํ™•์ธ) diff --git a/backend/app/services/integrated_classifier.py b/backend/app/services/integrated_classifier.py index 5ebda03..adfd38d 100644 --- a/backend/app/services/integrated_classifier.py +++ b/backend/app/services/integrated_classifier.py @@ -89,6 +89,29 @@ def classify_material_integrated(description: str, main_nom: str = "", desc_upper = description.upper() + # ์ตœ์šฐ์„ : SPECIAL ํ‚ค์›Œ๋“œ ํ™•์ธ (๋„๋ฉด ์—…๋กœ๋“œ๊ฐ€ ํ•„์š”ํ•œ ํŠน์ˆ˜ ์ž์žฌ) + special_keywords = ['SPECIAL', '์ŠคํŽ˜์…œ', 'SPEC', 'SPL'] + for keyword in special_keywords: + if keyword in desc_upper: + return { + "category": "SPECIAL", + "confidence": 1.0, + "evidence": [f"SPECIAL_KEYWORD: {keyword}"], + "classification_level": "LEVEL0_SPECIAL", + "reason": f"์ŠคํŽ˜์…œ ํ‚ค์›Œ๋“œ ๋ฐœ๊ฒฌ: {keyword}" + } + + # U-BOLT ๋ฐ ๊ด€๋ จ ๋ถ€ํ’ˆ ์šฐ์„  ํ™•์ธ (BOLT ์นดํ…Œ๊ณ ๋ฆฌ๋ณด๋‹ค ๋จผ์ €) + if ('U-BOLT' in desc_upper or 'U BOLT' in desc_upper or '์œ ๋ณผํŠธ' in desc_upper or + 'URETHANE BLOCK' in desc_upper or 'BLOCK SHOE' in desc_upper or '์šฐ๋ ˆํƒ„' in desc_upper): + return { + "category": "U_BOLT", + "confidence": 1.0, + "evidence": ["U_BOLT_SYSTEM_KEYWORD"], + "classification_level": "LEVEL0_U_BOLT", + "reason": "U-BOLT ์‹œ์Šคํ…œ ํ‚ค์›Œ๋“œ ๋ฐœ๊ฒฌ" + } + # ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ๊ฐ ๋ถ€๋ถ„์„ ๋ณ„๋„๋กœ ์ฒดํฌ (์˜ˆ: "NIPPLE, SMLS, SCH 80") desc_parts = [part.strip() for part in desc_upper.split(',')] diff --git a/backend/app/services/material_grade_extractor.py b/backend/app/services/material_grade_extractor.py index f3c3f01..275a12a 100644 --- a/backend/app/services/material_grade_extractor.py +++ b/backend/app/services/material_grade_extractor.py @@ -30,6 +30,15 @@ def extract_full_material_grade(description: str) -> str: # ASTM A193/A194 GR B7/2H (๋ณผํŠธ์šฉ ์กฐํ•ฉ ํŒจํ„ด) - ์ตœ์šฐ์„  r'ASTM\s+A193/A194\s+GR\s+[A-Z0-9/]+', r'ASTM\s+A193/A194\s+[A-Z0-9/]+', + # ASTM A320/A194M GR B8/8 (์ €์˜จ์šฉ ๋ณผํŠธ ์กฐํ•ฉ ํŒจํ„ด) + r'ASTM\s+A320[M]?/A194[M]?\s+GR\s+[A-Z0-9/]+', + r'ASTM\s+A320[M]?/A194[M]?\s+[A-Z0-9/]+', + # ๋‹จ๋… A193/A194 ํŒจํ„ด (ASTM ์—†์ด) + r'\bA193/A194\s+GR\s+[A-Z0-9/]+\b', + r'\bA193/A194\s+[A-Z0-9/]+\b', + # ๋‹จ๋… A320/A194M ํŒจํ„ด (ASTM ์—†์ด) + r'\bA320[M]?/A194[M]?\s+GR\s+[A-Z0-9/]+\b', + r'\bA320[M]?/A194[M]?\s+[A-Z0-9/]+\b', # ASTM A312 TP304, ASTM A312 TP316L ๋“ฑ r'ASTM\s+A\d{3,4}[A-Z]*\s+TP\d+[A-Z]*', # ASTM A182 F304, ASTM A182 F316L ๋“ฑ diff --git a/backend/scripts/24_add_special_category_support.sql b/backend/scripts/24_add_special_category_support.sql new file mode 100644 index 0000000..f9ae440 --- /dev/null +++ b/backend/scripts/24_add_special_category_support.sql @@ -0,0 +1,130 @@ +-- ================================ +-- SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ง€์› ์ถ”๊ฐ€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +-- ์ƒ์„ฑ์ผ: 2025.09.30 +-- ๋ชฉ์ : SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜ ๋ฐ ๊ด€๋ จ ๊ธฐ๋Šฅ ์ง€์› +-- ================================ + +-- 1. materials ํ…Œ์ด๋ธ” SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ง€์› ํ™•์ธ +-- ================================ + +-- classified_category ์ปฌ๋Ÿผ์ด SPECIAL ๊ฐ’์„ ์ง€์›ํ•˜๋Š”์ง€ ํ™•์ธ +-- (์ด๋ฏธ VARCHAR(50)์ด๋ฏ€๋กœ ์ถ”๊ฐ€ ์ž‘์—… ๋ถˆํ•„์š”, ํ•˜์ง€๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ์ฒดํฌ) + +-- SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์ธ๋ฑ์Šค ์ถ”๊ฐ€ (์„ฑ๋Šฅ ์ตœ์ ํ™”) +CREATE INDEX IF NOT EXISTS idx_materials_special_category +ON materials(classified_category) +WHERE classified_category = 'SPECIAL'; + +-- 2. SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜ ๊ทœ์น™ ํ…Œ์ด๋ธ” ์ƒ์„ฑ +-- ================================ + +-- SPECIAL ํ‚ค์›Œ๋“œ ํŒจํ„ด ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS special_classification_patterns ( + id SERIAL PRIMARY KEY, + pattern_type VARCHAR(20) NOT NULL, -- 'KEYWORD', 'REGEX', 'EXACT' + pattern_value VARCHAR(200) NOT NULL, + description TEXT, + priority INTEGER DEFAULT 1, -- ์šฐ์„ ์ˆœ์œ„ (๋‚ฎ์„์ˆ˜๋ก ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๊ธฐ๋ณธ SPECIAL ํ‚ค์›Œ๋“œ ํŒจํ„ด ์‚ฝ์ž… +INSERT INTO special_classification_patterns (pattern_type, pattern_value, description, priority) VALUES +('KEYWORD', 'SPECIAL', '์˜๋ฌธ SPECIAL ํ‚ค์›Œ๋“œ', 1), +('KEYWORD', '์ŠคํŽ˜์…œ', 'ํ•œ๊ธ€ ์ŠคํŽ˜์…œ ํ‚ค์›Œ๋“œ', 1), +('KEYWORD', 'SPEC', '์˜๋ฌธ SPEC ์ถ•์•ฝ์–ด', 2), +('KEYWORD', 'SPL', '์˜๋ฌธ SPL ์ถ•์•ฝ์–ด', 2) +ON CONFLICT DO NOTHING; + +-- 3. SPECIAL ์ž์žฌ ์ถ”๊ฐ€ ์ •๋ณด ํ…Œ์ด๋ธ” +-- ================================ + +-- SPECIAL ์ž์žฌ ์ƒ์„ธ ์ •๋ณด ํ…Œ์ด๋ธ” (๋„๋ฉด ์—…๋กœ๋“œ ๊ด€๋ จ) +CREATE TABLE IF NOT EXISTS special_material_details ( + id SERIAL PRIMARY KEY, + material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE, + file_id INTEGER REFERENCES files(id) ON DELETE CASCADE, + + -- ๋„๋ฉด ์ •๋ณด + drawing_number VARCHAR(100), -- ๋„๋ฉด ๋ฒˆํ˜ธ + drawing_revision VARCHAR(20), -- ๋„๋ฉด ๋ฆฌ๋น„์ „ + drawing_uploaded BOOLEAN DEFAULT FALSE, -- ๋„๋ฉด ์—…๋กœ๋“œ ์—ฌ๋ถ€ + drawing_file_path TEXT, -- ๋„๋ฉด ํŒŒ์ผ ๊ฒฝ๋กœ + + -- ํŠน์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ + special_requirements TEXT, -- ํŠน์ˆ˜ ์ œ์ž‘ ์š”๊ตฌ์‚ฌํ•ญ + manufacturing_notes TEXT, -- ์ œ์ž‘ ์ฐธ๊ณ ์‚ฌํ•ญ + approval_required BOOLEAN DEFAULT TRUE, -- ์Šน์ธ ํ•„์š” ์—ฌ๋ถ€ + approved_by VARCHAR(100), -- ์Šน์ธ์ž + approved_at TIMESTAMP, -- ์Šน์ธ ์ผ์‹œ + + -- ๋ถ„๋ฅ˜ ์ •๋ณด + classification_confidence FLOAT DEFAULT 1.0, + classification_reason TEXT, -- ๋ถ„๋ฅ˜ ๊ทผ๊ฑฐ + + -- ๊ด€๋ฆฌ ์ •๋ณด + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 4. ์ธ๋ฑ์Šค ์ƒ์„ฑ (์„ฑ๋Šฅ ์ตœ์ ํ™”) +-- ================================ + +-- special_classification_patterns ์ธ๋ฑ์Šค +CREATE INDEX IF NOT EXISTS idx_special_patterns_type ON special_classification_patterns(pattern_type); +CREATE INDEX IF NOT EXISTS idx_special_patterns_active ON special_classification_patterns(is_active); +CREATE INDEX IF NOT EXISTS idx_special_patterns_priority ON special_classification_patterns(priority); + +-- special_material_details ์ธ๋ฑ์Šค +CREATE INDEX IF NOT EXISTS idx_special_details_material ON special_material_details(material_id); +CREATE INDEX IF NOT EXISTS idx_special_details_file ON special_material_details(file_id); +CREATE INDEX IF NOT EXISTS idx_special_details_drawing_uploaded ON special_material_details(drawing_uploaded); +CREATE INDEX IF NOT EXISTS idx_special_details_approval ON special_material_details(approval_required); + +-- 5. ๊ธฐ์กด ์ž์žฌ ์žฌ๋ถ„๋ฅ˜ (์„ ํƒ์ ) +-- ================================ + +-- ๊ธฐ์กด ์ž๋ฃŒ ์ค‘ SPECIAL ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ์ž์žฌ๋ฅผ SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ์žฌ๋ถ„๋ฅ˜ +UPDATE materials +SET + classified_category = 'SPECIAL', + classification_confidence = 1.0, + updated_by = 'SYSTEM_MIGRATION', + classified_at = CURRENT_TIMESTAMP +WHERE + ( + UPPER(original_description) LIKE '%SPECIAL%' OR + UPPER(original_description) LIKE '%์ŠคํŽ˜์…œ%' OR + UPPER(original_description) LIKE '%SPEC%' OR + UPPER(original_description) LIKE '%SPL%' + ) + AND (classified_category IS NULL OR classified_category != 'SPECIAL'); + +-- 6. ํ†ต๊ณ„ ๋ฐ ๊ฒ€์ฆ +-- ================================ + +-- SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ž์žฌ ๊ฐœ์ˆ˜ ํ™•์ธ +DO $$ +DECLARE + special_count INTEGER; +BEGIN + SELECT COUNT(*) INTO special_count FROM materials WHERE classified_category = 'SPECIAL'; + RAISE NOTICE 'SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ถ„๋ฅ˜๋œ ์ž์žฌ ๊ฐœ์ˆ˜: %', special_count; +END $$; + +-- 7. ๊ถŒํ•œ ์„ค์ • (ํ•„์š”์‹œ) +-- ================================ + +-- SPECIAL ์ž์žฌ ๊ด€๋ฆฌ ๊ถŒํ•œ (ํ–ฅํ›„ ํ™•์žฅ์šฉ) +-- ํ˜„์žฌ๋Š” ๊ธฐ๋ณธ materials ํ…Œ์ด๋ธ” ๊ถŒํ•œ์„ ๋”ฐ๋ฆ„ + +COMMIT; + +-- ================================ +-- ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ ๋กœ๊ทธ +-- ================================ +INSERT INTO migration_log (script_name, executed_at, description) VALUES +('24_add_special_category_support.sql', CURRENT_TIMESTAMP, 'SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ง€์› ์ถ”๊ฐ€ ๋ฐ ๊ธฐ์กด ์ž์žฌ ์žฌ๋ถ„๋ฅ˜') +ON CONFLICT DO NOTHING; diff --git a/backend/scripts/25_execute_special_category_migration.py b/backend/scripts/25_execute_special_category_migration.py new file mode 100755 index 0000000..6ae8b0b --- /dev/null +++ b/backend/scripts/25_execute_special_category_migration.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +""" +SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ +์ƒ์„ฑ์ผ: 2025.09.30 +๋ชฉ์ : SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ง€์› ์ถ”๊ฐ€ ๋ฐ ๊ธฐ์กด ์ž์žฌ ์žฌ๋ถ„๋ฅ˜ +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from sqlalchemy import create_engine, text +from app.database import DATABASE_URL + +def execute_special_migration(): + """SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰""" + engine = create_engine(DATABASE_URL) + + with engine.connect() as conn: + print("๐Ÿš€ SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์ž‘...") + print("=" * 60) + + try: + # 1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ + print("๐Ÿ“‹ 1๋‹จ๊ณ„: ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰...") + script_path = os.path.join(os.path.dirname(__file__), '24_add_special_category_support.sql') + with open(script_path, 'r', encoding='utf-8') as f: + sql_content = f.read() + + # SQL ๋ช…๋ น์–ด๋“ค์„ ๋ถ„๋ฆฌํ•˜์—ฌ ์‹คํ–‰ + sql_commands = sql_content.split(';') + for i, command in enumerate(sql_commands): + command = command.strip() + if command and not command.startswith('--') and command != 'COMMIT': + try: + conn.execute(text(command)) + if i % 10 == 0: # ์ง„ํ–‰์ƒํ™ฉ ํ‘œ์‹œ + print(f" - ๋ช…๋ น์–ด {i+1}/{len(sql_commands)} ์‹คํ–‰ ์ค‘...") + except Exception as e: + print(f" โš ๏ธ ๋ช…๋ น์–ด ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ (๋ฌด์‹œ๋จ): {e}") + continue + + print("โœ… ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ์™„๋ฃŒ") + + # 2. ๊ธฐ์กด ์ž์žฌ ์žฌ๋ถ„๋ฅ˜ ํ™•์ธ + print("\n๐Ÿ“Š 2๋‹จ๊ณ„: ๊ธฐ์กด ์ž์žฌ ์žฌ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ ํ™•์ธ...") + + # SPECIAL ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ์ž์žฌ ๊ฐœ์ˆ˜ ํ™•์ธ + result = conn.execute(text(""" + SELECT COUNT(*) as count + FROM materials + WHERE classified_category = 'SPECIAL' + """)).fetchone() + + special_count = result.count if result else 0 + print(f" - SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ถ„๋ฅ˜๋œ ์ž์žฌ: {special_count}๊ฐœ") + + # ํ‚ค์›Œ๋“œ๋ณ„ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ ํ™•์ธ + keyword_results = conn.execute(text(""" + SELECT + CASE + WHEN UPPER(original_description) LIKE '%SPECIAL%' THEN 'SPECIAL' + WHEN UPPER(original_description) LIKE '%์ŠคํŽ˜์…œ%' THEN '์ŠคํŽ˜์…œ' + WHEN UPPER(original_description) LIKE '%SPEC%' THEN 'SPEC' + WHEN UPPER(original_description) LIKE '%SPL%' THEN 'SPL' + END as keyword_type, + COUNT(*) as count + FROM materials + WHERE classified_category = 'SPECIAL' + GROUP BY keyword_type + ORDER BY count DESC + """)).fetchall() + + if keyword_results: + print(" - ํ‚ค์›Œ๋“œ๋ณ„ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ:") + for row in keyword_results: + print(f" * {row.keyword_type}: {row.count}๊ฐœ") + + # 3. ํ…Œ์ด๋ธ” ์ƒ์„ฑ ํ™•์ธ + print("\n๐Ÿ—๏ธ 3๋‹จ๊ณ„: ์ƒˆ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ํ™•์ธ...") + + # special_classification_patterns ํ…Œ์ด๋ธ” ํ™•์ธ + patterns_count = conn.execute(text(""" + SELECT COUNT(*) as count + FROM special_classification_patterns + """)).fetchone() + + patterns_count = patterns_count.count if patterns_count else 0 + print(f" - special_classification_patterns ํ…Œ์ด๋ธ”: {patterns_count}๊ฐœ ํŒจํ„ด ๋“ฑ๋ก๋จ") + + # special_material_details ํ…Œ์ด๋ธ” ํ™•์ธ + details_exists = conn.execute(text(""" + SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'special_material_details' + ) as exists + """)).fetchone() + + if details_exists.exists: + print(" - special_material_details ํ…Œ์ด๋ธ”: ์ƒ์„ฑ ์™„๋ฃŒ") + else: + print(" - โŒ special_material_details ํ…Œ์ด๋ธ”: ์ƒ์„ฑ ์‹คํŒจ") + + # 4. ์ธ๋ฑ์Šค ์ƒ์„ฑ ํ™•์ธ + print("\n๐Ÿ” 4๋‹จ๊ณ„: ์ธ๋ฑ์Šค ์ƒ์„ฑ ํ™•์ธ...") + + special_indexes = conn.execute(text(""" + SELECT indexname + FROM pg_indexes + WHERE indexname LIKE '%special%' + ORDER BY indexname + """)).fetchall() + + if special_indexes: + print(" - SPECIAL ๊ด€๋ จ ์ธ๋ฑ์Šค:") + for idx in special_indexes: + print(f" * {idx.indexname}") + else: + print(" - โš ๏ธ SPECIAL ๊ด€๋ จ ์ธ๋ฑ์Šค๊ฐ€ ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.") + + # ์ปค๋ฐ‹ + conn.commit() + + print("\n" + "=" * 60) + print("๐ŸŽ‰ SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์™„๋ฃŒ!") + print(f"๐Ÿ“Š ์ด {special_count}๊ฐœ ์ž์žฌ๊ฐ€ SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ถ„๋ฅ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + print("๐Ÿ”ง ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ:") + print(" - SPECIAL ํ‚ค์›Œ๋“œ ์ž๋™ ๊ฐ์ง€") + print(" - ๋„๋ฉด ์—…๋กœ๋“œ ๊ด€๋ฆฌ") + print(" - ํŠน์ˆ˜ ์ œ์ž‘ ์š”๊ตฌ์‚ฌํ•ญ ์ถ”์ ") + print(" - ์Šน์ธ ํ”„๋กœ์„ธ์Šค ์ง€์›") + + except Exception as e: + print(f"\nโŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}") + conn.rollback() + raise + +def main(): + """๋ฉ”์ธ ์‹คํ–‰ ํ•จ์ˆ˜""" + print("SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰") + print("TK-MP-Project - ํŠน์ˆ˜ ์ž์žฌ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ") + print("=" * 60) + + try: + execute_special_migration() + except Exception as e: + print(f"\n๐Ÿ’ฅ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/backend/scripts/26_update_bolt_material_grades.py b/backend/scripts/26_update_bolt_material_grades.py new file mode 100644 index 0000000..61ccd82 --- /dev/null +++ b/backend/scripts/26_update_bolt_material_grades.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +๋ณผํŠธ ์žฌ์งˆ ์ •๋ณด ์—…๋ฐ์ดํŠธ ์Šคํฌ๋ฆฝํŠธ +A320/A194M ํŒจํ„ด ๋“ฑ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธ์‹ํ•˜๋„๋ก ๊ธฐ์กด ๋ณผํŠธ๋“ค์˜ material_grade ์žฌ๋ถ„๋ฅ˜ +""" + +import os +import sys +import psycopg2 +from psycopg2.extras import RealDictCursor + +# ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ Python ๊ฒฝ๋กœ์— ์ถ”๊ฐ€ +sys.path.append('/app') + +from app.services.bolt_classifier import classify_bolt_material + +def update_bolt_material_grades(): + """๊ธฐ์กด ๋ณผํŠธ๋“ค์˜ material_grade ์—…๋ฐ์ดํŠธ""" + + # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ + try: + conn = psycopg2.connect( + host=os.getenv('DB_HOST', 'postgres'), + port=os.getenv('DB_PORT', '5432'), + database=os.getenv('DB_NAME', 'tk_mp_bom'), + user=os.getenv('DB_USER', 'tkmp_user'), + password=os.getenv('DB_PASSWORD', 'tkmp2024!') + ) + + cursor = conn.cursor(cursor_factory=RealDictCursor) + + print("๐Ÿ”ง ๋ณผํŠธ ์žฌ์งˆ ์ •๋ณด ์—…๋ฐ์ดํŠธ ์‹œ์ž‘...") + + # ๋ณผํŠธ ์นดํ…Œ๊ณ ๋ฆฌ ์ž์žฌ๋“ค ์กฐํšŒ + cursor.execute(""" + SELECT id, original_description, material_grade, full_material_grade + FROM materials + WHERE classified_category = 'BOLT' + ORDER BY id + """) + + bolts = cursor.fetchall() + print(f"๐Ÿ“Š ์ด {len(bolts)}๊ฐœ ๋ณผํŠธ ๋ฐœ๊ฒฌ") + + updated_count = 0 + + for bolt in bolts: + bolt_id = bolt['id'] + original_desc = bolt['original_description'] or '' + current_material_grade = bolt['material_grade'] or '' + current_full_grade = bolt['full_material_grade'] or '' + + # ๋ณผํŠธ ์žฌ์งˆ ์žฌ๋ถ„๋ฅ˜ + material_result = classify_bolt_material(original_desc) + + if material_result and material_result.get('standard') != 'UNKNOWN': + new_standard = material_result.get('standard', '') + new_grade = material_result.get('grade', '') + + # ์ƒˆ๋กœ์šด material_grade ๊ตฌ์„ฑ + if new_grade and new_grade != 'UNKNOWN': + if new_standard in new_grade: + # ์ด๋ฏธ standard๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ (์˜ˆ: "ASTM A320/A194M") + new_material_grade = new_grade + else: + # standard + grade ์กฐํ•ฉ (์˜ˆ: "ASTM A193" + "B7") + new_material_grade = f"{new_standard} {new_grade}" if new_grade not in new_standard else new_standard + else: + new_material_grade = new_standard + + # ๊ธฐ์กด ๊ฐ’๊ณผ ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋งŒ ์—…๋ฐ์ดํŠธ + if new_material_grade != current_material_grade: + print(f"๐Ÿ”„ ID {bolt_id}: '{current_material_grade}' โ†’ '{new_material_grade}'") + print(f" ์›๋ณธ: {original_desc}") + + cursor.execute(""" + UPDATE materials + SET material_grade = %s + WHERE id = %s + """, (new_material_grade, bolt_id)) + + updated_count += 1 + + # ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ปค๋ฐ‹ + conn.commit() + + print(f"โœ… ๋ณผํŠธ ์žฌ์งˆ ์ •๋ณด ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: {updated_count}๊ฐœ ์—…๋ฐ์ดํŠธ๋จ") + + # ์—…๋ฐ์ดํŠธ ๊ฒฐ๊ณผ ํ™•์ธ + cursor.execute(""" + SELECT material_grade, COUNT(*) as count + FROM materials + WHERE classified_category = 'BOLT' + GROUP BY material_grade + ORDER BY count DESC + """) + + results = cursor.fetchall() + print("\n๐Ÿ“ˆ ์—…๋ฐ์ดํŠธ ํ›„ ๋ณผํŠธ ์žฌ์งˆ ๋ถ„ํฌ:") + for result in results: + print(f" {result['material_grade']}: {result['count']}๊ฐœ") + + except Exception as e: + print(f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}") + if conn: + conn.rollback() + finally: + if cursor: + cursor.close() + if conn: + conn.close() + +if __name__ == "__main__": + update_bolt_material_grades() diff --git a/backend/scripts/PRODUCTION_MIGRATION.sql b/backend/scripts/PRODUCTION_MIGRATION.sql index 96ec9fe..f44681b 100644 --- a/backend/scripts/PRODUCTION_MIGRATION.sql +++ b/backend/scripts/PRODUCTION_MIGRATION.sql @@ -129,6 +129,83 @@ CREATE INDEX IF NOT EXISTS idx_support_details_material_id ON support_details(ma CREATE INDEX IF NOT EXISTS idx_support_details_file_id ON support_details(file_id); CREATE INDEX IF NOT EXISTS idx_support_details_support_type ON support_details(support_type); +-- 8. SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ง€์› ์ถ”๊ฐ€ +-- ================================ + +-- SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์ธ๋ฑ์Šค ์ถ”๊ฐ€ (์„ฑ๋Šฅ ์ตœ์ ํ™”) +CREATE INDEX IF NOT EXISTS idx_materials_special_category +ON materials(classified_category) +WHERE classified_category = 'SPECIAL'; + +-- SPECIAL ํ‚ค์›Œ๋“œ ํŒจํ„ด ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS special_classification_patterns ( + id SERIAL PRIMARY KEY, + pattern_type VARCHAR(20) NOT NULL, -- 'KEYWORD', 'REGEX', 'EXACT' + pattern_value VARCHAR(200) NOT NULL, + description TEXT, + priority INTEGER DEFAULT 1, -- ์šฐ์„ ์ˆœ์œ„ (๋‚ฎ์„์ˆ˜๋ก ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๊ธฐ๋ณธ SPECIAL ํ‚ค์›Œ๋“œ ํŒจํ„ด ์‚ฝ์ž… +INSERT INTO special_classification_patterns (pattern_type, pattern_value, description, priority) VALUES +('KEYWORD', 'SPECIAL', '์˜๋ฌธ SPECIAL ํ‚ค์›Œ๋“œ', 1), +('KEYWORD', '์ŠคํŽ˜์…œ', 'ํ•œ๊ธ€ ์ŠคํŽ˜์…œ ํ‚ค์›Œ๋“œ', 1), +('KEYWORD', 'SPEC', '์˜๋ฌธ SPEC ์ถ•์•ฝ์–ด', 2), +('KEYWORD', 'SPL', '์˜๋ฌธ SPL ์ถ•์•ฝ์–ด', 2) +ON CONFLICT DO NOTHING; + +-- SPECIAL ์ž์žฌ ์ƒ์„ธ ์ •๋ณด ํ…Œ์ด๋ธ” (๋„๋ฉด ์—…๋กœ๋“œ ๊ด€๋ จ) +CREATE TABLE IF NOT EXISTS special_material_details ( + id SERIAL PRIMARY KEY, + material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE, + file_id INTEGER REFERENCES files(id) ON DELETE CASCADE, + + -- ๋„๋ฉด ์ •๋ณด + drawing_number VARCHAR(100), -- ๋„๋ฉด ๋ฒˆํ˜ธ + drawing_revision VARCHAR(20), -- ๋„๋ฉด ๋ฆฌ๋น„์ „ + drawing_uploaded BOOLEAN DEFAULT FALSE, -- ๋„๋ฉด ์—…๋กœ๋“œ ์—ฌ๋ถ€ + drawing_file_path TEXT, -- ๋„๋ฉด ํŒŒ์ผ ๊ฒฝ๋กœ + + -- ํŠน์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ + special_requirements TEXT, -- ํŠน์ˆ˜ ์ œ์ž‘ ์š”๊ตฌ์‚ฌํ•ญ + manufacturing_notes TEXT, -- ์ œ์ž‘ ์ฐธ๊ณ ์‚ฌํ•ญ + approval_required BOOLEAN DEFAULT TRUE, -- ์Šน์ธ ํ•„์š” ์—ฌ๋ถ€ + approved_by VARCHAR(100), -- ์Šน์ธ์ž + approved_at TIMESTAMP, -- ์Šน์ธ ์ผ์‹œ + + -- ๋ถ„๋ฅ˜ ์ •๋ณด + classification_confidence FLOAT DEFAULT 1.0, + classification_reason TEXT, -- ๋ถ„๋ฅ˜ ๊ทผ๊ฑฐ + + -- ๊ด€๋ฆฌ ์ •๋ณด + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- SPECIAL ๊ด€๋ จ ์ธ๋ฑ์Šค +CREATE INDEX IF NOT EXISTS idx_special_patterns_type ON special_classification_patterns(pattern_type); +CREATE INDEX IF NOT EXISTS idx_special_patterns_active ON special_classification_patterns(is_active); +CREATE INDEX IF NOT EXISTS idx_special_details_material ON special_material_details(material_id); +CREATE INDEX IF NOT EXISTS idx_special_details_drawing_uploaded ON special_material_details(drawing_uploaded); + +-- ๊ธฐ์กด ์ž์žฌ ์ค‘ SPECIAL ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋œ ์ž์žฌ๋ฅผ SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ์žฌ๋ถ„๋ฅ˜ +UPDATE materials +SET + classified_category = 'SPECIAL', + classification_confidence = 1.0, + classified_at = CURRENT_TIMESTAMP +WHERE + ( + UPPER(original_description) LIKE '%SPECIAL%' OR + UPPER(original_description) LIKE '%์ŠคํŽ˜์…œ%' OR + UPPER(original_description) LIKE '%SPEC%' OR + UPPER(original_description) LIKE '%SPL%' + ) + AND (classified_category IS NULL OR classified_category != 'SPECIAL'); + -- 7. ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ •๋ฆฌ (์„ ํƒ์‚ฌํ•ญ) -- ================================ diff --git a/database/init/99_complete_schema.sql b/database/init/99_complete_schema.sql index 476101d..84e5116 100644 --- a/database/init/99_complete_schema.sql +++ b/database/init/99_complete_schema.sql @@ -1160,6 +1160,50 @@ CREATE INDEX IF NOT EXISTS idx_user_activity_logs_activity_type ON user_activity CREATE INDEX IF NOT EXISTS idx_user_activity_logs_created_at ON user_activity_logs(created_at); CREATE INDEX IF NOT EXISTS idx_user_activity_logs_target ON user_activity_logs(target_type, target_id); +-- ================================ +-- SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ง€์› ํ…Œ์ด๋ธ” +-- ================================ + +-- SPECIAL ํ‚ค์›Œ๋“œ ํŒจํ„ด ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS special_classification_patterns ( + id SERIAL PRIMARY KEY, + pattern_type VARCHAR(20) NOT NULL, -- 'KEYWORD', 'REGEX', 'EXACT' + pattern_value VARCHAR(200) NOT NULL, + description TEXT, + priority INTEGER DEFAULT 1, -- ์šฐ์„ ์ˆœ์œ„ (๋‚ฎ์„์ˆ˜๋ก ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- SPECIAL ์ž์žฌ ์ƒ์„ธ ์ •๋ณด ํ…Œ์ด๋ธ” (๋„๋ฉด ์—…๋กœ๋“œ ๊ด€๋ จ) +CREATE TABLE IF NOT EXISTS special_material_details ( + id SERIAL PRIMARY KEY, + material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE, + file_id INTEGER REFERENCES files(id) ON DELETE CASCADE, + + -- ๋„๋ฉด ์ •๋ณด + drawing_number VARCHAR(100), -- ๋„๋ฉด ๋ฒˆํ˜ธ + drawing_revision VARCHAR(20), -- ๋„๋ฉด ๋ฆฌ๋น„์ „ + drawing_uploaded BOOLEAN DEFAULT FALSE, -- ๋„๋ฉด ์—…๋กœ๋“œ ์—ฌ๋ถ€ + drawing_file_path TEXT, -- ๋„๋ฉด ํŒŒ์ผ ๊ฒฝ๋กœ + + -- ํŠน์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ + special_requirements TEXT, -- ํŠน์ˆ˜ ์ œ์ž‘ ์š”๊ตฌ์‚ฌํ•ญ + manufacturing_notes TEXT, -- ์ œ์ž‘ ์ฐธ๊ณ ์‚ฌํ•ญ + approval_required BOOLEAN DEFAULT TRUE, -- ์Šน์ธ ํ•„์š” ์—ฌ๋ถ€ + approved_by VARCHAR(100), -- ์Šน์ธ์ž + approved_at TIMESTAMP, -- ์Šน์ธ ์ผ์‹œ + + -- ๋ถ„๋ฅ˜ ์ •๋ณด + classification_confidence FLOAT DEFAULT 1.0, + classification_reason TEXT, -- ๋ถ„๋ฅ˜ ๊ทผ๊ฑฐ + + -- ๊ด€๋ฆฌ ์ •๋ณด + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + -- ================================ -- ์ถ”๊ฐ€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ธ๋ฑ์Šค (16_performance_indexes.sql) -- ================================ @@ -1181,6 +1225,25 @@ CREATE INDEX IF NOT EXISTS idx_materials_quantity_desc ON materials(quantity DES CREATE INDEX IF NOT EXISTS idx_materials_unverified ON materials(classified_category, classification_confidence) WHERE is_verified = false; CREATE INDEX IF NOT EXISTS idx_materials_low_confidence ON materials(file_id, classified_category) WHERE classification_confidence < 0.8; +-- SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์ „์šฉ ์ธ๋ฑ์Šค +CREATE INDEX IF NOT EXISTS idx_materials_special_category ON materials(classified_category) WHERE classified_category = 'SPECIAL'; +CREATE INDEX IF NOT EXISTS idx_special_patterns_type ON special_classification_patterns(pattern_type); +CREATE INDEX IF NOT EXISTS idx_special_patterns_active ON special_classification_patterns(is_active); +CREATE INDEX IF NOT EXISTS idx_special_details_material ON special_material_details(material_id); +CREATE INDEX IF NOT EXISTS idx_special_details_drawing_uploaded ON special_material_details(drawing_uploaded); + +-- ================================ +-- SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… +-- ================================ + +-- ๊ธฐ๋ณธ SPECIAL ํ‚ค์›Œ๋“œ ํŒจํ„ด ์‚ฝ์ž… +INSERT INTO special_classification_patterns (pattern_type, pattern_value, description, priority) VALUES +('KEYWORD', 'SPECIAL', '์˜๋ฌธ SPECIAL ํ‚ค์›Œ๋“œ', 1), +('KEYWORD', '์ŠคํŽ˜์…œ', 'ํ•œ๊ธ€ ์ŠคํŽ˜์…œ ํ‚ค์›Œ๋“œ', 1), +('KEYWORD', 'SPEC', '์˜๋ฌธ SPEC ์ถ•์•ฝ์–ด', 2), +('KEYWORD', 'SPL', '์˜๋ฌธ SPL ์ถ•์•ฝ์–ด', 2) +ON CONFLICT DO NOTHING; + -- ๋ถ„๋ฅ˜ ๊ด€๋ จ ์ธ๋ฑ์Šค (05_add_classification_columns.sql) CREATE INDEX IF NOT EXISTS idx_materials_subcategory ON materials(subcategory); CREATE INDEX IF NOT EXISTS idx_materials_standard ON materials(standard); diff --git a/frontend/src/pages/NewMaterialsPage.css b/frontend/src/pages/NewMaterialsPage.css index c7450cd..62cae89 100644 --- a/frontend/src/pages/NewMaterialsPage.css +++ b/frontend/src/pages/NewMaterialsPage.css @@ -111,8 +111,15 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } +.header-info { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + .materials-header h1 { - font-size: 20px; + font-size: 18px; font-weight: 600; color: #1f2937; margin: 0; @@ -120,10 +127,18 @@ .job-info { color: #6b7280; - font-size: 14px; + font-size: 13px; font-weight: 400; } +.material-count-inline { + color: #6b7280; + font-size: 12px; + background: #f3f4f6; + padding: 2px 8px; + border-radius: 8px; +} + .material-count { color: #6b7280; font-size: 14px; @@ -132,10 +147,27 @@ border-radius: 12px; } +/* ๋ฉ”์ธ ํ—ค๋” */ +.materials-header { + background: white; + padding: 8px 24px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #e5e7eb; + min-height: 50px; +} + +.header-left { + display: flex; + align-items: center; + gap: 12px; +} + /* ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ */ .category-filters { background: white; - padding: 16px 24px; + padding: 12px 24px; display: flex; gap: 8px; align-items: center; @@ -262,69 +294,598 @@ margin: 0; min-width: 1500px; overflow-x: auto; + max-height: calc(100vh - 200px); /* ์ตœ๋Œ€ ๋†’์ด๋งŒ ์ œํ•œ */ + overflow-y: auto; + position: relative; } .detailed-grid-header { display: grid; - grid-template-columns: 40px 80px 250px 80px 80px 450px 120px 150px 100px; - padding: 12px 24px; + /* ๊ธฐ๋ณธ ๊ทธ๋ฆฌ๋“œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ - ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ „์šฉ ํด๋ž˜์Šค ์‚ฌ์šฉ */ + padding: 12px 0; + margin: 0 24px; background: #f9fafb; - border-bottom: 1px solid #e5e7eb; + border-bottom: 2px solid #e5e7eb; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; + align-items: center; + position: sticky; + top: 0; + z-index: 10; +} + +.detailed-grid-header > div { + text-align: center; + display: flex; + align-items: center; + justify-content: center; + min-height: 40px; +} + +.detailed-grid-header > div { + border-right: 1px solid #d1d5db; + padding: 0 8px; +} + +.detailed-grid-header .filterable-header { + border-right: 1px solid #d1d5db; + padding: 0 8px; +} + +.detailed-grid-header > div:last-child, +.detailed-grid-header .filterable-header:last-child { + border-right: none; + padding: 0 8px; +} + +/* PIPE ์ „์šฉ ํ—ค๋” - 9๊ฐœ ์ปฌ๋Ÿผ */ +.detailed-grid-header.pipe-header { + grid-template-columns: 60px 90px 130px 80px 100px 250px 120px 200px 100px !important; + padding: 12px 0; + margin: 0 24px; + position: sticky; + top: 0; + z-index: 10; + background: #f9fafb; +} + +.detailed-grid-header.pipe-header > div, +.detailed-grid-header.pipe-header .filterable-header { + border-right: 1px solid #d1d5db; +} + +.detailed-grid-header.pipe-header > div:last-child, +.detailed-grid-header.pipe-header .filterable-header:last-child { + border-right: none; +} + +/* PIPE ์ „์šฉ ํ–‰ - 9๊ฐœ ์ปฌ๋Ÿผ */ +.detailed-material-row.pipe-row { + grid-template-columns: 60px 90px 130px 80px 100px 250px 120px 200px 100px !important; + padding: 8px 0; + margin: 0 24px; +} + +.detailed-material-row.pipe-row .material-cell { + border-right: 1px solid #e5e7eb; +} + +.detailed-material-row.pipe-row .material-cell:last-child { + border-right: none; +} + +/* SPECIAL ์ „์šฉ ํ—ค๋” - 10๊ฐœ ์ปฌ๋Ÿผ */ +.detailed-grid-header.special-header { + grid-template-columns: 60px 90px 150px 80px 100px 200px 120px 120px 200px 100px; + padding: 12px 0; + margin: 0 24px; + position: sticky; + top: 0; + z-index: 10; + background: #f9fafb; +} + +.detailed-grid-header.special-header > div, +.detailed-grid-header.special-header .filterable-header { + border-right: 1px solid #d1d5db; +} + +.detailed-grid-header.special-header > div:last-child, +.detailed-grid-header.special-header .filterable-header:last-child { + border-right: none; +} + +/* SPECIAL ์ „์šฉ ํ–‰ - 10๊ฐœ ์ปฌ๋Ÿผ */ +.detailed-material-row.special-row { + grid-template-columns: 60px 90px 150px 80px 100px 200px 120px 120px 200px 100px; + padding: 8px 0; + margin: 0 24px; +} + +/* BOLT ์ „์šฉ ํ—ค๋” - 9๊ฐœ ์ปฌ๋Ÿผ */ +.detailed-grid-header.bolt-header { + grid-template-columns: 60px 90px 130px 80px 100px 250px 120px 200px 100px; + padding: 12px 0; + margin: 0 24px; + position: sticky; + top: 0; + z-index: 10; + background: #f9fafb; +} + +/* BOLT ์ „์šฉ ํ–‰ - 9๊ฐœ ์ปฌ๋Ÿผ */ +.detailed-material-row.bolt-row { + grid-template-columns: 60px 90px 130px 80px 100px 250px 120px 200px 100px; + padding: 8px 0; + margin: 0 24px; +} + +.detailed-material-row.special-row .material-cell { + border-right: 1px solid #e5e7eb; +} + +/* BOLT ํ—ค๋” ํ…Œ๋‘๋ฆฌ */ +.detailed-grid-header.bolt-header > div, +.detailed-grid-header.bolt-header .filterable-header { + border-right: 1px solid #d1d5db; +} + +.detailed-grid-header.bolt-header > div:last-child, +.detailed-grid-header.bolt-header .filterable-header:last-child { + border-right: none; +} + +/* BOLT ํ–‰ ํ…Œ๋‘๋ฆฌ */ +.detailed-material-row.bolt-row .material-cell { + border-right: 1px solid #e5e7eb; +} + +.detailed-material-row.bolt-row .material-cell:last-child { + border-right: none; +} + +/* BOLT ํƒ€์ž… ๋ฐฐ์ง€ */ +.type-badge.bolt { + background: #7c3aed; + color: white; + border: 2px solid #6d28d9; + font-weight: 600; +} + +/* U-BOLT ์ „์šฉ ํ—ค๋” - 8๊ฐœ ์ปฌ๋Ÿผ */ +.detailed-grid-header.ubolt-header { + grid-template-columns: 60px 90px 130px 80px 200px 120px 200px 100px; + padding: 12px 0; + margin: 0 24px; + position: sticky; + top: 0; + z-index: 10; + background: #f9fafb; +} + +/* U-BOLT ์ „์šฉ ํ–‰ - 8๊ฐœ ์ปฌ๋Ÿผ */ +.detailed-material-row.ubolt-row { + grid-template-columns: 60px 90px 130px 80px 200px 120px 200px 100px; + padding: 8px 0; + margin: 0 24px; +} + +/* U-BOLT ํ—ค๋” ํ…Œ๋‘๋ฆฌ */ +.detailed-grid-header.ubolt-header > div, +.detailed-grid-header.ubolt-header .filterable-header { + border-right: 1px solid #d1d5db; +} +.detailed-grid-header.ubolt-header > div:last-child, +.detailed-grid-header.ubolt-header .filterable-header:last-child { + border-right: none; +} + +/* U-BOLT ํ–‰ ํ…Œ๋‘๋ฆฌ */ +.detailed-material-row.ubolt-row .material-cell { + border-right: 1px solid #e5e7eb; +} +.detailed-material-row.ubolt-row .material-cell:last-child { + border-right: none; +} + +/* U-BOLT ํƒ€์ž… ๋ฐฐ์ง€ */ +.type-badge.ubolt { + background: #059669; + color: white; + border: 2px solid #047857; + font-weight: 600; +} + +/* URETHANE ํƒ€์ž… ๋ฐฐ์ง€ */ +.type-badge.urethane { + background: #ea580c; + color: white; + border: 2px solid #c2410c; + font-weight: 600; +} + +.detailed-material-row.special-row .material-cell:last-child { + border-right: none; } /* ํ”Œ๋žœ์ง€ ์ „์šฉ ํ—ค๋” - 10๊ฐœ ์ปฌ๋Ÿผ */ .detailed-grid-header.flange-header { - grid-template-columns: 40px 80px 150px 80px 100px 80px 350px 100px 150px 100px; + grid-template-columns: 60px 100px 150px 80px 100px 80px 350px 100px 150px 100px; + padding: 12px 0; + margin: 0 24px; + position: sticky; + top: 0; + z-index: 10; + background: #f9fafb; +} + +.detailed-grid-header.flange-header > div, +.detailed-grid-header.flange-header .filterable-header { + border-right: 1px solid #d1d5db; +} + +.detailed-grid-header.flange-header > div:last-child, +.detailed-grid-header.flange-header .filterable-header:last-child { + border-right: none; } /* ํ”Œ๋žœ์ง€ ์ „์šฉ ํ–‰ - 10๊ฐœ ์ปฌ๋Ÿผ */ .detailed-material-row.flange-row { - grid-template-columns: 40px 80px 150px 80px 100px 80px 350px 100px 150px 100px; + grid-template-columns: 60px 100px 150px 80px 100px 80px 350px 100px 150px 100px; + padding: 8px 0; + margin: 0 24px; +} + +.detailed-material-row.flange-row .material-cell { + border-right: 1px solid #e5e7eb; +} + +.detailed-material-row.flange-row .material-cell:last-child { + border-right: none; } /* ํ”ผํŒ… ์ „์šฉ ํ—ค๋” - 10๊ฐœ ์ปฌ๋Ÿผ */ .detailed-grid-header.fitting-header { grid-template-columns: 40px 80px 280px 80px 80px 140px 350px 100px 150px 100px; + padding: 12px 0; + margin: 0 24px; +} + +.detailed-grid-header.fitting-header > div, +.detailed-grid-header.fitting-header .filterable-header { + border-right: 1px solid #d1d5db; +} + +.detailed-grid-header.fitting-header > div:last-child, +.detailed-grid-header.fitting-header .filterable-header:last-child { + border-right: none; } /* ํ”ผํŒ… ์ „์šฉ ํ–‰ - 10๊ฐœ ์ปฌ๋Ÿผ */ .detailed-material-row.fitting-row { grid-template-columns: 40px 80px 280px 80px 80px 140px 350px 100px 150px 100px; + padding: 8px 0; + margin: 0 24px; +} + +.detailed-material-row.fitting-row .material-cell { + border-right: 1px solid #e5e7eb; +} + +.detailed-material-row.fitting-row .material-cell:last-child { + border-right: none; } /* ๋ฐธ๋ธŒ ์ „์šฉ ํ—ค๋” - 9๊ฐœ ์ปฌ๋Ÿผ (์Šค์ผ€์ค„ ์ œ๊ฑฐ, ํƒ€์ž… ๋„ˆ๋น„ ์ฆ๊ฐ€) */ .detailed-grid-header.valve-header { grid-template-columns: 40px 220px 100px 80px 80px 350px 100px 150px 100px; + padding: 12px 0; + margin: 0 24px; +} + +.detailed-grid-header.valve-header > div, +.detailed-grid-header.valve-header .filterable-header { + border-right: 1px solid #d1d5db; +} + +.detailed-grid-header.valve-header > div:last-child, +.detailed-grid-header.valve-header .filterable-header:last-child { + border-right: none; } /* ๋ฐธ๋ธŒ ์ „์šฉ ํ–‰ - 9๊ฐœ ์ปฌ๋Ÿผ (์Šค์ผ€์ค„ ์ œ๊ฑฐ, ํƒ€์ž… ๋„ˆ๋น„ ์ฆ๊ฐ€) */ .detailed-material-row.valve-row { grid-template-columns: 40px 220px 100px 80px 80px 350px 100px 150px 100px; + padding: 8px 0; + margin: 0 24px; +} + +.detailed-material-row.valve-row .material-cell { + border-right: 1px solid #e5e7eb; +} + +.detailed-material-row.valve-row .material-cell:last-child { + border-right: none; } /* ๊ฐ€์Šค์ผ“ ์ „์šฉ ํ—ค๋” - 11๊ฐœ ์ปฌ๋Ÿผ (ํƒ€์ž… ์ข๊ฒŒ, ์ƒ์„ธ๋‚ด์—ญ ๋„“๊ฒŒ, ๋‘๊ป˜ ์ถ”๊ฐ€) */ .detailed-grid-header.gasket-header { grid-template-columns: 40px 80px 120px 80px 80px 100px 400px 60px 80px 150px 100px; + padding: 12px 0; + margin: 0 24px; +} + +.detailed-grid-header.gasket-header > div, +.detailed-grid-header.gasket-header .filterable-header { + border-right: 1px solid #d1d5db; +} + +.detailed-grid-header.gasket-header > div:last-child, +.detailed-grid-header.gasket-header .filterable-header:last-child { + border-right: none; } /* ๊ฐ€์Šค์ผ“ ์ „์šฉ ํ–‰ - 11๊ฐœ ์ปฌ๋Ÿผ (ํƒ€์ž… ์ข๊ฒŒ, ์ƒ์„ธ๋‚ด์—ญ ๋„“๊ฒŒ, ๋‘๊ป˜ ์ถ”๊ฐ€) */ .detailed-material-row.gasket-row { grid-template-columns: 40px 80px 120px 80px 80px 100px 400px 60px 80px 150px 100px; + padding: 8px 0; + margin: 0 24px; +} + +.detailed-material-row.gasket-row .material-cell { + border-right: 1px solid #e5e7eb; +} + +.detailed-material-row.gasket-row .material-cell:last-child { + border-right: none; +} + +/* ํ•„ํ„ฐ๋ง ๊ฐ€๋Šฅํ•œ ํ—ค๋” ์Šคํƒ€์ผ */ +.filterable-header { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 4px; + padding: 0; + background: transparent; + cursor: pointer; + user-select: none; + transition: all 0.2s ease; + min-height: 40px; +} + +.filterable-header:hover { + background: #f1f5f9; +} + +.header-text { + font-size: 12px; + font-weight: 600; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.5px; + text-align: center; + white-space: nowrap; + margin: 0; +} + +.header-controls { + display: flex; + gap: 2px; + opacity: 0.7; + flex-shrink: 0; + transition: opacity 0.2s ease; +} + +.filterable-header:hover .header-controls { + opacity: 1; +} + +.sort-btn, .filter-btn { + background: white; + border: 1px solid #e2e8f0; + padding: 1px; + border-radius: 3px; + cursor: pointer; + font-size: 9px; + color: #64748b; + transition: all 0.15s ease; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.sort-btn:hover, .filter-btn:hover { + background: #f8fafc; + border-color: #cbd5e1; + color: #475569; +} + +.sort-btn.active { + background: #3b82f6; + border-color: #3b82f6; + color: white; +} + +.filter-btn.active { + background: #10b981; + border-color: #10b981; + color: white; +} + +/* ํ•„ํ„ฐ ๋“œ๋กญ๋‹ค์šด */ +.filter-dropdown { + position: absolute; + top: calc(100% + 2px); + left: 0; + right: 0; + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + z-index: 1000; + max-height: 240px; + overflow: hidden; + display: flex; + flex-direction: column; + animation: dropdownFadeIn 0.15s ease-out; +} + +@keyframes dropdownFadeIn { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.filter-search { + padding: 8px; + border-bottom: 1px solid #f1f5f9; + background: #fafbfc; + display: flex; + align-items: center; + gap: 6px; +} + +.filter-search input { + flex: 1; + padding: 6px 8px; + border: 1px solid #e2e8f0; + border-radius: 4px; + font-size: 12px; + outline: none; + transition: border-color 0.15s ease; + background: white; + color: #374151; +} + +.filter-search input:focus { + border-color: #3b82f6; +} + +.filter-search input::placeholder { + color: #9ca3af; + font-size: 11px; +} + +.clear-filter-btn { + background: #ef4444; + color: white; + border: none; + padding: 4px 6px; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + font-weight: 500; + transition: background-color 0.15s ease; +} + +.clear-filter-btn:hover { + background: #dc2626; +} + +.filter-options { + flex: 1; + overflow-y: auto; + max-height: 160px; +} + +.filter-option-header { + padding: 4px 10px; + font-size: 10px; + font-weight: 600; + color: #6b7280; + background: #f8fafc; + border-bottom: 1px solid #f1f5f9; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.filter-option { + padding: 4px 10px; + font-size: 11px; + cursor: pointer; + transition: background-color 0.1s ease; + color: #374151; +} + +.filter-option:hover { + background: #f8fafc; +} + +.filter-option-more { + padding: 4px 10px; + font-size: 10px; + color: #9ca3af; + font-style: italic; + text-align: center; + background: #f8fafc; +} + +/* ์•ก์…˜ ๋ฐ” ์Šคํƒ€์ผ ๊ฐœ์„  */ +.filter-info { + font-size: 11px; + color: #6b7280; + margin-left: 8px; +} + +.clear-filters-btn { + background: #f59e0b; + color: white; + border: none; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + margin-right: 8px; + transition: background-color 0.2s; +} + +.clear-filters-btn:hover { + background: #d97706; } /* UNKNOWN ์ „์šฉ ํ—ค๋” - 5๊ฐœ ์ปฌ๋Ÿผ */ .detailed-grid-header.unknown-header { grid-template-columns: 40px 100px 1fr 150px 100px; + padding: 12px 0; + margin: 0 24px; +} + +.detailed-grid-header.unknown-header > div, +.detailed-grid-header.unknown-header .filterable-header { + border-right: 1px solid #d1d5db; +} + +.detailed-grid-header.unknown-header > div:last-child, +.detailed-grid-header.unknown-header .filterable-header:last-child { + border-right: none; } /* UNKNOWN ์ „์šฉ ํ–‰ - 5๊ฐœ ์ปฌ๋Ÿผ */ .detailed-material-row.unknown-row { grid-template-columns: 40px 100px 1fr 150px 100px; + padding: 8px 0; + margin: 0 24px; +} + +.detailed-material-row.unknown-row .material-cell { + border-right: 1px solid #e5e7eb; +} + +.detailed-material-row.unknown-row .material-cell:last-child { + border-right: none; } /* UNKNOWN ์„ค๋ช… ์…€ ์Šคํƒ€์ผ */ @@ -345,14 +906,25 @@ .detailed-material-row { display: grid; - grid-template-columns: 40px 80px 250px 80px 80px 450px 120px 150px 100px; - padding: 12px 24px; - border-bottom: 1px solid #f3f4f6; + /* ๊ธฐ๋ณธ ๊ทธ๋ฆฌ๋“œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ - ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ „์šฉ ํด๋ž˜์Šค ์‚ฌ์šฉ */ + padding: 12px 0; + margin: 0 24px; + border-bottom: 1px solid #e5e7eb; align-items: center; transition: background 0.15s; font-size: 13px; } +.detailed-material-row .material-cell { + border-right: 1px solid #e5e7eb; + padding: 0 8px; +} + +.detailed-material-row .material-cell:last-child { + border-right: none; + padding: 0 8px; +} + .detailed-material-row:hover { background: #fafbfc; } @@ -364,28 +936,62 @@ .material-cell { overflow: visible !important; text-overflow: initial !important; + text-align: center; + display: flex; + align-items: center; + justify-content: center; white-space: normal !important; - padding-right: 12px; word-break: break-word; min-width: 120px; max-width: none !important; + min-height: 40px; +} + +.material-cell > * { + margin: 0; + flex-shrink: 0; +} + +/* ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ ํ•„๋“œ */ +.user-req-input { + width: 100%; + padding: 4px 8px; + border: 1px solid #d1d5db; + border-radius: 4px; + font-size: 12px; + outline: none; + text-align: center; + margin: 0; +} + +.user-req-input:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 1px #3b82f6; } .material-cell input[type="checkbox"] { - width: 16px; - height: 16px; + width: 14px; + height: 14px; cursor: pointer; + margin: 0; + padding: 0; + flex-shrink: 0; + box-sizing: border-box; } /* ํƒ€์ž… ๋ฐฐ์ง€ */ .type-badge { - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; + margin: 0; + flex-shrink: 0; } .type-badge.pipe { @@ -418,6 +1024,14 @@ color: white; } +.type-badge.special { + background: #dc2626; + color: white; + border: 2px solid #b91c1c; + font-weight: 700; + box-shadow: 0 2px 4px rgba(220, 38, 38, 0.2); +} + .type-badge.unknown { background: #6b7280; color: white; @@ -428,11 +1042,6 @@ color: white; } -.type-badge.unknown { - background: #9ca3af; - color: white; -} - /* ํ…์ŠคํŠธ ์Šคํƒ€์ผ */ .subtype-text, .size-text, diff --git a/frontend/src/pages/NewMaterialsPage.jsx b/frontend/src/pages/NewMaterialsPage.jsx index 7034956..c560002 100644 --- a/frontend/src/pages/NewMaterialsPage.jsx +++ b/frontend/src/pages/NewMaterialsPage.jsx @@ -28,6 +28,11 @@ const NewMaterialsPage = ({ // materialId: requirement ํ˜•ํƒœ const [savingRequirements, setSavingRequirements] = useState(false); + // ์ •๋ ฌ ๋ฐ ํ•„ํ„ฐ๋ง ์ƒํƒœ + const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' }); + const [columnFilters, setColumnFilters] = useState({}); + const [showFilterDropdown, setShowFilterDropdown] = useState(null); + // ๊ฐ™์€ BOM์˜ ๋‹ค๋ฅธ ๋ฆฌ๋น„์ „๋“ค ์กฐํšŒ const loadAvailableRevisions = async () => { try { @@ -63,6 +68,20 @@ const NewMaterialsPage = ({ } }, [fileId]); + // ์™ธ๋ถ€ ํด๋ฆญ ์‹œ ํ•„ํ„ฐ ๋“œ๋กญ๋‹ค์šด ๋‹ซ๊ธฐ + useEffect(() => { + const handleClickOutside = (event) => { + if (!event.target.closest('.filterable-header')) { + setShowFilterDropdown(null); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + const loadMaterials = async (id) => { try { setLoading(true); @@ -272,7 +291,9 @@ const NewMaterialsPage = ({ // ์นดํ…Œ๊ณ ๋ฆฌ ํ‘œ์‹œ๋ช… ๋งคํ•‘ const getCategoryDisplayName = (category) => { const categoryMap = { - 'SUPPORT': 'U-BOLT', + 'SPECIAL': 'SPECIAL', + 'U_BOLT': 'U-BOLT', + 'SUPPORT': 'SUPPORT', 'PIPE': 'PIPE', 'FITTING': 'FITTING', 'FLANGE': 'FLANGE', @@ -315,24 +336,24 @@ const NewMaterialsPage = ({ const descUpper = description.toUpperCase(); const additionalReqs = []; - // ํ‘œ๋ฉด์ฒ˜๋ฆฌ ํŒจํ„ด๋“ค + // ํ‘œ๋ฉด์ฒ˜๋ฆฌ ํŒจํ„ด๋“ค (์›๋ณธ ์˜์–ด ์•ฝ์–ด ์‚ฌ์šฉ) const surfaceTreatments = { - 'ELEC.GALV': '์ „๊ธฐ์•„์—ฐ๋„๊ธˆ', - 'ELEC GALV': '์ „๊ธฐ์•„์—ฐ๋„๊ธˆ', - 'GALVANIZED': '์•„์—ฐ๋„๊ธˆ', - 'GALV': '์•„์—ฐ๋„๊ธˆ', - 'HOT DIP GALV': '์šฉ์œต์•„์—ฐ๋„๊ธˆ', - 'HDG': '์šฉ์œต์•„์—ฐ๋„๊ธˆ', - 'ZINC PLATED': '์•„์—ฐ๋„๊ธˆ', - 'ZINC': '์•„์—ฐ๋„๊ธˆ', - 'STAINLESS': '์Šคํ…Œ์ธ๋ฆฌ์Šค', - 'SS': '์Šคํ…Œ์ธ๋ฆฌ์Šค' + 'ELEC.GALV': 'ELEC.GALV', + 'ELEC GALV': 'ELEC.GALV', + 'GALVANIZED': 'GALVANIZED', + 'GALV': 'GALV', + 'HOT DIP GALV': 'HDG', + 'HDG': 'HDG', + 'ZINC PLATED': 'ZINC PLATED', + 'ZINC': 'ZINC', + 'STAINLESS': 'STAINLESS', + 'SS': 'SS' }; // ํ‘œ๋ฉด์ฒ˜๋ฆฌ ํ™•์ธ - for (const [pattern, korean] of Object.entries(surfaceTreatments)) { + for (const [pattern, treatment] of Object.entries(surfaceTreatments)) { if (descUpper.includes(pattern)) { - additionalReqs.push(korean); + additionalReqs.push(treatment); } } @@ -581,35 +602,80 @@ const NewMaterialsPage = ({ const safetyQty = Math.ceil(qty * 1.05); // 5% ์—ฌ์œ ์œจ const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4์˜ ๋ฐฐ์ˆ˜ - // ๋ณผํŠธ ๊ธธ์ด ์ถ”์ถœ (์›๋ณธ ์„ค๋ช…์—์„œ) - const description = material.original_description || ''; + // ๋ณผํŠธ ์ƒ์„ธ ์ •๋ณด ์šฐ์„  ์‚ฌ์šฉ + const boltDetails = material.bolt_details || {}; + + // ๊ธธ์ด ์ •๋ณด (bolt_details ์šฐ์„ , ์—†์œผ๋ฉด ์›๋ณธ ์„ค๋ช…์—์„œ ์ถ”์ถœ) let boltLength = '-'; + if (boltDetails.length && boltDetails.length !== '-') { + boltLength = boltDetails.length; + } else { + // ์›๋ณธ ์„ค๋ช…์—์„œ ๊ธธ์ด ์ถ”์ถœ + const description = material.original_description || ''; + const lengthPatterns = [ + /(\d+(?:\.\d+)?)\s*LG/i, // 75 LG, 90.0000 LG + /(\d+(?:\.\d+)?)\s*mm/i, // 50mm + /(\d+(?:\.\d+)?)\s*MM/i, // 50MM + /LG[,\s]*(\d+(?:\.\d+)?)/i // LG, 75 ํ˜•ํƒœ + ]; + + for (const pattern of lengthPatterns) { + const match = description.match(pattern); + if (match) { + let lengthValue = match[1]; + // ์†Œ์ˆ˜์  ์ œ๊ฑฐ (145.0000 โ†’ 145) + if (lengthValue.includes('.') && lengthValue.endsWith('.0000')) { + lengthValue = lengthValue.split('.')[0]; + } else if (lengthValue.includes('.') && /\.0+$/.test(lengthValue)) { + lengthValue = lengthValue.split('.')[0]; + } + boltLength = `${lengthValue}mm`; + break; + } + } + } - // ๊ธธ์ด ํŒจํ„ด ์ถ”์ถœ (75 LG, 90.0000 LG, 50mm ๋“ฑ) - const lengthPatterns = [ - /(\d+(?:\.\d+)?)\s*LG/i, // 75 LG, 90.0000 LG - /(\d+(?:\.\d+)?)\s*mm/i, // 50mm - /(\d+(?:\.\d+)?)\s*MM/i, // 50MM - /LG[,\s]*(\d+(?:\.\d+)?)/i // LG, 75 ํ˜•ํƒœ - ]; + // ์žฌ์งˆ ์ •๋ณด (bolt_details ์šฐ์„ , ์—†์œผ๋ฉด ๊ธฐ๋ณธ ํ•„๋“œ ์‚ฌ์šฉ) + let boltGrade = '-'; + if (boltDetails.material_standard && boltDetails.material_grade) { + // bolt_details์—์„œ ์™„์ „ํ•œ ์žฌ์งˆ ์ •๋ณด ๊ตฌ์„ฑ + if (boltDetails.material_grade !== 'UNKNOWN' && boltDetails.material_grade !== boltDetails.material_standard) { + boltGrade = `${boltDetails.material_standard} ${boltDetails.material_grade}`; + } else { + boltGrade = boltDetails.material_standard; + } + } else if (material.full_material_grade && material.full_material_grade !== '-') { + boltGrade = material.full_material_grade; + } else if (material.material_grade && material.material_grade !== '-') { + boltGrade = material.material_grade; + } - for (const pattern of lengthPatterns) { - const match = description.match(pattern); - if (match) { - boltLength = `${match[1]}mm`; - break; + // ๋ณผํŠธ ํƒ€์ž… (PSV_BOLT, LT_BOLT ๋“ฑ) + let boltSubtype = 'BOLT_GENERAL'; + if (boltDetails.bolt_type && boltDetails.bolt_type !== 'UNKNOWN') { + boltSubtype = boltDetails.bolt_type; + } else { + // ์›๋ณธ ์„ค๋ช…์—์„œ ํŠน์ˆ˜ ๋ณผํŠธ ํƒ€์ž… ์ถ”์ถœ + const description = material.original_description || ''; + const upperDesc = description.toUpperCase(); + if (upperDesc.includes('PSV')) { + boltSubtype = 'PSV_BOLT'; + } else if (upperDesc.includes('LT')) { + boltSubtype = 'LT_BOLT'; + } else if (upperDesc.includes('CK')) { + boltSubtype = 'CK_BOLT'; } } // ์ถ”๊ฐ€์š”๊ตฌ์‚ฌํ•ญ ์ถ”์ถœ (ELEC.GALV ๋“ฑ) - const additionalReq = extractBoltAdditionalRequirements(description); + const additionalReq = extractBoltAdditionalRequirements(material.original_description || ''); return { type: 'BOLT', - subtype: material.bolt_details?.bolt_type || 'BOLT_GENERAL', - size: material.size_spec || '-', + subtype: boltSubtype, + size: material.size_spec || material.main_nom || '-', schedule: boltLength, // ๊ธธ์ด ์ •๋ณด - grade: material.full_material_grade || material.material_grade || '-', + grade: boltGrade, additionalReq: additionalReq, // ์ถ”๊ฐ€์š”๊ตฌ์‚ฌํ•ญ quantity: purchaseQty, unit: 'SETS' @@ -679,13 +745,130 @@ const NewMaterialsPage = ({ } }; - // ํ•„ํ„ฐ๋ง๋œ ์ž์žฌ ๋ชฉ๋ก - const filteredMaterials = materials.filter(material => { - if (selectedCategory === 'ALL') { - return true; // ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ์ผ ๋•Œ๋Š” ๋ชจ๋“  ์ž์žฌ ํ‘œ์‹œ + // ์ •๋ ฌ ํ•จ์ˆ˜ + const handleSort = (key) => { + let direction = 'asc'; + if (sortConfig.key === key && sortConfig.direction === 'asc') { + direction = 'desc'; } - return material.classified_category === selectedCategory; - }); + setSortConfig({ key, direction }); + }; + + // ํ•„ํ„ฐ ํ•จ์ˆ˜ + const handleFilter = (column, value) => { + setColumnFilters(prev => ({ + ...prev, + [column]: value + })); + }; + + // ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” + const clearFilter = (column) => { + setColumnFilters(prev => { + const newFilters = { ...prev }; + delete newFilters[column]; + return newFilters; + }); + }; + + // ๋ชจ๋“  ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” + const clearAllFilters = () => { + setColumnFilters({}); + setSortConfig({ key: null, direction: 'asc' }); + }; + + // ํ•„ํ„ฐ๋ง๋œ ์ž์žฌ ๋ชฉ๋ก + const filteredMaterials = materials + .filter(material => { + // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ + if (selectedCategory !== 'ALL' && material.classified_category !== selectedCategory) { + return false; + } + + // ์ปฌ๋Ÿผ ํ•„ํ„ฐ ์ ์šฉ + for (const [column, filterValue] of Object.entries(columnFilters)) { + if (!filterValue) continue; + + const info = parseMaterialInfo(material); + let materialValue = ''; + + switch (column) { + case 'type': + materialValue = info.type || ''; + break; + case 'subtype': + materialValue = info.subtype || ''; + break; + case 'size': + materialValue = info.size || ''; + break; + case 'schedule': + materialValue = info.schedule || ''; + break; + case 'grade': + materialValue = info.grade || ''; + break; + case 'quantity': + materialValue = info.quantity?.toString() || ''; + break; + default: + materialValue = material[column]?.toString() || ''; + } + + if (!materialValue.toLowerCase().includes(filterValue.toLowerCase())) { + return false; + } + } + + return true; + }) + .sort((a, b) => { + if (!sortConfig.key) return 0; + + const aInfo = parseMaterialInfo(a); + const bInfo = parseMaterialInfo(b); + + let aValue, bValue; + + switch (sortConfig.key) { + case 'type': + aValue = aInfo.type || ''; + bValue = bInfo.type || ''; + break; + case 'subtype': + aValue = aInfo.subtype || ''; + bValue = bInfo.subtype || ''; + break; + case 'size': + aValue = aInfo.size || ''; + bValue = bInfo.size || ''; + break; + case 'schedule': + aValue = aInfo.schedule || ''; + bValue = bInfo.schedule || ''; + break; + case 'grade': + aValue = aInfo.grade || ''; + bValue = bInfo.grade || ''; + break; + case 'quantity': + aValue = aInfo.quantity || 0; + bValue = bInfo.quantity || 0; + break; + default: + aValue = a[sortConfig.key] || ''; + bValue = b[sortConfig.key] || ''; + } + + // ์ˆซ์ž ๋น„๊ต + if (typeof aValue === 'number' && typeof bValue === 'number') { + return sortConfig.direction === 'asc' ? aValue - bValue : bValue - aValue; + } + + // ๋ฌธ์ž์—ด ๋น„๊ต + const comparison = aValue.toString().localeCompare(bValue.toString()); + return sortConfig.direction === 'asc' ? comparison : -comparison; + }); // ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ‰์ƒ (์ œ๊ฑฐ - CSS์—์„œ ์ฒ˜๋ฆฌ) @@ -709,7 +892,124 @@ const NewMaterialsPage = ({ setSelectedMaterials(newSelection); }; - // ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ - ํ™”๋ฉด์— ํ‘œ์‹œ๋œ ๊ทธ๋Œ€๋กœ + // ํ•„ํ„ฐ ํ—ค๋” ์ปดํฌ๋„ŒํŠธ + const FilterableHeader = ({ children, sortKey, filterKey, className = "" }) => { + const uniqueValues = React.useMemo(() => { + const values = new Set(); + + // ํ˜„์žฌ ์„ ํƒ๋œ ์นดํ…Œ๊ณ ๋ฆฌ์˜ ์ž์žฌ๋“ค๋งŒ ํ•„ํ„ฐ๋ง + const categoryMaterials = materials.filter(material => { + if (selectedCategory === 'ALL') return true; + return material.classified_category === selectedCategory; + }); + + categoryMaterials.forEach(material => { + const info = parseMaterialInfo(material); + let value = ''; + + switch (filterKey) { + case 'type': + value = info.type || ''; + break; + case 'subtype': + value = info.subtype || ''; + break; + case 'size': + value = info.size || ''; + break; + case 'schedule': + value = info.schedule || ''; + break; + case 'grade': + value = info.grade || ''; + break; + case 'quantity': + value = info.quantity?.toString() || ''; + break; + default: + value = material[filterKey]?.toString() || ''; + } + + if (value) values.add(value); + }); + + return Array.from(values).sort(); + }, [materials, filterKey, selectedCategory]); + + return ( +
+ {children} +
+ {/* ์ •๋ ฌ ๋ฒ„ํŠผ */} + + + {/* ํ•„ํ„ฐ ๋ฒ„ํŠผ */} + +
+ + {/* ํ•„ํ„ฐ ๋“œ๋กญ๋‹ค์šด */} + {showFilterDropdown === filterKey && ( +
+
+ handleFilter(filterKey, e.target.value)} + autoFocus + /> + {columnFilters[filterKey] && ( + + )} +
+ +
+
๊ฐ’ ๋ชฉ๋ก:
+ {uniqueValues.slice(0, 20).map(value => ( +
{ + handleFilter(filterKey, value); + setShowFilterDropdown(null); + }} + > + {value} +
+ ))} + {uniqueValues.length > 20 && ( +
+ +{uniqueValues.length - 20}๊ฐœ ๋”... +
+ )} +
+
+ )} +
+ ); + }; + + // ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ - ๊ฐœ์„ ๋œ ๋ฒ„์ „ ์‚ฌ์šฉ const exportToExcel = () => { try { // ๋‚ด๋ณด๋‚ผ ๋ฐ์ดํ„ฐ ๊ฒฐ์ • (์„ ํƒ ํ•ญ๋ชฉ ๋˜๋Š” ํ˜„์žฌ ์นดํ…Œ๊ณ ๋ฆฌ ์ „์ฒด) @@ -719,68 +1019,23 @@ const NewMaterialsPage = ({ console.log('๐Ÿ“Š ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ:', dataToExport.length, '๊ฐœ ํ•ญ๋ชฉ'); - // ์ƒˆ๋กœ์šด ์—‘์…€ ์–‘์‹์— ๋งž์ถ˜ ์ปฌ๋Ÿผ ๊ตฌ์„ฑ - const getExcelData = (material) => { - const info = parseMaterialInfo(material); - - // ํ’ˆ๋ชฉ๋ช… ์ƒ์„ฑ (๊ฐ„๋‹จํ•˜๊ฒŒ) - let itemName = ''; - if (selectedCategory === 'PIPE') { - itemName = info.subtype || 'PIPE'; - } else if (selectedCategory === 'FITTING') { - itemName = info.subtype || 'FITTING'; - } else if (selectedCategory === 'FLANGE') { - itemName = info.subtype || 'FLANGE'; - } else if (selectedCategory === 'VALVE') { - itemName = info.valveType || info.subtype || 'VALVE'; - } else if (selectedCategory === 'GASKET') { - itemName = info.subtype || 'GASKET'; - } else if (selectedCategory === 'BOLT') { - itemName = info.subtype || 'BOLT'; - } else { - itemName = info.subtype || info.type || 'UNKNOWN'; - } - - // ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ํ™•์ธ - const userReq = userRequirements[material.id] || ''; - console.log(`๐Ÿ“‹ ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ - ์ž์žฌ ID ${material.id}: ์‚ฌ์šฉ์ž์š”๊ตฌ = "${userReq}"`); - - // ํ†ต์ผ๋œ ์—‘์…€ ์–‘์‹ ๋ฐ˜ํ™˜ - return { - 'TAGNO': '', // ๋น„์›Œ๋‘  - 'ํ’ˆ๋ชฉ๋ช…': itemName.trim(), - '์ˆ˜๋Ÿ‰': info.quantity, - 'ํ†ตํ™”๊ตฌ๋ถ„': 'KRW', // ๊ธฐ๋ณธ๊ฐ’ - '๋‹จ๊ฐ€': 1, // ์ผ๊ด„ 1๋กœ ์„ค์ • - 'ํฌ๊ธฐ': info.size, - '์••๋ ฅ๋“ฑ๊ธ‰': info.pressure || '-', - '์Šค์ผ€์ค„': info.schedule || '-', - '์žฌ์งˆ': info.grade, - '์‚ฌ์šฉ์ž์š”๊ตฌ': userReq, - '๊ด€๋ฆฌํ•ญ๋ชฉ1': '', // ๋นˆ์นธ - '๊ด€๋ฆฌํ•ญ๋ชฉ7': '', // ๋นˆ์นธ - '๊ด€๋ฆฌํ•ญ๋ชฉ8': '', // ๋นˆ์นธ - '๊ด€๋ฆฌํ•ญ๋ชฉ9': '', // ๋นˆ์นธ - '๊ด€๋ฆฌํ•ญ๋ชฉ10': '', // ๋นˆ์นธ - '๋‚ฉ๊ธฐ์ผ(YYYY-MM-DD)': new Date().toISOString().split('T')[0] // ์˜ค๋Š˜ ๋‚ ์งœ - }; + // ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ์„ ์ž์žฌ์— ์ถ”๊ฐ€ + const dataWithRequirements = dataToExport.map(material => ({ + ...material, + user_requirement: userRequirements[material.id] || '' + })); + + // ๊ฐœ์„ ๋œ ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ํ•จ์ˆ˜ ์‚ฌ์šฉ + const additionalInfo = { + filename: filename || bomName, + jobNo: jobNo, + revision: currentRevision, + uploadDate: new Date().toLocaleDateString() }; - // ์—‘์…€ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - const excelData = dataToExport.map(material => getExcelData(material)); - - // ์›Œํฌ๋ถ ์ƒ์„ฑ - const ws = XLSX.utils.json_to_sheet(excelData); - const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, selectedCategory); - - // ํŒŒ์ผ๋ช… ์ƒ์„ฑ const fileName = `${selectedCategory}_${jobNo || 'export'}_${new Date().toISOString().split('T')[0]}.xlsx`; - // ํŒŒ์ผ ์ €์žฅ - const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); - const data = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); - saveAs(data, fileName); + exportMaterialsToExcel(dataWithRequirements, fileName, additionalInfo); console.log('โœ… ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ ์„ฑ๊ณต'); } catch (error) { @@ -822,12 +1077,17 @@ const NewMaterialsPage = ({ > โ† -

์ž์žฌ ๋ชฉ๋ก

- {jobNo && ( - - {jobNo} - {bomName} +
+

์ž์žฌ ๋ชฉ๋ก

+ {jobNo && ( + + {jobNo} - {bomName} + + )} + + ์ด {materials.length}๊ฐœ ์ž์žฌ ({currentRevision}) - )} +
{availableRevisions.length > 1 && ( @@ -868,15 +1128,11 @@ const NewMaterialsPage = ({ border: 'none', borderRadius: '4px', fontSize: '11px', - cursor: 'pointer', - marginRight: '12px' + cursor: 'pointer' }} > ๐Ÿ”— URL - - ์ด {materials.length}๊ฐœ ์ž์žฌ ({currentRevision}) -
@@ -891,7 +1147,16 @@ const NewMaterialsPage = ({ ์ „์ฒด {materials.length} - {Object.entries(categoryCounts).map(([category, count]) => ( + {/* SPECIAL ์นดํ…Œ๊ณ ๋ฆฌ ์šฐ์„  ํ‘œ์‹œ */} + + + {Object.entries(categoryCounts).filter(([category]) => category !== 'SPECIAL').map(([category, count]) => ( + )}