fix(expense): zip 버튼 라벨 변경 + 경로 폴백/진단 헤더
- "첨부 일괄(zip)" → "전부 다운로드(zip)" - stored_path 깨진 경우 업로드루트/item_id/파일명 으로 폴백 탐색 - X-Attachment-Stats 헤더(total/added/missing)로 빈 zip 원인 진단 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -472,13 +472,33 @@ async def approved_attachments_zip(
|
|||||||
year, mon, raw = _parse_month(request)
|
year, mon, raw = _parse_month(request)
|
||||||
atts = store.approved_attachments(year=year, month=mon)
|
atts = store.approved_attachments(year=year, month=mon)
|
||||||
names = _name_map(request)
|
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()
|
buf = io.BytesIO()
|
||||||
used: dict[str, int] = {}
|
used: dict[str, int] = {}
|
||||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
for a in atts:
|
for a in atts:
|
||||||
path = Path(a.get("stored_path", ""))
|
path = _resolve(a)
|
||||||
if not path.exists():
|
if path is None:
|
||||||
|
missing += 1
|
||||||
continue
|
continue
|
||||||
nm = _display_name(a.get("owner", ""), names)
|
nm = _display_name(a.get("owner", ""), names)
|
||||||
fname = _att_download_name(a.get("uploaded_at"), nm, a.get("filename", ""))
|
fname = _att_download_name(a.get("uploaded_at"), nm, a.get("filename", ""))
|
||||||
@@ -491,12 +511,17 @@ async def approved_attachments_zip(
|
|||||||
used[fname] = 1
|
used[fname] = 1
|
||||||
arcname = fname
|
arcname = fname
|
||||||
zf.write(str(path), arcname=arcname)
|
zf.write(str(path), arcname=arcname)
|
||||||
|
added += 1
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
zipname = f"expense_approved_{raw}.zip"
|
zipname = f"expense_approved_{raw}.zip"
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
buf,
|
buf,
|
||||||
media_type="application/zip",
|
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}",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<button type="submit" class="erp-btn erp-btn-outline">조회</button>
|
<button type="submit" class="erp-btn erp-btn-outline">조회</button>
|
||||||
</form>
|
</form>
|
||||||
<a class="erp-btn erp-btn-primary" href="/expense/api/approved/export.xlsx?month={{ month }}">엑셀 다운</a>
|
<a class="erp-btn erp-btn-primary" href="/expense/api/approved/export.xlsx?month={{ month }}">엑셀 다운</a>
|
||||||
<a class="erp-btn erp-btn-outline" href="/expense/api/approved/attachments.zip?month={{ month }}">첨부 일괄(zip)</a>
|
<a class="erp-btn erp-btn-outline" href="/expense/api/approved/attachments.zip?month={{ month }}">전부 다운로드(zip)</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── 직원별 합계 ── -->
|
<!-- ── 직원별 합계 ── -->
|
||||||
|
|||||||
Reference in New Issue
Block a user