""" PIPE 리비전 관리 API 라우터 Cutting Plan 전/후 리비전 처리 및 비교 기능 """ import logging from typing import Dict, List, Optional, Any from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from pydantic import BaseModel from ..database import get_db from ..services.pipe_revision_service import get_pipe_revision_service, PipeRevisionService from ..models import PipeRevisionComparison, PipeRevisionChange logger = logging.getLogger(__name__) router = APIRouter(prefix="/pipe-revision", tags=["pipe-revision"]) # Pydantic 모델들 class RevisionStatusRequest(BaseModel): job_no: str new_file_id: int class PreRevisionRequest(BaseModel): job_no: str new_file_id: int class PostRevisionRequest(BaseModel): job_no: str new_file_id: int class RevisionStatusResponse(BaseModel): revision_type: str requires_action: bool message: str previous_file_id: Optional[int] = None class PreRevisionResponse(BaseModel): status: str revision_type: str deleted_items: int new_pipe_materials: int message: str next_action: str pipe_materials: List[Dict[str, Any]] class PostRevisionResponse(BaseModel): status: str revision_type: str comparison_id: int summary: Dict[str, Any] changed_drawings: List[Dict[str, Any]] unchanged_drawings: List[Dict[str, Any]] message: str next_action: str @router.post("/check-status", response_model=RevisionStatusResponse) async def check_revision_status( request: RevisionStatusRequest, db: Session = Depends(get_db) ): """ PIPE 리비전 상태 확인 - 리비전 유형 판단 (pre/post cutting plan) - 필요한 처리 방식 결정 """ try: service = get_pipe_revision_service(db) result = service.check_revision_status(request.job_no, request.new_file_id) return RevisionStatusResponse(**result) except Exception as e: logger.error(f"Failed to check pipe revision status: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"리비전 상태 확인 실패: {str(e)}" ) @router.post("/handle-pre-cutting-plan", response_model=PreRevisionResponse) async def handle_pre_cutting_plan_revision( request: PreRevisionRequest, db: Session = Depends(get_db) ): """ Cutting Plan 작성 전 리비전 처리 - 기존 PIPE 데이터 삭제 - 새 BOM 데이터로 초기화 """ try: service = get_pipe_revision_service(db) result = service.handle_pre_cutting_plan_revision(request.job_no, request.new_file_id) if result["status"] != "success": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=result["message"] ) return PreRevisionResponse(**result) except HTTPException: raise except Exception as e: logger.error(f"Failed to handle pre-cutting-plan revision: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Cutting Plan 작성 전 리비전 처리 실패: {str(e)}" ) @router.post("/handle-post-cutting-plan", response_model=PostRevisionResponse) async def handle_post_cutting_plan_revision( request: PostRevisionRequest, db: Session = Depends(get_db) ): """ Cutting Plan 작성 후 리비전 처리 - 기존 Cutting Plan과 신규 BOM 비교 - 변경사항 상세 분석 """ try: service = get_pipe_revision_service(db) result = service.handle_post_cutting_plan_revision(request.job_no, request.new_file_id) if result["status"] != "success": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=result["message"] ) return PostRevisionResponse(**result) except HTTPException: raise except Exception as e: logger.error(f"Failed to handle post-cutting-plan revision: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Cutting Plan 작성 후 리비전 처리 실패: {str(e)}" ) @router.get("/comparison/{comparison_id}") async def get_revision_comparison( comparison_id: int, db: Session = Depends(get_db) ): """ 리비전 비교 결과 상세 조회 """ try: # 비교 결과 조회 comparison = db.query(PipeRevisionComparison).filter( PipeRevisionComparison.id == comparison_id ).first() if not comparison: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="비교 결과를 찾을 수 없습니다." ) # 상세 변경사항 조회 changes = db.query(PipeRevisionChange).filter( PipeRevisionChange.comparison_id == comparison_id ).all() # 도면별로 변경사항 그룹화 changes_by_drawing = {} for change in changes: drawing = change.drawing_name if drawing not in changes_by_drawing: changes_by_drawing[drawing] = [] changes_by_drawing[drawing].append({ "id": change.id, "change_type": change.change_type, "old_data": { "line_no": change.old_line_no, "material_grade": change.old_material_grade, "schedule_spec": change.old_schedule_spec, "nominal_size": change.old_nominal_size, "length_mm": change.old_length_mm, "end_preparation": change.old_end_preparation } if change.old_line_no else None, "new_data": { "line_no": change.new_line_no, "material_grade": change.new_material_grade, "schedule_spec": change.new_schedule_spec, "nominal_size": change.new_nominal_size, "length_mm": change.new_length_mm, "end_preparation": change.new_end_preparation } if change.new_line_no else None, "change_reason": change.change_reason }) return { "comparison_id": comparison.id, "job_no": comparison.job_no, "comparison_date": comparison.comparison_date, "summary": { "total_drawings": comparison.total_drawings, "changed_drawings": comparison.changed_drawings, "unchanged_drawings": comparison.unchanged_drawings, "total_segments": comparison.total_segments, "added_segments": comparison.added_segments, "removed_segments": comparison.removed_segments, "modified_segments": comparison.modified_segments, "unchanged_segments": comparison.unchanged_segments }, "changes_by_drawing": changes_by_drawing, "is_applied": comparison.is_applied, "applied_at": comparison.applied_at, "applied_by": comparison.applied_by } except HTTPException: raise except Exception as e: logger.error(f"Failed to get revision comparison: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"리비전 비교 결과 조회 실패: {str(e)}" ) @router.post("/comparison/{comparison_id}/apply") async def apply_revision_changes( comparison_id: int, db: Session = Depends(get_db) ): """ 리비전 변경사항 적용 """ try: # 비교 결과 조회 comparison = db.query(PipeRevisionComparison).filter( PipeRevisionComparison.id == comparison_id ).first() if not comparison: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="비교 결과를 찾을 수 없습니다." ) if comparison.is_applied: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="이미 적용된 리비전입니다." ) # 적용 상태 업데이트 comparison.is_applied = True comparison.applied_at = datetime.utcnow() comparison.applied_by = "system" # 추후 사용자 정보로 변경 db.commit() return { "status": "success", "message": "리비전 변경사항이 적용되었습니다.", "comparison_id": comparison_id, "applied_at": comparison.applied_at } except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Failed to apply revision changes: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"리비전 변경사항 적용 실패: {str(e)}" ) @router.get("/job/{job_no}/history") async def get_revision_history( job_no: str, db: Session = Depends(get_db) ): """ 작업별 PIPE 리비전 이력 조회 """ try: comparisons = db.query(PipeRevisionComparison).filter( PipeRevisionComparison.job_no == job_no ).order_by(PipeRevisionComparison.comparison_date.desc()).all() history = [] for comp in comparisons: history.append({ "comparison_id": comp.id, "comparison_date": comp.comparison_date, "summary": { "total_drawings": comp.total_drawings, "changed_drawings": comp.changed_drawings, "total_segments": comp.total_segments, "added_segments": comp.added_segments, "removed_segments": comp.removed_segments, "modified_segments": comp.modified_segments }, "is_applied": comp.is_applied, "applied_at": comp.applied_at, "applied_by": comp.applied_by, "created_by": comp.created_by }) return { "job_no": job_no, "total_revisions": len(history), "history": history } except Exception as e: logger.error(f"Failed to get revision history: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"리비전 이력 조회 실패: {str(e)}" ) @router.get("/comparison/{comparison_id}/purchase-impact") async def get_purchase_impact( comparison_id: int, db: Session = Depends(get_db) ): """ 리비전이 구매량에 미치는 영향 분석 """ try: # 비교 결과 조회 comparison = db.query(PipeRevisionComparison).filter( PipeRevisionComparison.id == comparison_id ).first() if not comparison: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="비교 결과를 찾을 수 없습니다." ) # 변경사항 조회 changes = db.query(PipeRevisionChange).filter( PipeRevisionChange.comparison_id == comparison_id ).all() # 재질별 길이 변화 계산 material_impacts = {} for change in changes: # 재질 정보 추출 material = None length_change = 0 if change.change_type == "added" and change.new_material_grade: material = change.new_material_grade length_change = change.new_length_mm or 0 elif change.change_type == "removed" and change.old_material_grade: material = change.old_material_grade length_change = -(change.old_length_mm or 0) elif change.change_type == "modified": # 재질이 같은 경우만 길이 변화 계산 if (change.old_material_grade == change.new_material_grade and change.old_material_grade): material = change.old_material_grade old_length = change.old_length_mm or 0 new_length = change.new_length_mm or 0 length_change = new_length - old_length if material and length_change != 0: if material not in material_impacts: material_impacts[material] = { "material": material, "total_length_change": 0, "change_count": 0 } material_impacts[material]["total_length_change"] += length_change material_impacts[material]["change_count"] += 1 # 결과 정리 impact_summary = [] for material, impact in material_impacts.items(): length_change_m = impact["total_length_change"] / 1000 # mm to m impact_summary.append({ "material": material, "length_change_mm": impact["total_length_change"], "length_change_m": round(length_change_m, 3), "change_count": impact["change_count"], "impact_type": "increase" if length_change_m > 0 else "decrease", "requires_additional_purchase": length_change_m > 0 }) # 전체 영향 요약 total_additional_purchase = sum( impact["length_change_m"] for impact in impact_summary if impact["requires_additional_purchase"] ) return { "comparison_id": comparison_id, "job_no": comparison.job_no, "material_impacts": impact_summary, "summary": { "total_materials_affected": len(impact_summary), "materials_requiring_additional_purchase": len([ i for i in impact_summary if i["requires_additional_purchase"] ]), "total_additional_purchase_needed": total_additional_purchase > 0, "total_additional_length_m": round(total_additional_purchase, 3) } } except HTTPException: raise except Exception as e: logger.error(f"Failed to get purchase impact: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"구매 영향 분석 실패: {str(e)}" )