""" PIPE 이슈 관리 API 라우터 스냅샷 기반 도면별/단관별 이슈 관리 기능 """ 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 datetime import datetime from ..database import get_db from ..models import ( PipeIssueSnapshot, PipeIssueSegment, PipeDrawingIssue, PipeSegmentIssue ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/pipe-issue", tags=["pipe-issue"]) # Pydantic 모델들 class CreateDrawingIssueRequest(BaseModel): snapshot_id: int area: str drawing_name: str issue_description: str severity: str = 'medium' # low, medium, high, critical reported_by: Optional[str] = 'user' class CreateSegmentIssueRequest(BaseModel): snapshot_id: int segment_id: int issue_description: str issue_type: Optional[str] = 'other' # cutting, installation, material, routing, other length_change: Optional[float] = None new_length: Optional[float] = None material_change: Optional[str] = None severity: str = 'medium' reported_by: Optional[str] = 'user' class UpdateIssueStatusRequest(BaseModel): status: str # open, in_progress, resolved resolution_notes: Optional[str] = None resolved_by: Optional[str] = None class DrawingIssueResponse(BaseModel): id: int snapshot_id: int area: str drawing_name: str issue_description: str severity: str status: str resolution_notes: Optional[str] = None resolved_by: Optional[str] = None resolved_at: Optional[str] = None reported_by: Optional[str] = None reported_at: str updated_at: str class SegmentIssueResponse(BaseModel): id: int snapshot_id: int segment_id: int issue_description: str issue_type: Optional[str] = None length_change: Optional[float] = None new_length: Optional[float] = None material_change: Optional[str] = None severity: str status: str resolution_notes: Optional[str] = None resolved_by: Optional[str] = None resolved_at: Optional[str] = None reported_by: Optional[str] = None reported_at: str updated_at: str @router.get("/snapshots/{job_no}") async def get_job_snapshots( job_no: str, db: Session = Depends(get_db) ): """작업의 활성 스냅샷 조회""" try: snapshots = db.query(PipeIssueSnapshot).filter( PipeIssueSnapshot.job_no == job_no, PipeIssueSnapshot.is_active == True ).all() result = [] for snapshot in snapshots: result.append({ "snapshot_id": snapshot.id, "snapshot_name": snapshot.snapshot_name, "is_locked": snapshot.is_locked, "total_segments": snapshot.total_segments, "total_drawings": snapshot.total_drawings, "created_at": snapshot.created_at.isoformat() if snapshot.created_at else None, "locked_at": snapshot.locked_at.isoformat() if snapshot.locked_at else None }) return { "job_no": job_no, "snapshots": result, "total_count": len(result) } except Exception as e: logger.error(f"Failed to get job snapshots: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"스냅샷 조회 실패: {str(e)}" ) @router.get("/drawing-issues/{snapshot_id}") async def get_drawing_issues( snapshot_id: int, area: Optional[str] = None, drawing_name: Optional[str] = None, status_filter: Optional[str] = None, db: Session = Depends(get_db) ): """도면 이슈 목록 조회""" try: query = db.query(PipeDrawingIssue).filter( PipeDrawingIssue.snapshot_id == snapshot_id ) if area: query = query.filter(PipeDrawingIssue.area == area) if drawing_name: query = query.filter(PipeDrawingIssue.drawing_name == drawing_name) if status_filter: query = query.filter(PipeDrawingIssue.status == status_filter) issues = query.order_by(PipeDrawingIssue.reported_at.desc()).all() result = [] for issue in issues: result.append(DrawingIssueResponse( id=issue.id, snapshot_id=issue.snapshot_id, area=issue.area, drawing_name=issue.drawing_name, issue_description=issue.issue_description, severity=issue.severity, status=issue.status, resolution_notes=issue.resolution_notes, resolved_by=issue.resolved_by, resolved_at=issue.resolved_at.isoformat() if issue.resolved_at else None, reported_by=issue.reported_by, reported_at=issue.reported_at.isoformat() if issue.reported_at else '', updated_at=issue.updated_at.isoformat() if issue.updated_at else '' )) return { "snapshot_id": snapshot_id, "issues": result, "total_count": len(result) } except Exception as e: logger.error(f"Failed to get drawing issues: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"도면 이슈 조회 실패: {str(e)}" ) @router.post("/drawing-issues", response_model=DrawingIssueResponse) async def create_drawing_issue( request: CreateDrawingIssueRequest, db: Session = Depends(get_db) ): """도면 이슈 생성""" try: # 스냅샷 존재 확인 snapshot = db.query(PipeIssueSnapshot).filter( PipeIssueSnapshot.id == request.snapshot_id ).first() if not snapshot: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="스냅샷을 찾을 수 없습니다." ) # 이슈 생성 issue = PipeDrawingIssue( snapshot_id=request.snapshot_id, area=request.area, drawing_name=request.drawing_name, issue_description=request.issue_description, severity=request.severity, reported_by=request.reported_by ) db.add(issue) db.commit() db.refresh(issue) return DrawingIssueResponse( id=issue.id, snapshot_id=issue.snapshot_id, area=issue.area, drawing_name=issue.drawing_name, issue_description=issue.issue_description, severity=issue.severity, status=issue.status, resolution_notes=issue.resolution_notes, resolved_by=issue.resolved_by, resolved_at=issue.resolved_at.isoformat() if issue.resolved_at else None, reported_by=issue.reported_by, reported_at=issue.reported_at.isoformat() if issue.reported_at else '', updated_at=issue.updated_at.isoformat() if issue.updated_at else '' ) except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Failed to create drawing issue: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"도면 이슈 생성 실패: {str(e)}" ) @router.get("/segment-issues/{snapshot_id}") async def get_segment_issues( snapshot_id: int, segment_id: Optional[int] = None, status_filter: Optional[str] = None, db: Session = Depends(get_db) ): """단관 이슈 목록 조회""" try: query = db.query(PipeSegmentIssue).filter( PipeSegmentIssue.snapshot_id == snapshot_id ) if segment_id: query = query.filter(PipeSegmentIssue.segment_id == segment_id) if status_filter: query = query.filter(PipeSegmentIssue.status == status_filter) issues = query.order_by(PipeSegmentIssue.reported_at.desc()).all() result = [] for issue in issues: result.append(SegmentIssueResponse( id=issue.id, snapshot_id=issue.snapshot_id, segment_id=issue.segment_id, issue_description=issue.issue_description, issue_type=issue.issue_type, length_change=float(issue.length_change) if issue.length_change else None, new_length=float(issue.new_length) if issue.new_length else None, material_change=issue.material_change, severity=issue.severity, status=issue.status, resolution_notes=issue.resolution_notes, resolved_by=issue.resolved_by, resolved_at=issue.resolved_at.isoformat() if issue.resolved_at else None, reported_by=issue.reported_by, reported_at=issue.reported_at.isoformat() if issue.reported_at else '', updated_at=issue.updated_at.isoformat() if issue.updated_at else '' )) return { "snapshot_id": snapshot_id, "issues": result, "total_count": len(result) } except Exception as e: logger.error(f"Failed to get segment issues: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"단관 이슈 조회 실패: {str(e)}" ) @router.post("/segment-issues", response_model=SegmentIssueResponse) async def create_segment_issue( request: CreateSegmentIssueRequest, db: Session = Depends(get_db) ): """단관 이슈 생성""" try: # 단관 존재 확인 segment = db.query(PipeIssueSegment).filter( PipeIssueSegment.id == request.segment_id ).first() if not segment: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="단관을 찾을 수 없습니다." ) # 이슈 생성 issue = PipeSegmentIssue( snapshot_id=request.snapshot_id, segment_id=request.segment_id, issue_description=request.issue_description, issue_type=request.issue_type, length_change=request.length_change, new_length=request.new_length, material_change=request.material_change, severity=request.severity, reported_by=request.reported_by ) db.add(issue) db.commit() db.refresh(issue) return SegmentIssueResponse( id=issue.id, snapshot_id=issue.snapshot_id, segment_id=issue.segment_id, issue_description=issue.issue_description, issue_type=issue.issue_type, length_change=float(issue.length_change) if issue.length_change else None, new_length=float(issue.new_length) if issue.new_length else None, material_change=issue.material_change, severity=issue.severity, status=issue.status, resolution_notes=issue.resolution_notes, resolved_by=issue.resolved_by, resolved_at=issue.resolved_at.isoformat() if issue.resolved_at else None, reported_by=issue.reported_by, reported_at=issue.reported_at.isoformat() if issue.reported_at else '', updated_at=issue.updated_at.isoformat() if issue.updated_at else '' ) except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Failed to create segment issue: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"단관 이슈 생성 실패: {str(e)}" ) @router.put("/drawing-issues/{issue_id}/status") async def update_drawing_issue_status( issue_id: int, request: UpdateIssueStatusRequest, db: Session = Depends(get_db) ): """도면 이슈 상태 업데이트""" try: issue = db.query(PipeDrawingIssue).filter( PipeDrawingIssue.id == issue_id ).first() if not issue: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="이슈를 찾을 수 없습니다." ) # 상태 업데이트 issue.status = request.status if request.resolution_notes: issue.resolution_notes = request.resolution_notes if request.resolved_by: issue.resolved_by = request.resolved_by if request.status == 'resolved': issue.resolved_at = datetime.utcnow() db.commit() return {"message": "이슈 상태가 업데이트되었습니다.", "issue_id": issue_id} except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Failed to update drawing issue status: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"이슈 상태 업데이트 실패: {str(e)}" ) @router.put("/segment-issues/{issue_id}/status") async def update_segment_issue_status( issue_id: int, request: UpdateIssueStatusRequest, db: Session = Depends(get_db) ): """단관 이슈 상태 업데이트""" try: issue = db.query(PipeSegmentIssue).filter( PipeSegmentIssue.id == issue_id ).first() if not issue: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="이슈를 찾을 수 없습니다." ) # 상태 업데이트 issue.status = request.status if request.resolution_notes: issue.resolution_notes = request.resolution_notes if request.resolved_by: issue.resolved_by = request.resolved_by if request.status == 'resolved': issue.resolved_at = datetime.utcnow() db.commit() return {"message": "이슈 상태가 업데이트되었습니다.", "issue_id": issue_id} except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Failed to update segment issue status: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"이슈 상태 업데이트 실패: {str(e)}" ) @router.get("/report/{snapshot_id}") async def generate_issue_report( snapshot_id: int, db: Session = Depends(get_db) ): """이슈 리포트 생성""" try: # 스냅샷 정보 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 = db.query(PipeDrawingIssue).filter( PipeDrawingIssue.snapshot_id == snapshot_id ).all() # 단관 이슈 통계 segment_issues = db.query(PipeSegmentIssue).filter( PipeSegmentIssue.snapshot_id == snapshot_id ).all() # 통계 계산 drawing_stats = { "total": len(drawing_issues), "by_status": {}, "by_severity": {}, "by_area": {} } for issue in drawing_issues: # 상태별 status = issue.status drawing_stats["by_status"][status] = drawing_stats["by_status"].get(status, 0) + 1 # 심각도별 severity = issue.severity drawing_stats["by_severity"][severity] = drawing_stats["by_severity"].get(severity, 0) + 1 # 구역별 area = issue.area drawing_stats["by_area"][area] = drawing_stats["by_area"].get(area, 0) + 1 segment_stats = { "total": len(segment_issues), "by_status": {}, "by_severity": {}, "by_type": {} } for issue in segment_issues: # 상태별 status = issue.status segment_stats["by_status"][status] = segment_stats["by_status"].get(status, 0) + 1 # 심각도별 severity = issue.severity segment_stats["by_severity"][severity] = segment_stats["by_severity"].get(severity, 0) + 1 # 유형별 issue_type = issue.issue_type or 'other' segment_stats["by_type"][issue_type] = segment_stats["by_type"].get(issue_type, 0) + 1 return { "snapshot_id": snapshot_id, "snapshot_name": snapshot.snapshot_name, "job_no": snapshot.job_no, "report_generated_at": datetime.utcnow().isoformat(), "drawing_issues": drawing_stats, "segment_issues": segment_stats, "total_issues": len(drawing_issues) + len(segment_issues) } except HTTPException: raise except Exception as e: logger.error(f"Failed to generate issue report: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"이슈 리포트 생성 실패: {str(e)}" )