🔧 완전한 스키마 자동화 시스템 구축
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
This commit is contained in:
338
backend/app/routers/pipe_snapshot.py
Normal file
338
backend/app/routers/pipe_snapshot.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
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)}"
|
||||
)
|
||||
Reference in New Issue
Block a user