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 ( +