""" PIPE 스냅샷 관리 API 라우터 Cutting Plan 확정, 스냅샷 생성, 이슈 관리 준비 등의 기능 제공 """ import logging from typing import Dict, List, Optional, Any from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from pydantic import BaseModel from ..database import get_db from ..services.pipe_issue_snapshot_service import get_pipe_issue_snapshot_service, PipeIssueSnapshotService logger = logging.getLogger(__name__) router = APIRouter(prefix="/pipe-snapshot", tags=["pipe-snapshot"]) # Pydantic 모델들 class FinalizeCuttingPlanRequest(BaseModel): job_no: str created_by: Optional[str] = "system" class SnapshotStatusResponse(BaseModel): has_snapshot: bool snapshot_id: Optional[int] = None snapshot_name: Optional[str] = None is_locked: bool = False created_at: Optional[str] = None created_by: Optional[str] = None locked_at: Optional[str] = None locked_by: Optional[str] = None total_segments: int = 0 total_drawings: int = 0 drawing_issues_count: int = 0 segment_issues_count: int = 0 can_start_issue_management: bool = False message: str class SnapshotSegmentsResponse(BaseModel): snapshot_id: int segments: List[Dict[str, Any]] total_count: int areas: List[str] drawings: List[str] class FinalizeCuttingPlanResponse(BaseModel): success: bool snapshot_id: Optional[int] = None snapshot_name: Optional[str] = None total_segments: int = 0 total_drawings: int = 0 is_locked: bool = False locked_at: Optional[str] = None message: str next_action: Optional[str] = None @router.post("/finalize-cutting-plan", response_model=FinalizeCuttingPlanResponse) async def finalize_cutting_plan( request: FinalizeCuttingPlanRequest, db: Session = Depends(get_db) ): """ Cutting Plan 확정 및 스냅샷 생성 - 현재 단관 데이터를 스냅샷으로 고정 - 이슈 관리 시작 가능 상태로 변경 """ try: service = get_pipe_issue_snapshot_service(db) result = service.create_and_lock_snapshot_on_finalize( request.job_no, request.created_by ) if not result["success"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=result["message"] ) return FinalizeCuttingPlanResponse(**result) except HTTPException: raise except Exception as e: logger.error(f"Failed to finalize cutting plan: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Cutting Plan 확정 실패: {str(e)}" ) @router.get("/status/{job_no}", response_model=SnapshotStatusResponse) async def get_snapshot_status( job_no: str, db: Session = Depends(get_db) ): """ 작업의 스냅샷 상태 조회 - 스냅샷 존재 여부 - 잠금 상태 - 이슈 관리 가능 여부 """ try: service = get_pipe_issue_snapshot_service(db) result = service.get_snapshot_info(job_no) return SnapshotStatusResponse(**result) except Exception as e: logger.error(f"Failed to get snapshot status: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"스냅샷 상태 조회 실패: {str(e)}" ) @router.get("/segments/{snapshot_id}", response_model=SnapshotSegmentsResponse) async def get_snapshot_segments( snapshot_id: int, area: Optional[str] = None, drawing_name: Optional[str] = None, db: Session = Depends(get_db) ): """ 스냅샷된 단관 정보 조회 - 구역별/도면별 필터링 가능 - 이슈 관리 페이지에서 사용 """ try: service = get_pipe_issue_snapshot_service(db) # 단관 데이터 조회 segments = service.get_snapshot_segments(snapshot_id, area, drawing_name) # 사용 가능한 구역/도면 목록 areas = service.get_available_areas(snapshot_id) drawings = service.get_available_drawings(snapshot_id, area) return SnapshotSegmentsResponse( snapshot_id=snapshot_id, segments=segments, total_count=len(segments), areas=areas, drawings=drawings ) except Exception as e: logger.error(f"Failed to get snapshot segments: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"스냅샷 단관 조회 실패: {str(e)}" ) @router.get("/areas/{snapshot_id}") async def get_available_areas( snapshot_id: int, db: Session = Depends(get_db) ): """스냅샷의 사용 가능한 구역 목록 조회""" try: service = get_pipe_issue_snapshot_service(db) areas = service.get_available_areas(snapshot_id) return { "snapshot_id": snapshot_id, "areas": areas, "total_count": len(areas) } except Exception as e: logger.error(f"Failed to get available areas: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"구역 목록 조회 실패: {str(e)}" ) @router.get("/drawings/{snapshot_id}") async def get_available_drawings( snapshot_id: int, area: Optional[str] = None, db: Session = Depends(get_db) ): """스냅샷의 사용 가능한 도면 목록 조회""" try: service = get_pipe_issue_snapshot_service(db) drawings = service.get_available_drawings(snapshot_id, area) return { "snapshot_id": snapshot_id, "area": area, "drawings": drawings, "total_count": len(drawings) } except Exception as e: logger.error(f"Failed to get available drawings: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"도면 목록 조회 실패: {str(e)}" ) @router.get("/revision-protection/{job_no}") async def check_revision_protection( job_no: str, db: Session = Depends(get_db) ): """ 리비전 보호 상태 확인 - 잠긴 스냅샷이 있으면 리비전 영향 받지 않음 """ try: service = get_pipe_issue_snapshot_service(db) result = service.check_revision_protection(job_no) return result except Exception as e: logger.error(f"Failed to check revision protection: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"리비전 보호 상태 확인 실패: {str(e)}" ) @router.delete("/snapshot/{snapshot_id}") async def delete_snapshot( snapshot_id: int, force: bool = False, db: Session = Depends(get_db) ): """ 스냅샷 삭제 (개발/테스트용) - force=True: 강제 삭제 (이슈 데이터 포함) - force=False: 이슈가 없는 경우만 삭제 """ try: from ..models import PipeIssueSnapshot, PipeDrawingIssue, PipeSegmentIssue # 스냅샷 존재 확인 snapshot = db.query(PipeIssueSnapshot).filter( PipeIssueSnapshot.id == snapshot_id ).first() if not snapshot: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="스냅샷을 찾을 수 없습니다." ) # 이슈 데이터 확인 drawing_issues_count = db.query(PipeDrawingIssue).filter( PipeDrawingIssue.snapshot_id == snapshot_id ).count() segment_issues_count = db.query(PipeSegmentIssue).filter( PipeSegmentIssue.snapshot_id == snapshot_id ).count() total_issues = drawing_issues_count + segment_issues_count if total_issues > 0 and not force: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"이슈 데이터가 {total_issues}개 있습니다. force=true로 강제 삭제하거나 이슈를 먼저 정리해주세요." ) # 스냅샷 삭제 (CASCADE로 관련 데이터 자동 삭제) db.delete(snapshot) db.commit() return { "success": True, "message": f"스냅샷 '{snapshot.snapshot_name}'이 삭제되었습니다.", "deleted_snapshot_id": snapshot_id, "deleted_issues_count": total_issues } except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Failed to delete snapshot: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"스냅샷 삭제 실패: {str(e)}" ) @router.get("/job/{job_no}/history") async def get_snapshot_history( job_no: str, db: Session = Depends(get_db) ): """작업별 스냅샷 이력 조회""" try: from ..models import PipeIssueSnapshot snapshots = db.query(PipeIssueSnapshot).filter( PipeIssueSnapshot.job_no == job_no ).order_by(PipeIssueSnapshot.created_at.desc()).all() history = [] for snapshot in snapshots: history.append({ "snapshot_id": snapshot.id, "snapshot_name": snapshot.snapshot_name, "is_active": snapshot.is_active, "is_locked": snapshot.is_locked, "created_at": snapshot.created_at.isoformat() if snapshot.created_at else None, "created_by": snapshot.created_by, "locked_at": snapshot.locked_at.isoformat() if snapshot.locked_at else None, "locked_by": snapshot.locked_by, "total_segments": snapshot.total_segments, "total_drawings": snapshot.total_drawings }) return { "job_no": job_no, "total_snapshots": len(history), "history": history } except Exception as e: logger.error(f"Failed to get snapshot history: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"스냅샷 이력 조회 실패: {str(e)}" )