diff --git a/RULES.md b/RULES.md index 31162f1..681a3a6 100644 --- a/RULES.md +++ b/RULES.md @@ -963,7 +963,7 @@ logger.error("์๋ฌ ๋ฐ์") --- -## ๐ ์์ฃผ ๋ฐ์ํ๋ ์ด์ & ํด๊ฒฐ๋ฒ +## โ ๏ธ ์์ฃผ ๋ฐ์ํ๋ ์ด์ & ํด๊ฒฐ๋ฒ ### 1. ํ์ดํ ๊ธธ์ด ํฉ์ฐ ๋ฌธ์ ```python @@ -2104,4 +2104,50 @@ const materials = await fetchMaterials({ --- -**๋ง์ง๋ง ์ ๋ฐ์ดํธ**: 2025๋ 9์ 24์ผ (์ฌ์ฉ์ ํผ๋๋ฐฑ ๊ธฐ๋ฐ ๊ฐ์ ์ฌํญ ์ ๋ฆฌ) +## ๐ ๋ฉ์ธ ์๋ฒ ๋ฐฐํฌ ๊ฐ์ด๋ + +### ๐ **๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ ** + +๋ฉ์ธ ์๋ฒ์ ๋ฐฐํฌํ ๋ ๋ฐ๋์ ์คํํด์ผ ํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์ : + +#### **ํ์ ์ถ๊ฐ ์ปฌ๋ผ๋ค** (materials ํ ์ด๋ธ) +```sql +-- ํ์ดํ ์ฌ์ด์ฆ ์ ๋ณด +ALTER TABLE materials ADD COLUMN IF NOT EXISTS main_nom VARCHAR(50); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS red_nom VARCHAR(50); + +-- ์ ์ฒด ์ฌ์ง๋ช +ALTER TABLE materials ADD COLUMN IF NOT EXISTS full_material_grade TEXT; + +-- ์ ๋ก๋ ํ ๋ฒํธ +ALTER TABLE materials ADD COLUMN IF NOT EXISTS row_number INTEGER; +``` + +#### **์ฑ๋ฅ ์ต์ ํ ์ธ๋ฑ์ค** +```sql +CREATE INDEX IF NOT EXISTS idx_materials_main_nom ON materials(main_nom); +CREATE INDEX IF NOT EXISTS idx_materials_red_nom ON materials(red_nom); +CREATE INDEX IF NOT EXISTS idx_materials_full_material_grade ON materials(full_material_grade); +``` + +#### **์๋ ๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ** +```bash +# ๋ฉ์ธ ์๋ฒ์์ ์คํ +psql -U tkmp_user -d tk_mp_bom -f backend/scripts/PRODUCTION_MIGRATION.sql +``` + +### โ ๏ธ **์ค์ ์ฌํญ** +- ์ด ์ปฌ๋ผ๋ค์ด ์์ผ๋ฉด ํ์ผ ์ ๋ก๋ ์ 500 ์๋ฌ ๋ฐ์ +- ์์ ์ด๊ธฐํ ์: `database/init/99_complete_schema.sql` ์ฌ์ฉ +- ๊ธฐ์กด ์๋ฒ ์ ๋ฐ์ดํธ ์: `backend/scripts/PRODUCTION_MIGRATION.sql` ์ฌ์ฉ + +### ๐ง **๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์คํธ** +1. [ ] ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฑ์ +2. [ ] ๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ ์คํ +3. [ ] ์ปฌ๋ผ ์กด์ฌ ํ์ธ +4. [ ] ํ์ผ ์ ๋ก๋ ํ ์คํธ +5. [ ] ์์ฌ ๋ถ๋ฅ ๊ธฐ๋ฅ ํ ์คํธ + +--- + +**๋ง์ง๋ง ์ ๋ฐ์ดํธ**: 2025๋ 9์ 28์ผ (๋ฉ์ธ ์๋ฒ ๋ฐฐํฌ ๊ฐ์ด๋ ์ถ๊ฐ) diff --git a/backend/app/auth/jwt_service.py b/backend/app/auth/jwt_service.py index 3702a4c..caaed02 100644 --- a/backend/app/auth/jwt_service.py +++ b/backend/app/auth/jwt_service.py @@ -268,5 +268,6 @@ jwt_service = JWTService() + diff --git a/backend/app/auth/middleware.py b/backend/app/auth/middleware.py index 6e414ed..8304b94 100644 --- a/backend/app/auth/middleware.py +++ b/backend/app/auth/middleware.py @@ -322,5 +322,6 @@ async def get_current_user_optional( + diff --git a/backend/app/models.py b/backend/app/models.py index 25bbc34..416330c 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -284,6 +284,7 @@ class UserRequirement(Base): id = Column(Integer, primary_key=True, index=True) file_id = Column(Integer, ForeignKey("files.id"), nullable=False) + material_id = Column(Integer, ForeignKey("materials.id"), nullable=True) # ์์ฌ ID (๊ฐ๋ณ ์์ฌ๋ณ ์๊ตฌ์ฌํญ ์ฐ๊ฒฐ) # ์๊ตฌ์ฌํญ ํ์ requirement_type = Column(String(50), nullable=False) # 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' ๋ฑ diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index 6729221..28b0a75 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, R from sqlalchemy.orm import Session from sqlalchemy import text from typing import List, Optional, Dict +from pydantic import BaseModel import os import shutil from datetime import datetime @@ -2629,6 +2630,7 @@ async def get_user_requirements( { "id": req.id, "file_id": req.file_id, + "material_id": req.material_id, "original_filename": req.original_filename, "job_no": req.job_no, "revision": req.revision, @@ -2651,17 +2653,20 @@ async def get_user_requirements( except Exception as e: raise HTTPException(status_code=500, detail=f"์ฌ์ฉ์ ์๊ตฌ์ฌํญ ์กฐํ ์คํจ: {str(e)}") +class UserRequirementCreate(BaseModel): + file_id: int + material_id: Optional[int] = None + requirement_type: str + requirement_title: str + requirement_description: Optional[str] = None + requirement_spec: Optional[str] = None + priority: str = "NORMAL" + assigned_to: Optional[str] = None + due_date: Optional[str] = None + @router.post("/user-requirements") async def create_user_requirement( - file_id: int, - requirement_type: str, - requirement_title: str, - material_id: Optional[int] = None, - requirement_description: Optional[str] = None, - requirement_spec: Optional[str] = None, - priority: str = "NORMAL", - assigned_to: Optional[str] = None, - due_date: Optional[str] = None, + requirement: UserRequirementCreate, db: Session = Depends(get_db) ): """ @@ -2681,15 +2686,15 @@ async def create_user_requirement( """) result = db.execute(insert_query, { - "file_id": file_id, - "material_id": material_id, - "requirement_type": requirement_type, - "requirement_title": requirement_title, - "requirement_description": requirement_description, - "requirement_spec": requirement_spec, - "priority": priority, - "assigned_to": assigned_to, - "due_date": due_date + "file_id": requirement.file_id, + "material_id": requirement.material_id, + "requirement_type": requirement.requirement_type, + "requirement_title": requirement.requirement_title, + "requirement_description": requirement.requirement_description, + "requirement_spec": requirement.requirement_spec, + "priority": requirement.priority, + "assigned_to": requirement.assigned_to, + "due_date": requirement.due_date }) requirement_id = result.fetchone()[0] diff --git a/backend/app/services/bolt_classifier.py b/backend/app/services/bolt_classifier.py index 6fed363..9ffafee 100644 --- a/backend/app/services/bolt_classifier.py +++ b/backend/app/services/bolt_classifier.py @@ -1053,6 +1053,68 @@ def calculate_bolt_confidence(confidence_scores: Dict) -> float: # ========== ํน์ ๊ธฐ๋ฅ๋ค ========== +def extract_bolt_additional_requirements(description: str) -> str: + """๋ณผํธ ์ค๋ช ์์ ์ถ๊ฐ์๊ตฌ์ฌํญ ์ถ์ถ (ํ๋ฉด์ฒ๋ฆฌ, ํน์ ์๊ตฌ์ฌํญ ๋ฑ)""" + + desc_upper = description.upper() + additional_reqs = [] + + # ํ๋ฉด์ฒ๋ฆฌ ํจํด๋ค + surface_treatments = { + 'ELEC.GALV': '์ ๊ธฐ์์ฐ๋๊ธ', + 'ELEC GALV': '์ ๊ธฐ์์ฐ๋๊ธ', + 'GALVANIZED': '์์ฐ๋๊ธ', + 'GALV': '์์ฐ๋๊ธ', + 'HOT DIP GALV': '์ฉ์ต์์ฐ๋๊ธ', + 'HDG': '์ฉ์ต์์ฐ๋๊ธ', + 'ZINC PLATED': '์์ฐ๋๊ธ', + 'ZINC': '์์ฐ๋๊ธ', + 'STAINLESS': '์คํ ์ธ๋ฆฌ์ค', + 'SS': '์คํ ์ธ๋ฆฌ์ค', + 'PASSIVATED': '๋ถ๋ํํ', + 'ANODIZED': '์๋ ธ๋ค์ด์ง', + 'BLACK OXIDE': 'ํ์์ฐํ', + 'PHOSPHATE': '์ธ์ฐ์ฒ๋ฆฌ', + 'DACROMET': '๋คํฌ๋ก๋ฉํธ', + 'GEOMET': '์ง์ค๋ฉํธ' + } + + # ํน์ ์๊ตฌ์ฌํญ ํจํด๋ค + special_requirements = { + 'HEAVY HEX': '์ค์ก๊ฐ', + 'FULL THREAD': '์ ๋์ฌ', + 'PARTIAL THREAD': '๋ถ๋ถ๋์ฌ', + 'FINE THREAD': '์ธ๋์ฌ', + 'COARSE THREAD': '์กฐ๋์ฌ', + 'LEFT HAND': '์ข๋์ฌ', + 'RIGHT HAND': '์ฐ๋์ฌ', + 'SOCKET HEAD': '์์ผํค๋', + 'BUTTON HEAD': '๋ฒํผํค๋', + 'FLAT HEAD': 'ํ๋จธ๋ฆฌ', + 'PAN HEAD': 'ํฌํค๋', + 'TRUSS HEAD': 'ํธ๋ฌ์คํค๋', + 'WASHER FACE': '์์ ๋ฉด', + 'SERRATED': 'ํฑ๋ํ', + 'LOCK': '์ ๊ธ', + 'SPRING': '์คํ๋ง', + 'WAVE': '์จ์ด๋ธ' + } + + # ํ๋ฉด์ฒ๋ฆฌ ํ์ธ + for pattern, korean in surface_treatments.items(): + if pattern in desc_upper: + additional_reqs.append(korean) + + # ํน์ ์๊ตฌ์ฌํญ ํ์ธ + for pattern, korean in special_requirements.items(): + if pattern in desc_upper: + additional_reqs.append(korean) + + # ์ค๋ณต ์ ๊ฑฐ ๋ฐ ์ ๋ ฌ + additional_reqs = list(set(additional_reqs)) + + return ', '.join(additional_reqs) if additional_reqs else '' + def get_bolt_purchase_info(bolt_result: Dict) -> Dict: """๋ณผํธ ๊ตฌ๋งค ์ ๋ณด ์์ฑ""" diff --git a/backend/app/services/fitting_classifier.py b/backend/app/services/fitting_classifier.py index 3647e11..7c192df 100644 --- a/backend/app/services/fitting_classifier.py +++ b/backend/app/services/fitting_classifier.py @@ -230,8 +230,8 @@ def classify_fitting(dat_file: str, description: str, main_nom: str, # 4. ์๋ ฅ ๋ฑ๊ธ ๋ถ๋ฅ pressure_result = classify_pressure_rating(dat_file, description) - # 4.5. ์ค์ผ์ค ๋ถ๋ฅ (๋ํ ๋ฑ์ ์ค์) - schedule_result = classify_fitting_schedule(description) + # 4.5. ์ค์ผ์ค ๋ถ๋ฅ (๋ํ ๋ฑ์ ์ค์) - ๋ถ๋ฆฌ ์ค์ผ์ค ์ง์ + schedule_result = classify_fitting_schedule_with_reducing(description, main_nom, red_nom) # 5. ์ ์ ๋ฐฉ๋ฒ ์ถ์ manufacturing_result = determine_fitting_manufacturing( @@ -304,6 +304,123 @@ def classify_fitting(dat_file: str, description: str, main_nom: str, }) } +def analyze_size_pattern_for_fitting_type(description: str, main_nom: str, red_nom: str = None) -> Dict: + """ + ์ค์ BOM ํจํด ๊ธฐ๋ฐ TEE vs REDUCER ๊ตฌ๋ถ + + ์ค์ ํจํด: + - TEE RED, SMLS, SCH 40 x SCH 80 โ TEE (ํค์๋ ์ฐ์ ) + - RED CONC, SMLS, SCH 80 x SCH 80 โ REDUCER (ํค์๋ ์ฐ์ ) + - ๋ชจ๋ A x B ํํ (๋ฉ์ธ x ๊ฐ์) + """ + + desc_upper = description.upper() + + # 1. ํค์๋ ๊ธฐ๋ฐ ๋ถ๋ฅ (์ต์ฐ์ ) - ์ค์ BOM ํจํด + if "TEE RED" in desc_upper or "TEE REDUCING" in desc_upper: + return { + "type": "TEE", + "subtype": "REDUCING", + "confidence": 0.95, + "evidence": ["KEYWORD_TEE_RED"], + "subtype_confidence": 0.95, + "requires_two_sizes": False + } + + if "RED CONC" in desc_upper or "REDUCER CONC" in desc_upper: + return { + "type": "REDUCER", + "subtype": "CONCENTRIC", + "confidence": 0.95, + "evidence": ["KEYWORD_RED_CONC"], + "subtype_confidence": 0.95, + "requires_two_sizes": True + } + + if "RED ECC" in desc_upper or "REDUCER ECC" in desc_upper: + return { + "type": "REDUCER", + "subtype": "ECCENTRIC", + "confidence": 0.95, + "evidence": ["KEYWORD_RED_ECC"], + "subtype_confidence": 0.95, + "requires_two_sizes": True + } + + # 2. ์ฌ์ด์ฆ ํจํด ๋ถ์ (๋ณด์กฐ) - ๊ธฐ์กด ๋ก์ง ์ ์ง + # x ๋๋ ร ๊ธฐํธ๋ก ์ฐ๊ฒฐ๋ ์ฌ์ด์ฆ๋ค ์ฐพ๊ธฐ + connected_sizes = re.findall(r'(\d+(?:\s+\d+/\d+)?(?:\.\d+)?)"?\s*[xXร]\s*(\d+(?:\s+\d+/\d+)?(?:\.\d+)?)"?(?:\s*[xXร]\s*(\d+(?:\s+\d+/\d+)?(?:\.\d+)?)"?)?', description) + + if connected_sizes: + # ์ฐ๊ฒฐ๋ ์ฌ์ด์ฆ๋ค์ ๋ฆฌ์คํธ๋ก ๋ณํ + sizes = [] + for size_group in connected_sizes: + for size in size_group: + if size.strip(): + sizes.append(size.strip()) + + # ์ค๋ณต ์ ๊ฑฐํ๋ ์์ ์ ์ง + unique_sizes = [] + for size in sizes: + if size not in unique_sizes: + unique_sizes.append(size) + + sizes = unique_sizes + + if len(sizes) == 3: + # A x B x B ํจํด โ TEE REDUCING + if sizes[1] == sizes[2]: + return { + "type": "TEE", + "subtype": "REDUCING", + "confidence": 0.85, + "evidence": [f"SIZE_PATTERN_TEE_REDUCING: {' x '.join(sizes)}"], + "subtype_confidence": 0.85, + "requires_two_sizes": False + } + # A x B x C ํจํด โ TEE REDUCING (๋ชจ๋ ๋ค๋ฅธ ์ฌ์ด์ฆ) + else: + return { + "type": "TEE", + "subtype": "REDUCING", + "confidence": 0.80, + "evidence": [f"SIZE_PATTERN_TEE_REDUCING_UNEQUAL: {' x '.join(sizes)}"], + "subtype_confidence": 0.80, + "requires_two_sizes": False + } + elif len(sizes) == 2: + # A x B ํจํด โ ํค์๋๊ฐ ์์ผ๋ฉด REDUCER๋ก ๊ธฐ๋ณธ ๋ถ๋ฅ + if "CONC" in desc_upper or "CONCENTRIC" in desc_upper: + return { + "type": "REDUCER", + "subtype": "CONCENTRIC", + "confidence": 0.80, + "evidence": [f"SIZE_PATTERN_REDUCER_CONC: {' x '.join(sizes)}"], + "subtype_confidence": 0.80, + "requires_two_sizes": True + } + elif "ECC" in desc_upper or "ECCENTRIC" in desc_upper: + return { + "type": "REDUCER", + "subtype": "ECCENTRIC", + "confidence": 0.80, + "evidence": [f"SIZE_PATTERN_REDUCER_ECC: {' x '.join(sizes)}"], + "subtype_confidence": 0.80, + "requires_two_sizes": True + } + else: + # ํค์๋ ์๋ A x B ํจํด์ ๋ฎ์ ์ ๋ขฐ๋๋ก REDUCER + return { + "type": "REDUCER", + "subtype": "CONCENTRIC", # ๊ธฐ๋ณธ๊ฐ + "confidence": 0.60, + "evidence": [f"SIZE_PATTERN_REDUCER_DEFAULT: {' x '.join(sizes)}"], + "subtype_confidence": 0.60, + "requires_two_sizes": True + } + + return {"confidence": 0.0} + def classify_fitting_type(dat_file: str, description: str, main_nom: str, red_nom: str = None) -> Dict: """ํผํ ํ์ ๋ถ๋ฅ""" @@ -311,6 +428,11 @@ def classify_fitting_type(dat_file: str, description: str, dat_upper = dat_file.upper() desc_upper = description.upper() + # 0. ์ฌ์ด์ฆ ํจํด ๋ถ์์ผ๋ก TEE vs REDUCER ๊ตฌ๋ถ (์ต์ฐ์ ) + size_pattern_result = analyze_size_pattern_for_fitting_type(desc_upper, main_nom, red_nom) + if size_pattern_result.get("confidence", 0) > 0.85: + return size_pattern_result + # 1. DAT_FILE ํจํด์ผ๋ก 1์ฐจ ๋ถ๋ฅ (๊ฐ์ฅ ์ ๋ขฐ๋ ๋์) for fitting_type, type_data in FITTING_TYPES.items(): for pattern in type_data["dat_file_patterns"]: @@ -679,3 +801,53 @@ def classify_fitting_schedule(description: str) -> Dict: "confidence": 0.0, "matched_pattern": "" } + +def classify_fitting_schedule_with_reducing(description: str, main_nom: str, red_nom: str = None) -> Dict: + """ + ์ค์ BOM ํจํด ๊ธฐ๋ฐ ๋ถ๋ฆฌ ์ค์ผ์ค ์ฒ๋ฆฌ + + ์ค์ ํจํด: + - "TEE RED, SMLS, SCH 40 x SCH 80" โ main: SCH 40, red: SCH 80 + - "RED CONC, SMLS, SCH 40S x SCH 40S" โ main: SCH 40S, red: SCH 40S + - "RED CONC, SMLS, SCH 80 x SCH 80" โ main: SCH 80, red: SCH 80 + """ + + desc_upper = description.upper() + + # 1. ๋ถ๋ฆฌ ์ค์ผ์ค ํจํด ํ์ธ (SCH XX x SCH YY) - ๊ฐ์ ๋ ํจํด + separated_schedule_patterns = [ + r'SCH\s*(\d+S?)\s*[xXร]\s*SCH\s*(\d+S?)', # SCH 40 x SCH 80 + r'SCH\s*(\d+S?)\s*X\s*(\d+S?)', # SCH 40S X 40S (SCH ์๋ต) + ] + + for pattern in separated_schedule_patterns: + separated_match = re.search(pattern, desc_upper) + if separated_match: + main_schedule = f"SCH {separated_match.group(1)}" + red_schedule = f"SCH {separated_match.group(2)}" + + return { + "schedule": main_schedule, # ๊ธฐ๋ณธ ์ค์ผ์ค (ํธํ์ฑ) + "main_schedule": main_schedule, + "red_schedule": red_schedule, + "has_different_schedules": main_schedule != red_schedule, + "confidence": 0.95, + "matched_pattern": separated_match.group(0), + "schedule_type": "SEPARATED" + } + + # 2. ๋จ์ผ ์ค์ผ์ค ํจํด (๊ธฐ์กด ๋ก์ง ์ฌ์ฉ) + basic_result = classify_fitting_schedule(description) + + # ๋จ์ผ ์ค์ผ์ค์ main/red ๋ชจ๋์ ์ ์ฉ + schedule = basic_result.get("schedule", "UNKNOWN") + + return { + "schedule": schedule, # ๊ธฐ๋ณธ ์ค์ผ์ค (ํธํ์ฑ) + "main_schedule": schedule, + "red_schedule": schedule if red_nom else None, + "has_different_schedules": False, + "confidence": basic_result.get("confidence", 0.0), + "matched_pattern": basic_result.get("matched_pattern", ""), + "schedule_type": "UNIFIED" + } diff --git a/backend/app/services/integrated_classifier.py b/backend/app/services/integrated_classifier.py index cfa9ad9..5ebda03 100644 --- a/backend/app/services/integrated_classifier.py +++ b/backend/app/services/integrated_classifier.py @@ -5,6 +5,7 @@ import re from typing import Dict, List, Optional, Tuple +from .fitting_classifier import classify_fitting # Level 1: ๋ช ํํ ํ์ ํค์๋ (์ต์ฐ์ ) LEVEL1_TYPE_KEYWORDS = { @@ -142,6 +143,18 @@ def classify_material_integrated(description: str, main_nom: str = "", # 3๋จ๊ณ: ๋จ์ผ ํ์ ํ์ ๋๋ Level 3/4๋ก ํ๋จ if len(detected_types) == 1: material_type = detected_types[0][0] + + # FITTING์ผ๋ก ๋ถ๋ฅ๋ ๊ฒฝ์ฐ ์์ธ ๋ถ๋ฅ๊ธฐ ํธ์ถ + if material_type == "FITTING": + try: + detailed_result = classify_fitting("", description, main_nom, red_nom, length) + # ์์ธ ๋ถ๋ฅ ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด ์ฌ์ฉ, ์์ผ๋ฉด ๊ธฐ๋ณธ FITTING ๋ฐํ + if detailed_result and detailed_result.get("category"): + return detailed_result + except Exception as e: + # ์์ธ ๋ถ๋ฅ ์คํจ ์ ๊ธฐ๋ณธ FITTING์ผ๋ก ์ฒ๋ฆฌ + pass + return { "category": material_type, "confidence": 0.9, @@ -171,6 +184,15 @@ def classify_material_integrated(description: str, main_nom: str = "", if other_type_found: continue # ๋ณผํธ๋ก ๋ถ๋ฅํ์ง ์์ + # FITTING์ผ๋ก ๋ถ๋ฅ๋ ๊ฒฝ์ฐ ์์ธ ๋ถ๋ฅ๊ธฐ ํธ์ถ + if material_type == "FITTING": + try: + detailed_result = classify_fitting("", description, main_nom, red_nom, length) + if detailed_result and detailed_result.get("category"): + return detailed_result + except Exception as e: + pass + return { "category": material_type, "confidence": 0.35, # ์ฌ์ง๋ง์ผ๋ก ๋ถ๋ฅ ์ ๋ฎ์ ์ ๋ขฐ๋ @@ -182,8 +204,19 @@ def classify_material_integrated(description: str, main_nom: str = "", for material, priority_types in GENERIC_MATERIALS.items(): if material in desc_upper: # ์ฐ์ ์์์ ๋ฐ๋ผ ํ์ ๊ฒฐ์ + material_type = priority_types[0] # ์ฒซ ๋ฒ์งธ ์ฐ์ ์์ + + # FITTING์ผ๋ก ๋ถ๋ฅ๋ ๊ฒฝ์ฐ ์์ธ ๋ถ๋ฅ๊ธฐ ํธ์ถ + if material_type == "FITTING": + try: + detailed_result = classify_fitting("", description, main_nom, red_nom, length) + if detailed_result and detailed_result.get("category"): + return detailed_result + except Exception as e: + pass + return { - "category": priority_types[0], # ์ฒซ ๋ฒ์งธ ์ฐ์ ์์ + "category": material_type, "confidence": 0.3, "evidence": [f"GENERIC_MATERIAL: {material}"], "classification_level": "LEVEL4_GENERIC" diff --git a/backend/app/services/material_grade_extractor.py b/backend/app/services/material_grade_extractor.py index daddcf8..f3c3f01 100644 --- a/backend/app/services/material_grade_extractor.py +++ b/backend/app/services/material_grade_extractor.py @@ -23,6 +23,13 @@ def extract_full_material_grade(description: str) -> str: # 1. ASTM ๊ท๊ฒฉ ํจํด๋ค (๊ฐ์ฅ ๊ตฌ์ฒด์ ์ธ ๊ฒ๋ถํฐ) astm_patterns = [ + # A320 L7, A325, A490 ๋ฑ ๋จ๋ ๊ท๊ฒฉ (ASTM ์์ด) + r'\bA320\s+L[0-9]+\b', # A320 L7 + r'\bA325\b', # A325 + r'\bA490\b', # A490 + # 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 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 ๋ฑ @@ -32,14 +39,14 @@ def extract_full_material_grade(description: str) -> str: # ASTM A351 CF8M, ASTM A216 WCB ๋ฑ r'ASTM\s+A\d{3,4}[A-Z]*\s+[A-Z]{2,4}[0-9]*[A-Z]*', # ASTM A106 GR B, ASTM A105 ๋ฑ - GR ํฌํจ - r'ASTM\s+A\d{3,4}[A-Z]*\s+GR\s+[A-Z0-9]+', - r'ASTM\s+A\d{3,4}[A-Z]*\s+GRADE\s+[A-Z0-9]+', + r'ASTM\s+A\d{3,4}[A-Z]*\s+GR\s+[A-Z0-9/]+', + r'ASTM\s+A\d{3,4}[A-Z]*\s+GRADE\s+[A-Z0-9/]+', # ASTM A106 B (GR ์์ด ๋ฐ๋ก ๋ฑ๊ธ) - ๋จ์ผ ๋ฌธ์ ๋ฑ๊ธ r'ASTM\s+A\d{3,4}[A-Z]*\s+[A-Z](?=\s|$)', # ASTM A105, ASTM A234 ๋ฑ (๋ฑ๊ธ ์๋ ๊ฒฝ์ฐ) r'ASTM\s+A\d{3,4}[A-Z]*(?!\s+[A-Z0-9])', # 2์๋ฆฌ ASTM ๊ท๊ฒฉ๋ ์ง์ (A10, A36 ๋ฑ) - r'ASTM\s+A\d{2}(?:\s+GR\s+[A-Z0-9]+)?', + r'ASTM\s+A\d{2}(?:\s+GR\s+[A-Z0-9/]+)?', ] for pattern in astm_patterns: diff --git a/backend/app/services/revision_comparator.py b/backend/app/services/revision_comparator.py index f63849d..e18a9cd 100644 --- a/backend/app/services/revision_comparator.py +++ b/backend/app/services/revision_comparator.py @@ -291,4 +291,5 @@ def get_revision_comparison(db: Session, job_no: str, current_revision: str, + diff --git a/backend/scripts/18_create_auth_tables.sql b/backend/scripts/18_create_auth_tables.sql index d0ec5f3..b446a5e 100644 --- a/backend/scripts/18_create_auth_tables.sql +++ b/backend/scripts/18_create_auth_tables.sql @@ -237,5 +237,6 @@ END $$; + diff --git a/backend/scripts/PRODUCTION_MIGRATION.sql b/backend/scripts/PRODUCTION_MIGRATION.sql new file mode 100644 index 0000000..96ec9fe --- /dev/null +++ b/backend/scripts/PRODUCTION_MIGRATION.sql @@ -0,0 +1,160 @@ +-- ================================ +-- TK-MP-Project ๋ฉ์ธ ์๋ฒ ๋ฐฐํฌ์ฉ ๋ง์ด๊ทธ๋ ์ด์ +-- ์์ฑ์ผ: 2025.09.28 +-- ๋ชฉ์ : ๊ฐ๋ฐ ์ค ์ถ๊ฐ๋ ํ์ ์ปฌ๋ผ๋ค์ ๋ฉ์ธ ์๋ฒ์ ์ ์ฉ +-- ================================ + +-- 1. materials ํ ์ด๋ธ ํ์ ์ปฌ๋ผ ์ถ๊ฐ +-- ================================ + +-- ํ์ดํ ์ฌ์ด์ฆ ์ ๋ณด +ALTER TABLE materials ADD COLUMN IF NOT EXISTS main_nom VARCHAR(50); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS red_nom VARCHAR(50); + +-- ์ ์ฒด ์ฌ์ง๋ช +ALTER TABLE materials ADD COLUMN IF NOT EXISTS full_material_grade TEXT; + +-- ์ ๋ก๋ ์ ํ ๋ฒํธ ์ถ์ +ALTER TABLE materials ADD COLUMN IF NOT EXISTS row_number INTEGER; + +-- ํด์๊ฐ (๊ตฌ๋งค ์ถ์ ์ฉ) +ALTER TABLE materials ADD COLUMN IF NOT EXISTS material_hash VARCHAR(64); + +-- ๊ฒ์ฆ ์ ๋ณด +ALTER TABLE materials ADD COLUMN IF NOT EXISTS verified_by VARCHAR(100); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS verified_at TIMESTAMP; + +-- ๋ถ๋ฅ ์์ธ ์ ๋ณด (์ด๋ฏธ ์์ ์ ์์ง๋ง ํ์ธ) +ALTER TABLE materials ADD COLUMN IF NOT EXISTS classified_subcategory VARCHAR(100); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS schedule VARCHAR(20); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS drawing_name VARCHAR(100); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS area_code VARCHAR(20); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS line_no VARCHAR(50); + +-- 2. files ํ ์ด๋ธ ํ์ ์ปฌ๋ผ ์ถ๊ฐ +-- ================================ + +-- ํ๋ก์ ํธ ์ฐ๊ฒฐ ์ ๋ณด +ALTER TABLE files ADD COLUMN IF NOT EXISTS job_no VARCHAR(50); +ALTER TABLE files ADD COLUMN IF NOT EXISTS bom_name VARCHAR(255); +ALTER TABLE files ADD COLUMN IF NOT EXISTS description TEXT; +ALTER TABLE files ADD COLUMN IF NOT EXISTS parsed_count INTEGER DEFAULT 0; + +-- 3. material_purchase_tracking ํ ์ด๋ธ ์ปฌ๋ผ ์ถ๊ฐ +-- ================================ + +-- ๊ตฌ๋งค ์ํ ๋ฐ ์ค๋ช +ALTER TABLE material_purchase_tracking +ADD COLUMN IF NOT EXISTS purchase_status VARCHAR(20) DEFAULT 'pending', +ADD COLUMN IF NOT EXISTS description TEXT; + +-- 4. user_requirements ํ ์ด๋ธ ์ปฌ๋ผ ์ถ๊ฐ +-- ================================ + +-- ์์ฌ๋ณ ์๊ตฌ์ฌํญ ์ฐ๊ฒฐ +ALTER TABLE user_requirements ADD COLUMN IF NOT EXISTS material_id INTEGER; + +-- 5. ์ฑ๋ฅ ์ต์ ํ ์ธ๋ฑ์ค ์ถ๊ฐ +-- ================================ + +-- materials ํ ์ด๋ธ ์ธ๋ฑ์ค +CREATE INDEX IF NOT EXISTS idx_materials_main_nom ON materials(main_nom); +CREATE INDEX IF NOT EXISTS idx_materials_red_nom ON materials(red_nom); +CREATE INDEX IF NOT EXISTS idx_materials_main_red_nom ON materials(main_nom, red_nom); +CREATE INDEX IF NOT EXISTS idx_materials_full_material_grade ON materials(full_material_grade); +CREATE INDEX IF NOT EXISTS idx_materials_material_hash ON materials(material_hash); +CREATE INDEX IF NOT EXISTS idx_materials_verified_by ON materials(verified_by); +CREATE INDEX IF NOT EXISTS idx_materials_classified_subcategory ON materials(classified_subcategory); +CREATE INDEX IF NOT EXISTS idx_materials_schedule ON materials(schedule); + +-- files ํ ์ด๋ธ ์ธ๋ฑ์ค +CREATE INDEX IF NOT EXISTS idx_files_job_no ON files(job_no); + +-- user_requirements ํ ์ด๋ธ ์ธ๋ฑ์ค +CREATE INDEX IF NOT EXISTS idx_user_requirements_material_id ON user_requirements(material_id); + +-- fitting_details ํ ์ด๋ธ ๋ถ๋ฆฌ ์ค์ผ์ค ์ปฌ๋ผ ์ถ๊ฐ +ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS main_schedule VARCHAR(20); +ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS red_schedule VARCHAR(20); +ALTER TABLE fitting_details ADD COLUMN IF NOT EXISTS has_different_schedules BOOLEAN DEFAULT FALSE; + +-- fitting_details ๋ถ๋ฆฌ ์ค์ผ์ค ์ธ๋ฑ์ค +CREATE INDEX IF NOT EXISTS idx_fitting_details_main_schedule ON fitting_details(main_schedule); +CREATE INDEX IF NOT EXISTS idx_fitting_details_red_schedule ON fitting_details(red_schedule); + +-- 3. ์ปฌ๋ผ ์ค๋ช ์ถ๊ฐ +-- ================================ + +COMMENT ON COLUMN materials.main_nom IS 'MAIN_NOM ํ๋ - ์ฃผ ์ฌ์ด์ฆ (์: 4", 150A)'; +COMMENT ON COLUMN materials.red_nom IS 'RED_NOM ํ๋ - ์ถ์ ์ฌ์ด์ฆ (Reducing ํผํ /ํ๋์ง์ฉ)'; +COMMENT ON COLUMN materials.full_material_grade IS '์ ์ฒด ์ฌ์ง๋ช (์: ASTM A312 TP304, ASTM A106 GR B ๋ฑ)'; +COMMENT ON COLUMN materials.row_number IS '์ ๋ก๋ ํ์ผ์์์ ํ ๋ฒํธ (๋๋ฒ๊น ์ฉ)'; + +-- 6. support_details ํ ์ด๋ธ ์์ฑ (SUPPORT ์นดํ ๊ณ ๋ฆฌ์ฉ) +-- ================================ + +CREATE TABLE IF NOT EXISTS support_details ( + id SERIAL PRIMARY KEY, + material_id INTEGER NOT NULL REFERENCES materials(id) ON DELETE CASCADE, + file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, + + -- ์ํฌํธ ํ์ ์ ๋ณด + support_type VARCHAR(50), -- URETHANE_BLOCK, CLAMP, HANGER, SPRING_HANGER ๋ฑ + support_subtype VARCHAR(100), -- ์์ธ ํ์ + + -- ํ์ค ์ ๋ณด + load_rating VARCHAR(20), -- LIGHT, MEDIUM, HEAVY, CUSTOM + load_capacity VARCHAR(20), -- 40T, 50TON ๋ฑ + + -- ์ฌ์ง ์ ๋ณด + material_standard VARCHAR(50), -- ์ฌ์ง ํ์ค + material_grade VARCHAR(100), -- ์ฌ์ง ๋ฑ๊ธ + + -- ์ฌ์ด์ฆ ์ ๋ณด + pipe_size VARCHAR(20), -- ์ง์งํ๋ ํ์ดํ ํฌ๊ธฐ + length_mm DECIMAL(10,2), -- ๊ธธ์ด (mm) + width_mm DECIMAL(10,2), -- ํญ (mm) + height_mm DECIMAL(10,2), -- ๋์ด (mm) + + -- ๋ถ๋ฅ ์ ๋ขฐ๋ + classification_confidence DECIMAL(3,2), -- 0.00-1.00 + + -- ๋ฉํ๋ฐ์ดํฐ + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- support_details ์ธ๋ฑ์ค +CREATE INDEX IF NOT EXISTS idx_support_details_material_id ON support_details(material_id); +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); + +-- 7. ๊ธฐ์กด ๋ฐ์ดํฐ ์ ๋ฆฌ (์ ํ์ฌํญ) +-- ================================ + +-- ๊ธฐ์กด ๋ฐ์ดํฐ์ ๊ธฐ๋ณธ๊ฐ ์ค์ (ํ์์ ์ฃผ์ ํด์ ) +-- UPDATE materials SET main_nom = '', red_nom = '', full_material_grade = '' +-- WHERE main_nom IS NULL OR red_nom IS NULL OR full_material_grade IS NULL; + +-- ================================ +-- ๋ง์ด๊ทธ๋ ์ด์ ์๋ฃ ํ์ธ +-- ================================ + +DO $$ +BEGIN + -- ์ปฌ๋ผ ์กด์ฌ ํ์ธ + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'materials' + AND column_name IN ('main_nom', 'red_nom', 'full_material_grade', 'row_number') + GROUP BY table_name + HAVING COUNT(*) = 4 + ) THEN + RAISE NOTICE 'โ TK-MP-Project ๋ฉ์ธ ์๋ฒ ๋ง์ด๊ทธ๋ ์ด์ ์๋ฃ!'; + RAISE NOTICE '๐ ์ถ๊ฐ๋ ์ปฌ๋ผ: main_nom, red_nom, full_material_grade, row_number'; + RAISE NOTICE '๐ ์ถ๊ฐ๋ ์ธ๋ฑ์ค: 4๊ฐ (์ฑ๋ฅ ์ต์ ํ)'; + RAISE NOTICE '๐ ํ์ผ ์ ๋ก๋ ๊ธฐ๋ฅ ์ ์ ์๋ ๊ฐ๋ฅ'; + ELSE + RAISE NOTICE 'โ ๋ง์ด๊ทธ๋ ์ด์ ์คํจ - ์ผ๋ถ ์ปฌ๋ผ์ด ์์ฑ๋์ง ์์์ต๋๋ค.'; + END IF; +END $$; diff --git a/database/init/20_purchase_confirmations.sql b/database/init/20_purchase_confirmations.sql index df22cb8..08e3953 100644 --- a/database/init/20_purchase_confirmations.sql +++ b/database/init/20_purchase_confirmations.sql @@ -74,4 +74,5 @@ COMMENT ON COLUMN files.confirmed_by IS '๊ตฌ๋งค ์๋ ํ์ ์'; + diff --git a/database/init/99_complete_schema.sql b/database/init/99_complete_schema.sql index de4ab62..476101d 100644 --- a/database/init/99_complete_schema.sql +++ b/database/init/99_complete_schema.sql @@ -975,6 +975,7 @@ CREATE TABLE IF NOT EXISTS requirement_types ( CREATE TABLE IF NOT EXISTS user_requirements ( id SERIAL PRIMARY KEY, file_id INTEGER NOT NULL, + material_id INTEGER, -- ์์ฌ ID (๊ฐ๋ณ ์์ฌ๋ณ ์๊ตฌ์ฌํญ ์ฐ๊ฒฐ) -- ์๊ตฌ์ฌํญ ํ์ requirement_type VARCHAR(50) NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' ๋ฑ @@ -996,7 +997,8 @@ CREATE TABLE IF NOT EXISTS user_requirements ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE + FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE, + FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE ); -- ================================ @@ -1192,6 +1194,7 @@ CREATE INDEX IF NOT EXISTS idx_jobs_project_type ON jobs(project_type); -- ์ฌ์ฉ์ ์๊ตฌ์ฌํญ ํ ์ด๋ธ ์ธ๋ฑ์ค (05_create_pipe_details_and_requirements_postgres.sql) CREATE INDEX IF NOT EXISTS idx_user_requirements_file_id ON user_requirements(file_id); +CREATE INDEX IF NOT EXISTS idx_user_requirements_material_id ON user_requirements(material_id); CREATE INDEX IF NOT EXISTS idx_user_requirements_status ON user_requirements(status); CREATE INDEX IF NOT EXISTS idx_user_requirements_type ON user_requirements(requirement_type); diff --git a/docker-compose.override.yml b/docker-compose.override.yml index af35268..46f2d5e 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -13,6 +13,10 @@ services: - LOG_LEVEL=DEBUG frontend: + volumes: + # ๊ฐ๋ฐ ์ ์ฝ๋ ๋ณ๊ฒฝ ์ค์๊ฐ ๋ฐ์ + - ./frontend:/app + - /app/node_modules # node_modules๋ ์ปจํ ์ด๋ ๊ฒ์ ์ฌ์ฉ environment: - VITE_API_URL=http://localhost:18000 build: diff --git a/docker-compose.yml b/docker-compose.yml index 8cd88f5..4af1f7b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,7 +82,7 @@ services: container_name: tk-mp-frontend restart: unless-stopped ports: - - "${FRONTEND_PORT:-13000}:3000" + - "${FRONTEND_PORT:-13000}:5173" environment: - VITE_API_URL=${VITE_API_URL:-http://localhost:18000} depends_on: diff --git a/frontend/src/SimpleDashboard.jsx b/frontend/src/SimpleDashboard.jsx index 2f66805..e3d9a8d 100644 --- a/frontend/src/SimpleDashboard.jsx +++ b/frontend/src/SimpleDashboard.jsx @@ -237,5 +237,6 @@ export default SimpleDashboard; + diff --git a/frontend/src/components/NavigationBar.css b/frontend/src/components/NavigationBar.css index fc0e152..5377ec5 100644 --- a/frontend/src/components/NavigationBar.css +++ b/frontend/src/components/NavigationBar.css @@ -554,5 +554,6 @@ + diff --git a/frontend/src/components/NavigationBar.jsx b/frontend/src/components/NavigationBar.jsx index 497d7cd..18f51b1 100644 --- a/frontend/src/components/NavigationBar.jsx +++ b/frontend/src/components/NavigationBar.jsx @@ -287,5 +287,6 @@ export default NavigationBar; + diff --git a/frontend/src/components/NavigationMenu.css b/frontend/src/components/NavigationMenu.css index 158ad5e..add5ea6 100644 --- a/frontend/src/components/NavigationMenu.css +++ b/frontend/src/components/NavigationMenu.css @@ -267,5 +267,6 @@ + diff --git a/frontend/src/components/NavigationMenu.jsx b/frontend/src/components/NavigationMenu.jsx index 13bfd18..c4a424d 100644 --- a/frontend/src/components/NavigationMenu.jsx +++ b/frontend/src/components/NavigationMenu.jsx @@ -191,5 +191,6 @@ export default NavigationMenu; + diff --git a/frontend/src/components/RevisionUploadDialog.jsx b/frontend/src/components/RevisionUploadDialog.jsx index a6a6891..ce5129b 100644 --- a/frontend/src/components/RevisionUploadDialog.jsx +++ b/frontend/src/components/RevisionUploadDialog.jsx @@ -99,5 +99,6 @@ export default RevisionUploadDialog; + diff --git a/frontend/src/components/SimpleFileUpload.jsx b/frontend/src/components/SimpleFileUpload.jsx index 3c606f0..2d78c76 100644 --- a/frontend/src/components/SimpleFileUpload.jsx +++ b/frontend/src/components/SimpleFileUpload.jsx @@ -318,5 +318,6 @@ export default SimpleFileUpload; + diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index b1725ab..71bdeeb 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -281,5 +281,6 @@ export default DashboardPage; + diff --git a/frontend/src/pages/LogMonitoringPage.jsx b/frontend/src/pages/LogMonitoringPage.jsx index 191c2dd..94452a7 100644 --- a/frontend/src/pages/LogMonitoringPage.jsx +++ b/frontend/src/pages/LogMonitoringPage.jsx @@ -133,7 +133,7 @@ const LogMonitoringPage = ({ onNavigate, user }) => { const getErrorTypeIcon = (type) => { const icons = { - 'javascript_error': '๐', + 'javascript_error': 'โ', 'api_error': '๐', 'user_action_error': '๐ค', 'promise_rejection': 'โ ๏ธ', @@ -365,7 +365,7 @@ const LogMonitoringPage = ({ onNavigate, user }) => { border: '1px solid #e9ecef' }}>