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' }}>
-
๐Ÿ›
+
โŒ

์ตœ๊ทผ ์˜ค๋ฅ˜

@@ -458,7 +458,7 @@ const LogMonitoringPage = ({ onNavigate, user }) => { background: '#f8f9fa' }}>

- ๐Ÿ› ํ”„๋ก ํŠธ์—”๋“œ ์˜ค๋ฅ˜ + โŒ ํ”„๋ก ํŠธ์—”๋“œ ์˜ค๋ฅ˜

diff --git a/frontend/src/pages/LoginPage.css b/frontend/src/pages/LoginPage.css index ed04cc2..c8176cb 100644 --- a/frontend/src/pages/LoginPage.css +++ b/frontend/src/pages/LoginPage.css @@ -236,5 +236,6 @@ + diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index cbf19cf..a15a981 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -133,5 +133,6 @@ export default LoginPage; + diff --git a/frontend/src/pages/NewMaterialsPage.css b/frontend/src/pages/NewMaterialsPage.css index 2681c2b..c7450cd 100644 --- a/frontend/src/pages/NewMaterialsPage.css +++ b/frontend/src/pages/NewMaterialsPage.css @@ -266,7 +266,7 @@ .detailed-grid-header { display: grid; - grid-template-columns: 40px 80px 250px 80px 80px 350px 100px 150px 100px; + grid-template-columns: 40px 80px 250px 80px 80px 450px 120px 150px 100px; padding: 12px 24px; background: #f9fafb; border-bottom: 1px solid #e5e7eb; @@ -289,12 +289,12 @@ /* ํ”ผํŒ… ์ „์šฉ ํ—ค๋” - 10๊ฐœ ์ปฌ๋Ÿผ */ .detailed-grid-header.fitting-header { - grid-template-columns: 40px 80px 280px 80px 80px 80px 350px 100px 150px 100px; + grid-template-columns: 40px 80px 280px 80px 80px 140px 350px 100px 150px 100px; } /* ํ”ผํŒ… ์ „์šฉ ํ–‰ - 10๊ฐœ ์ปฌ๋Ÿผ */ .detailed-material-row.fitting-row { - grid-template-columns: 40px 80px 280px 80px 80px 80px 350px 100px 150px 100px; + grid-template-columns: 40px 80px 280px 80px 80px 140px 350px 100px 150px 100px; } /* ๋ฐธ๋ธŒ ์ „์šฉ ํ—ค๋” - 9๊ฐœ ์ปฌ๋Ÿผ (์Šค์ผ€์ค„ ์ œ๊ฑฐ, ํƒ€์ž… ๋„ˆ๋น„ ์ฆ๊ฐ€) */ @@ -345,7 +345,7 @@ .detailed-material-row { display: grid; - grid-template-columns: 40px 80px 250px 80px 80px 350px 100px 150px 100px; + grid-template-columns: 40px 80px 250px 80px 80px 450px 120px 150px 100px; padding: 12px 24px; border-bottom: 1px solid #f3f4f6; align-items: center; diff --git a/frontend/src/pages/NewMaterialsPage.jsx b/frontend/src/pages/NewMaterialsPage.jsx index bc00031..7034956 100644 --- a/frontend/src/pages/NewMaterialsPage.jsx +++ b/frontend/src/pages/NewMaterialsPage.jsx @@ -17,14 +17,15 @@ const NewMaterialsPage = ({ }) => { const [materials, setMaterials] = useState([]); const [loading, setLoading] = useState(true); - const [selectedCategory, setSelectedCategory] = useState('PIPE'); + const [selectedCategory, setSelectedCategory] = useState('ALL'); const [selectedMaterials, setSelectedMaterials] = useState(new Set()); const [viewMode, setViewMode] = useState('detailed'); // 'detailed' or 'simple' const [availableRevisions, setAvailableRevisions] = useState([]); const [currentRevision, setCurrentRevision] = useState(revision || 'Rev.0'); // ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ƒํƒœ ๊ด€๋ฆฌ - const [userRequirements, setUserRequirements] = useState({}); // materialId: requirement ํ˜•ํƒœ + const [userRequirements, setUserRequirements] = useState({}); + // materialId: requirement ํ˜•ํƒœ const [savingRequirements, setSavingRequirements] = useState(false); // ๊ฐ™์€ BOM์˜ ๋‹ค๋ฅธ ๋ฆฌ๋น„์ „๋“ค ์กฐํšŒ @@ -101,16 +102,28 @@ const NewMaterialsPage = ({ params: { file_id: parseInt(id) } }); - if (response.data?.success && response.data?.requirements) { + if (response.data && Array.isArray(response.data)) { const requirements = {}; - response.data.requirements.forEach(req => { + console.log('๐Ÿ“ฆ API ์‘๋‹ต ๋ฐ์ดํ„ฐ:', response.data); + response.data.forEach(req => { // material_id๋ฅผ ํ‚ค๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์š”๊ตฌ์‚ฌํ•ญ ์ €์žฅ if (req.material_id) { requirements[req.material_id] = req.requirement_description || req.requirement_title || ''; + console.log(`๐Ÿ“ฅ ๋กœ๋“œ๋œ ์š”๊ตฌ์‚ฌํ•ญ: ์ž์žฌ ID ${req.material_id} = "${requirements[req.material_id]}"`); + } else { + console.warn('โš ๏ธ material_id๊ฐ€ ์—†๋Š” ์š”๊ตฌ์‚ฌํ•ญ:', req); } }); + + console.log('๐Ÿ”„ setUserRequirements ํ˜ธ์ถœ ์ „ ์ƒํƒœ:', userRequirements); setUserRequirements(requirements); + console.log('๐Ÿ”„ setUserRequirements ํ˜ธ์ถœ ํ›„ ์ƒˆ๋กœ์šด ์ƒํƒœ:', requirements); console.log('โœ… ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ๋กœ๋“œ ์™„๋ฃŒ:', Object.keys(requirements).length, '๊ฐœ'); + + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ™•์ธ์„ ์œ„ํ•œ ์ง€์—ฐ๋œ ๋กœ๊ทธ + setTimeout(() => { + console.log('โฐ 1์ดˆ ํ›„ ์‹ค์ œ userRequirements ์ƒํƒœ:', userRequirements); + }, 1000); } } catch (error) { console.error('โŒ ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ๋กœ๋”ฉ ์‹คํŒจ:', error); @@ -122,11 +135,30 @@ const NewMaterialsPage = ({ const saveUserRequirements = async () => { try { setSavingRequirements(true); + + // ๊ฐ•์ œ ํ…Œ์ŠคํŠธ: userRequirements๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์ž์žฌ์— ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + let currentRequirements = { ...userRequirements }; + if (Object.keys(currentRequirements).length === 0 && materials.length > 0) { + const firstMaterialId = materials[0].id; + currentRequirements[firstMaterialId] = '๊ฐ•์ œ ํ…Œ์ŠคํŠธ ์š”๊ตฌ์‚ฌํ•ญ'; + setUserRequirements(currentRequirements); + console.log('โš ๏ธ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ฐ•์ œ ์ถ”๊ฐ€:', currentRequirements); + } + + // ๋””๋ฒ„๊น…: ํ˜„์žฌ userRequirements ์ƒํƒœ ํ™•์ธ + console.log('๐Ÿ’พ ์ €์žฅ ์‹œ์ž‘ - ํ˜„์žฌ userRequirements:', currentRequirements); + console.log('๐Ÿ’พ ์ €์žฅ ์‹œ์ž‘ - userRequirements ํ‚ค ๊ฐœ์ˆ˜:', Object.keys(currentRequirements).length); + console.log('๐Ÿ’พ ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ €์žฅ ์ค‘...', userRequirements); + console.log('๐Ÿ“‹ ์ „์ฒด userRequirements ๊ฐ์ฒด:', Object.keys(userRequirements).length, '๊ฐœ'); // ์š”๊ตฌ์‚ฌํ•ญ์ด ์žˆ๋Š” ์ž์žฌ๋“ค๋งŒ ์ €์žฅ - const requirementsToSave = Object.entries(userRequirements) - .filter(([materialId, requirement]) => requirement && requirement.trim()) + const requirementsToSave = Object.entries(currentRequirements) + .filter(([materialId, requirement]) => { + const hasValue = requirement && requirement.trim() && requirement.trim().length > 0; + console.log(`๐Ÿ” ์ž์žฌ ID ${materialId}: "${requirement}" (๊ธธ์ด: ${requirement ? requirement.length : 0}) -> ${hasValue ? '์ €์žฅ' : '์ œ์™ธ'}`); + return hasValue; + }) .map(([materialId, requirement]) => ({ material_id: parseInt(materialId), file_id: parseInt(fileId), @@ -136,24 +168,52 @@ const NewMaterialsPage = ({ priority: 'NORMAL' })); + console.log('๐Ÿ“ ์ €์žฅํ•  ์š”๊ตฌ์‚ฌํ•ญ ๊ฐœ์ˆ˜:', requirementsToSave.length); + if (requirementsToSave.length === 0) { alert('์ €์žฅํ•  ์š”๊ตฌ์‚ฌํ•ญ์ด ์—†์Šต๋‹ˆ๋‹ค.'); return; } // ๊ธฐ์กด ์š”๊ตฌ์‚ฌํ•ญ ์‚ญ์ œ ํ›„ ์ƒˆ๋กœ ์ €์žฅ - await api.delete(`/files/user-requirements`, { - params: { file_id: parseInt(fileId) } - }); + console.log('๐Ÿ—‘๏ธ ๊ธฐ์กด ์š”๊ตฌ์‚ฌํ•ญ ์‚ญ์ œ ์ค‘...', { file_id: parseInt(fileId) }); + console.log('๐ŸŒ API Base URL:', api.defaults.baseURL); + console.log('๐Ÿ”‘ Authorization Header:', api.defaults.headers.Authorization); + + try { + const deleteResponse = await api.delete(`/files/user-requirements`, { + params: { file_id: parseInt(fileId) } + }); + console.log('โœ… ๊ธฐ์กด ์š”๊ตฌ์‚ฌํ•ญ ์‚ญ์ œ ์™„๋ฃŒ:', deleteResponse.data); + } catch (deleteError) { + console.error('โŒ ๊ธฐ์กด ์š”๊ตฌ์‚ฌํ•ญ ์‚ญ์ œ ์‹คํŒจ:', deleteError); + console.error('โŒ ์‚ญ์ œ ์—๋Ÿฌ ์ƒ์„ธ:', deleteError.response?.data); + console.error('โŒ ์‚ญ์ œ ์—๋Ÿฌ ์ „์ฒด:', deleteError); + // ์‚ญ์ œ ์‹คํŒจํ•ด๋„ ๊ณ„์† ์ง„ํ–‰ + } // ์ƒˆ ์š”๊ตฌ์‚ฌํ•ญ๋“ค ์ €์žฅ + console.log('๐Ÿš€ API ํ˜ธ์ถœ ์‹œ์ž‘ - ์ €์žฅํ•  ๋ฐ์ดํ„ฐ:', requirementsToSave); for (const req of requirementsToSave) { - await api.post('/files/user-requirements', req); + console.log('๐Ÿš€ ๊ฐœ๋ณ„ API ํ˜ธ์ถœ:', req); + try { + const response = await api.post('/files/user-requirements', req); + console.log('โœ… API ์‘๋‹ต:', response.data); + } catch (apiError) { + console.error('โŒ API ํ˜ธ์ถœ ์‹คํŒจ:', apiError); + console.error('โŒ API ์—๋Ÿฌ ์ƒ์„ธ:', apiError.response?.data); + throw apiError; + } } alert(`${requirementsToSave.length}๊ฐœ์˜ ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); console.log('โœ… ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ €์žฅ ์™„๋ฃŒ'); + // ์ €์žฅ ํ›„ ๋‹ค์‹œ ๋กœ๋“œํ•˜์—ฌ ์ตœ์‹  ์ƒํƒœ ๋ฐ˜์˜ + console.log('๐Ÿ”„ ์ €์žฅ ์™„๋ฃŒ ํ›„ ๋‹ค์‹œ ๋กœ๋“œ ์‹œ์ž‘...'); + await loadUserRequirements(fileId); + console.log('๐Ÿ”„ ์ €์žฅ ์™„๋ฃŒ ํ›„ ๋‹ค์‹œ ๋กœ๋“œ ์™„๋ฃŒ!'); + } catch (error) { console.error('โŒ ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ €์žฅ ์‹คํŒจ:', error); alert('์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: ' + (error.response?.data?.detail || error.message)); @@ -164,10 +224,15 @@ const NewMaterialsPage = ({ // ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ ํ•ธ๋“ค๋Ÿฌ const handleUserRequirementChange = (materialId, value) => { - setUserRequirements(prev => ({ - ...prev, - [materialId]: value - })); + console.log(`๐Ÿ“ ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ: ์ž์žฌ ID ${materialId} = "${value}"`); + setUserRequirements(prev => { + const updated = { + ...prev, + [materialId]: value + }; + console.log('๐Ÿ”„ ์—…๋ฐ์ดํŠธ๋œ userRequirements:', updated); + return updated; + }); }; // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ž์žฌ ์ˆ˜ ๊ณ„์‚ฐ @@ -204,6 +269,78 @@ const NewMaterialsPage = ({ }; }; + // ์นดํ…Œ๊ณ ๋ฆฌ ํ‘œ์‹œ๋ช… ๋งคํ•‘ + const getCategoryDisplayName = (category) => { + const categoryMap = { + 'SUPPORT': 'U-BOLT', + 'PIPE': 'PIPE', + 'FITTING': 'FITTING', + 'FLANGE': 'FLANGE', + 'VALVE': 'VALVE', + 'BOLT': 'BOLT', + 'GASKET': 'GASKET', + 'INSTRUMENT': 'INSTRUMENT', + 'UNKNOWN': 'UNKNOWN' + }; + return categoryMap[category] || category; + }; + + // ๋‹ˆํ”Œ ๋๋‹จ ์ •๋ณด ์ถ”์ถœ + const extractNippleEndInfo = (description) => { + const descUpper = description.toUpperCase(); + + // ๋‹ˆํ”Œ ๋๋‹จ ํŒจํ„ด๋“ค + const endPatterns = { + 'PBE': 'PBE', // Plain Both End + 'BBE': 'BBE', // Bevel Both End + 'POE': 'POE', // Plain One End + 'BOE': 'BOE', // Bevel One End + 'TOE': 'TOE', // Thread One End + 'SW X NPT': 'SWร—NPT', // Socket Weld x NPT + 'SW X SW': 'SWร—SW', // Socket Weld x Socket Weld + 'NPT X NPT': 'NPTร—NPT', // NPT x NPT + }; + + for (const [pattern, display] of Object.entries(endPatterns)) { + if (descUpper.includes(pattern)) { + return display; + } + } + + return ''; + }; + + // ๋ณผํŠธ ์ถ”๊ฐ€์š”๊ตฌ์‚ฌํ•ญ ์ถ”์ถœ + const extractBoltAdditionalRequirements = (description) => { + const descUpper = description.toUpperCase(); + const additionalReqs = []; + + // ํ‘œ๋ฉด์ฒ˜๋ฆฌ ํŒจํ„ด๋“ค + const surfaceTreatments = { + 'ELEC.GALV': '์ „๊ธฐ์•„์—ฐ๋„๊ธˆ', + 'ELEC GALV': '์ „๊ธฐ์•„์—ฐ๋„๊ธˆ', + 'GALVANIZED': '์•„์—ฐ๋„๊ธˆ', + 'GALV': '์•„์—ฐ๋„๊ธˆ', + 'HOT DIP GALV': '์šฉ์œต์•„์—ฐ๋„๊ธˆ', + 'HDG': '์šฉ์œต์•„์—ฐ๋„๊ธˆ', + 'ZINC PLATED': '์•„์—ฐ๋„๊ธˆ', + 'ZINC': '์•„์—ฐ๋„๊ธˆ', + 'STAINLESS': '์Šคํ…Œ์ธ๋ฆฌ์Šค', + 'SS': '์Šคํ…Œ์ธ๋ฆฌ์Šค' + }; + + // ํ‘œ๋ฉด์ฒ˜๋ฆฌ ํ™•์ธ + for (const [pattern, korean] of Object.entries(surfaceTreatments)) { + if (descUpper.includes(pattern)) { + additionalReqs.push(korean); + } + } + + // ์ค‘๋ณต ์ œ๊ฑฐ + const uniqueReqs = [...new Set(additionalReqs)]; + return uniqueReqs.join(', '); + }; + // ์ž์žฌ ์ •๋ณด ํŒŒ์‹ฑ const parseMaterialInfo = (material) => { const category = material.classified_category; @@ -223,15 +360,41 @@ const NewMaterialsPage = ({ }; } else if (category === 'FITTING') { const fittingDetails = material.fitting_details || {}; - const fittingType = fittingDetails.fitting_type || ''; - const fittingSubtype = fittingDetails.fitting_subtype || ''; + const classificationDetails = material.classification_details || {}; + + // ๊ฐœ์„ ๋œ ๋ถ„๋ฅ˜๊ธฐ ๊ฒฐ๊ณผ ์šฐ์„  ์‚ฌ์šฉ + const fittingTypeInfo = classificationDetails.fitting_type || {}; + const scheduleInfo = classificationDetails.schedule_info || {}; + + // ๊ธฐ์กด ํ•„๋“œ์™€ ์ƒˆ ํ•„๋“œ ํ†ตํ•ฉ + const fittingType = fittingTypeInfo.type || fittingDetails.fitting_type || ''; + const fittingSubtype = fittingTypeInfo.subtype || fittingDetails.fitting_subtype || ''; + const mainSchedule = scheduleInfo.main_schedule || fittingDetails.schedule || ''; + const redSchedule = scheduleInfo.red_schedule || ''; + const hasDifferentSchedules = scheduleInfo.has_different_schedules || false; + const description = material.original_description || ''; // ํ”ผํŒ… ํƒ€์ž…๋ณ„ ์ƒ์„ธ ํ‘œ์‹œ let displayType = ''; - // CAP๊ณผ PLUG ๋จผ์ € ํ™•์ธ (fitting_type์ด ์—†์„ ์ˆ˜ ์žˆ์Œ) - if (description.toUpperCase().includes('CAP')) { + // ๊ฐœ์„ ๋œ ๋ถ„๋ฅ˜๊ธฐ ๊ฒฐ๊ณผ ์šฐ์„  ํ‘œ์‹œ + if (fittingType === 'TEE' && fittingSubtype === 'REDUCING') { + displayType = 'TEE REDUCING'; + } else if (fittingType === 'REDUCER' && fittingSubtype === 'CONCENTRIC') { + displayType = 'REDUCER CONC'; + } else if (fittingType === 'REDUCER' && fittingSubtype === 'ECCENTRIC') { + displayType = 'REDUCER ECC'; + } else if (description.toUpperCase().includes('TEE RED')) { + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์˜ TEE RED ํŒจํ„ด + displayType = 'TEE REDUCING'; + } else if (description.toUpperCase().includes('RED CONC')) { + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์˜ RED CONC ํŒจํ„ด + displayType = 'REDUCER CONC'; + } else if (description.toUpperCase().includes('RED ECC')) { + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์˜ RED ECC ํŒจํ„ด + displayType = 'REDUCER ECC'; + } else if (description.toUpperCase().includes('CAP')) { // CAP: ์—ฐ๊ฒฐ ๋ฐฉ์‹ ํ‘œ์‹œ (์˜ˆ: CAP, NPT(F), 3000LB, ASTM A105) if (description.includes('NPT(F)')) { displayType = 'CAP NPT(F)'; @@ -260,7 +423,13 @@ const NewMaterialsPage = ({ } else if (fittingType === 'NIPPLE') { // ๋‹ˆํ”Œ: ๊ธธ์ด์™€ ๋๋‹จ ๊ฐ€๊ณต ์ •๋ณด const length = fittingDetails.length_mm || fittingDetails.avg_length_mm; - displayType = length ? `NIPPLE ${length}mm` : 'NIPPLE'; + const endInfo = extractNippleEndInfo(description); + + let nippleType = 'NIPPLE'; + if (length) nippleType += ` ${length}mm`; + if (endInfo) nippleType += ` ${endInfo}`; + + displayType = nippleType; } else if (fittingType === 'ELBOW') { // ์—˜๋ณด: ๊ฐ๋„์™€ ์—ฐ๊ฒฐ ๋ฐฉ์‹ const angle = fittingSubtype === '90DEG' ? '90ยฐ' : fittingSubtype === '45DEG' ? '45ยฐ' : ''; @@ -295,11 +464,25 @@ const NewMaterialsPage = ({ pressure = `${pressureMatch[1]}LB`; } - // ์Šค์ผ€์ค„ ์ฐพ๊ธฐ - if (description.includes('SCH')) { - const schMatch = description.match(/SCH\s*(\d+[A-Z]*)/i); - if (schMatch) { - schedule = `SCH ${schMatch[1]}`; + // ์Šค์ผ€์ค„ ํ‘œ์‹œ (๋ถ„๋ฆฌ ์Šค์ผ€์ค„ ์ง€์›) + if (hasDifferentSchedules && mainSchedule && redSchedule) { + // ๋ถ„๋ฆฌ ์Šค์ผ€์ค„: "SCH 40 x SCH 80" + schedule = `${mainSchedule} x ${redSchedule}`; + } else if (mainSchedule && mainSchedule !== 'UNKNOWN') { + // ๋‹จ์ผ ์Šค์ผ€์ค„: "SCH 40" + schedule = mainSchedule; + } else if (description.includes('SCH')) { + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ์—์„œ ๋ถ„๋ฆฌ ์Šค์ผ€์ค„ ํŒจํ„ด ํ™•์ธ + const separatedSchMatch = description.match(/SCH\s*(\d+[A-Z]*)\s*[xXร—]\s*SCH\s*(\d+[A-Z]*)/i); + if (separatedSchMatch) { + // ๋ถ„๋ฆฌ ์Šค์ผ€์ค„ ๋ฐœ๊ฒฌ: "SCH 40 x SCH 80" + schedule = `SCH ${separatedSchMatch[1]} x SCH ${separatedSchMatch[2]}`; + } else { + // ๋‹จ์ผ ์Šค์ผ€์ค„ + const schMatch = description.match(/SCH\s*(\d+[A-Z]*)/i); + if (schMatch) { + schedule = `SCH ${schMatch[1]}`; + } } } @@ -397,12 +580,37 @@ const NewMaterialsPage = ({ const qty = Math.round(material.quantity || 0); const safetyQty = Math.ceil(qty * 1.05); // 5% ์—ฌ์œ ์œจ const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4์˜ ๋ฐฐ์ˆ˜ + + // ๋ณผํŠธ ๊ธธ์ด ์ถ”์ถœ (์›๋ณธ ์„ค๋ช…์—์„œ) + const description = material.original_description || ''; + let boltLength = '-'; + + // ๊ธธ์ด ํŒจํ„ด ์ถ”์ถœ (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 ํ˜•ํƒœ + ]; + + for (const pattern of lengthPatterns) { + const match = description.match(pattern); + if (match) { + boltLength = `${match[1]}mm`; + break; + } + } + + // ์ถ”๊ฐ€์š”๊ตฌ์‚ฌํ•ญ ์ถ”์ถœ (ELEC.GALV ๋“ฑ) + const additionalReq = extractBoltAdditionalRequirements(description); + return { type: 'BOLT', - subtype: material.bolt_details?.bolt_type || '-', + subtype: material.bolt_details?.bolt_type || 'BOLT_GENERAL', size: material.size_spec || '-', - schedule: material.bolt_details?.length || '-', - grade: material.material_grade || '-', + schedule: boltLength, // ๊ธธ์ด ์ •๋ณด + grade: material.full_material_grade || material.material_grade || '-', + additionalReq: additionalReq, // ์ถ”๊ฐ€์š”๊ตฌ์‚ฌํ•ญ quantity: purchaseQty, unit: 'SETS' }; @@ -473,6 +681,9 @@ const NewMaterialsPage = ({ // ํ•„ํ„ฐ๋ง๋œ ์ž์žฌ ๋ชฉ๋ก const filteredMaterials = materials.filter(material => { + if (selectedCategory === 'ALL') { + return true; // ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ์ผ ๋•Œ๋Š” ๋ชจ๋“  ์ž์žฌ ํ‘œ์‹œ + } return material.classified_category === selectedCategory; }); @@ -508,101 +719,51 @@ const NewMaterialsPage = ({ console.log('๐Ÿ“Š ์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ:', dataToExport.length, '๊ฐœ ํ•ญ๋ชฉ'); - // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ปฌ๋Ÿผ ๊ตฌ์„ฑ + // ์ƒˆ๋กœ์šด ์—‘์…€ ์–‘์‹์— ๋งž์ถ˜ ์ปฌ๋Ÿผ ๊ตฌ์„ฑ const getExcelData = (material) => { const info = parseMaterialInfo(material); + // ํ’ˆ๋ชฉ๋ช… ์ƒ์„ฑ (๊ฐ„๋‹จํ•˜๊ฒŒ) + let itemName = ''; if (selectedCategory === 'PIPE') { - return { - '์ข…๋ฅ˜': info.type, - 'ํƒ€์ž…': info.subtype, - 'ํฌ๊ธฐ': info.size, - '์Šค์ผ€์ค„': info.schedule, - '์žฌ์งˆ': info.grade, - '์ถ”๊ฐ€์š”๊ตฌ': '-', - '์‚ฌ์šฉ์ž์š”๊ตฌ': userRequirements[material.id] || '', - '์ˆ˜๋Ÿ‰': `${info.quantity} ${info.unit}`, - '์ƒ์„ธ': `๋‹จ๊ด€ ${info.itemCount || 0}๊ฐœ โ†’ ${info.totalLength || 0}mm` - }; - } else if (selectedCategory === 'FLANGE' && info.isFlange) { - return { - '์ข…๋ฅ˜': info.type, - 'ํƒ€์ž…': info.subtype, - 'ํฌ๊ธฐ': info.size, - '์••๋ ฅ(ํŒŒ์šด๋“œ)': info.pressure, - '์Šค์ผ€์ค„': info.schedule, - '์žฌ์งˆ': info.grade, - '์ถ”๊ฐ€์š”๊ตฌ': '-', - '์‚ฌ์šฉ์ž์š”๊ตฌ': userRequirements[material.id] || '', - '์ˆ˜๋Ÿ‰': `${info.quantity} ${info.unit}` - }; - } else if (selectedCategory === 'FITTING' && info.isFitting) { - return { - '์ข…๋ฅ˜': info.type, - 'ํƒ€์ž…/์ƒ์„ธ': info.subtype, - 'ํฌ๊ธฐ': info.size, - '์••๋ ฅ': info.pressure, - '์Šค์ผ€์ค„': info.schedule, - '์žฌ์งˆ': info.grade, - '์ถ”๊ฐ€์š”๊ตฌ': '-', - '์‚ฌ์šฉ์ž์š”๊ตฌ': userRequirements[material.id] || '', - '์ˆ˜๋Ÿ‰': `${info.quantity} ${info.unit}` - }; - } else if (selectedCategory === 'VALVE' && info.isValve) { - return { - 'ํƒ€์ž…': info.valveType, - '์—ฐ๊ฒฐ๋ฐฉ์‹': info.connectionType, - 'ํฌ๊ธฐ': info.size, - '์••๋ ฅ': info.pressure, - '์žฌ์งˆ': info.grade, - '์ถ”๊ฐ€์š”๊ตฌ': '-', - '์‚ฌ์šฉ์ž์š”๊ตฌ': userRequirements[material.id] || '', - '์ˆ˜๋Ÿ‰': `${info.quantity} ${info.unit}` - }; - } else if (selectedCategory === 'GASKET' && info.isGasket) { - return { - '์ข…๋ฅ˜': info.type, - 'ํƒ€์ž…': info.subtype, - 'ํฌ๊ธฐ': info.size, - '์••๋ ฅ': info.pressure, - '์žฌ์งˆ': info.materialStructure, - '์ƒ์„ธ๋‚ด์—ญ': info.materialDetail, - '๋‘๊ป˜': info.thickness, - '์ถ”๊ฐ€์š”๊ตฌ': '-', - '์‚ฌ์šฉ์ž์š”๊ตฌ': '', - '์ˆ˜๋Ÿ‰': `${info.quantity} ${info.unit}` - }; + 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') { - return { - '์ข…๋ฅ˜': info.type, - 'ํƒ€์ž…': info.subtype, - 'ํฌ๊ธฐ': info.size, - '์Šค์ผ€์ค„': info.schedule, - '์žฌ์งˆ': info.grade, - '์ถ”๊ฐ€์š”๊ตฌ': '-', - '์‚ฌ์šฉ์ž์š”๊ตฌ': userRequirements[material.id] || '', - '์ˆ˜๋Ÿ‰': `${info.quantity} ${info.unit}` - }; - } else if (selectedCategory === 'UNKNOWN' && info.isUnknown) { - return { - '์ข…๋ฅ˜': info.type, - '์„ค๋ช…': info.description, - '์‚ฌ์šฉ์ž์š”๊ตฌ': '', - '์ˆ˜๋Ÿ‰': `${info.quantity} ${info.unit}` - }; + itemName = info.subtype || 'BOLT'; } else { - // ๊ธฐ๋ณธ ํ˜•์‹ - return { - '์ข…๋ฅ˜': info.type, - 'ํƒ€์ž…': info.subtype, - 'ํฌ๊ธฐ': info.size, - '์Šค์ผ€์ค„': info.schedule, - '์žฌ์งˆ': info.grade, - '์ถ”๊ฐ€์š”๊ตฌ': '-', - '์‚ฌ์šฉ์ž์š”๊ตฌ': userRequirements[material.id] || '', - '์ˆ˜๋Ÿ‰': `${info.quantity} ${info.unit}` - }; + 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] // ์˜ค๋Š˜ ๋‚ ์งœ + }; }; // ์—‘์…€ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ @@ -694,6 +855,25 @@ const NewMaterialsPage = ({ )}
+ ์ด {materials.length}๊ฐœ ์ž์žฌ ({currentRevision}) @@ -702,13 +882,22 @@ const NewMaterialsPage = ({ {/* ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ */}
+ {/* ์ „์ฒด ์นดํ…Œ๊ณ ๋ฆฌ ๋ฒ„ํŠผ */} + + {Object.entries(categoryCounts).map(([category, count]) => ( ))}
@@ -885,7 +1074,7 @@ const NewMaterialsPage = ({ {/* ์ถ”๊ฐ€์š”๊ตฌ */}
- - + {info.additionalReq || '-'}
{/* ์‚ฌ์šฉ์ž์š”๊ตฌ */} @@ -895,7 +1084,10 @@ const NewMaterialsPage = ({ className="user-req-input" placeholder="์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ" value={userRequirements[material.id] || ''} - onChange={(e) => handleUserRequirementChange(material.id, e.target.value)} + onChange={(e) => { + console.log('๐ŸŽฏ ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ!', material.id, e.target.value); + handleUserRequirementChange(material.id, e.target.value); + }} />
@@ -954,7 +1146,7 @@ const NewMaterialsPage = ({ {/* ์ถ”๊ฐ€์š”๊ตฌ */}
- - + {info.additionalReq || '-'}
{/* ์‚ฌ์šฉ์ž์š”๊ตฌ */} @@ -964,7 +1156,10 @@ const NewMaterialsPage = ({ className="user-req-input" placeholder="์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ" value={userRequirements[material.id] || ''} - onChange={(e) => handleUserRequirementChange(material.id, e.target.value)} + onChange={(e) => { + console.log('๐ŸŽฏ ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ!', material.id, e.target.value); + handleUserRequirementChange(material.id, e.target.value); + }} /> @@ -1030,7 +1225,7 @@ const NewMaterialsPage = ({ {/* ์ถ”๊ฐ€์š”๊ตฌ */}
- - + {info.additionalReq || '-'}
{/* ์‚ฌ์šฉ์ž์š”๊ตฌ */} @@ -1040,7 +1235,10 @@ const NewMaterialsPage = ({ className="user-req-input" placeholder="์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ" value={userRequirements[material.id] || ''} - onChange={(e) => handleUserRequirementChange(material.id, e.target.value)} + onChange={(e) => { + console.log('๐ŸŽฏ ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ!', material.id, e.target.value); + handleUserRequirementChange(material.id, e.target.value); + }} /> @@ -1093,7 +1291,10 @@ const NewMaterialsPage = ({ className="user-req-input" placeholder="์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ" value={userRequirements[material.id] || ''} - onChange={(e) => handleUserRequirementChange(material.id, e.target.value)} + onChange={(e) => { + console.log('๐ŸŽฏ ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ!', material.id, e.target.value); + handleUserRequirementChange(material.id, e.target.value); + }} /> @@ -1164,7 +1365,7 @@ const NewMaterialsPage = ({ {/* ์ถ”๊ฐ€์š”๊ตฌ */}
- - + {info.additionalReq || '-'}
{/* ์‚ฌ์šฉ์ž์š”๊ตฌ */} @@ -1174,7 +1375,10 @@ const NewMaterialsPage = ({ className="user-req-input" placeholder="์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ" value={userRequirements[material.id] || ''} - onChange={(e) => handleUserRequirementChange(material.id, e.target.value)} + onChange={(e) => { + console.log('๐ŸŽฏ ์ž…๋ ฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ!', material.id, e.target.value); + handleUserRequirementChange(material.id, e.target.value); + }} /> @@ -1234,7 +1438,7 @@ const NewMaterialsPage = ({ {/* ์ถ”๊ฐ€์š”๊ตฌ */}
- - + {info.additionalReq || '-'}
{/* ์‚ฌ์šฉ์ž์š”๊ตฌ */} @@ -1243,6 +1447,8 @@ const NewMaterialsPage = ({ type="text" className="user-req-input" placeholder="์š”๊ตฌ์‚ฌํ•ญ ์ž…๋ ฅ" + value={userRequirements[material.id] || ''} + onChange={(e) => handleUserRequirementChange(material.id, e.target.value)} /> diff --git a/frontend/src/pages/ProjectsPage.jsx b/frontend/src/pages/ProjectsPage.jsx index 5937617..fdbc366 100644 --- a/frontend/src/pages/ProjectsPage.jsx +++ b/frontend/src/pages/ProjectsPage.jsx @@ -405,5 +405,6 @@ export default ProjectsPage; + diff --git a/frontend/src/pages/UserManagementPage.css b/frontend/src/pages/UserManagementPage.css index 2d11f93..5a0bb74 100644 --- a/frontend/src/pages/UserManagementPage.css +++ b/frontend/src/pages/UserManagementPage.css @@ -447,5 +447,6 @@ + diff --git a/frontend/src/utils/excelExport.js b/frontend/src/utils/excelExport.js index b1127ba..cf5d8e7 100644 --- a/frontend/src/utils/excelExport.js +++ b/frontend/src/utils/excelExport.js @@ -120,7 +120,6 @@ const consolidateMaterials = (materials, isComparison = false) => { */ const formatMaterialForExcel = (material, includeComparison = false) => { const category = material.classified_category || material.category || '-'; - const isPipe = category === 'PIPE'; // ์—‘์…€์šฉ ์ž์žฌ ์„ค๋ช… ์ •์ œ let cleanDescription = material.original_description || material.description || '-'; @@ -135,16 +134,12 @@ const formatMaterialForExcel = (material, includeComparison = false) => { // ๋‹ˆํ”Œ์˜ ๊ฒฝ์šฐ ๊ธธ์ด ์ •๋ณด ๋ช…์‹œ์  ์ถ”๊ฐ€ if (category === 'FITTING' && cleanDescription.toLowerCase().includes('nipple')) { - // fitting_details์—์„œ ๊ธธ์ด ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ if (material.fitting_details && material.fitting_details.length_mm) { const lengthMm = Math.round(material.fitting_details.length_mm); - // ์ด๋ฏธ ๊ธธ์ด ์ •๋ณด๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ if (!cleanDescription.match(/\d+\s*mm/i)) { cleanDescription += ` ${lengthMm}mm`; } - } - // ๋˜๋Š” ๊ธฐ์กด ์„ค๋ช…์—์„œ ๊ธธ์ด ์ •๋ณด ์ถ”์ถœ - else { + } else { const lengthMatch = material.original_description?.match(/(\d+)\s*mm/i); if (lengthMatch && !cleanDescription.match(/\d+\s*mm/i)) { cleanDescription += ` ${lengthMatch[1]}mm`; @@ -155,31 +150,79 @@ const formatMaterialForExcel = (material, includeComparison = false) => { // ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ const purchaseInfo = calculatePurchaseQuantity(material); + // ํ’ˆ๋ชฉ๋ช… ์ƒ์„ฑ (๊ฐ„๋‹จํ•˜๊ฒŒ) + let itemName = ''; + if (category === 'PIPE') { + itemName = material.pipe_details?.manufacturing_method || 'PIPE'; + } else if (category === 'FITTING') { + itemName = material.fitting_details?.fitting_type || 'FITTING'; + } else if (category === 'FLANGE') { + itemName = 'FLANGE'; + } else if (category === 'VALVE') { + itemName = 'VALVE'; + } else if (category === 'GASKET') { + itemName = 'GASKET'; + } else if (category === 'BOLT') { + itemName = 'BOLT'; + } else { + itemName = category || 'UNKNOWN'; + } + + // ์••๋ ฅ ๋“ฑ๊ธ‰ ์ถ”์ถœ + let pressure = '-'; + const pressureMatch = cleanDescription.match(/(\d+)LB/i); + if (pressureMatch) { + pressure = `${pressureMatch[1]}LB`; + } + + // ์Šค์ผ€์ค„ ์ถ”์ถœ + let schedule = '-'; + const scheduleMatch = cleanDescription.match(/SCH\s*(\d+[A-Z]*(?:\s*[xXร—]\s*SCH\s*\d+[A-Z]*)?)/i); + if (scheduleMatch) { + schedule = scheduleMatch[0]; + } + + // ์žฌ์งˆ ์ถ”์ถœ (ASTM ๋“ฑ) + let grade = '-'; + const gradeMatch = cleanDescription.match(/(ASTM\s+[A-Z0-9\s]+(?:TP\d+|GR\s*[A-Z0-9]+|WP\d+)?)/i); + if (gradeMatch) { + grade = gradeMatch[1].trim(); + } + + // ์ƒˆ๋กœ์šด ์—‘์…€ ์–‘์‹์— ๋งž์ถ˜ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ const base = { - '์นดํ…Œ๊ณ ๋ฆฌ': category, - '์ž์žฌ ์„ค๋ช…': cleanDescription, - '์‚ฌ์ด์ฆˆ': material.size_spec || '-' + 'TAGNO': '', // ๋น„์›Œ๋‘  + 'ํ’ˆ๋ชฉ๋ช…': itemName, + '์ˆ˜๋Ÿ‰': purchaseInfo.purchaseQuantity || material.quantity || 0, + 'ํ†ตํ™”๊ตฌ๋ถ„': 'KRW', // ๊ธฐ๋ณธ๊ฐ’ + '๋‹จ๊ฐ€': 1, // ์ผ๊ด„ 1๋กœ ์„ค์ • + 'ํฌ๊ธฐ': material.size_spec || '-', + '์••๋ ฅ๋“ฑ๊ธ‰': pressure, + '์Šค์ผ€์ค„': schedule, + '์žฌ์งˆ': grade, + '์‚ฌ์šฉ์ž์š”๊ตฌ': '', + '๊ด€๋ฆฌํ•ญ๋ชฉ1': '', // ๋นˆ์นธ + '๊ด€๋ฆฌํ•ญ๋ชฉ7': '', // ๋นˆ์นธ + '๊ด€๋ฆฌํ•ญ๋ชฉ8': '', // ๋นˆ์นธ + '๊ด€๋ฆฌํ•ญ๋ชฉ9': '', // ๋นˆ์นธ + '๊ด€๋ฆฌํ•ญ๋ชฉ10': '', // ๋นˆ์นธ + '๋‚ฉ๊ธฐ์ผ(YYYY-MM-DD)': new Date().toISOString().split('T')[0] // ์˜ค๋Š˜ ๋‚ ์งœ }; - // ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ์ •๋ณด๋งŒ ์ถ”๊ฐ€ (๊ธฐ์กด ์ˆ˜๋Ÿ‰/๋‹จ์œ„ ์ •๋ณด ์ œ๊ฑฐ) - base['ํ•„์š” ์ˆ˜๋Ÿ‰'] = purchaseInfo.purchaseQuantity || 0; - base['๊ตฌ๋งค ๋‹จ์œ„'] = purchaseInfo.unit || 'EA'; - - // ๋น„๊ต ๋ชจ๋“œ์ธ ๊ฒฝ์šฐ ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ๋ณ€ํ™” ์ •๋ณด๋งŒ ์ถ”๊ฐ€ + // ๋น„๊ต ๋ชจ๋“œ์ธ ๊ฒฝ์šฐ ์ถ”๊ฐ€ ์ •๋ณด if (includeComparison) { if (material.previous_quantity !== undefined) { - // ์ด์ „ ๊ตฌ๋งค ์ˆ˜๋Ÿ‰ ๊ณ„์‚ฐ const prevPurchaseInfo = calculatePurchaseQuantity({ ...material, quantity: material.previous_quantity, totalLength: material.previousTotalLength || 0 }); - base['์ด์ „ ํ•„์š” ์ˆ˜๋Ÿ‰'] = prevPurchaseInfo.purchaseQuantity || 0; - base['ํ•„์š” ์ˆ˜๋Ÿ‰ ๋ณ€๊ฒฝ'] = (purchaseInfo.purchaseQuantity - prevPurchaseInfo.purchaseQuantity); + base['์ด์ „์ˆ˜๋Ÿ‰'] = prevPurchaseInfo.purchaseQuantity || 0; + base['์ˆ˜๋Ÿ‰๋ณ€๊ฒฝ'] = (purchaseInfo.purchaseQuantity - prevPurchaseInfo.purchaseQuantity); } - base['๋ณ€๊ฒฝ ์œ ํ˜•'] = material.change_type || ( + base['๋ณ€๊ฒฝ์œ ํ˜•'] = material.change_type || ( material.previous_quantity !== undefined ? '์ˆ˜๋Ÿ‰ ๋ณ€๊ฒฝ' : material.quantity_change === undefined ? '์‹ ๊ทœ' : '๋ณ€๊ฒฝ' ); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index bf677be..83fc69d 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,9 +5,9 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { - port: 13000, + port: 5173, host: true, - open: true + open: false }, build: { outDir: 'dist', diff --git a/test_bom.csv b/test_bom.csv new file mode 100644 index 0000000..f74fdb7 --- /dev/null +++ b/test_bom.csv @@ -0,0 +1,2 @@ +Item,Description,Quantity +1,Test Item,10