feat: 사용자 요구사항 기능 완전 구현 및 전체 카테고리 추가
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 사용자 요구사항 저장/로드/엑셀 내보내기 기능 완전 구현 - 백엔드 API 수정: Request Body 방식으로 변경 - 데이터베이스 스키마: material_id 컬럼 추가 - 프론트엔드 상태 관리 개선: 저장 후 자동 리로드 - 입력 필드 연결 문제 해결: 누락된 onChange 핸들러 추가 - NewMaterialsPage에 '전체' 카테고리 버튼 추가 (기본 선택) - Docker 환경 개선: 프론트엔드 볼륨 마운트 및 포트 수정 - UI 개선: 벌레 이모지 제거, 디버그 코드 정리
This commit is contained in:
50
RULES.md
50
RULES.md
@@ -963,7 +963,7 @@ logger.error("에러 발생")
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐛 자주 발생하는 이슈 & 해결법
|
## ⚠️ 자주 발생하는 이슈 & 해결법
|
||||||
|
|
||||||
### 1. 파이프 길이 합산 문제
|
### 1. 파이프 길이 합산 문제
|
||||||
```python
|
```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일 (메인 서버 배포 가이드 추가)
|
||||||
|
|||||||
@@ -268,5 +268,6 @@ jwt_service = JWTService()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -322,5 +322,6 @@ async def get_current_user_optional(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ class UserRequirement(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
file_id = Column(Integer, ForeignKey("files.id"), nullable=False)
|
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' 등
|
requirement_type = Column(String(50), nullable=False) # 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' 등
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, R
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from typing import List, Optional, Dict
|
from typing import List, Optional, Dict
|
||||||
|
from pydantic import BaseModel
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -2629,6 +2630,7 @@ async def get_user_requirements(
|
|||||||
{
|
{
|
||||||
"id": req.id,
|
"id": req.id,
|
||||||
"file_id": req.file_id,
|
"file_id": req.file_id,
|
||||||
|
"material_id": req.material_id,
|
||||||
"original_filename": req.original_filename,
|
"original_filename": req.original_filename,
|
||||||
"job_no": req.job_no,
|
"job_no": req.job_no,
|
||||||
"revision": req.revision,
|
"revision": req.revision,
|
||||||
@@ -2651,17 +2653,20 @@ async def get_user_requirements(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"사용자 요구사항 조회 실패: {str(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")
|
@router.post("/user-requirements")
|
||||||
async def create_user_requirement(
|
async def create_user_requirement(
|
||||||
file_id: int,
|
requirement: UserRequirementCreate,
|
||||||
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,
|
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -2681,15 +2686,15 @@ async def create_user_requirement(
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
result = db.execute(insert_query, {
|
result = db.execute(insert_query, {
|
||||||
"file_id": file_id,
|
"file_id": requirement.file_id,
|
||||||
"material_id": material_id,
|
"material_id": requirement.material_id,
|
||||||
"requirement_type": requirement_type,
|
"requirement_type": requirement.requirement_type,
|
||||||
"requirement_title": requirement_title,
|
"requirement_title": requirement.requirement_title,
|
||||||
"requirement_description": requirement_description,
|
"requirement_description": requirement.requirement_description,
|
||||||
"requirement_spec": requirement_spec,
|
"requirement_spec": requirement.requirement_spec,
|
||||||
"priority": priority,
|
"priority": requirement.priority,
|
||||||
"assigned_to": assigned_to,
|
"assigned_to": requirement.assigned_to,
|
||||||
"due_date": due_date
|
"due_date": requirement.due_date
|
||||||
})
|
})
|
||||||
|
|
||||||
requirement_id = result.fetchone()[0]
|
requirement_id = result.fetchone()[0]
|
||||||
|
|||||||
@@ -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:
|
def get_bolt_purchase_info(bolt_result: Dict) -> Dict:
|
||||||
"""볼트 구매 정보 생성"""
|
"""볼트 구매 정보 생성"""
|
||||||
|
|
||||||
|
|||||||
@@ -230,8 +230,8 @@ def classify_fitting(dat_file: str, description: str, main_nom: str,
|
|||||||
# 4. 압력 등급 분류
|
# 4. 압력 등급 분류
|
||||||
pressure_result = classify_pressure_rating(dat_file, description)
|
pressure_result = classify_pressure_rating(dat_file, description)
|
||||||
|
|
||||||
# 4.5. 스케줄 분류 (니플 등에 중요)
|
# 4.5. 스케줄 분류 (니플 등에 중요) - 분리 스케줄 지원
|
||||||
schedule_result = classify_fitting_schedule(description)
|
schedule_result = classify_fitting_schedule_with_reducing(description, main_nom, red_nom)
|
||||||
|
|
||||||
# 5. 제작 방법 추정
|
# 5. 제작 방법 추정
|
||||||
manufacturing_result = determine_fitting_manufacturing(
|
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,
|
def classify_fitting_type(dat_file: str, description: str,
|
||||||
main_nom: str, red_nom: str = None) -> Dict:
|
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()
|
dat_upper = dat_file.upper()
|
||||||
desc_upper = description.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차 분류 (가장 신뢰도 높음)
|
# 1. DAT_FILE 패턴으로 1차 분류 (가장 신뢰도 높음)
|
||||||
for fitting_type, type_data in FITTING_TYPES.items():
|
for fitting_type, type_data in FITTING_TYPES.items():
|
||||||
for pattern in type_data["dat_file_patterns"]:
|
for pattern in type_data["dat_file_patterns"]:
|
||||||
@@ -679,3 +801,53 @@ def classify_fitting_schedule(description: str) -> Dict:
|
|||||||
"confidence": 0.0,
|
"confidence": 0.0,
|
||||||
"matched_pattern": ""
|
"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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
from .fitting_classifier import classify_fitting
|
||||||
|
|
||||||
# Level 1: 명확한 타입 키워드 (최우선)
|
# Level 1: 명확한 타입 키워드 (최우선)
|
||||||
LEVEL1_TYPE_KEYWORDS = {
|
LEVEL1_TYPE_KEYWORDS = {
|
||||||
@@ -142,6 +143,18 @@ def classify_material_integrated(description: str, main_nom: str = "",
|
|||||||
# 3단계: 단일 타입 확정 또는 Level 3/4로 판단
|
# 3단계: 단일 타입 확정 또는 Level 3/4로 판단
|
||||||
if len(detected_types) == 1:
|
if len(detected_types) == 1:
|
||||||
material_type = detected_types[0][0]
|
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 {
|
return {
|
||||||
"category": material_type,
|
"category": material_type,
|
||||||
"confidence": 0.9,
|
"confidence": 0.9,
|
||||||
@@ -171,6 +184,15 @@ def classify_material_integrated(description: str, main_nom: str = "",
|
|||||||
if other_type_found:
|
if other_type_found:
|
||||||
continue # 볼트로 분류하지 않음
|
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 {
|
return {
|
||||||
"category": material_type,
|
"category": material_type,
|
||||||
"confidence": 0.35, # 재질만으로 분류 시 낮은 신뢰도
|
"confidence": 0.35, # 재질만으로 분류 시 낮은 신뢰도
|
||||||
@@ -182,8 +204,19 @@ def classify_material_integrated(description: str, main_nom: str = "",
|
|||||||
for material, priority_types in GENERIC_MATERIALS.items():
|
for material, priority_types in GENERIC_MATERIALS.items():
|
||||||
if material in desc_upper:
|
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 {
|
return {
|
||||||
"category": priority_types[0], # 첫 번째 우선순위
|
"category": material_type,
|
||||||
"confidence": 0.3,
|
"confidence": 0.3,
|
||||||
"evidence": [f"GENERIC_MATERIAL: {material}"],
|
"evidence": [f"GENERIC_MATERIAL: {material}"],
|
||||||
"classification_level": "LEVEL4_GENERIC"
|
"classification_level": "LEVEL4_GENERIC"
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ def extract_full_material_grade(description: str) -> str:
|
|||||||
|
|
||||||
# 1. ASTM 규격 패턴들 (가장 구체적인 것부터)
|
# 1. ASTM 규격 패턴들 (가장 구체적인 것부터)
|
||||||
astm_patterns = [
|
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 등
|
# ASTM A312 TP304, ASTM A312 TP316L 등
|
||||||
r'ASTM\s+A\d{3,4}[A-Z]*\s+TP\d+[A-Z]*',
|
r'ASTM\s+A\d{3,4}[A-Z]*\s+TP\d+[A-Z]*',
|
||||||
# ASTM A182 F304, ASTM A182 F316L 등
|
# ASTM A182 F304, ASTM A182 F316L 등
|
||||||
@@ -32,14 +39,14 @@ def extract_full_material_grade(description: str) -> str:
|
|||||||
# ASTM A351 CF8M, ASTM A216 WCB 등
|
# ASTM A351 CF8M, ASTM A216 WCB 등
|
||||||
r'ASTM\s+A\d{3,4}[A-Z]*\s+[A-Z]{2,4}[0-9]*[A-Z]*',
|
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 포함
|
# 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+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+GRADE\s+[A-Z0-9/]+',
|
||||||
# ASTM A106 B (GR 없이 바로 등급) - 단일 문자 등급
|
# ASTM A106 B (GR 없이 바로 등급) - 단일 문자 등급
|
||||||
r'ASTM\s+A\d{3,4}[A-Z]*\s+[A-Z](?=\s|$)',
|
r'ASTM\s+A\d{3,4}[A-Z]*\s+[A-Z](?=\s|$)',
|
||||||
# ASTM A105, ASTM A234 등 (등급 없는 경우)
|
# ASTM A105, ASTM A234 등 (등급 없는 경우)
|
||||||
r'ASTM\s+A\d{3,4}[A-Z]*(?!\s+[A-Z0-9])',
|
r'ASTM\s+A\d{3,4}[A-Z]*(?!\s+[A-Z0-9])',
|
||||||
# 2자리 ASTM 규격도 지원 (A10, A36 등)
|
# 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:
|
for pattern in astm_patterns:
|
||||||
|
|||||||
@@ -291,4 +291,5 @@ def get_revision_comparison(db: Session, job_no: str, current_revision: str,
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -237,5 +237,6 @@ END $$;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
160
backend/scripts/PRODUCTION_MIGRATION.sql
Normal file
160
backend/scripts/PRODUCTION_MIGRATION.sql
Normal file
@@ -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 $$;
|
||||||
@@ -74,4 +74,5 @@ COMMENT ON COLUMN files.confirmed_by IS '구매 수량 확정자';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -975,6 +975,7 @@ CREATE TABLE IF NOT EXISTS requirement_types (
|
|||||||
CREATE TABLE IF NOT EXISTS user_requirements (
|
CREATE TABLE IF NOT EXISTS user_requirements (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
file_id INTEGER NOT NULL,
|
file_id INTEGER NOT NULL,
|
||||||
|
material_id INTEGER, -- 자재 ID (개별 자재별 요구사항 연결)
|
||||||
|
|
||||||
-- 요구사항 타입
|
-- 요구사항 타입
|
||||||
requirement_type VARCHAR(50) NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' 등
|
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,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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)
|
-- 사용자 요구사항 테이블 인덱스 (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_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_status ON user_requirements(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_requirements_type ON user_requirements(requirement_type);
|
CREATE INDEX IF NOT EXISTS idx_user_requirements_type ON user_requirements(requirement_type);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ services:
|
|||||||
- LOG_LEVEL=DEBUG
|
- LOG_LEVEL=DEBUG
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
volumes:
|
||||||
|
# 개발 시 코드 변경 실시간 반영
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules # node_modules는 컨테이너 것을 사용
|
||||||
environment:
|
environment:
|
||||||
- VITE_API_URL=http://localhost:18000
|
- VITE_API_URL=http://localhost:18000
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ services:
|
|||||||
container_name: tk-mp-frontend
|
container_name: tk-mp-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${FRONTEND_PORT:-13000}:3000"
|
- "${FRONTEND_PORT:-13000}:5173"
|
||||||
environment:
|
environment:
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:18000}
|
- VITE_API_URL=${VITE_API_URL:-http://localhost:18000}
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -237,5 +237,6 @@ export default SimpleDashboard;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -554,5 +554,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -287,5 +287,6 @@ export default NavigationBar;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -267,5 +267,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -191,5 +191,6 @@ export default NavigationMenu;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,5 +99,6 @@ export default RevisionUploadDialog;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -318,5 +318,6 @@ export default SimpleFileUpload;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -281,5 +281,6 @@ export default DashboardPage;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const LogMonitoringPage = ({ onNavigate, user }) => {
|
|||||||
|
|
||||||
const getErrorTypeIcon = (type) => {
|
const getErrorTypeIcon = (type) => {
|
||||||
const icons = {
|
const icons = {
|
||||||
'javascript_error': '🐛',
|
'javascript_error': '❌',
|
||||||
'api_error': '🌐',
|
'api_error': '🌐',
|
||||||
'user_action_error': '👤',
|
'user_action_error': '👤',
|
||||||
'promise_rejection': '⚠️',
|
'promise_rejection': '⚠️',
|
||||||
@@ -365,7 +365,7 @@ const LogMonitoringPage = ({ onNavigate, user }) => {
|
|||||||
border: '1px solid #e9ecef'
|
border: '1px solid #e9ecef'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
|
||||||
<div style={{ fontSize: '24px' }}>🐛</div>
|
<div style={{ fontSize: '24px' }}>❌</div>
|
||||||
<h3 style={{ fontSize: '16px', fontWeight: '600', color: '#495057', margin: 0 }}>
|
<h3 style={{ fontSize: '16px', fontWeight: '600', color: '#495057', margin: 0 }}>
|
||||||
최근 오류
|
최근 오류
|
||||||
</h3>
|
</h3>
|
||||||
@@ -458,7 +458,7 @@ const LogMonitoringPage = ({ onNavigate, user }) => {
|
|||||||
background: '#f8f9fa'
|
background: '#f8f9fa'
|
||||||
}}>
|
}}>
|
||||||
<h2 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', margin: 0 }}>
|
<h2 style={{ fontSize: '18px', fontWeight: '600', color: '#2d3748', margin: 0 }}>
|
||||||
🐛 프론트엔드 오류
|
❌ 프론트엔드 오류
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -236,5 +236,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -133,5 +133,6 @@ export default LoginPage;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -266,7 +266,7 @@
|
|||||||
|
|
||||||
.detailed-grid-header {
|
.detailed-grid-header {
|
||||||
display: grid;
|
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;
|
padding: 12px 24px;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
@@ -289,12 +289,12 @@
|
|||||||
|
|
||||||
/* 피팅 전용 헤더 - 10개 컬럼 */
|
/* 피팅 전용 헤더 - 10개 컬럼 */
|
||||||
.detailed-grid-header.fitting-header {
|
.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개 컬럼 */
|
/* 피팅 전용 행 - 10개 컬럼 */
|
||||||
.detailed-material-row.fitting-row {
|
.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개 컬럼 (스케줄 제거, 타입 너비 증가) */
|
/* 밸브 전용 헤더 - 9개 컬럼 (스케줄 제거, 타입 너비 증가) */
|
||||||
@@ -345,7 +345,7 @@
|
|||||||
|
|
||||||
.detailed-material-row {
|
.detailed-material-row {
|
||||||
display: grid;
|
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;
|
padding: 12px 24px;
|
||||||
border-bottom: 1px solid #f3f4f6;
|
border-bottom: 1px solid #f3f4f6;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ const NewMaterialsPage = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [materials, setMaterials] = useState([]);
|
const [materials, setMaterials] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedCategory, setSelectedCategory] = useState('PIPE');
|
const [selectedCategory, setSelectedCategory] = useState('ALL');
|
||||||
const [selectedMaterials, setSelectedMaterials] = useState(new Set());
|
const [selectedMaterials, setSelectedMaterials] = useState(new Set());
|
||||||
const [viewMode, setViewMode] = useState('detailed'); // 'detailed' or 'simple'
|
const [viewMode, setViewMode] = useState('detailed'); // 'detailed' or 'simple'
|
||||||
const [availableRevisions, setAvailableRevisions] = useState([]);
|
const [availableRevisions, setAvailableRevisions] = useState([]);
|
||||||
const [currentRevision, setCurrentRevision] = useState(revision || 'Rev.0');
|
const [currentRevision, setCurrentRevision] = useState(revision || 'Rev.0');
|
||||||
|
|
||||||
// 사용자 요구사항 상태 관리
|
// 사용자 요구사항 상태 관리
|
||||||
const [userRequirements, setUserRequirements] = useState({}); // materialId: requirement 형태
|
const [userRequirements, setUserRequirements] = useState({});
|
||||||
|
// materialId: requirement 형태
|
||||||
const [savingRequirements, setSavingRequirements] = useState(false);
|
const [savingRequirements, setSavingRequirements] = useState(false);
|
||||||
|
|
||||||
// 같은 BOM의 다른 리비전들 조회
|
// 같은 BOM의 다른 리비전들 조회
|
||||||
@@ -101,16 +102,28 @@ const NewMaterialsPage = ({
|
|||||||
params: { file_id: parseInt(id) }
|
params: { file_id: parseInt(id) }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data?.success && response.data?.requirements) {
|
if (response.data && Array.isArray(response.data)) {
|
||||||
const requirements = {};
|
const requirements = {};
|
||||||
response.data.requirements.forEach(req => {
|
console.log('📦 API 응답 데이터:', response.data);
|
||||||
|
response.data.forEach(req => {
|
||||||
// material_id를 키로 사용하여 요구사항 저장
|
// material_id를 키로 사용하여 요구사항 저장
|
||||||
if (req.material_id) {
|
if (req.material_id) {
|
||||||
requirements[req.material_id] = req.requirement_description || req.requirement_title || '';
|
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);
|
setUserRequirements(requirements);
|
||||||
|
console.log('🔄 setUserRequirements 호출 후 새로운 상태:', requirements);
|
||||||
console.log('✅ 사용자 요구사항 로드 완료:', Object.keys(requirements).length, '개');
|
console.log('✅ 사용자 요구사항 로드 완료:', Object.keys(requirements).length, '개');
|
||||||
|
|
||||||
|
// 상태 업데이트 확인을 위한 지연된 로그
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('⏰ 1초 후 실제 userRequirements 상태:', userRequirements);
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 사용자 요구사항 로딩 실패:', error);
|
console.error('❌ 사용자 요구사항 로딩 실패:', error);
|
||||||
@@ -122,11 +135,30 @@ const NewMaterialsPage = ({
|
|||||||
const saveUserRequirements = async () => {
|
const saveUserRequirements = async () => {
|
||||||
try {
|
try {
|
||||||
setSavingRequirements(true);
|
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);
|
||||||
|
console.log('📋 전체 userRequirements 객체:', Object.keys(userRequirements).length, '개');
|
||||||
|
|
||||||
// 요구사항이 있는 자재들만 저장
|
// 요구사항이 있는 자재들만 저장
|
||||||
const requirementsToSave = Object.entries(userRequirements)
|
const requirementsToSave = Object.entries(currentRequirements)
|
||||||
.filter(([materialId, requirement]) => requirement && requirement.trim())
|
.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]) => ({
|
.map(([materialId, requirement]) => ({
|
||||||
material_id: parseInt(materialId),
|
material_id: parseInt(materialId),
|
||||||
file_id: parseInt(fileId),
|
file_id: parseInt(fileId),
|
||||||
@@ -136,24 +168,52 @@ const NewMaterialsPage = ({
|
|||||||
priority: 'NORMAL'
|
priority: 'NORMAL'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log('📝 저장할 요구사항 개수:', requirementsToSave.length);
|
||||||
|
|
||||||
if (requirementsToSave.length === 0) {
|
if (requirementsToSave.length === 0) {
|
||||||
alert('저장할 요구사항이 없습니다.');
|
alert('저장할 요구사항이 없습니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 기존 요구사항 삭제 후 새로 저장
|
// 기존 요구사항 삭제 후 새로 저장
|
||||||
await api.delete(`/files/user-requirements`, {
|
console.log('🗑️ 기존 요구사항 삭제 중...', { file_id: parseInt(fileId) });
|
||||||
params: { 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) {
|
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}개의 사용자 요구사항이 저장되었습니다.`);
|
alert(`${requirementsToSave.length}개의 사용자 요구사항이 저장되었습니다.`);
|
||||||
console.log('✅ 사용자 요구사항 저장 완료');
|
console.log('✅ 사용자 요구사항 저장 완료');
|
||||||
|
|
||||||
|
// 저장 후 다시 로드하여 최신 상태 반영
|
||||||
|
console.log('🔄 저장 완료 후 다시 로드 시작...');
|
||||||
|
await loadUserRequirements(fileId);
|
||||||
|
console.log('🔄 저장 완료 후 다시 로드 완료!');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 사용자 요구사항 저장 실패:', error);
|
console.error('❌ 사용자 요구사항 저장 실패:', error);
|
||||||
alert('사용자 요구사항 저장에 실패했습니다: ' + (error.response?.data?.detail || error.message));
|
alert('사용자 요구사항 저장에 실패했습니다: ' + (error.response?.data?.detail || error.message));
|
||||||
@@ -164,10 +224,15 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
// 사용자 요구사항 입력 핸들러
|
// 사용자 요구사항 입력 핸들러
|
||||||
const handleUserRequirementChange = (materialId, value) => {
|
const handleUserRequirementChange = (materialId, value) => {
|
||||||
setUserRequirements(prev => ({
|
console.log(`📝 사용자 요구사항 입력: 자재 ID ${materialId} = "${value}"`);
|
||||||
...prev,
|
setUserRequirements(prev => {
|
||||||
[materialId]: value
|
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 parseMaterialInfo = (material) => {
|
||||||
const category = material.classified_category;
|
const category = material.classified_category;
|
||||||
@@ -223,15 +360,41 @@ const NewMaterialsPage = ({
|
|||||||
};
|
};
|
||||||
} else if (category === 'FITTING') {
|
} else if (category === 'FITTING') {
|
||||||
const fittingDetails = material.fitting_details || {};
|
const fittingDetails = material.fitting_details || {};
|
||||||
const fittingType = fittingDetails.fitting_type || '';
|
const classificationDetails = material.classification_details || {};
|
||||||
const fittingSubtype = fittingDetails.fitting_subtype || '';
|
|
||||||
|
// 개선된 분류기 결과 우선 사용
|
||||||
|
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 || '';
|
const description = material.original_description || '';
|
||||||
|
|
||||||
// 피팅 타입별 상세 표시
|
// 피팅 타입별 상세 표시
|
||||||
let displayType = '';
|
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)
|
// CAP: 연결 방식 표시 (예: CAP, NPT(F), 3000LB, ASTM A105)
|
||||||
if (description.includes('NPT(F)')) {
|
if (description.includes('NPT(F)')) {
|
||||||
displayType = 'CAP NPT(F)';
|
displayType = 'CAP NPT(F)';
|
||||||
@@ -260,7 +423,13 @@ const NewMaterialsPage = ({
|
|||||||
} else if (fittingType === 'NIPPLE') {
|
} else if (fittingType === 'NIPPLE') {
|
||||||
// 니플: 길이와 끝단 가공 정보
|
// 니플: 길이와 끝단 가공 정보
|
||||||
const length = fittingDetails.length_mm || fittingDetails.avg_length_mm;
|
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') {
|
} else if (fittingType === 'ELBOW') {
|
||||||
// 엘보: 각도와 연결 방식
|
// 엘보: 각도와 연결 방식
|
||||||
const angle = fittingSubtype === '90DEG' ? '90°' : fittingSubtype === '45DEG' ? '45°' : '';
|
const angle = fittingSubtype === '90DEG' ? '90°' : fittingSubtype === '45DEG' ? '45°' : '';
|
||||||
@@ -295,11 +464,25 @@ const NewMaterialsPage = ({
|
|||||||
pressure = `${pressureMatch[1]}LB`;
|
pressure = `${pressureMatch[1]}LB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 스케줄 찾기
|
// 스케줄 표시 (분리 스케줄 지원)
|
||||||
if (description.includes('SCH')) {
|
if (hasDifferentSchedules && mainSchedule && redSchedule) {
|
||||||
const schMatch = description.match(/SCH\s*(\d+[A-Z]*)/i);
|
// 분리 스케줄: "SCH 40 x SCH 80"
|
||||||
if (schMatch) {
|
schedule = `${mainSchedule} x ${redSchedule}`;
|
||||||
schedule = `SCH ${schMatch[1]}`;
|
} 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 qty = Math.round(material.quantity || 0);
|
||||||
const safetyQty = Math.ceil(qty * 1.05); // 5% 여유율
|
const safetyQty = Math.ceil(qty * 1.05); // 5% 여유율
|
||||||
const purchaseQty = Math.ceil(safetyQty / 4) * 4; // 4의 배수
|
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 {
|
return {
|
||||||
type: 'BOLT',
|
type: 'BOLT',
|
||||||
subtype: material.bolt_details?.bolt_type || '-',
|
subtype: material.bolt_details?.bolt_type || 'BOLT_GENERAL',
|
||||||
size: material.size_spec || '-',
|
size: material.size_spec || '-',
|
||||||
schedule: material.bolt_details?.length || '-',
|
schedule: boltLength, // 길이 정보
|
||||||
grade: material.material_grade || '-',
|
grade: material.full_material_grade || material.material_grade || '-',
|
||||||
|
additionalReq: additionalReq, // 추가요구사항
|
||||||
quantity: purchaseQty,
|
quantity: purchaseQty,
|
||||||
unit: 'SETS'
|
unit: 'SETS'
|
||||||
};
|
};
|
||||||
@@ -473,6 +681,9 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
// 필터링된 자재 목록
|
// 필터링된 자재 목록
|
||||||
const filteredMaterials = materials.filter(material => {
|
const filteredMaterials = materials.filter(material => {
|
||||||
|
if (selectedCategory === 'ALL') {
|
||||||
|
return true; // 전체 카테고리일 때는 모든 자재 표시
|
||||||
|
}
|
||||||
return material.classified_category === selectedCategory;
|
return material.classified_category === selectedCategory;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -508,101 +719,51 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
console.log('📊 엑셀 내보내기:', dataToExport.length, '개 항목');
|
console.log('📊 엑셀 내보내기:', dataToExport.length, '개 항목');
|
||||||
|
|
||||||
// 카테고리별 컬럼 구성
|
// 새로운 엑셀 양식에 맞춘 컬럼 구성
|
||||||
const getExcelData = (material) => {
|
const getExcelData = (material) => {
|
||||||
const info = parseMaterialInfo(material);
|
const info = parseMaterialInfo(material);
|
||||||
|
|
||||||
|
// 품목명 생성 (간단하게)
|
||||||
|
let itemName = '';
|
||||||
if (selectedCategory === 'PIPE') {
|
if (selectedCategory === 'PIPE') {
|
||||||
return {
|
itemName = info.subtype || 'PIPE';
|
||||||
'종류': info.type,
|
} else if (selectedCategory === 'FITTING') {
|
||||||
'타입': info.subtype,
|
itemName = info.subtype || 'FITTING';
|
||||||
'크기': info.size,
|
} else if (selectedCategory === 'FLANGE') {
|
||||||
'스케줄': info.schedule,
|
itemName = info.subtype || 'FLANGE';
|
||||||
'재질': info.grade,
|
} else if (selectedCategory === 'VALVE') {
|
||||||
'추가요구': '-',
|
itemName = info.valveType || info.subtype || 'VALVE';
|
||||||
'사용자요구': userRequirements[material.id] || '',
|
} else if (selectedCategory === 'GASKET') {
|
||||||
'수량': `${info.quantity} ${info.unit}`,
|
itemName = info.subtype || 'GASKET';
|
||||||
'상세': `단관 ${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}`
|
|
||||||
};
|
|
||||||
} else if (selectedCategory === 'BOLT') {
|
} else if (selectedCategory === 'BOLT') {
|
||||||
return {
|
itemName = info.subtype || 'BOLT';
|
||||||
'종류': 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}`
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
// 기본 형식
|
itemName = info.subtype || info.type || 'UNKNOWN';
|
||||||
return {
|
|
||||||
'종류': info.type,
|
|
||||||
'타입': info.subtype,
|
|
||||||
'크기': info.size,
|
|
||||||
'스케줄': info.schedule,
|
|
||||||
'재질': info.grade,
|
|
||||||
'추가요구': '-',
|
|
||||||
'사용자요구': userRequirements[material.id] || '',
|
|
||||||
'수량': `${info.quantity} ${info.unit}`
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 사용자 요구사항 확인
|
||||||
|
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 = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="header-right">
|
<div className="header-right">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const pageInfo = `페이지: NewMaterialsPage\nURL: ${currentUrl}\nJob: ${jobNo}\nRevision: ${currentRevision}`;
|
||||||
|
alert(pageInfo);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
background: '#667eea',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '11px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
marginRight: '12px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🔗 URL
|
||||||
|
</button>
|
||||||
<span className="material-count">
|
<span className="material-count">
|
||||||
총 {materials.length}개 자재 ({currentRevision})
|
총 {materials.length}개 자재 ({currentRevision})
|
||||||
</span>
|
</span>
|
||||||
@@ -702,13 +882,22 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
{/* 카테고리 필터 */}
|
{/* 카테고리 필터 */}
|
||||||
<div className="category-filters">
|
<div className="category-filters">
|
||||||
|
{/* 전체 카테고리 버튼 */}
|
||||||
|
<button
|
||||||
|
key="ALL"
|
||||||
|
className={`category-btn ${selectedCategory === 'ALL' ? 'active' : ''}`}
|
||||||
|
onClick={() => setSelectedCategory('ALL')}
|
||||||
|
>
|
||||||
|
전체 <span className="count">{materials.length}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{Object.entries(categoryCounts).map(([category, count]) => (
|
{Object.entries(categoryCounts).map(([category, count]) => (
|
||||||
<button
|
<button
|
||||||
key={category}
|
key={category}
|
||||||
className={`category-btn ${selectedCategory === category ? 'active' : ''}`}
|
className={`category-btn ${selectedCategory === category ? 'active' : ''}`}
|
||||||
onClick={() => setSelectedCategory(category)}
|
onClick={() => setSelectedCategory(category)}
|
||||||
>
|
>
|
||||||
{category} <span className="count">{count}</span>
|
{getCategoryDisplayName(category)} <span className="count">{count}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -885,7 +1074,7 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
{/* 추가요구 */}
|
{/* 추가요구 */}
|
||||||
<div className="material-cell">
|
<div className="material-cell">
|
||||||
<span>-</span>
|
<span>{info.additionalReq || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사용자요구 */}
|
{/* 사용자요구 */}
|
||||||
@@ -895,7 +1084,10 @@ const NewMaterialsPage = ({
|
|||||||
className="user-req-input"
|
className="user-req-input"
|
||||||
placeholder="요구사항 입력"
|
placeholder="요구사항 입력"
|
||||||
value={userRequirements[material.id] || ''}
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -954,7 +1146,7 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
{/* 추가요구 */}
|
{/* 추가요구 */}
|
||||||
<div className="material-cell">
|
<div className="material-cell">
|
||||||
<span>-</span>
|
<span>{info.additionalReq || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사용자요구 */}
|
{/* 사용자요구 */}
|
||||||
@@ -964,7 +1156,10 @@ const NewMaterialsPage = ({
|
|||||||
className="user-req-input"
|
className="user-req-input"
|
||||||
placeholder="요구사항 입력"
|
placeholder="요구사항 입력"
|
||||||
value={userRequirements[material.id] || ''}
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1030,7 +1225,7 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
{/* 추가요구 */}
|
{/* 추가요구 */}
|
||||||
<div className="material-cell">
|
<div className="material-cell">
|
||||||
<span>-</span>
|
<span>{info.additionalReq || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사용자요구 */}
|
{/* 사용자요구 */}
|
||||||
@@ -1040,7 +1235,10 @@ const NewMaterialsPage = ({
|
|||||||
className="user-req-input"
|
className="user-req-input"
|
||||||
placeholder="요구사항 입력"
|
placeholder="요구사항 입력"
|
||||||
value={userRequirements[material.id] || ''}
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1093,7 +1291,10 @@ const NewMaterialsPage = ({
|
|||||||
className="user-req-input"
|
className="user-req-input"
|
||||||
placeholder="요구사항 입력"
|
placeholder="요구사항 입력"
|
||||||
value={userRequirements[material.id] || ''}
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1164,7 +1365,7 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
{/* 추가요구 */}
|
{/* 추가요구 */}
|
||||||
<div className="material-cell">
|
<div className="material-cell">
|
||||||
<span>-</span>
|
<span>{info.additionalReq || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사용자요구 */}
|
{/* 사용자요구 */}
|
||||||
@@ -1174,7 +1375,10 @@ const NewMaterialsPage = ({
|
|||||||
className="user-req-input"
|
className="user-req-input"
|
||||||
placeholder="요구사항 입력"
|
placeholder="요구사항 입력"
|
||||||
value={userRequirements[material.id] || ''}
|
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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1234,7 +1438,7 @@ const NewMaterialsPage = ({
|
|||||||
|
|
||||||
{/* 추가요구 */}
|
{/* 추가요구 */}
|
||||||
<div className="material-cell">
|
<div className="material-cell">
|
||||||
<span>-</span>
|
<span>{info.additionalReq || '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 사용자요구 */}
|
{/* 사용자요구 */}
|
||||||
@@ -1243,6 +1447,8 @@ const NewMaterialsPage = ({
|
|||||||
type="text"
|
type="text"
|
||||||
className="user-req-input"
|
className="user-req-input"
|
||||||
placeholder="요구사항 입력"
|
placeholder="요구사항 입력"
|
||||||
|
value={userRequirements[material.id] || ''}
|
||||||
|
onChange={(e) => handleUserRequirementChange(material.id, e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -405,5 +405,6 @@ export default ProjectsPage;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -447,5 +447,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ const consolidateMaterials = (materials, isComparison = false) => {
|
|||||||
*/
|
*/
|
||||||
const formatMaterialForExcel = (material, includeComparison = false) => {
|
const formatMaterialForExcel = (material, includeComparison = false) => {
|
||||||
const category = material.classified_category || material.category || '-';
|
const category = material.classified_category || material.category || '-';
|
||||||
const isPipe = category === 'PIPE';
|
|
||||||
|
|
||||||
// 엑셀용 자재 설명 정제
|
// 엑셀용 자재 설명 정제
|
||||||
let cleanDescription = material.original_description || material.description || '-';
|
let cleanDescription = material.original_description || material.description || '-';
|
||||||
@@ -135,16 +134,12 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
|
|
||||||
// 니플의 경우 길이 정보 명시적 추가
|
// 니플의 경우 길이 정보 명시적 추가
|
||||||
if (category === 'FITTING' && cleanDescription.toLowerCase().includes('nipple')) {
|
if (category === 'FITTING' && cleanDescription.toLowerCase().includes('nipple')) {
|
||||||
// fitting_details에서 길이 정보 가져오기
|
|
||||||
if (material.fitting_details && material.fitting_details.length_mm) {
|
if (material.fitting_details && material.fitting_details.length_mm) {
|
||||||
const lengthMm = Math.round(material.fitting_details.length_mm);
|
const lengthMm = Math.round(material.fitting_details.length_mm);
|
||||||
// 이미 길이 정보가 있는지 확인
|
|
||||||
if (!cleanDescription.match(/\d+\s*mm/i)) {
|
if (!cleanDescription.match(/\d+\s*mm/i)) {
|
||||||
cleanDescription += ` ${lengthMm}mm`;
|
cleanDescription += ` ${lengthMm}mm`;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
// 또는 기존 설명에서 길이 정보 추출
|
|
||||||
else {
|
|
||||||
const lengthMatch = material.original_description?.match(/(\d+)\s*mm/i);
|
const lengthMatch = material.original_description?.match(/(\d+)\s*mm/i);
|
||||||
if (lengthMatch && !cleanDescription.match(/\d+\s*mm/i)) {
|
if (lengthMatch && !cleanDescription.match(/\d+\s*mm/i)) {
|
||||||
cleanDescription += ` ${lengthMatch[1]}mm`;
|
cleanDescription += ` ${lengthMatch[1]}mm`;
|
||||||
@@ -155,31 +150,79 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
// 구매 수량 계산
|
// 구매 수량 계산
|
||||||
const purchaseInfo = calculatePurchaseQuantity(material);
|
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 = {
|
const base = {
|
||||||
'카테고리': category,
|
'TAGNO': '', // 비워둠
|
||||||
'자재 설명': cleanDescription,
|
'품목명': itemName,
|
||||||
'사이즈': material.size_spec || '-'
|
'수량': 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 (includeComparison) {
|
||||||
if (material.previous_quantity !== undefined) {
|
if (material.previous_quantity !== undefined) {
|
||||||
// 이전 구매 수량 계산
|
|
||||||
const prevPurchaseInfo = calculatePurchaseQuantity({
|
const prevPurchaseInfo = calculatePurchaseQuantity({
|
||||||
...material,
|
...material,
|
||||||
quantity: material.previous_quantity,
|
quantity: material.previous_quantity,
|
||||||
totalLength: material.previousTotalLength || 0
|
totalLength: material.previousTotalLength || 0
|
||||||
});
|
});
|
||||||
|
|
||||||
base['이전 필요 수량'] = prevPurchaseInfo.purchaseQuantity || 0;
|
base['이전수량'] = prevPurchaseInfo.purchaseQuantity || 0;
|
||||||
base['필요 수량 변경'] = (purchaseInfo.purchaseQuantity - prevPurchaseInfo.purchaseQuantity);
|
base['수량변경'] = (purchaseInfo.purchaseQuantity - prevPurchaseInfo.purchaseQuantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
base['변경 유형'] = material.change_type || (
|
base['변경유형'] = material.change_type || (
|
||||||
material.previous_quantity !== undefined ? '수량 변경' :
|
material.previous_quantity !== undefined ? '수량 변경' :
|
||||||
material.quantity_change === undefined ? '신규' : '변경'
|
material.quantity_change === undefined ? '신규' : '변경'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import react from '@vitejs/plugin-react'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 13000,
|
port: 5173,
|
||||||
host: true,
|
host: true,
|
||||||
open: true
|
open: false
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
|||||||
2
test_bom.csv
Normal file
2
test_bom.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Item,Description,Quantity
|
||||||
|
1,Test Item,10
|
||||||
|
Reference in New Issue
Block a user