🔧 완전한 스키마 자동화 시스템 구축
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:
409
backend/app/routers/enhanced_revision.py
Normal file
409
backend/app/routers/enhanced_revision.py
Normal file
@@ -0,0 +1,409 @@
|
||||
"""
|
||||
강화된 리비전 관리 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)}"
|
||||
)
|
||||
224
backend/app/routers/pipe_excel.py
Normal file
224
backend/app/routers/pipe_excel.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
PIPE 스냅샷 Excel 내보내기 API 라우터
|
||||
|
||||
확정된 Cutting Plan의 고정된 Excel 내보내기 기능
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from io import BytesIO
|
||||
|
||||
from ..database import get_db
|
||||
from ..services.pipe_snapshot_excel_service import get_pipe_snapshot_excel_service, PipeSnapshotExcelService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/pipe-excel", tags=["pipe-excel"])
|
||||
|
||||
|
||||
@router.get("/export-finalized/{job_no}")
|
||||
async def export_finalized_cutting_plan(
|
||||
job_no: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
확정된 Cutting Plan Excel 내보내기
|
||||
- 스냅샷 데이터 기준으로 고정된 Excel 생성
|
||||
- 리비전과 무관하게 동일한 데이터 제공
|
||||
"""
|
||||
try:
|
||||
service = get_pipe_snapshot_excel_service(db)
|
||||
result = service.export_finalized_cutting_plan(job_no)
|
||||
|
||||
if not result["success"]:
|
||||
if "확정된 Cutting Plan이 없습니다" in result["message"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=result["message"]
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=result["message"]
|
||||
)
|
||||
|
||||
# Excel 파일 스트리밍 응답
|
||||
excel_buffer = result["excel_buffer"]
|
||||
filename = result["filename"]
|
||||
|
||||
# 파일 다운로드를 위한 헤더 설정
|
||||
headers = {
|
||||
'Content-Disposition': f'attachment; filename="{filename}"',
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
}
|
||||
|
||||
return StreamingResponse(
|
||||
BytesIO(excel_buffer.getvalue()),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers=headers
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to export finalized cutting plan: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"확정된 Excel 내보내기 실패: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/check-finalization/{job_no}")
|
||||
async def check_finalization_status(
|
||||
job_no: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Cutting Plan 확정 상태 확인
|
||||
- 확정된 Excel 내보내기 가능 여부 확인
|
||||
"""
|
||||
try:
|
||||
service = get_pipe_snapshot_excel_service(db)
|
||||
result = service.check_finalization_status(job_no)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check finalization status: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"확정 상태 확인 실패: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/preview-finalized/{job_no}")
|
||||
async def preview_finalized_data(
|
||||
job_no: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
확정된 데이터 미리보기
|
||||
- Excel 생성 전 데이터 확인용
|
||||
"""
|
||||
try:
|
||||
from ..services.pipe_issue_snapshot_service import get_pipe_issue_snapshot_service
|
||||
|
||||
# 스냅샷 상태 확인
|
||||
snapshot_service = get_pipe_issue_snapshot_service(db)
|
||||
snapshot_info = snapshot_service.get_snapshot_info(job_no)
|
||||
|
||||
if not snapshot_info["has_snapshot"] or not snapshot_info["is_locked"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="확정된 Cutting Plan이 없습니다."
|
||||
)
|
||||
|
||||
# 스냅샷 데이터 조회
|
||||
snapshot_id = snapshot_info["snapshot_id"]
|
||||
segments = snapshot_service.get_snapshot_segments(snapshot_id)
|
||||
|
||||
# 구역별/도면별 통계
|
||||
area_stats = {}
|
||||
drawing_stats = {}
|
||||
material_stats = {}
|
||||
|
||||
for segment in segments:
|
||||
# 구역별 통계
|
||||
area = segment.get("area", "미할당")
|
||||
if area not in area_stats:
|
||||
area_stats[area] = {"count": 0, "total_length": 0}
|
||||
area_stats[area]["count"] += 1
|
||||
area_stats[area]["total_length"] += segment.get("length_mm", 0)
|
||||
|
||||
# 도면별 통계
|
||||
drawing = segment.get("drawing_name", "UNKNOWN")
|
||||
if drawing not in drawing_stats:
|
||||
drawing_stats[drawing] = {"count": 0, "total_length": 0}
|
||||
drawing_stats[drawing]["count"] += 1
|
||||
drawing_stats[drawing]["total_length"] += segment.get("length_mm", 0)
|
||||
|
||||
# 재질별 통계
|
||||
material = segment.get("material_grade", "UNKNOWN")
|
||||
if material not in material_stats:
|
||||
material_stats[material] = {"count": 0, "total_length": 0}
|
||||
material_stats[material]["count"] += 1
|
||||
material_stats[material]["total_length"] += segment.get("length_mm", 0)
|
||||
|
||||
return {
|
||||
"job_no": job_no,
|
||||
"snapshot_info": snapshot_info,
|
||||
"preview_data": {
|
||||
"total_segments": len(segments),
|
||||
"area_stats": area_stats,
|
||||
"drawing_stats": drawing_stats,
|
||||
"material_stats": material_stats
|
||||
},
|
||||
"sample_segments": segments[:10] if segments else [], # 처음 10개만 미리보기
|
||||
"can_export": True,
|
||||
"message": "확정된 데이터 미리보기가 준비되었습니다."
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to preview finalized data: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"데이터 미리보기 실패: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/export-temp/{job_no}")
|
||||
async def export_temp_cutting_plan(
|
||||
job_no: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
임시 Cutting Plan Excel 내보내기 (구현 예정)
|
||||
- 현재 작업 중인 데이터 기준
|
||||
- 리비전 시 변경될 수 있는 데이터
|
||||
"""
|
||||
try:
|
||||
# TODO: 임시 Excel 내보내기 구현
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="임시 Excel 내보내기 기능은 구현 예정입니다."
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to export temp cutting plan: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"임시 Excel 내보내기 실패: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/download-history/{job_no}")
|
||||
async def get_download_history(
|
||||
job_no: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Excel 다운로드 이력 조회 (구현 예정)
|
||||
- 확정된 Excel 다운로드 기록
|
||||
- 다운로드 시간 및 사용자 추적
|
||||
"""
|
||||
try:
|
||||
# TODO: 다운로드 이력 추적 구현
|
||||
return {
|
||||
"job_no": job_no,
|
||||
"download_history": [],
|
||||
"message": "다운로드 이력 추적 기능은 구현 예정입니다."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get download history: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"다운로드 이력 조회 실패: {str(e)}"
|
||||
)
|
||||
535
backend/app/routers/pipe_issue.py
Normal file
535
backend/app/routers/pipe_issue.py
Normal file
@@ -0,0 +1,535 @@
|
||||
"""
|
||||
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)}"
|
||||
)
|
||||
435
backend/app/routers/pipe_revision.py
Normal file
435
backend/app/routers/pipe_revision.py
Normal file
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
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)}"
|
||||
)
|
||||
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)}"
|
||||
)
|
||||
97
backend/app/routers/revision_comparison.py
Normal file
97
backend/app/routers/revision_comparison.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
리비전 비교 API 엔드포인트
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from ..database import get_db
|
||||
from ..auth.middleware import get_current_user
|
||||
from ..services.revision_comparison_service import RevisionComparisonService
|
||||
from ..auth.models import User
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/revision-comparison", tags=["Revision Comparison"])
|
||||
|
||||
|
||||
@router.post("/compare")
|
||||
async def compare_revisions(
|
||||
current_file_id: int = Query(..., description="현재 파일 ID"),
|
||||
previous_file_id: int = Query(..., description="이전 파일 ID"),
|
||||
category_filter: Optional[str] = Query(None, description="카테고리 필터"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
두 리비전 간 자재 비교
|
||||
"""
|
||||
|
||||
try:
|
||||
comparison_service = RevisionComparisonService(db)
|
||||
|
||||
comparison_result = comparison_service.compare_revisions(
|
||||
current_file_id=current_file_id,
|
||||
previous_file_id=previous_file_id,
|
||||
category_filter=category_filter
|
||||
)
|
||||
|
||||
logger.info(f"Revision comparison completed: {current_file_id} vs {previous_file_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": comparison_result,
|
||||
"message": "리비전 비교 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to compare revisions: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"리비전 비교 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/category/{current_file_id}/{previous_file_id}/{category}")
|
||||
async def get_category_comparison(
|
||||
current_file_id: int,
|
||||
previous_file_id: int,
|
||||
category: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
특정 카테고리의 리비전 비교
|
||||
"""
|
||||
|
||||
if category == 'PIPE':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="PIPE 카테고리는 별도 처리가 필요합니다."
|
||||
)
|
||||
|
||||
try:
|
||||
comparison_service = RevisionComparisonService(db)
|
||||
|
||||
comparison_result = comparison_service.get_category_comparison(
|
||||
current_file_id=current_file_id,
|
||||
previous_file_id=previous_file_id,
|
||||
category=category
|
||||
)
|
||||
|
||||
logger.info(f"Category comparison completed: {category} ({current_file_id} vs {previous_file_id})")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": comparison_result,
|
||||
"message": f"{category} 카테고리 비교 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to compare category {category}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"카테고리 비교 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
199
backend/app/routers/revision_material.py
Normal file
199
backend/app/routers/revision_material.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
리비전 자재 처리 API 엔드포인트
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Dict, Any
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..database import get_db
|
||||
from ..auth.middleware import get_current_user
|
||||
from ..services.revision_material_service import RevisionMaterialService
|
||||
from ..auth.models import User
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/revision-material", tags=["Revision Material"])
|
||||
|
||||
|
||||
class ProcessingResultRequest(BaseModel):
|
||||
processing_results: List[Dict[str, Any]]
|
||||
|
||||
|
||||
class MaterialProcessRequest(BaseModel):
|
||||
action: str
|
||||
additional_data: Dict[str, Any] = {}
|
||||
|
||||
|
||||
@router.get("/category/{file_id}/{category}")
|
||||
async def get_category_materials(
|
||||
file_id: int,
|
||||
category: str,
|
||||
include_processing_info: bool = True,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
리비전 페이지용 카테고리별 자재 조회
|
||||
"""
|
||||
|
||||
if category == 'PIPE':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="PIPE 카테고리는 별도 처리가 필요합니다."
|
||||
)
|
||||
|
||||
try:
|
||||
material_service = RevisionMaterialService(db)
|
||||
|
||||
materials = material_service.get_category_materials_for_revision(
|
||||
file_id=file_id,
|
||||
category=category,
|
||||
include_processing_info=include_processing_info
|
||||
)
|
||||
|
||||
# 처리 정보 요약
|
||||
processing_info = {
|
||||
"total_materials": len(materials),
|
||||
"by_status": {},
|
||||
"by_priority": {"high": 0, "medium": 0, "low": 0}
|
||||
}
|
||||
|
||||
for material in materials:
|
||||
proc_info = material.get('processing_info', {})
|
||||
status = proc_info.get('display_status', 'UNKNOWN')
|
||||
priority = proc_info.get('priority', 'medium')
|
||||
|
||||
processing_info["by_status"][status] = processing_info["by_status"].get(status, 0) + 1
|
||||
processing_info["by_priority"][priority] += 1
|
||||
|
||||
logger.info(f"Retrieved {len(materials)} materials for category {category} in file {file_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"materials": materials,
|
||||
"processing_info": processing_info
|
||||
},
|
||||
"message": f"{category} 카테고리 자재 조회 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get category materials: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"카테고리 자재 조회 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/process/{material_id}")
|
||||
async def process_material(
|
||||
material_id: int,
|
||||
request: MaterialProcessRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
개별 자재 처리
|
||||
"""
|
||||
|
||||
try:
|
||||
material_service = RevisionMaterialService(db)
|
||||
|
||||
# 자재 정보 조회
|
||||
material_query = """
|
||||
SELECT * FROM materials WHERE id = :material_id
|
||||
"""
|
||||
|
||||
result = material_service.db_service.execute_query(
|
||||
material_query, {"material_id": material_id}
|
||||
)
|
||||
material = result.fetchone()
|
||||
|
||||
if not material:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="자재를 찾을 수 없습니다."
|
||||
)
|
||||
|
||||
material_dict = dict(material._mapping)
|
||||
|
||||
# 액션에 따른 처리
|
||||
if request.action == "new_material":
|
||||
processing_result = material_service.process_new_material(
|
||||
material_dict,
|
||||
material_dict.get('classified_category', 'UNKNOWN')
|
||||
)
|
||||
elif request.action == "remove_material":
|
||||
processing_result = material_service.process_removed_material(
|
||||
material_dict,
|
||||
material_dict.get('classified_category', 'UNKNOWN')
|
||||
)
|
||||
else:
|
||||
# 기본 처리 (구매 상태별)
|
||||
# 이전 자재 정보가 필요한 경우 additional_data에서 가져옴
|
||||
prev_material = request.additional_data.get('previous_material', material_dict)
|
||||
|
||||
processing_result = material_service.process_material_by_purchase_status(
|
||||
prev_material,
|
||||
material_dict,
|
||||
material_dict.get('classified_category', 'UNKNOWN')
|
||||
)
|
||||
|
||||
# 처리 결과 적용
|
||||
apply_result = material_service.apply_material_processing_results([processing_result])
|
||||
|
||||
logger.info(f"Processed material {material_id} with action {request.action}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"processing_result": processing_result,
|
||||
"apply_result": apply_result
|
||||
},
|
||||
"message": "자재 처리 완료"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process material {material_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"자재 처리 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/apply-results")
|
||||
async def apply_processing_results(
|
||||
request: ProcessingResultRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
자재 처리 결과를 일괄 적용
|
||||
"""
|
||||
|
||||
try:
|
||||
material_service = RevisionMaterialService(db)
|
||||
|
||||
apply_result = material_service.apply_material_processing_results(
|
||||
request.processing_results
|
||||
)
|
||||
|
||||
logger.info(f"Applied {len(request.processing_results)} processing results")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": apply_result,
|
||||
"message": "처리 결과 적용 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply processing results: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"처리 결과 적용 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
130
backend/app/routers/revision_redirect.py
Normal file
130
backend/app/routers/revision_redirect.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
리비전 리다이렉트 API 엔드포인트
|
||||
BOM 페이지 접근 시 리비전 페이지로 리다이렉트 필요성 판단
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from ..database import get_db
|
||||
from ..auth.middleware import get_current_user
|
||||
from ..services.revision_logic_service import RevisionLogicService
|
||||
from ..auth.models import User
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/revision-redirect", tags=["Revision Redirect"])
|
||||
|
||||
|
||||
@router.get("/check/{job_no}/{file_id}")
|
||||
async def check_revision_redirect(
|
||||
job_no: str,
|
||||
file_id: int,
|
||||
previous_file_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
리비전 페이지 리다이렉트 필요성 확인
|
||||
|
||||
Args:
|
||||
job_no: 작업 번호
|
||||
file_id: 현재 파일 ID
|
||||
previous_file_id: 이전 파일 ID (선택사항)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"should_redirect": bool,
|
||||
"reason": str,
|
||||
"redirect_url": str,
|
||||
"processing_summary": dict
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
revision_service = RevisionLogicService(db)
|
||||
|
||||
# 리다이렉트 필요성 판단
|
||||
should_redirect, reason = revision_service.should_redirect_to_revision_page(
|
||||
job_no=job_no,
|
||||
current_file_id=file_id,
|
||||
previous_file_id=previous_file_id
|
||||
)
|
||||
|
||||
result = {
|
||||
"should_redirect": should_redirect,
|
||||
"reason": reason,
|
||||
"redirect_url": f"/enhanced-revision?job_no={job_no}¤t_file_id={file_id}",
|
||||
"processing_summary": None
|
||||
}
|
||||
|
||||
# 상세 처리 결과도 함께 반환 (필요시)
|
||||
if should_redirect:
|
||||
processing_result = revision_service.process_revision_by_purchase_status(
|
||||
job_no=job_no,
|
||||
current_file_id=file_id,
|
||||
previous_file_id=previous_file_id
|
||||
)
|
||||
result["processing_summary"] = processing_result["summary"]
|
||||
|
||||
logger.info(f"Revision redirect check for {job_no}/{file_id}: {should_redirect}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result,
|
||||
"message": "리비전 리다이렉트 확인 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check revision redirect: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"리비전 리다이렉트 확인 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/process-revision/{job_no}/{file_id}")
|
||||
async def process_revision_logic(
|
||||
job_no: str,
|
||||
file_id: int,
|
||||
previous_file_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
리비전 로직 처리 및 결과 반환
|
||||
|
||||
Args:
|
||||
job_no: 작업 번호
|
||||
file_id: 현재 파일 ID
|
||||
previous_file_id: 이전 파일 ID (선택사항)
|
||||
|
||||
Returns:
|
||||
전체 리비전 처리 결과
|
||||
"""
|
||||
|
||||
try:
|
||||
revision_service = RevisionLogicService(db)
|
||||
|
||||
processing_result = revision_service.process_revision_by_purchase_status(
|
||||
job_no=job_no,
|
||||
current_file_id=file_id,
|
||||
previous_file_id=previous_file_id
|
||||
)
|
||||
|
||||
logger.info(f"Revision processing completed for {job_no}/{file_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": processing_result,
|
||||
"message": "리비전 처리 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process revision logic: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"리비전 처리 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
258
backend/app/routers/revision_status.py
Normal file
258
backend/app/routers/revision_status.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
리비전 상태 관리 API 엔드포인트
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..database import get_db
|
||||
from ..auth.middleware import get_current_user
|
||||
from ..services.revision_status_service import RevisionStatusService
|
||||
from ..auth.models import User
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/revision-status", tags=["Revision Status"])
|
||||
|
||||
|
||||
class CreateComparisonRequest(BaseModel):
|
||||
job_no: str
|
||||
current_file_id: int
|
||||
previous_file_id: int
|
||||
comparison_result: dict
|
||||
|
||||
|
||||
class RejectComparisonRequest(BaseModel):
|
||||
reason: str = ""
|
||||
|
||||
|
||||
@router.get("/{job_no}/{file_id}")
|
||||
async def get_revision_status(
|
||||
job_no: str,
|
||||
file_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
리비전 상태 조회
|
||||
"""
|
||||
|
||||
try:
|
||||
status_service = RevisionStatusService(db)
|
||||
|
||||
revision_status = status_service.get_revision_status(job_no, file_id)
|
||||
|
||||
if "error" in revision_status:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=revision_status["error"]
|
||||
)
|
||||
|
||||
logger.info(f"Retrieved revision status for {job_no}/{file_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": revision_status,
|
||||
"message": "리비전 상태 조회 완료"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get revision status: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"리비전 상태 조회 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/history/{job_no}")
|
||||
async def get_revision_history(
|
||||
job_no: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
작업의 전체 리비전 히스토리 조회
|
||||
"""
|
||||
|
||||
try:
|
||||
status_service = RevisionStatusService(db)
|
||||
|
||||
history = status_service.get_revision_history(job_no)
|
||||
|
||||
logger.info(f"Retrieved revision history for {job_no}: {len(history)} revisions")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": history,
|
||||
"message": "리비전 히스토리 조회 완료"
|
||||
}
|
||||
|
||||
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.post("/create-comparison")
|
||||
async def create_comparison_record(
|
||||
request: CreateComparisonRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
리비전 비교 기록 생성
|
||||
"""
|
||||
|
||||
try:
|
||||
status_service = RevisionStatusService(db)
|
||||
|
||||
comparison_id = status_service.create_revision_comparison_record(
|
||||
job_no=request.job_no,
|
||||
current_file_id=request.current_file_id,
|
||||
previous_file_id=request.previous_file_id,
|
||||
comparison_result=request.comparison_result,
|
||||
created_by=current_user.username
|
||||
)
|
||||
|
||||
logger.info(f"Created revision comparison record: {comparison_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {"comparison_id": comparison_id},
|
||||
"message": "리비전 비교 기록 생성 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create comparison record: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"비교 기록 생성 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/apply-comparison/{comparison_id}")
|
||||
async def apply_comparison(
|
||||
comparison_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
리비전 비교 결과 적용
|
||||
"""
|
||||
|
||||
try:
|
||||
status_service = RevisionStatusService(db)
|
||||
|
||||
apply_result = status_service.apply_revision_comparison(
|
||||
comparison_id=comparison_id,
|
||||
applied_by=current_user.username
|
||||
)
|
||||
|
||||
if not apply_result["success"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=apply_result["error"]
|
||||
)
|
||||
|
||||
logger.info(f"Applied revision comparison: {comparison_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": apply_result,
|
||||
"message": "리비전 비교 결과 적용 완료"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply comparison {comparison_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"비교 결과 적용 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/pending")
|
||||
async def get_pending_revisions(
|
||||
job_no: Optional[str] = Query(None, description="특정 작업의 대기 중인 리비전만 조회"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
대기 중인 리비전 목록 조회
|
||||
"""
|
||||
|
||||
try:
|
||||
status_service = RevisionStatusService(db)
|
||||
|
||||
pending_revisions = status_service.get_pending_revisions(job_no)
|
||||
|
||||
logger.info(f"Retrieved {len(pending_revisions)} pending revisions")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": pending_revisions,
|
||||
"message": "대기 중인 리비전 조회 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get pending revisions: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"대기 중인 리비전 조회 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/reject-comparison/{comparison_id}")
|
||||
async def reject_comparison(
|
||||
comparison_id: int,
|
||||
request: RejectComparisonRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
리비전 비교 결과 거부
|
||||
"""
|
||||
|
||||
try:
|
||||
# 비교 기록을 거부 상태로 업데이트
|
||||
update_query = """
|
||||
UPDATE revision_comparisons
|
||||
SET is_applied = false,
|
||||
notes = CONCAT(COALESCE(notes, ''), '\n거부됨: ', :reason, ' (by ', :rejected_by, ')')
|
||||
WHERE id = :comparison_id
|
||||
"""
|
||||
|
||||
from ..services.database_service import DatabaseService
|
||||
db_service = DatabaseService(db)
|
||||
|
||||
db_service.execute_query(update_query, {
|
||||
"comparison_id": comparison_id,
|
||||
"reason": request.reason or "사유 없음",
|
||||
"rejected_by": current_user.username
|
||||
})
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Rejected revision comparison: {comparison_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {"comparison_id": comparison_id, "reason": request.reason},
|
||||
"message": "리비전 비교 결과 거부 완료"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reject comparison {comparison_id}: {e}")
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"비교 결과 거부 중 오류가 발생했습니다: {str(e)}"
|
||||
)
|
||||
Reference in New Issue
Block a user