- 모든 카테고리 구매신청 기능 완성 (PIPE, FITTING, VALVE, FLANGE, GASKET, BOLT, SUPPORT, SPECIAL, UNKNOWN) - 구매신청 완료 항목: 회색 배경, 체크박스 비활성화, '구매신청완료' 배지 표시 - 전체 선택/구매신청 시 이미 구매신청된 항목 자동 제외 - 구매신청 quantity 타입 에러 수정 (문자열 -> 정수 변환) SUPPORT 카테고리 (구 U-BOLT): - U-BOLT -> SUPPORT로 카테고리명 변경 - 클램프, 유볼트, 우레탄블럭슈 분류 개선 - 테이블 헤더: 선택-종류-타입-크기-디스크립션-추가요구-사용자요구-수량 - 크기 정보 main_nom 필드에서 가져오기 (배관 인치) - 엑셀 내보내기 형식 조정 SPECIAL 카테고리: - SPECIAL 키워드 자재 자동 분류 (SPECIFICATION 제외) - 파일 업로드 시 SPECIAL 카테고리 처리 로직 추가 - 도면번호 필드 추가 (drawing_name, line_no) - 타입 필드: 크기/스케줄/재질 제외한 핵심 정보 표시 - 엑셀 DWG_NAME, LINE_NUM 컬럼 파싱 및 저장 FITTING 카테고리: - 테이블 컬럼 너비 조정 (선택 2%, 종류 8.5%, 수량 12%) 구매신청 관리: - 엑셀 재다운로드 형식 개선 (BOM 페이지와 동일한 형식) - 그룹화된 자재 정보 포함하여 저장 및 다운로드
592 lines
22 KiB
Python
592 lines
22 KiB
Python
"""
|
|
엑셀 내보내기 및 구매 배치 관리 API
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Response
|
|
from fastapi.responses import FileResponse
|
|
from sqlalchemy import text
|
|
from sqlalchemy.orm import Session
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime
|
|
import json
|
|
import os
|
|
import openpyxl
|
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
from openpyxl.utils import get_column_letter
|
|
import uuid
|
|
|
|
from ..database import get_db
|
|
from ..auth.jwt_service import get_current_user
|
|
from ..utils.logger import logger
|
|
|
|
router = APIRouter(prefix="/export", tags=["Export Management"])
|
|
|
|
# 엑셀 파일 저장 경로
|
|
EXPORT_DIR = "exports"
|
|
os.makedirs(EXPORT_DIR, exist_ok=True)
|
|
|
|
|
|
def create_excel_from_materials(materials: List[Dict], batch_info: Dict) -> str:
|
|
"""
|
|
자재 목록으로 엑셀 파일 생성
|
|
"""
|
|
wb = openpyxl.Workbook()
|
|
ws = wb.active
|
|
ws.title = batch_info.get("category", "자재목록")
|
|
|
|
# 헤더 스타일
|
|
header_fill = PatternFill(start_color="B8E6FF", end_color="B8E6FF", fill_type="solid")
|
|
header_font = Font(bold=True, size=11)
|
|
header_alignment = Alignment(horizontal="center", vertical="center")
|
|
thin_border = Border(
|
|
left=Side(style='thin'),
|
|
right=Side(style='thin'),
|
|
top=Side(style='thin'),
|
|
bottom=Side(style='thin')
|
|
)
|
|
|
|
# 배치 정보 추가 (상단 3줄)
|
|
ws.merge_cells('A1:J1')
|
|
ws['A1'] = f"구매 배치: {batch_info.get('batch_no', 'N/A')}"
|
|
ws['A1'].font = Font(bold=True, size=14)
|
|
|
|
ws.merge_cells('A2:J2')
|
|
ws['A2'] = f"프로젝트: {batch_info.get('job_no', '')} - {batch_info.get('job_name', '')}"
|
|
|
|
ws.merge_cells('A3:J3')
|
|
ws['A3'] = f"내보낸 날짜: {batch_info.get('export_date', datetime.now().strftime('%Y-%m-%d %H:%M'))}"
|
|
|
|
# 빈 줄
|
|
ws.append([])
|
|
|
|
# 헤더 행
|
|
headers = [
|
|
"No.", "카테고리", "자재 설명", "크기", "스케줄/등급",
|
|
"재질", "수량", "단위", "추가요구", "사용자요구",
|
|
"구매상태", "PR번호", "PO번호", "공급업체", "예정일"
|
|
]
|
|
|
|
for col, header in enumerate(headers, 1):
|
|
cell = ws.cell(row=5, column=col, value=header)
|
|
cell.fill = header_fill
|
|
cell.font = header_font
|
|
cell.alignment = header_alignment
|
|
cell.border = thin_border
|
|
|
|
# 데이터 행
|
|
row_num = 6
|
|
for idx, material in enumerate(materials, 1):
|
|
row_data = [
|
|
idx,
|
|
material.get("category", ""),
|
|
material.get("description", ""),
|
|
material.get("size", ""),
|
|
material.get("schedule", ""),
|
|
material.get("material_grade", ""),
|
|
material.get("quantity", ""),
|
|
material.get("unit", ""),
|
|
material.get("additional_req", ""),
|
|
material.get("user_requirement", ""),
|
|
material.get("purchase_status", "pending"),
|
|
material.get("purchase_request_no", ""),
|
|
material.get("purchase_order_no", ""),
|
|
material.get("vendor_name", ""),
|
|
material.get("expected_date", "")
|
|
]
|
|
|
|
for col, value in enumerate(row_data, 1):
|
|
cell = ws.cell(row=row_num, column=col, value=value)
|
|
cell.border = thin_border
|
|
if col == 11: # 구매상태 컬럼
|
|
if value == "pending":
|
|
cell.fill = PatternFill(start_color="FFFFE0", end_color="FFFFE0", fill_type="solid")
|
|
elif value == "requested":
|
|
cell.fill = PatternFill(start_color="FFE4B5", end_color="FFE4B5", fill_type="solid")
|
|
elif value == "ordered":
|
|
cell.fill = PatternFill(start_color="ADD8E6", end_color="ADD8E6", fill_type="solid")
|
|
elif value == "received":
|
|
cell.fill = PatternFill(start_color="90EE90", end_color="90EE90", fill_type="solid")
|
|
|
|
row_num += 1
|
|
|
|
# 열 너비 자동 조정
|
|
for column in ws.columns:
|
|
max_length = 0
|
|
column_letter = get_column_letter(column[0].column)
|
|
for cell in column:
|
|
try:
|
|
if len(str(cell.value)) > max_length:
|
|
max_length = len(str(cell.value))
|
|
except:
|
|
pass
|
|
adjusted_width = min(max_length + 2, 50)
|
|
ws.column_dimensions[column_letter].width = adjusted_width
|
|
|
|
# 파일 저장
|
|
file_name = f"batch_{batch_info.get('batch_no', uuid.uuid4().hex[:8])}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
|
file_path = os.path.join(EXPORT_DIR, file_name)
|
|
wb.save(file_path)
|
|
|
|
return file_name
|
|
|
|
|
|
@router.post("/create-batch")
|
|
async def create_export_batch(
|
|
file_id: int,
|
|
job_no: Optional[str] = None,
|
|
category: Optional[str] = None,
|
|
materials: List[Dict] = [],
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
엑셀 내보내기 배치 생성 (자재 그룹화)
|
|
"""
|
|
try:
|
|
# 배치 번호 생성 (YYYYMMDD-XXX 형식)
|
|
batch_date = datetime.now().strftime('%Y%m%d')
|
|
|
|
# 오늘 생성된 배치 수 확인
|
|
count_query = text("""
|
|
SELECT COUNT(*) as count
|
|
FROM excel_export_history
|
|
WHERE DATE(export_date) = CURRENT_DATE
|
|
""")
|
|
count_result = db.execute(count_query).fetchone()
|
|
batch_seq = (count_result.count + 1) if count_result else 1
|
|
batch_no = f"{batch_date}-{str(batch_seq).zfill(3)}"
|
|
|
|
# Job 정보 조회
|
|
job_name = ""
|
|
if job_no:
|
|
job_query = text("SELECT job_name FROM jobs WHERE job_no = :job_no")
|
|
job_result = db.execute(job_query, {"job_no": job_no}).fetchone()
|
|
if job_result:
|
|
job_name = job_result.job_name
|
|
|
|
# 배치 정보
|
|
batch_info = {
|
|
"batch_no": batch_no,
|
|
"job_no": job_no,
|
|
"job_name": job_name,
|
|
"category": category,
|
|
"export_date": datetime.now().strftime('%Y-%m-%d %H:%M')
|
|
}
|
|
|
|
# 엑셀 파일 생성
|
|
excel_file_name = create_excel_from_materials(materials, batch_info)
|
|
|
|
# 내보내기 이력 저장
|
|
insert_history = text("""
|
|
INSERT INTO excel_export_history (
|
|
file_id, job_no, exported_by, export_type,
|
|
category, material_count, file_name, notes
|
|
) VALUES (
|
|
:file_id, :job_no, :exported_by, :export_type,
|
|
:category, :material_count, :file_name, :notes
|
|
) RETURNING export_id
|
|
""")
|
|
|
|
result = db.execute(insert_history, {
|
|
"file_id": file_id,
|
|
"job_no": job_no,
|
|
"exported_by": current_user.get("user_id"),
|
|
"export_type": "batch",
|
|
"category": category,
|
|
"material_count": len(materials),
|
|
"file_name": excel_file_name,
|
|
"notes": f"배치번호: {batch_no}"
|
|
})
|
|
|
|
export_id = result.fetchone().export_id
|
|
|
|
# 자재별 내보내기 기록
|
|
material_ids = []
|
|
for material in materials:
|
|
material_id = material.get("id")
|
|
if material_id:
|
|
material_ids.append(material_id)
|
|
|
|
insert_material = text("""
|
|
INSERT INTO exported_materials (
|
|
export_id, material_id, purchase_status,
|
|
quantity_exported
|
|
) VALUES (
|
|
:export_id, :material_id, 'pending',
|
|
:quantity
|
|
)
|
|
""")
|
|
|
|
db.execute(insert_material, {
|
|
"export_id": export_id,
|
|
"material_id": material_id,
|
|
"quantity": material.get("quantity", 0)
|
|
})
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"Export batch created: {batch_no} with {len(materials)} materials")
|
|
|
|
return {
|
|
"success": True,
|
|
"batch_no": batch_no,
|
|
"export_id": export_id,
|
|
"file_name": excel_file_name,
|
|
"material_count": len(materials),
|
|
"message": f"배치 {batch_no}가 생성되었습니다"
|
|
}
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to create export batch: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"배치 생성 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/batches")
|
|
async def get_export_batches(
|
|
file_id: Optional[int] = None,
|
|
job_no: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
limit: int = 50,
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
내보내기 배치 목록 조회
|
|
"""
|
|
try:
|
|
query = text("""
|
|
SELECT
|
|
eeh.export_id,
|
|
eeh.file_id,
|
|
eeh.job_no,
|
|
eeh.export_date,
|
|
eeh.category,
|
|
eeh.material_count,
|
|
eeh.file_name,
|
|
eeh.notes,
|
|
u.name as exported_by,
|
|
j.job_name,
|
|
f.original_filename,
|
|
-- 상태별 집계
|
|
COUNT(DISTINCT em.material_id) as total_materials,
|
|
COUNT(DISTINCT CASE WHEN em.purchase_status = 'pending' THEN em.material_id END) as pending_count,
|
|
COUNT(DISTINCT CASE WHEN em.purchase_status = 'requested' THEN em.material_id END) as requested_count,
|
|
COUNT(DISTINCT CASE WHEN em.purchase_status = 'ordered' THEN em.material_id END) as ordered_count,
|
|
COUNT(DISTINCT CASE WHEN em.purchase_status = 'received' THEN em.material_id END) as received_count,
|
|
-- 전체 상태 계산
|
|
CASE
|
|
WHEN COUNT(DISTINCT em.material_id) = COUNT(DISTINCT CASE WHEN em.purchase_status = 'received' THEN em.material_id END)
|
|
THEN 'completed'
|
|
WHEN COUNT(DISTINCT CASE WHEN em.purchase_status = 'ordered' THEN em.material_id END) > 0
|
|
THEN 'in_progress'
|
|
WHEN COUNT(DISTINCT CASE WHEN em.purchase_status = 'requested' THEN em.material_id END) > 0
|
|
THEN 'requested'
|
|
ELSE 'pending'
|
|
END as batch_status
|
|
FROM excel_export_history eeh
|
|
LEFT JOIN users u ON eeh.exported_by = u.user_id
|
|
LEFT JOIN jobs j ON eeh.job_no = j.job_no
|
|
LEFT JOIN files f ON eeh.file_id = f.id
|
|
LEFT JOIN exported_materials em ON eeh.export_id = em.export_id
|
|
WHERE eeh.export_type = 'batch'
|
|
AND (:file_id IS NULL OR eeh.file_id = :file_id)
|
|
AND (:job_no IS NULL OR eeh.job_no = :job_no)
|
|
GROUP BY
|
|
eeh.export_id, eeh.file_id, eeh.job_no, eeh.export_date,
|
|
eeh.category, eeh.material_count, eeh.file_name, eeh.notes,
|
|
u.name, j.job_name, f.original_filename
|
|
HAVING (:status IS NULL OR
|
|
CASE
|
|
WHEN COUNT(DISTINCT em.material_id) = COUNT(DISTINCT CASE WHEN em.purchase_status = 'received' THEN em.material_id END)
|
|
THEN 'completed'
|
|
WHEN COUNT(DISTINCT CASE WHEN em.purchase_status = 'ordered' THEN em.material_id END) > 0
|
|
THEN 'in_progress'
|
|
WHEN COUNT(DISTINCT CASE WHEN em.purchase_status = 'requested' THEN em.material_id END) > 0
|
|
THEN 'requested'
|
|
ELSE 'pending'
|
|
END = :status)
|
|
ORDER BY eeh.export_date DESC
|
|
LIMIT :limit
|
|
""")
|
|
|
|
results = db.execute(query, {
|
|
"file_id": file_id,
|
|
"job_no": job_no,
|
|
"status": status,
|
|
"limit": limit
|
|
}).fetchall()
|
|
|
|
batches = []
|
|
for row in results:
|
|
# 배치 번호 추출 (notes에서)
|
|
batch_no = ""
|
|
if row.notes and "배치번호:" in row.notes:
|
|
batch_no = row.notes.split("배치번호:")[1].strip()
|
|
|
|
batches.append({
|
|
"export_id": row.export_id,
|
|
"batch_no": batch_no,
|
|
"file_id": row.file_id,
|
|
"job_no": row.job_no,
|
|
"job_name": row.job_name,
|
|
"export_date": row.export_date.isoformat() if row.export_date else None,
|
|
"category": row.category,
|
|
"material_count": row.total_materials,
|
|
"file_name": row.file_name,
|
|
"exported_by": row.exported_by,
|
|
"source_file": row.original_filename,
|
|
"batch_status": row.batch_status,
|
|
"status_detail": {
|
|
"pending": row.pending_count,
|
|
"requested": row.requested_count,
|
|
"ordered": row.ordered_count,
|
|
"received": row.received_count,
|
|
"total": row.total_materials
|
|
}
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"batches": batches,
|
|
"count": len(batches)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get export batches: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"배치 목록 조회 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/batch/{export_id}/materials")
|
|
async def get_batch_materials(
|
|
export_id: int,
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
배치에 포함된 자재 목록 조회
|
|
"""
|
|
try:
|
|
query = text("""
|
|
SELECT
|
|
em.id as exported_material_id,
|
|
em.material_id,
|
|
m.original_description,
|
|
m.classified_category,
|
|
m.size_inch,
|
|
m.schedule,
|
|
m.material_grade,
|
|
m.quantity,
|
|
m.unit,
|
|
em.purchase_status,
|
|
em.purchase_request_no,
|
|
em.purchase_order_no,
|
|
em.vendor_name,
|
|
em.expected_date,
|
|
em.quantity_ordered,
|
|
em.quantity_received,
|
|
em.unit_price,
|
|
em.total_price,
|
|
em.notes,
|
|
ur.requirement as user_requirement
|
|
FROM exported_materials em
|
|
JOIN materials m ON em.material_id = m.id
|
|
LEFT JOIN user_requirements ur ON m.id = ur.material_id
|
|
WHERE em.export_id = :export_id
|
|
ORDER BY m.classified_category, m.original_description
|
|
""")
|
|
|
|
results = db.execute(query, {"export_id": export_id}).fetchall()
|
|
|
|
materials = []
|
|
for row in results:
|
|
materials.append({
|
|
"exported_material_id": row.exported_material_id,
|
|
"material_id": row.material_id,
|
|
"description": row.original_description,
|
|
"category": row.classified_category,
|
|
"size": row.size_inch,
|
|
"schedule": row.schedule,
|
|
"material_grade": row.material_grade,
|
|
"quantity": row.quantity,
|
|
"unit": row.unit,
|
|
"user_requirement": row.user_requirement,
|
|
"purchase_status": row.purchase_status,
|
|
"purchase_request_no": row.purchase_request_no,
|
|
"purchase_order_no": row.purchase_order_no,
|
|
"vendor_name": row.vendor_name,
|
|
"expected_date": row.expected_date.isoformat() if row.expected_date else None,
|
|
"quantity_ordered": row.quantity_ordered,
|
|
"quantity_received": row.quantity_received,
|
|
"unit_price": float(row.unit_price) if row.unit_price else None,
|
|
"total_price": float(row.total_price) if row.total_price else None,
|
|
"notes": row.notes
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"materials": materials,
|
|
"count": len(materials)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get batch materials: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"배치 자재 조회 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/batch/{export_id}/download")
|
|
async def download_batch_excel(
|
|
export_id: int,
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
저장된 배치 엑셀 파일 다운로드
|
|
"""
|
|
try:
|
|
# 배치 정보 조회
|
|
query = text("""
|
|
SELECT file_name, notes
|
|
FROM excel_export_history
|
|
WHERE export_id = :export_id
|
|
""")
|
|
result = db.execute(query, {"export_id": export_id}).fetchone()
|
|
|
|
if not result:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="배치를 찾을 수 없습니다"
|
|
)
|
|
|
|
file_path = os.path.join(EXPORT_DIR, result.file_name)
|
|
|
|
if not os.path.exists(file_path):
|
|
# 파일이 없으면 재생성
|
|
materials = await get_batch_materials(export_id, current_user, db)
|
|
|
|
batch_no = ""
|
|
if result.notes and "배치번호:" in result.notes:
|
|
batch_no = result.notes.split("배치번호:")[1].strip()
|
|
|
|
batch_info = {
|
|
"batch_no": batch_no,
|
|
"export_date": datetime.now().strftime('%Y-%m-%d %H:%M')
|
|
}
|
|
|
|
file_name = create_excel_from_materials(materials["materials"], batch_info)
|
|
file_path = os.path.join(EXPORT_DIR, file_name)
|
|
|
|
# DB 업데이트
|
|
update_query = text("""
|
|
UPDATE excel_export_history
|
|
SET file_name = :file_name
|
|
WHERE export_id = :export_id
|
|
""")
|
|
db.execute(update_query, {
|
|
"file_name": file_name,
|
|
"export_id": export_id
|
|
})
|
|
db.commit()
|
|
|
|
return FileResponse(
|
|
path=file_path,
|
|
filename=result.file_name,
|
|
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to download batch excel: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"엑셀 다운로드 실패: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.patch("/batch/{export_id}/status")
|
|
async def update_batch_status(
|
|
export_id: int,
|
|
status: str,
|
|
purchase_request_no: Optional[str] = None,
|
|
purchase_order_no: Optional[str] = None,
|
|
vendor_name: Optional[str] = None,
|
|
current_user: dict = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
배치 전체 상태 일괄 업데이트
|
|
"""
|
|
try:
|
|
# 배치의 모든 자재 상태 업데이트
|
|
update_query = text("""
|
|
UPDATE exported_materials
|
|
SET
|
|
purchase_status = :status,
|
|
purchase_request_no = COALESCE(:pr_no, purchase_request_no),
|
|
purchase_order_no = COALESCE(:po_no, purchase_order_no),
|
|
vendor_name = COALESCE(:vendor, vendor_name),
|
|
updated_by = :updated_by,
|
|
requested_date = CASE WHEN :status = 'requested' THEN CURRENT_TIMESTAMP ELSE requested_date END,
|
|
ordered_date = CASE WHEN :status = 'ordered' THEN CURRENT_TIMESTAMP ELSE ordered_date END,
|
|
received_date = CASE WHEN :status = 'received' THEN CURRENT_TIMESTAMP ELSE received_date END
|
|
WHERE export_id = :export_id
|
|
""")
|
|
|
|
result = db.execute(update_query, {
|
|
"export_id": export_id,
|
|
"status": status,
|
|
"pr_no": purchase_request_no,
|
|
"po_no": purchase_order_no,
|
|
"vendor": vendor_name,
|
|
"updated_by": current_user.get("user_id")
|
|
})
|
|
|
|
# 이력 기록
|
|
history_query = text("""
|
|
INSERT INTO purchase_status_history (
|
|
exported_material_id, material_id,
|
|
previous_status, new_status,
|
|
changed_by, reason
|
|
)
|
|
SELECT
|
|
em.id, em.material_id,
|
|
em.purchase_status, :new_status,
|
|
:changed_by, :reason
|
|
FROM exported_materials em
|
|
WHERE em.export_id = :export_id
|
|
""")
|
|
|
|
db.execute(history_query, {
|
|
"export_id": export_id,
|
|
"new_status": status,
|
|
"changed_by": current_user.get("user_id"),
|
|
"reason": f"배치 일괄 업데이트"
|
|
})
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"Batch {export_id} status updated to {status}")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"배치의 모든 자재가 {status} 상태로 변경되었습니다",
|
|
"updated_count": result.rowcount
|
|
}
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to update batch status: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"배치 상태 업데이트 실패: {str(e)}"
|
|
)
|