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:
@@ -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)
|
||||
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' 등
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
"""볼트 구매 정보 생성"""
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 $$;
|
||||
Reference in New Issue
Block a user