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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
436 lines
15 KiB
Python
436 lines
15 KiB
Python
"""
|
|
PIPE 리비전 관리 API 라우터
|
|
|
|
Cutting Plan 전/후 리비전 처리 및 비교 기능
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, List, Optional, Any
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
|
|
from ..database import get_db
|
|
from ..services.pipe_revision_service import get_pipe_revision_service, PipeRevisionService
|
|
from ..models import PipeRevisionComparison, PipeRevisionChange
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/pipe-revision", tags=["pipe-revision"])
|
|
|
|
|
|
# Pydantic 모델들
|
|
class RevisionStatusRequest(BaseModel):
|
|
job_no: str
|
|
new_file_id: int
|
|
|
|
|
|
class PreRevisionRequest(BaseModel):
|
|
job_no: str
|
|
new_file_id: int
|
|
|
|
|
|
class PostRevisionRequest(BaseModel):
|
|
job_no: str
|
|
new_file_id: int
|
|
|
|
|
|
class RevisionStatusResponse(BaseModel):
|
|
revision_type: str
|
|
requires_action: bool
|
|
message: str
|
|
previous_file_id: Optional[int] = None
|
|
|
|
|
|
class PreRevisionResponse(BaseModel):
|
|
status: str
|
|
revision_type: str
|
|
deleted_items: int
|
|
new_pipe_materials: int
|
|
message: str
|
|
next_action: str
|
|
pipe_materials: List[Dict[str, Any]]
|
|
|
|
|
|
class PostRevisionResponse(BaseModel):
|
|
status: str
|
|
revision_type: str
|
|
comparison_id: int
|
|
summary: Dict[str, Any]
|
|
changed_drawings: List[Dict[str, Any]]
|
|
unchanged_drawings: List[Dict[str, Any]]
|
|
message: str
|
|
next_action: str
|
|
|
|
|
|
@router.post("/check-status", response_model=RevisionStatusResponse)
|
|
async def check_revision_status(
|
|
request: RevisionStatusRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
PIPE 리비전 상태 확인
|
|
- 리비전 유형 판단 (pre/post cutting plan)
|
|
- 필요한 처리 방식 결정
|
|
"""
|
|
try:
|
|
service = get_pipe_revision_service(db)
|
|
result = service.check_revision_status(request.job_no, request.new_file_id)
|
|
|
|
return RevisionStatusResponse(**result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to check pipe revision status: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"리비전 상태 확인 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/handle-pre-cutting-plan", response_model=PreRevisionResponse)
|
|
async def handle_pre_cutting_plan_revision(
|
|
request: PreRevisionRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Cutting Plan 작성 전 리비전 처리
|
|
- 기존 PIPE 데이터 삭제
|
|
- 새 BOM 데이터로 초기화
|
|
"""
|
|
try:
|
|
service = get_pipe_revision_service(db)
|
|
result = service.handle_pre_cutting_plan_revision(request.job_no, request.new_file_id)
|
|
|
|
if result["status"] != "success":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=result["message"]
|
|
)
|
|
|
|
return PreRevisionResponse(**result)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to handle pre-cutting-plan revision: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Cutting Plan 작성 전 리비전 처리 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/handle-post-cutting-plan", response_model=PostRevisionResponse)
|
|
async def handle_post_cutting_plan_revision(
|
|
request: PostRevisionRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Cutting Plan 작성 후 리비전 처리
|
|
- 기존 Cutting Plan과 신규 BOM 비교
|
|
- 변경사항 상세 분석
|
|
"""
|
|
try:
|
|
service = get_pipe_revision_service(db)
|
|
result = service.handle_post_cutting_plan_revision(request.job_no, request.new_file_id)
|
|
|
|
if result["status"] != "success":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=result["message"]
|
|
)
|
|
|
|
return PostRevisionResponse(**result)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to handle post-cutting-plan revision: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Cutting Plan 작성 후 리비전 처리 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/comparison/{comparison_id}")
|
|
async def get_revision_comparison(
|
|
comparison_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
리비전 비교 결과 상세 조회
|
|
"""
|
|
try:
|
|
# 비교 결과 조회
|
|
comparison = db.query(PipeRevisionComparison).filter(
|
|
PipeRevisionComparison.id == comparison_id
|
|
).first()
|
|
|
|
if not comparison:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="비교 결과를 찾을 수 없습니다."
|
|
)
|
|
|
|
# 상세 변경사항 조회
|
|
changes = db.query(PipeRevisionChange).filter(
|
|
PipeRevisionChange.comparison_id == comparison_id
|
|
).all()
|
|
|
|
# 도면별로 변경사항 그룹화
|
|
changes_by_drawing = {}
|
|
for change in changes:
|
|
drawing = change.drawing_name
|
|
if drawing not in changes_by_drawing:
|
|
changes_by_drawing[drawing] = []
|
|
|
|
changes_by_drawing[drawing].append({
|
|
"id": change.id,
|
|
"change_type": change.change_type,
|
|
"old_data": {
|
|
"line_no": change.old_line_no,
|
|
"material_grade": change.old_material_grade,
|
|
"schedule_spec": change.old_schedule_spec,
|
|
"nominal_size": change.old_nominal_size,
|
|
"length_mm": change.old_length_mm,
|
|
"end_preparation": change.old_end_preparation
|
|
} if change.old_line_no else None,
|
|
"new_data": {
|
|
"line_no": change.new_line_no,
|
|
"material_grade": change.new_material_grade,
|
|
"schedule_spec": change.new_schedule_spec,
|
|
"nominal_size": change.new_nominal_size,
|
|
"length_mm": change.new_length_mm,
|
|
"end_preparation": change.new_end_preparation
|
|
} if change.new_line_no else None,
|
|
"change_reason": change.change_reason
|
|
})
|
|
|
|
return {
|
|
"comparison_id": comparison.id,
|
|
"job_no": comparison.job_no,
|
|
"comparison_date": comparison.comparison_date,
|
|
"summary": {
|
|
"total_drawings": comparison.total_drawings,
|
|
"changed_drawings": comparison.changed_drawings,
|
|
"unchanged_drawings": comparison.unchanged_drawings,
|
|
"total_segments": comparison.total_segments,
|
|
"added_segments": comparison.added_segments,
|
|
"removed_segments": comparison.removed_segments,
|
|
"modified_segments": comparison.modified_segments,
|
|
"unchanged_segments": comparison.unchanged_segments
|
|
},
|
|
"changes_by_drawing": changes_by_drawing,
|
|
"is_applied": comparison.is_applied,
|
|
"applied_at": comparison.applied_at,
|
|
"applied_by": comparison.applied_by
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to get revision comparison: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"리비전 비교 결과 조회 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/comparison/{comparison_id}/apply")
|
|
async def apply_revision_changes(
|
|
comparison_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
리비전 변경사항 적용
|
|
"""
|
|
try:
|
|
# 비교 결과 조회
|
|
comparison = db.query(PipeRevisionComparison).filter(
|
|
PipeRevisionComparison.id == comparison_id
|
|
).first()
|
|
|
|
if not comparison:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="비교 결과를 찾을 수 없습니다."
|
|
)
|
|
|
|
if comparison.is_applied:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="이미 적용된 리비전입니다."
|
|
)
|
|
|
|
# 적용 상태 업데이트
|
|
comparison.is_applied = True
|
|
comparison.applied_at = datetime.utcnow()
|
|
comparison.applied_by = "system" # 추후 사용자 정보로 변경
|
|
|
|
db.commit()
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": "리비전 변경사항이 적용되었습니다.",
|
|
"comparison_id": comparison_id,
|
|
"applied_at": comparison.applied_at
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to apply revision changes: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"리비전 변경사항 적용 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/job/{job_no}/history")
|
|
async def get_revision_history(
|
|
job_no: str,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
작업별 PIPE 리비전 이력 조회
|
|
"""
|
|
try:
|
|
comparisons = db.query(PipeRevisionComparison).filter(
|
|
PipeRevisionComparison.job_no == job_no
|
|
).order_by(PipeRevisionComparison.comparison_date.desc()).all()
|
|
|
|
history = []
|
|
for comp in comparisons:
|
|
history.append({
|
|
"comparison_id": comp.id,
|
|
"comparison_date": comp.comparison_date,
|
|
"summary": {
|
|
"total_drawings": comp.total_drawings,
|
|
"changed_drawings": comp.changed_drawings,
|
|
"total_segments": comp.total_segments,
|
|
"added_segments": comp.added_segments,
|
|
"removed_segments": comp.removed_segments,
|
|
"modified_segments": comp.modified_segments
|
|
},
|
|
"is_applied": comp.is_applied,
|
|
"applied_at": comp.applied_at,
|
|
"applied_by": comp.applied_by,
|
|
"created_by": comp.created_by
|
|
})
|
|
|
|
return {
|
|
"job_no": job_no,
|
|
"total_revisions": len(history),
|
|
"history": history
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get revision history: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"리비전 이력 조회 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/comparison/{comparison_id}/purchase-impact")
|
|
async def get_purchase_impact(
|
|
comparison_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
리비전이 구매량에 미치는 영향 분석
|
|
"""
|
|
try:
|
|
# 비교 결과 조회
|
|
comparison = db.query(PipeRevisionComparison).filter(
|
|
PipeRevisionComparison.id == comparison_id
|
|
).first()
|
|
|
|
if not comparison:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="비교 결과를 찾을 수 없습니다."
|
|
)
|
|
|
|
# 변경사항 조회
|
|
changes = db.query(PipeRevisionChange).filter(
|
|
PipeRevisionChange.comparison_id == comparison_id
|
|
).all()
|
|
|
|
# 재질별 길이 변화 계산
|
|
material_impacts = {}
|
|
|
|
for change in changes:
|
|
# 재질 정보 추출
|
|
material = None
|
|
length_change = 0
|
|
|
|
if change.change_type == "added" and change.new_material_grade:
|
|
material = change.new_material_grade
|
|
length_change = change.new_length_mm or 0
|
|
elif change.change_type == "removed" and change.old_material_grade:
|
|
material = change.old_material_grade
|
|
length_change = -(change.old_length_mm or 0)
|
|
elif change.change_type == "modified":
|
|
# 재질이 같은 경우만 길이 변화 계산
|
|
if (change.old_material_grade == change.new_material_grade and
|
|
change.old_material_grade):
|
|
material = change.old_material_grade
|
|
old_length = change.old_length_mm or 0
|
|
new_length = change.new_length_mm or 0
|
|
length_change = new_length - old_length
|
|
|
|
if material and length_change != 0:
|
|
if material not in material_impacts:
|
|
material_impacts[material] = {
|
|
"material": material,
|
|
"total_length_change": 0,
|
|
"change_count": 0
|
|
}
|
|
|
|
material_impacts[material]["total_length_change"] += length_change
|
|
material_impacts[material]["change_count"] += 1
|
|
|
|
# 결과 정리
|
|
impact_summary = []
|
|
for material, impact in material_impacts.items():
|
|
length_change_m = impact["total_length_change"] / 1000 # mm to m
|
|
impact_summary.append({
|
|
"material": material,
|
|
"length_change_mm": impact["total_length_change"],
|
|
"length_change_m": round(length_change_m, 3),
|
|
"change_count": impact["change_count"],
|
|
"impact_type": "increase" if length_change_m > 0 else "decrease",
|
|
"requires_additional_purchase": length_change_m > 0
|
|
})
|
|
|
|
# 전체 영향 요약
|
|
total_additional_purchase = sum(
|
|
impact["length_change_m"] for impact in impact_summary
|
|
if impact["requires_additional_purchase"]
|
|
)
|
|
|
|
return {
|
|
"comparison_id": comparison_id,
|
|
"job_no": comparison.job_no,
|
|
"material_impacts": impact_summary,
|
|
"summary": {
|
|
"total_materials_affected": len(impact_summary),
|
|
"materials_requiring_additional_purchase": len([
|
|
i for i in impact_summary if i["requires_additional_purchase"]
|
|
]),
|
|
"total_additional_purchase_needed": total_additional_purchase > 0,
|
|
"total_additional_length_m": round(total_additional_purchase, 3)
|
|
}
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to get purchase impact: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"구매 영향 분석 실패: {str(e)}"
|
|
)
|