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

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