Files
TK-BOM-Project/backend/app/routers/pipe_issue.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

536 lines
18 KiB
Python

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