From 09e2f06a144361196f697aa9774c2c7fb7952d41 Mon Sep 17 00:00:00 2001 From: king Date: Tue, 2 Jun 2026 17:50:49 +0900 Subject: [PATCH] =?UTF-8?q?fix(expense):=20zip=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=EB=B3=80=EA=B2=BD=20+=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=ED=8F=B4=EB=B0=B1/=EC=A7=84=EB=8B=A8=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "첨부 일괄(zip)" → "전부 다운로드(zip)" - stored_path 깨진 경우 업로드루트/item_id/파일명 으로 폴백 탐색 - X-Attachment-Stats 헤더(total/added/missing)로 빈 zip 원인 진단 Co-Authored-By: Claude Opus 4.8 --- app/modules/expense/router.py | 31 +++++++++++++++++-- .../expense/templates/expense/approved.html | 2 +- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/app/modules/expense/router.py b/app/modules/expense/router.py index c17ea0c..f9e48ef 100644 --- a/app/modules/expense/router.py +++ b/app/modules/expense/router.py @@ -472,13 +472,33 @@ async def approved_attachments_zip( year, mon, raw = _parse_month(request) atts = store.approved_attachments(year=year, month=mon) names = _name_map(request) + upload_root = _upload_dir(request) + def _resolve(a: dict[str, Any]) -> Path | None: + """stored_path 우선, 없으면 현재 업로드 루트/item_id/파일명 으로 폴백.""" + raw_path = a.get("stored_path") or "" + p = Path(raw_path) + if p.exists(): + return p + # DATA_DIR 변경 등으로 절대경로가 깨진 경우 — basename 으로 재구성 + item_id = a.get("item_id", "") + base = os.path.basename(raw_path) or a.get("filename", "") + if item_id and base: + alt = upload_root / item_id / base + if alt.exists(): + return alt + return None + + total = len(atts) + added = 0 + missing = 0 buf = io.BytesIO() used: dict[str, int] = {} with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: for a in atts: - path = Path(a.get("stored_path", "")) - if not path.exists(): + path = _resolve(a) + if path is None: + missing += 1 continue nm = _display_name(a.get("owner", ""), names) fname = _att_download_name(a.get("uploaded_at"), nm, a.get("filename", "")) @@ -491,12 +511,17 @@ async def approved_attachments_zip( used[fname] = 1 arcname = fname zf.write(str(path), arcname=arcname) + added += 1 buf.seek(0) zipname = f"expense_approved_{raw}.zip" return StreamingResponse( buf, media_type="application/zip", - headers={"Content-Disposition": f'attachment; filename="{zipname}"'}, + headers={ + "Content-Disposition": f'attachment; filename="{zipname}"', + # 진단용: 첨부 총개수 / 담긴 개수 / 디스크에서 못 찾은 개수 + "X-Attachment-Stats": f"total={total};added={added};missing={missing}", + }, ) diff --git a/app/modules/expense/templates/expense/approved.html b/app/modules/expense/templates/expense/approved.html index 97a7e0a..ad18ad1 100644 --- a/app/modules/expense/templates/expense/approved.html +++ b/app/modules/expense/templates/expense/approved.html @@ -11,7 +11,7 @@ 엑셀 다운 - 첨부 일괄(zip) + 전부 다운로드(zip)