feat(expense): 영수증 파일명 규칙 변경 + 등록 폼 필수 검증

- zip 첨부 파일명: 날짜_이름_분류_결제수단_금액_가맹점.확장자 (날짜=사용일)
  - 가맹점 없으면 생략, 금액 쉼표 표기, 금지문자 정리
- db.approved_attachments 에 category/method/amount/merchant 추가
- 등록 폼: 사용일/분류/결제수단/금액/가맹점 필수 — 누락 시 경고창 + 포커스
  (가맹점 input required, 금액 1원 이상)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 18:08:56 +09:00
parent b678d7c37f
commit 5582c72467
3 changed files with 73 additions and 12 deletions
+42 -10
View File
@@ -105,15 +105,47 @@ def _display_name(email: str, names: dict[str, str]) -> str:
return names.get(email) or (email.split("@")[0] if email else "")
def _att_download_name(uploaded_at: Any, name: str, original: str) -> str:
"""첨부 다운로드 파일명: `yy년 mm월 dd일(요일)_이름{ext}`. 날짜=업로드일(KST)."""
if isinstance(uploaded_at, datetime):
d = uploaded_at.astimezone(KST)
else:
d = now_kst()
wd = _WEEKDAYS_KR[d.weekday()]
base = f"{d.strftime('%y')}{d.strftime('%m')}{d.strftime('%d')}일({wd})_{name}"
ext = os.path.splitext(original or "")[1].lower()
def _safe_part(value: Any) -> str:
"""파일명 구성요소 정리 — 경로/금지문자 제거, 공백 압축."""
s = str(value or "").strip()
s = re.sub(r"[\\/:*?\"<>|\r\n\t]", " ", s)
s = re.sub(r"\s+", " ", s).strip()
return s
def _fmt_date_kr(value: Any) -> str:
"""날짜 → `yy년 mm월 dd일(요일)`. date/datetime/문자열(YYYY-MM-DD) 허용."""
d: Any = value
if isinstance(value, str):
try:
d = datetime.strptime(value[:10], "%Y-%m-%d").date()
except ValueError:
return _safe_part(value)
if isinstance(d, datetime):
d = d.astimezone(KST)
if hasattr(d, "weekday"):
wd = _WEEKDAYS_KR[d.weekday()]
return f"{d.strftime('%y')}{d.strftime('%m')}{d.strftime('%d')}일({wd})"
return _safe_part(value)
def _att_download_name(att: dict[str, Any], name: str) -> str:
"""첨부 다운로드 파일명: `날짜_이름_분류_결제수단_금액_가맹점{ext}`.
날짜=사용일(spent_at). 금액=쉼표 표기. 빈 가맹점은 생략.
"""
parts = [
_fmt_date_kr(att.get("spent_at")),
_safe_part(name),
_safe_part(att.get("category")),
_safe_part(att.get("method")),
f"{int(att.get('amount', 0) or 0):,}",
]
merchant = _safe_part(att.get("merchant"))
if merchant:
parts.append(merchant)
base = "_".join(p for p in parts if p)
ext = os.path.splitext(att.get("filename") or "")[1].lower()
return f"{base}{ext}"
@@ -559,7 +591,7 @@ async def approved_attachments_zip(
missing += 1
continue
nm = _display_name(a.get("owner", ""), names)
fname = _att_download_name(a.get("uploaded_at"), nm, a.get("filename", ""))
fname = _att_download_name(a, nm)
# 동일명 충돌 → 접미사
if fname in used:
used[fname] += 1