Files
TK-BOM-Project/backend/app/routers/pipe_snapshot.py
Hyungi Ahn 8f42a1054e
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
🔧 완전한 스키마 자동화 시스템 구축
 주요 기능:
- 완전한 데이터베이스 스키마 분석 및 자동 마이그레이션 시스템
- 44개 테이블 완전 지원 (운영 서버 43개 + 1개 추가)
- 누락된 테이블/컬럼 자동 감지 및 생성

🔧 해결된 스키마 문제:
- users.status 컬럼 누락 → 자동 추가
- files 테이블 4개 컬럼 누락 → 자동 추가
- materials 테이블 22개 컬럼 누락 → 자동 추가
- support_details, purchase_requests, purchase_request_items 테이블 누락 → 자동 생성
- material_purchase_tracking.description, purchase_status 컬럼 누락 → 자동 추가

🚀 자동화 도구:
- schema_analyzer.py: 코드와 DB 스키마 비교 분석
- auto_migrator.py: 자동 마이그레이션 실행
- docker_migrator.py: Docker 환경용 간편 마이그레이션
- schema_monitor.py: 실시간 스키마 모니터링

📋 리비전 관리 시스템:
- 8개 카테고리별 리비전 페이지 구현
- PIPE Cutting Plan 관리 시스템
- PIPE Issue Management 시스템
- 완전한 리비전 비교 및 추적 기능

🎯 사용법:
docker exec tk-mp-backend python3 scripts/docker_migrator.py

앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
2025-10-21 10:34:45 +09:00

339 lines
10 KiB
Python

"""
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)}"
)