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 앞으로 스키마 문제가 발생하면 위 명령 하나로 자동 해결!
410 lines
14 KiB
Python
410 lines
14 KiB
Python
"""
|
|
강화된 리비전 관리 API 엔드포인트
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from typing import List, Dict, Any, Optional
|
|
from datetime import datetime
|
|
|
|
from ..database import get_db
|
|
from ..auth.middleware import get_current_user
|
|
from ..services.enhanced_revision_service import EnhancedRevisionService
|
|
from ..auth.models import User
|
|
from ..models import RevisionComparison, RevisionChangeLog
|
|
from ..utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter(prefix="/enhanced-revision", tags=["Enhanced Revision Management"])
|
|
|
|
|
|
@router.post("/compare-revisions")
|
|
async def compare_revisions_enhanced(
|
|
job_no: str,
|
|
current_file_id: int,
|
|
previous_file_id: Optional[int] = None,
|
|
save_comparison: bool = True,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
강화된 리비전 비교 수행
|
|
|
|
Args:
|
|
job_no: 작업 번호
|
|
current_file_id: 현재 파일 ID
|
|
previous_file_id: 이전 파일 ID (None이면 자동 탐지)
|
|
save_comparison: 비교 결과 저장 여부
|
|
"""
|
|
|
|
try:
|
|
revision_service = EnhancedRevisionService(db)
|
|
|
|
# 리비전 비교 수행
|
|
comparison_result = revision_service.compare_revisions_with_purchase_status(
|
|
job_no=job_no,
|
|
current_file_id=current_file_id,
|
|
previous_file_id=previous_file_id
|
|
)
|
|
|
|
# 비교 결과 저장 (옵션)
|
|
if save_comparison:
|
|
comparison_record = RevisionComparison(
|
|
job_no=job_no,
|
|
current_file_id=current_file_id,
|
|
previous_file_id=previous_file_id,
|
|
comparison_result=comparison_result,
|
|
summary_stats=comparison_result.get("summary", {}),
|
|
created_by=current_user.username,
|
|
is_applied=False
|
|
)
|
|
|
|
db.add(comparison_record)
|
|
db.commit()
|
|
db.refresh(comparison_record)
|
|
|
|
comparison_result["comparison_id"] = comparison_record.id
|
|
|
|
logger.info(f"Enhanced revision comparison completed for job {job_no}")
|
|
|
|
return {
|
|
"success": True,
|
|
"data": comparison_result,
|
|
"message": "강화된 리비전 비교가 완료되었습니다."
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Enhanced revision comparison failed: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"리비전 비교 중 오류가 발생했습니다: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/apply-revision-changes/{comparison_id}")
|
|
async def apply_revision_changes(
|
|
comparison_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
리비전 변경사항을 실제 DB에 적용
|
|
|
|
Args:
|
|
comparison_id: 비교 결과 ID
|
|
"""
|
|
|
|
try:
|
|
# 비교 결과 조회
|
|
comparison = db.query(RevisionComparison).filter(
|
|
RevisionComparison.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="이미 적용된 비교 결과입니다."
|
|
)
|
|
|
|
revision_service = EnhancedRevisionService(db)
|
|
|
|
# 변경사항 적용
|
|
apply_result = revision_service.apply_revision_changes(
|
|
comparison_result=comparison.comparison_result,
|
|
current_file_id=comparison.current_file_id
|
|
)
|
|
|
|
if apply_result["success"]:
|
|
# 적용 완료 표시
|
|
comparison.is_applied = True
|
|
comparison.applied_at = datetime.utcnow()
|
|
comparison.applied_by = current_user.username
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"Revision changes applied for comparison {comparison_id}")
|
|
|
|
return {
|
|
"success": True,
|
|
"data": apply_result,
|
|
"message": "리비전 변경사항이 성공적으로 적용되었습니다."
|
|
}
|
|
else:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=apply_result.get("message", "변경사항 적용 중 오류가 발생했습니다.")
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
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("/comparison-history/{job_no}")
|
|
async def get_comparison_history(
|
|
job_no: str,
|
|
limit: int = 10,
|
|
offset: int = 0,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
작업별 리비전 비교 이력 조회
|
|
|
|
Args:
|
|
job_no: 작업 번호
|
|
limit: 조회 개수 제한
|
|
offset: 조회 시작 위치
|
|
"""
|
|
|
|
try:
|
|
comparisons = db.query(RevisionComparison).filter(
|
|
RevisionComparison.job_no == job_no
|
|
).order_by(
|
|
RevisionComparison.comparison_date.desc()
|
|
).offset(offset).limit(limit).all()
|
|
|
|
result = []
|
|
for comp in comparisons:
|
|
result.append({
|
|
"id": comp.id,
|
|
"job_no": comp.job_no,
|
|
"current_file_id": comp.current_file_id,
|
|
"previous_file_id": comp.previous_file_id,
|
|
"comparison_date": comp.comparison_date.isoformat(),
|
|
"summary_stats": comp.summary_stats,
|
|
"created_by": comp.created_by,
|
|
"is_applied": comp.is_applied,
|
|
"applied_at": comp.applied_at.isoformat() if comp.applied_at else None,
|
|
"applied_by": comp.applied_by
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"data": result,
|
|
"total": len(result),
|
|
"message": "리비전 비교 이력을 조회했습니다."
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get comparison history: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"비교 이력 조회 중 오류가 발생했습니다: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/comparison-details/{comparison_id}")
|
|
async def get_comparison_details(
|
|
comparison_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
특정 비교 결과의 상세 정보 조회
|
|
|
|
Args:
|
|
comparison_id: 비교 결과 ID
|
|
"""
|
|
|
|
try:
|
|
comparison = db.query(RevisionComparison).filter(
|
|
RevisionComparison.id == comparison_id
|
|
).first()
|
|
|
|
if not comparison:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="비교 결과를 찾을 수 없습니다."
|
|
)
|
|
|
|
# 변경 로그도 함께 조회
|
|
change_logs = db.query(RevisionChangeLog).filter(
|
|
RevisionChangeLog.comparison_id == comparison_id
|
|
).all()
|
|
|
|
result = {
|
|
"comparison": {
|
|
"id": comparison.id,
|
|
"job_no": comparison.job_no,
|
|
"current_file_id": comparison.current_file_id,
|
|
"previous_file_id": comparison.previous_file_id,
|
|
"comparison_date": comparison.comparison_date.isoformat(),
|
|
"comparison_result": comparison.comparison_result,
|
|
"summary_stats": comparison.summary_stats,
|
|
"created_by": comparison.created_by,
|
|
"is_applied": comparison.is_applied,
|
|
"applied_at": comparison.applied_at.isoformat() if comparison.applied_at else None,
|
|
"applied_by": comparison.applied_by
|
|
},
|
|
"change_logs": [
|
|
{
|
|
"id": log.id,
|
|
"material_id": log.material_id,
|
|
"change_type": log.change_type,
|
|
"previous_data": log.previous_data,
|
|
"current_data": log.current_data,
|
|
"action_taken": log.action_taken,
|
|
"notes": log.notes,
|
|
"created_at": log.created_at.isoformat()
|
|
}
|
|
for log in change_logs
|
|
]
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"data": result,
|
|
"message": "비교 결과 상세 정보를 조회했습니다."
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to get comparison details: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"비교 결과 조회 중 오류가 발생했습니다: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/pipe-length-summary/{file_id}")
|
|
async def get_pipe_length_summary(
|
|
file_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
PIPE 자재 길이 요약 정보 조회
|
|
|
|
Args:
|
|
file_id: 파일 ID
|
|
"""
|
|
|
|
try:
|
|
revision_service = EnhancedRevisionService(db)
|
|
|
|
# PIPE 자재만 조회하여 도면-라인넘버별 길이 집계
|
|
pipe_materials = revision_service._get_materials_with_purchase_status(file_id)
|
|
|
|
pipe_summary = {}
|
|
for key, material in pipe_materials.items():
|
|
if material.get('classified_category') == 'PIPE':
|
|
drawing_line = f"{material.get('drawing_name', 'Unknown')} - {material.get('line_no', 'Unknown')}"
|
|
|
|
if drawing_line not in pipe_summary:
|
|
pipe_summary[drawing_line] = {
|
|
"drawing_name": material.get('drawing_name'),
|
|
"line_no": material.get('line_no'),
|
|
"material_grade": material.get('material_grade'),
|
|
"schedule": material.get('schedule'),
|
|
"nominal_size": material.get('main_nom'),
|
|
"total_length": 0,
|
|
"segment_count": 0,
|
|
"purchase_status": "mixed"
|
|
}
|
|
|
|
pipe_summary[drawing_line]["total_length"] += material.get('total_length', 0)
|
|
pipe_summary[drawing_line]["segment_count"] += 1
|
|
|
|
# 구매 상태 확인
|
|
if material.get('purchase_confirmed'):
|
|
if pipe_summary[drawing_line]["purchase_status"] == "mixed":
|
|
pipe_summary[drawing_line]["purchase_status"] = "purchased"
|
|
else:
|
|
if pipe_summary[drawing_line]["purchase_status"] == "purchased":
|
|
pipe_summary[drawing_line]["purchase_status"] = "mixed"
|
|
elif pipe_summary[drawing_line]["purchase_status"] != "mixed":
|
|
pipe_summary[drawing_line]["purchase_status"] = "pending"
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"file_id": file_id,
|
|
"pipe_lines": list(pipe_summary.values()),
|
|
"total_lines": len(pipe_summary),
|
|
"total_length": sum(line["total_length"] for line in pipe_summary.values())
|
|
},
|
|
"message": "PIPE 자재 길이 요약을 조회했습니다."
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get pipe length summary: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"PIPE 길이 요약 조회 중 오류가 발생했습니다: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/recalculate-pipe-lengths/{file_id}")
|
|
async def recalculate_pipe_lengths(
|
|
file_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""
|
|
PIPE 자재 길이 재계산 및 업데이트
|
|
|
|
Args:
|
|
file_id: 파일 ID
|
|
"""
|
|
|
|
try:
|
|
revision_service = EnhancedRevisionService(db)
|
|
|
|
# PIPE 자재 조회 및 길이 재계산
|
|
pipe_materials = revision_service._get_materials_with_purchase_status(file_id)
|
|
|
|
updated_count = 0
|
|
for key, material in pipe_materials.items():
|
|
if material.get('classified_category') == 'PIPE':
|
|
# total_length 업데이트
|
|
total_length = material.get('total_length', 0)
|
|
|
|
update_query = """
|
|
UPDATE materials
|
|
SET total_length = :total_length,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = :material_id
|
|
"""
|
|
|
|
revision_service.db_service.execute_query(update_query, {
|
|
"total_length": total_length,
|
|
"material_id": material["id"]
|
|
})
|
|
|
|
updated_count += 1
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"Recalculated pipe lengths for {updated_count} materials in file {file_id}")
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"file_id": file_id,
|
|
"updated_count": updated_count
|
|
},
|
|
"message": f"PIPE 자재 {updated_count}개의 길이를 재계산했습니다."
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to recalculate pipe lengths: {e}")
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"PIPE 길이 재계산 중 오류가 발생했습니다: {str(e)}"
|
|
)
|