feat: SWG 가스켓 전체 구성 정보 표시 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- H/F/I/O SS304/GRAPHITE/CS/CS 패턴에서 4개 구성요소 모두 표시 - 기존 SS304 + GRAPHITE → SS304/GRAPHITE/CS/CS로 완전한 구성 표시 - 외부링/필러/내부링/추가구성 모든 정보 포함 - 구매수량 계산 모달에서 정확한 재질 정보 확인 가능
This commit is contained in:
@@ -12,7 +12,11 @@ from pathlib import Path
|
||||
import json
|
||||
|
||||
from ..database import get_db
|
||||
from ..utils.logger import get_logger
|
||||
from app.services.material_classifier import classify_material
|
||||
|
||||
# 로거 설정
|
||||
logger = get_logger(__name__)
|
||||
from app.services.integrated_classifier import classify_material_integrated, should_exclude_material
|
||||
from app.services.bolt_classifier import classify_bolt
|
||||
from app.services.flange_classifier import classify_flange
|
||||
@@ -664,10 +668,15 @@ async def upload_file(
|
||||
else:
|
||||
gasket_type = str(gasket_type_info) if gasket_type_info else "UNKNOWN"
|
||||
|
||||
# 가스켓 소재 (GRAPHITE, PTFE 등)
|
||||
# 가스켓 소재 - SWG의 경우 메탈 부분을 우선으로
|
||||
material_type = ""
|
||||
if isinstance(gasket_material_info, dict):
|
||||
material_type = gasket_material_info.get("material", "UNKNOWN")
|
||||
# SWG 상세 정보가 있으면 메탈 부분을 material_type으로 사용
|
||||
swg_details = gasket_material_info.get("swg_details", {})
|
||||
if swg_details and swg_details.get("outer_ring"):
|
||||
material_type = swg_details.get("outer_ring", "UNKNOWN") # SS304
|
||||
else:
|
||||
material_type = gasket_material_info.get("material", "UNKNOWN")
|
||||
else:
|
||||
material_type = str(gasket_material_info) if gasket_material_info else "UNKNOWN"
|
||||
|
||||
@@ -978,7 +987,7 @@ async def get_files(
|
||||
try:
|
||||
query = """
|
||||
SELECT id, filename, original_filename, job_no, revision,
|
||||
description, file_size, parsed_count, created_at, is_active
|
||||
description, file_size, parsed_count, upload_date, is_active
|
||||
FROM files
|
||||
WHERE is_active = TRUE
|
||||
"""
|
||||
@@ -988,7 +997,7 @@ async def get_files(
|
||||
query += " AND job_no = :job_no"
|
||||
params["job_no"] = job_no
|
||||
|
||||
query += " ORDER BY created_at DESC"
|
||||
query += " ORDER BY upload_date DESC"
|
||||
|
||||
result = db.execute(text(query), params)
|
||||
files = result.fetchall()
|
||||
@@ -1003,7 +1012,7 @@ async def get_files(
|
||||
"description": file.description,
|
||||
"file_size": file.file_size,
|
||||
"parsed_count": file.parsed_count,
|
||||
"created_at": file.created_at,
|
||||
"created_at": file.upload_date,
|
||||
"is_active": file.is_active
|
||||
}
|
||||
for file in files
|
||||
@@ -1012,6 +1021,47 @@ async def get_files(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"파일 목록 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_files_stats(db: Session = Depends(get_db)):
|
||||
"""파일 및 자재 통계 조회"""
|
||||
try:
|
||||
# 총 파일 수
|
||||
files_query = text("SELECT COUNT(*) FROM files WHERE is_active = true")
|
||||
total_files = db.execute(files_query).fetchone()[0]
|
||||
|
||||
# 총 자재 수
|
||||
materials_query = text("SELECT COUNT(*) FROM materials")
|
||||
total_materials = db.execute(materials_query).fetchone()[0]
|
||||
|
||||
# 최근 업로드 (최근 5개)
|
||||
recent_query = text("""
|
||||
SELECT f.original_filename, f.upload_date, f.parsed_count, j.job_name
|
||||
FROM files f
|
||||
LEFT JOIN jobs j ON f.job_no = j.job_no
|
||||
WHERE f.is_active = true
|
||||
ORDER BY f.upload_date DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
recent_uploads = db.execute(recent_query).fetchall()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"totalFiles": total_files,
|
||||
"totalMaterials": total_materials,
|
||||
"recentUploads": [
|
||||
{
|
||||
"filename": upload.original_filename,
|
||||
"created_at": upload.upload_date,
|
||||
"parsed_count": upload.parsed_count or 0,
|
||||
"project_name": upload.job_name or "Unknown"
|
||||
}
|
||||
for upload in recent_uploads
|
||||
]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"통계 조회 실패: {str(e)}")
|
||||
|
||||
@router.delete("/files/{file_id}")
|
||||
async def delete_file(file_id: int, db: Session = Depends(get_db)):
|
||||
"""파일 삭제"""
|
||||
|
||||
@@ -20,6 +20,7 @@ class JobCreate(BaseModel):
|
||||
contract_date: Optional[date] = None
|
||||
delivery_date: Optional[date] = None
|
||||
delivery_terms: Optional[str] = None
|
||||
project_type: Optional[str] = "냉동기"
|
||||
description: Optional[str] = None
|
||||
|
||||
@router.get("/")
|
||||
@@ -34,7 +35,7 @@ async def get_jobs(
|
||||
query = """
|
||||
SELECT job_no, job_name, client_name, end_user, epc_company,
|
||||
project_site, contract_date, delivery_date, delivery_terms,
|
||||
status, description, created_by, created_at, updated_at, is_active
|
||||
project_type, status, description, created_by, created_at, updated_at, is_active
|
||||
FROM jobs
|
||||
WHERE is_active = true
|
||||
"""
|
||||
@@ -66,6 +67,7 @@ async def get_jobs(
|
||||
"contract_date": job.contract_date,
|
||||
"delivery_date": job.delivery_date,
|
||||
"delivery_terms": job.delivery_terms,
|
||||
"project_type": job.project_type,
|
||||
"status": job.status,
|
||||
"description": job.description,
|
||||
"created_at": job.created_at,
|
||||
@@ -85,7 +87,7 @@ async def get_job(job_no: str, db: Session = Depends(get_db)):
|
||||
query = text("""
|
||||
SELECT job_no, job_name, client_name, end_user, epc_company,
|
||||
project_site, contract_date, delivery_date, delivery_terms,
|
||||
status, description, created_by, created_at, updated_at, is_active
|
||||
project_type, status, description, created_by, created_at, updated_at, is_active
|
||||
FROM jobs
|
||||
WHERE job_no = :job_no AND is_active = true
|
||||
""")
|
||||
@@ -108,6 +110,7 @@ async def get_job(job_no: str, db: Session = Depends(get_db)):
|
||||
"contract_date": job.contract_date,
|
||||
"delivery_date": job.delivery_date,
|
||||
"delivery_terms": job.delivery_terms,
|
||||
"project_type": job.project_type,
|
||||
"status": job.status,
|
||||
"description": job.description,
|
||||
"created_by": job.created_by,
|
||||
@@ -139,14 +142,14 @@ async def create_job(job: JobCreate, db: Session = Depends(get_db)):
|
||||
INSERT INTO jobs (
|
||||
job_no, job_name, client_name, end_user, epc_company,
|
||||
project_site, contract_date, delivery_date, delivery_terms,
|
||||
description, created_by, status, is_active
|
||||
project_type, description, created_by, status, is_active
|
||||
)
|
||||
VALUES (
|
||||
:job_no, :job_name, :client_name, :end_user, :epc_company,
|
||||
:project_site, :contract_date, :delivery_date, :delivery_terms,
|
||||
:description, :created_by, :status, :is_active
|
||||
:project_type, :description, :created_by, :status, :is_active
|
||||
)
|
||||
RETURNING job_no, job_name, client_name
|
||||
RETURNING job_no, job_name, client_name, project_type
|
||||
""")
|
||||
|
||||
result = db.execute(insert_query, {
|
||||
@@ -165,7 +168,8 @@ async def create_job(job: JobCreate, db: Session = Depends(get_db)):
|
||||
"job": {
|
||||
"job_no": new_job.job_no,
|
||||
"job_name": new_job.job_name,
|
||||
"client_name": new_job.client_name
|
||||
"client_name": new_job.client_name,
|
||||
"project_type": new_job.project_type
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
538
backend/app/routers/tubing.py
Normal file
538
backend/app/routers/tubing.py
Normal file
@@ -0,0 +1,538 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import text
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, date
|
||||
from pydantic import BaseModel
|
||||
from decimal import Decimal
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
TubingCategory, TubingSpecification, TubingManufacturer,
|
||||
TubingProduct, MaterialTubingMapping, Material
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ================================
|
||||
# Pydantic 모델들
|
||||
# ================================
|
||||
|
||||
class TubingCategoryResponse(BaseModel):
|
||||
id: int
|
||||
category_code: str
|
||||
category_name: str
|
||||
description: Optional[str] = None
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class TubingManufacturerResponse(BaseModel):
|
||||
id: int
|
||||
manufacturer_code: str
|
||||
manufacturer_name: str
|
||||
country: Optional[str] = None
|
||||
website: Optional[str] = None
|
||||
is_active: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class TubingSpecificationResponse(BaseModel):
|
||||
id: int
|
||||
spec_code: str
|
||||
spec_name: str
|
||||
category_name: Optional[str] = None
|
||||
outer_diameter_mm: Optional[float] = None
|
||||
wall_thickness_mm: Optional[float] = None
|
||||
inner_diameter_mm: Optional[float] = None
|
||||
material_grade: Optional[str] = None
|
||||
material_standard: Optional[str] = None
|
||||
max_pressure_bar: Optional[float] = None
|
||||
max_temperature_c: Optional[float] = None
|
||||
min_temperature_c: Optional[float] = None
|
||||
standard_length_m: Optional[float] = None
|
||||
surface_finish: Optional[str] = None
|
||||
is_active: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class TubingProductResponse(BaseModel):
|
||||
id: int
|
||||
specification_id: int
|
||||
manufacturer_id: int
|
||||
manufacturer_part_number: str
|
||||
manufacturer_product_name: Optional[str] = None
|
||||
spec_name: Optional[str] = None
|
||||
manufacturer_name: Optional[str] = None
|
||||
list_price: Optional[float] = None
|
||||
currency: Optional[str] = 'KRW'
|
||||
lead_time_days: Optional[int] = None
|
||||
availability_status: Optional[str] = None
|
||||
datasheet_url: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
is_active: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class TubingProductCreate(BaseModel):
|
||||
specification_id: int
|
||||
manufacturer_id: int
|
||||
manufacturer_part_number: str
|
||||
manufacturer_product_name: Optional[str] = None
|
||||
list_price: Optional[float] = None
|
||||
currency: str = 'KRW'
|
||||
lead_time_days: Optional[int] = None
|
||||
minimum_order_qty: Optional[float] = None
|
||||
standard_packaging_qty: Optional[float] = None
|
||||
availability_status: Optional[str] = None
|
||||
datasheet_url: Optional[str] = None
|
||||
catalog_page: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
class MaterialTubingMappingCreate(BaseModel):
|
||||
material_id: int
|
||||
tubing_product_id: int
|
||||
confidence_score: Optional[float] = None
|
||||
mapping_method: str = 'manual'
|
||||
required_length_m: Optional[float] = None
|
||||
calculated_quantity: Optional[float] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
# ================================
|
||||
# API 엔드포인트들
|
||||
# ================================
|
||||
|
||||
@router.get("/categories", response_model=List[TubingCategoryResponse])
|
||||
async def get_tubing_categories(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Tubing 카테고리 목록 조회"""
|
||||
try:
|
||||
categories = db.query(TubingCategory)\
|
||||
.filter(TubingCategory.is_active == True)\
|
||||
.offset(skip)\
|
||||
.limit(limit)\
|
||||
.all()
|
||||
|
||||
return categories
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"카테고리 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/manufacturers", response_model=List[TubingManufacturerResponse])
|
||||
async def get_tubing_manufacturers(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: Optional[str] = Query(None),
|
||||
country: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Tubing 제조사 목록 조회"""
|
||||
try:
|
||||
query = db.query(TubingManufacturer)\
|
||||
.filter(TubingManufacturer.is_active == True)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
TubingManufacturer.manufacturer_name.ilike(f"%{search}%") |
|
||||
TubingManufacturer.manufacturer_code.ilike(f"%{search}%")
|
||||
)
|
||||
|
||||
if country:
|
||||
query = query.filter(TubingManufacturer.country.ilike(f"%{country}%"))
|
||||
|
||||
manufacturers = query.offset(skip).limit(limit).all()
|
||||
return manufacturers
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"제조사 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/specifications", response_model=List[TubingSpecificationResponse])
|
||||
async def get_tubing_specifications(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
category_id: Optional[int] = Query(None),
|
||||
material_grade: Optional[str] = Query(None),
|
||||
outer_diameter_min: Optional[float] = Query(None),
|
||||
outer_diameter_max: Optional[float] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Tubing 규격 목록 조회"""
|
||||
try:
|
||||
query = db.query(TubingSpecification)\
|
||||
.options(joinedload(TubingSpecification.category))\
|
||||
.filter(TubingSpecification.is_active == True)
|
||||
|
||||
if category_id:
|
||||
query = query.filter(TubingSpecification.category_id == category_id)
|
||||
|
||||
if material_grade:
|
||||
query = query.filter(TubingSpecification.material_grade.ilike(f"%{material_grade}%"))
|
||||
|
||||
if outer_diameter_min:
|
||||
query = query.filter(TubingSpecification.outer_diameter_mm >= outer_diameter_min)
|
||||
|
||||
if outer_diameter_max:
|
||||
query = query.filter(TubingSpecification.outer_diameter_mm <= outer_diameter_max)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
TubingSpecification.spec_name.ilike(f"%{search}%") |
|
||||
TubingSpecification.spec_code.ilike(f"%{search}%") |
|
||||
TubingSpecification.material_grade.ilike(f"%{search}%")
|
||||
)
|
||||
|
||||
specifications = query.offset(skip).limit(limit).all()
|
||||
|
||||
# 응답 데이터 변환
|
||||
result = []
|
||||
for spec in specifications:
|
||||
spec_dict = {
|
||||
"id": spec.id,
|
||||
"spec_code": spec.spec_code,
|
||||
"spec_name": spec.spec_name,
|
||||
"category_name": spec.category.category_name if spec.category else None,
|
||||
"outer_diameter_mm": float(spec.outer_diameter_mm) if spec.outer_diameter_mm else None,
|
||||
"wall_thickness_mm": float(spec.wall_thickness_mm) if spec.wall_thickness_mm else None,
|
||||
"inner_diameter_mm": float(spec.inner_diameter_mm) if spec.inner_diameter_mm else None,
|
||||
"material_grade": spec.material_grade,
|
||||
"material_standard": spec.material_standard,
|
||||
"max_pressure_bar": float(spec.max_pressure_bar) if spec.max_pressure_bar else None,
|
||||
"max_temperature_c": float(spec.max_temperature_c) if spec.max_temperature_c else None,
|
||||
"min_temperature_c": float(spec.min_temperature_c) if spec.min_temperature_c else None,
|
||||
"standard_length_m": float(spec.standard_length_m) if spec.standard_length_m else None,
|
||||
"surface_finish": spec.surface_finish,
|
||||
"is_active": spec.is_active
|
||||
}
|
||||
result.append(spec_dict)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"규격 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/products", response_model=List[TubingProductResponse])
|
||||
async def get_tubing_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
specification_id: Optional[int] = Query(None),
|
||||
manufacturer_id: Optional[int] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Tubing 제품 목록 조회"""
|
||||
try:
|
||||
query = db.query(TubingProduct)\
|
||||
.options(
|
||||
joinedload(TubingProduct.specification),
|
||||
joinedload(TubingProduct.manufacturer)
|
||||
)\
|
||||
.filter(TubingProduct.is_active == True)
|
||||
|
||||
if specification_id:
|
||||
query = query.filter(TubingProduct.specification_id == specification_id)
|
||||
|
||||
if manufacturer_id:
|
||||
query = query.filter(TubingProduct.manufacturer_id == manufacturer_id)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
TubingProduct.manufacturer_part_number.ilike(f"%{search}%") |
|
||||
TubingProduct.manufacturer_product_name.ilike(f"%{search}%")
|
||||
)
|
||||
|
||||
products = query.offset(skip).limit(limit).all()
|
||||
|
||||
# 응답 데이터 변환
|
||||
result = []
|
||||
for product in products:
|
||||
product_dict = {
|
||||
"id": product.id,
|
||||
"specification_id": product.specification_id,
|
||||
"manufacturer_id": product.manufacturer_id,
|
||||
"manufacturer_part_number": product.manufacturer_part_number,
|
||||
"manufacturer_product_name": product.manufacturer_product_name,
|
||||
"spec_name": product.specification.spec_name if product.specification else None,
|
||||
"manufacturer_name": product.manufacturer.manufacturer_name if product.manufacturer else None,
|
||||
"list_price": float(product.list_price) if product.list_price else None,
|
||||
"currency": product.currency,
|
||||
"lead_time_days": product.lead_time_days,
|
||||
"availability_status": product.availability_status,
|
||||
"datasheet_url": product.datasheet_url,
|
||||
"notes": product.notes,
|
||||
"is_active": product.is_active
|
||||
}
|
||||
result.append(product_dict)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"제품 조회 실패: {str(e)}")
|
||||
|
||||
@router.post("/products", response_model=TubingProductResponse)
|
||||
async def create_tubing_product(
|
||||
product_data: TubingProductCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""새 Tubing 제품 등록"""
|
||||
try:
|
||||
# 중복 확인
|
||||
existing = db.query(TubingProduct)\
|
||||
.filter(
|
||||
TubingProduct.specification_id == product_data.specification_id,
|
||||
TubingProduct.manufacturer_id == product_data.manufacturer_id,
|
||||
TubingProduct.manufacturer_part_number == product_data.manufacturer_part_number
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"동일한 제품이 이미 등록되어 있습니다: {product_data.manufacturer_part_number}"
|
||||
)
|
||||
|
||||
# 새 제품 생성
|
||||
new_product = TubingProduct(**product_data.dict())
|
||||
db.add(new_product)
|
||||
db.commit()
|
||||
db.refresh(new_product)
|
||||
|
||||
# 관련 정보와 함께 조회
|
||||
product_with_relations = db.query(TubingProduct)\
|
||||
.options(
|
||||
joinedload(TubingProduct.specification),
|
||||
joinedload(TubingProduct.manufacturer)
|
||||
)\
|
||||
.filter(TubingProduct.id == new_product.id)\
|
||||
.first()
|
||||
|
||||
return {
|
||||
"id": product_with_relations.id,
|
||||
"specification_id": product_with_relations.specification_id,
|
||||
"manufacturer_id": product_with_relations.manufacturer_id,
|
||||
"manufacturer_part_number": product_with_relations.manufacturer_part_number,
|
||||
"manufacturer_product_name": product_with_relations.manufacturer_product_name,
|
||||
"spec_name": product_with_relations.specification.spec_name if product_with_relations.specification else None,
|
||||
"manufacturer_name": product_with_relations.manufacturer.manufacturer_name if product_with_relations.manufacturer else None,
|
||||
"list_price": float(product_with_relations.list_price) if product_with_relations.list_price else None,
|
||||
"currency": product_with_relations.currency,
|
||||
"lead_time_days": product_with_relations.lead_time_days,
|
||||
"availability_status": product_with_relations.availability_status,
|
||||
"datasheet_url": product_with_relations.datasheet_url,
|
||||
"notes": product_with_relations.notes,
|
||||
"is_active": product_with_relations.is_active
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"제품 등록 실패: {str(e)}")
|
||||
|
||||
@router.post("/material-mapping")
|
||||
async def create_material_tubing_mapping(
|
||||
mapping_data: MaterialTubingMappingCreate,
|
||||
mapped_by: str = "admin",
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""BOM 자재와 Tubing 제품 매핑 생성"""
|
||||
try:
|
||||
# 기존 매핑 확인
|
||||
existing = db.query(MaterialTubingMapping)\
|
||||
.filter(
|
||||
MaterialTubingMapping.material_id == mapping_data.material_id,
|
||||
MaterialTubingMapping.tubing_product_id == mapping_data.tubing_product_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="이미 매핑된 자재와 제품입니다"
|
||||
)
|
||||
|
||||
# 새 매핑 생성
|
||||
new_mapping = MaterialTubingMapping(
|
||||
**mapping_data.dict(),
|
||||
mapped_by=mapped_by
|
||||
)
|
||||
db.add(new_mapping)
|
||||
db.commit()
|
||||
db.refresh(new_mapping)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "매핑이 성공적으로 생성되었습니다",
|
||||
"mapping_id": new_mapping.id
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"매핑 생성 실패: {str(e)}")
|
||||
|
||||
@router.get("/material-mappings/{material_id}")
|
||||
async def get_material_tubing_mappings(
|
||||
material_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""특정 자재의 Tubing 매핑 조회"""
|
||||
try:
|
||||
mappings = db.query(MaterialTubingMapping)\
|
||||
.options(
|
||||
joinedload(MaterialTubingMapping.tubing_product)
|
||||
.joinedload(TubingProduct.specification),
|
||||
joinedload(MaterialTubingMapping.tubing_product)
|
||||
.joinedload(TubingProduct.manufacturer)
|
||||
)\
|
||||
.filter(MaterialTubingMapping.material_id == material_id)\
|
||||
.all()
|
||||
|
||||
result = []
|
||||
for mapping in mappings:
|
||||
product = mapping.tubing_product
|
||||
mapping_dict = {
|
||||
"mapping_id": mapping.id,
|
||||
"confidence_score": float(mapping.confidence_score) if mapping.confidence_score else None,
|
||||
"mapping_method": mapping.mapping_method,
|
||||
"mapped_by": mapping.mapped_by,
|
||||
"mapped_at": mapping.mapped_at,
|
||||
"required_length_m": float(mapping.required_length_m) if mapping.required_length_m else None,
|
||||
"calculated_quantity": float(mapping.calculated_quantity) if mapping.calculated_quantity else None,
|
||||
"is_verified": mapping.is_verified,
|
||||
"tubing_product": {
|
||||
"id": product.id,
|
||||
"manufacturer_part_number": product.manufacturer_part_number,
|
||||
"manufacturer_product_name": product.manufacturer_product_name,
|
||||
"spec_name": product.specification.spec_name if product.specification else None,
|
||||
"manufacturer_name": product.manufacturer.manufacturer_name if product.manufacturer else None,
|
||||
"list_price": float(product.list_price) if product.list_price else None,
|
||||
"currency": product.currency,
|
||||
"availability_status": product.availability_status
|
||||
}
|
||||
}
|
||||
result.append(mapping_dict)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"material_id": material_id,
|
||||
"mappings": result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"매핑 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/search")
|
||||
async def search_tubing_products(
|
||||
query: str = Query(..., min_length=2),
|
||||
category: Optional[str] = Query(None),
|
||||
manufacturer: Optional[str] = Query(None),
|
||||
min_diameter: Optional[float] = Query(None),
|
||||
max_diameter: Optional[float] = Query(None),
|
||||
material_grade: Optional[str] = Query(None),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""통합 Tubing 검색 (규격, 제품, 제조사)"""
|
||||
try:
|
||||
# SQL 쿼리로 복합 검색
|
||||
sql_query = """
|
||||
SELECT DISTINCT
|
||||
tp.id as product_id,
|
||||
tp.manufacturer_part_number,
|
||||
tp.manufacturer_product_name,
|
||||
tp.list_price,
|
||||
tp.currency,
|
||||
tp.availability_status,
|
||||
ts.spec_code,
|
||||
ts.spec_name,
|
||||
ts.outer_diameter_mm,
|
||||
ts.wall_thickness_mm,
|
||||
ts.material_grade,
|
||||
tc.category_name,
|
||||
tm.manufacturer_name,
|
||||
tm.country
|
||||
FROM tubing_products tp
|
||||
JOIN tubing_specifications ts ON tp.specification_id = ts.id
|
||||
JOIN tubing_categories tc ON ts.category_id = tc.id
|
||||
JOIN tubing_manufacturers tm ON tp.manufacturer_id = tm.id
|
||||
WHERE tp.is_active = true
|
||||
AND ts.is_active = true
|
||||
AND tc.is_active = true
|
||||
AND tm.is_active = true
|
||||
AND (
|
||||
tp.manufacturer_part_number ILIKE :query OR
|
||||
tp.manufacturer_product_name ILIKE :query OR
|
||||
ts.spec_name ILIKE :query OR
|
||||
ts.spec_code ILIKE :query OR
|
||||
ts.material_grade ILIKE :query OR
|
||||
tm.manufacturer_name ILIKE :query
|
||||
)
|
||||
"""
|
||||
|
||||
params = {"query": f"%{query}%"}
|
||||
|
||||
# 필터 조건 추가
|
||||
if category:
|
||||
sql_query += " AND tc.category_code = :category"
|
||||
params["category"] = category
|
||||
|
||||
if manufacturer:
|
||||
sql_query += " AND tm.manufacturer_code = :manufacturer"
|
||||
params["manufacturer"] = manufacturer
|
||||
|
||||
if min_diameter:
|
||||
sql_query += " AND ts.outer_diameter_mm >= :min_diameter"
|
||||
params["min_diameter"] = min_diameter
|
||||
|
||||
if max_diameter:
|
||||
sql_query += " AND ts.outer_diameter_mm <= :max_diameter"
|
||||
params["max_diameter"] = max_diameter
|
||||
|
||||
if material_grade:
|
||||
sql_query += " AND ts.material_grade ILIKE :material_grade"
|
||||
params["material_grade"] = f"%{material_grade}%"
|
||||
|
||||
sql_query += " ORDER BY tp.manufacturer_part_number LIMIT :limit"
|
||||
params["limit"] = limit
|
||||
|
||||
result = db.execute(text(sql_query), params)
|
||||
products = result.fetchall()
|
||||
|
||||
search_results = []
|
||||
for product in products:
|
||||
product_dict = {
|
||||
"product_id": product.product_id,
|
||||
"manufacturer_part_number": product.manufacturer_part_number,
|
||||
"manufacturer_product_name": product.manufacturer_product_name,
|
||||
"list_price": float(product.list_price) if product.list_price else None,
|
||||
"currency": product.currency,
|
||||
"availability_status": product.availability_status,
|
||||
"spec_code": product.spec_code,
|
||||
"spec_name": product.spec_name,
|
||||
"outer_diameter_mm": float(product.outer_diameter_mm) if product.outer_diameter_mm else None,
|
||||
"wall_thickness_mm": float(product.wall_thickness_mm) if product.wall_thickness_mm else None,
|
||||
"material_grade": product.material_grade,
|
||||
"category_name": product.category_name,
|
||||
"manufacturer_name": product.manufacturer_name,
|
||||
"country": product.country
|
||||
}
|
||||
search_results.append(product_dict)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"query": query,
|
||||
"total_results": len(search_results),
|
||||
"results": search_results
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"검색 실패: {str(e)}")
|
||||
Reference in New Issue
Block a user