feat(dispatch): 출고 배치별 업로드 원본 zip 다운로드
배치 목록에 '다운로드' 버튼 추가. 피킹/라벨 PDF + 주문 엑셀 중 실제 존재하는 파일만 zip 으로 묶어 내려준다. 한글 파일명은 RFC5987 filename* 으로 인코딩. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,14 +12,17 @@ Order ID / Package ID / Tracking ID / Seller SKU / Quantity 뿐이다.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import re
|
||||
import shutil
|
||||
import unicodedata
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
|
||||
|
||||
from app.timezone import now_kst, today_kst
|
||||
|
||||
@@ -259,6 +262,57 @@ async def batch_delete(
|
||||
return RedirectResponse(url="/dispatch/", status_code=303)
|
||||
|
||||
|
||||
@router.get("/batches/{batch_id:int}/download")
|
||||
async def batch_download(
|
||||
request: Request, batch_id: int, user: dict[str, Any] = Depends(_require_user)
|
||||
) -> Response:
|
||||
"""배치에 업로드된 원본 파일(피킹/라벨 PDF + 주문 엑셀)을 zip 으로 묶어 다운로드."""
|
||||
st = _store(request)
|
||||
if st is None:
|
||||
raise HTTPException(status_code=503, detail="dispatch_db 미설정")
|
||||
batch = st.get_batch(batch_id=batch_id)
|
||||
if batch is None:
|
||||
raise HTTPException(status_code=404, detail="배치를 찾을 수 없습니다.")
|
||||
|
||||
# 저장된 경로 중 실제로 존재하는 파일만 묶는다. 같은 파일명 충돌 시 번호 부여.
|
||||
paths = [
|
||||
batch.get("picking_pdf_path"),
|
||||
batch.get("label_pdf_path"),
|
||||
batch.get("order_export_path"),
|
||||
]
|
||||
buf = io.BytesIO()
|
||||
used: set[str] = set()
|
||||
count = 0
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for raw in paths:
|
||||
p = Path(raw) if raw else None
|
||||
if not p or not p.is_file():
|
||||
continue
|
||||
arcname = p.name
|
||||
n = 1
|
||||
while arcname in used:
|
||||
arcname = f"{p.stem}_{n}{p.suffix}"
|
||||
n += 1
|
||||
used.add(arcname)
|
||||
zf.write(p, arcname=arcname)
|
||||
count += 1
|
||||
|
||||
if count == 0:
|
||||
raise HTTPException(status_code=404, detail="다운로드할 업로드 파일이 없습니다.")
|
||||
|
||||
slug = _slugify(batch.get("batch_name") or "dispatch")
|
||||
fname = f"{batch.get('dispatch_date', '')}_{slug}.zip".lstrip("_")
|
||||
headers = {
|
||||
"Content-Disposition": (
|
||||
f"attachment; filename=\"dispatch_{batch_id}.zip\"; "
|
||||
f"filename*=UTF-8''{quote(fname)}"
|
||||
)
|
||||
}
|
||||
return Response(
|
||||
content=buf.getvalue(), media_type="application/zip", headers=headers
|
||||
)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 출고 작업 리스트 (1박스 = 1카드)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user