""" PIPE 이슈 관리용 스냅샷 시스템 단관 관리 DB의 특정 시점 데이터를 고정하여 이후 리비전이 발생해도 이슈 관리에 영향을 주지 않도록 함 """ import logging from typing import Dict, List, Optional, Any from datetime import datetime from sqlalchemy.orm import Session from sqlalchemy import text, and_, or_ from ..database import get_db from ..models import ( PipeCuttingPlan, PipeIssueSnapshot, PipeIssueSegment, PipeDrawingIssue, PipeSegmentIssue ) logger = logging.getLogger(__name__) class PipeIssueSnapshotService: """PIPE 이슈 관리용 스냅샷 서비스""" def __init__(self, db: Session): self.db = db def create_and_lock_snapshot_on_finalize(self, job_no: str, created_by: str = "system") -> Dict[str, Any]: """ Cutting Plan 확정 시 스냅샷 생성 및 즉시 잠금 Args: job_no: 작업 번호 created_by: 생성자 Returns: 생성된 스냅샷 정보 """ try: # 1. 기존 활성 스냅샷 확인 existing_snapshot = self.db.query(PipeIssueSnapshot).filter( and_( PipeIssueSnapshot.job_no == job_no, PipeIssueSnapshot.is_active == True ) ).first() if existing_snapshot: return { "success": False, "message": f"이미 확정된 Cutting Plan이 존재합니다: {existing_snapshot.snapshot_name}", "existing_snapshot_id": existing_snapshot.id, "can_manage_issues": True } # 2. 현재 단관 데이터 조회 current_segments = self._get_current_cutting_plan_data(job_no) if not current_segments: return { "success": False, "message": "확정할 Cutting Plan 데이터가 없습니다." } # 3. 자동 스냅샷 이름 생성 snapshot_name = f"Cutting Plan 확정 - {datetime.now().strftime('%Y%m%d_%H%M%S')}" # 4. 스냅샷 레코드 생성 (즉시 잠금 상태) snapshot = PipeIssueSnapshot( job_no=job_no, snapshot_name=snapshot_name, created_by=created_by, is_locked=True, # 확정과 동시에 잠금 locked_at=datetime.utcnow(), locked_by=created_by, total_segments=len(current_segments), total_drawings=len(set(seg["drawing_name"] for seg in current_segments)) ) self.db.add(snapshot) self.db.flush() # ID 생성을 위해 # 4. 단관 데이터 스냅샷 저장 snapshot_segments = [] for segment_data in current_segments: segment = PipeIssueSegment( snapshot_id=snapshot.id, area=segment_data.get("area"), drawing_name=segment_data["drawing_name"], line_no=segment_data["line_no"], material_grade=segment_data.get("material_grade"), schedule_spec=segment_data.get("schedule_spec"), nominal_size=segment_data.get("nominal_size"), length_mm=segment_data["length_mm"], end_preparation=segment_data.get("end_preparation", "무개선"), original_cutting_plan_id=segment_data.get("original_id") ) snapshot_segments.append(segment) self.db.add_all(snapshot_segments) self.db.commit() logger.info(f"Created snapshot {snapshot.id} for job {job_no} with {len(snapshot_segments)} segments") return { "success": True, "snapshot_id": snapshot.id, "snapshot_name": snapshot_name, "total_segments": len(snapshot_segments), "total_drawings": snapshot.total_drawings, "is_locked": True, "locked_at": snapshot.locked_at, "message": f"Cutting Plan이 확정되었습니다! 이제 이슈 관리를 시작할 수 있습니다.", "next_action": "start_issue_management" } except Exception as e: self.db.rollback() logger.error(f"Failed to create snapshot: {e}") return { "success": False, "message": f"스냅샷 생성 실패: {str(e)}" } def lock_snapshot(self, snapshot_id: int, locked_by: str = "system") -> Dict[str, Any]: """ 스냅샷 잠금 (이슈 등록 시작) 잠금 후에는 더 이상 리비전 영향을 받지 않음 """ try: snapshot = self.db.query(PipeIssueSnapshot).filter( PipeIssueSnapshot.id == snapshot_id ).first() if not snapshot: return { "success": False, "message": "스냅샷을 찾을 수 없습니다." } if snapshot.is_locked: return { "success": False, "message": f"이미 잠긴 스냅샷입니다. (잠금자: {snapshot.locked_by})" } # 스냅샷 잠금 snapshot.is_locked = True snapshot.locked_at = datetime.utcnow() snapshot.locked_by = locked_by self.db.commit() logger.info(f"Locked snapshot {snapshot_id} by {locked_by}") return { "success": True, "message": f"스냅샷 '{snapshot.snapshot_name}'이 잠금되었습니다. 이제 이슈 관리를 시작할 수 있습니다.", "locked_at": snapshot.locked_at } except Exception as e: self.db.rollback() logger.error(f"Failed to lock snapshot: {e}") return { "success": False, "message": f"스냅샷 잠금 실패: {str(e)}" } def get_snapshot_info(self, job_no: str) -> Dict[str, Any]: """작업의 스냅샷 정보 조회""" try: snapshot = self.db.query(PipeIssueSnapshot).filter( and_( PipeIssueSnapshot.job_no == job_no, PipeIssueSnapshot.is_active == True ) ).first() if not snapshot: return { "has_snapshot": False, "message": "생성된 스냅샷이 없습니다." } # 이슈 통계 조회 drawing_issues_count = self.db.query(PipeDrawingIssue).filter( PipeDrawingIssue.snapshot_id == snapshot.id ).count() segment_issues_count = self.db.query(PipeSegmentIssue).filter( PipeSegmentIssue.snapshot_id == snapshot.id ).count() return { "has_snapshot": True, "snapshot_id": snapshot.id, "snapshot_name": snapshot.snapshot_name, "is_locked": snapshot.is_locked, "created_at": snapshot.created_at, "created_by": snapshot.created_by, "locked_at": snapshot.locked_at, "locked_by": snapshot.locked_by, "total_segments": snapshot.total_segments, "total_drawings": snapshot.total_drawings, "drawing_issues_count": drawing_issues_count, "segment_issues_count": segment_issues_count, "can_start_issue_management": not snapshot.is_locked, "message": "잠긴 스냅샷 - 이슈 관리 진행 중" if snapshot.is_locked else "스냅샷 준비 완료" } except Exception as e: logger.error(f"Failed to get snapshot info: {e}") return { "has_snapshot": False, "message": f"스냅샷 정보 조회 실패: {str(e)}" } def get_snapshot_segments(self, snapshot_id: int, area: str = None, drawing_name: str = None) -> List[Dict[str, Any]]: """스냅샷된 단관 데이터 조회""" try: query = self.db.query(PipeIssueSegment).filter( PipeIssueSegment.snapshot_id == snapshot_id ) if area: query = query.filter(PipeIssueSegment.area == area) if drawing_name: query = query.filter(PipeIssueSegment.drawing_name == drawing_name) segments = query.order_by( PipeIssueSegment.area, PipeIssueSegment.drawing_name, PipeIssueSegment.line_no ).all() result = [] for segment in segments: result.append({ "id": segment.id, "area": segment.area, "drawing_name": segment.drawing_name, "line_no": segment.line_no, "material_grade": segment.material_grade, "schedule_spec": segment.schedule_spec, "nominal_size": segment.nominal_size, "length_mm": float(segment.length_mm) if segment.length_mm else 0, "end_preparation": segment.end_preparation, "material_info": f"{segment.material_grade or ''} {segment.schedule_spec or ''} {segment.nominal_size or ''}".strip() }) return result except Exception as e: logger.error(f"Failed to get snapshot segments: {e}") return [] def get_available_areas(self, snapshot_id: int) -> List[str]: """스냅샷의 사용 가능한 구역 목록""" try: result = self.db.query(PipeIssueSegment.area).filter( and_( PipeIssueSegment.snapshot_id == snapshot_id, PipeIssueSegment.area.isnot(None) ) ).distinct().all() areas = [row.area for row in result if row.area] return sorted(areas) except Exception as e: logger.error(f"Failed to get available areas: {e}") return [] def get_available_drawings(self, snapshot_id: int, area: str = None) -> List[str]: """스냅샷의 사용 가능한 도면 목록""" try: query = self.db.query(PipeIssueSegment.drawing_name).filter( PipeIssueSegment.snapshot_id == snapshot_id ) if area: query = query.filter(PipeIssueSegment.area == area) result = query.distinct().all() drawings = [row.drawing_name for row in result if row.drawing_name] return sorted(drawings) except Exception as e: logger.error(f"Failed to get available drawings: {e}") return [] def _get_current_cutting_plan_data(self, job_no: str) -> List[Dict[str, Any]]: """현재 단관 관리 DB에서 데이터 조회""" try: cutting_plans = self.db.query(PipeCuttingPlan).filter( PipeCuttingPlan.job_no == job_no ).all() segments = [] for plan in cutting_plans: segments.append({ "original_id": plan.id, "area": plan.area, "drawing_name": plan.drawing_name, "line_no": plan.line_no, "material_grade": plan.material_grade, "schedule_spec": plan.schedule_spec, "nominal_size": plan.nominal_size, "length_mm": float(plan.length_mm) if plan.length_mm else 0, "end_preparation": plan.end_preparation or "무개선" }) return segments except Exception as e: logger.error(f"Failed to get current cutting plan data: {e}") return [] def check_revision_protection(self, job_no: str) -> Dict[str, Any]: """ 리비전 보호 상태 확인 잠긴 스냅샷이 있으면 더 이상 리비전 영향을 받지 않음 """ try: snapshot = self.db.query(PipeIssueSnapshot).filter( and_( PipeIssueSnapshot.job_no == job_no, PipeIssueSnapshot.is_active == True, PipeIssueSnapshot.is_locked == True ) ).first() if snapshot: return { "is_protected": True, "snapshot_id": snapshot.id, "snapshot_name": snapshot.snapshot_name, "locked_at": snapshot.locked_at, "locked_by": snapshot.locked_by, "message": f"이슈 관리가 진행 중입니다. 스냅샷 '{snapshot.snapshot_name}'이 보호되고 있습니다." } else: return { "is_protected": False, "message": "리비전 보호가 활성화되지 않았습니다." } except Exception as e: logger.error(f"Failed to check revision protection: {e}") return { "is_protected": False, "message": f"리비전 보호 상태 확인 실패: {str(e)}" } def get_pipe_issue_snapshot_service(db: Session = None) -> PipeIssueSnapshotService: """PipeIssueSnapshotService 인스턴스 생성""" if db is None: db = next(get_db()) return PipeIssueSnapshotService(db)