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:
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