From 5582c724674d47b7c6aded30906003c912520c19 Mon Sep 17 00:00:00 2001 From: king Date: Tue, 2 Jun 2026 18:08:56 +0900 Subject: [PATCH] =?UTF-8?q?feat(expense):=20=EC=98=81=EC=88=98=EC=A6=9D=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EA=B7=9C=EC=B9=99=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20+=20=EB=93=B1=EB=A1=9D=20=ED=8F=BC=20=ED=95=84?= =?UTF-8?q?=EC=88=98=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - zip 첨부 파일명: 날짜_이름_분류_결제수단_금액_가맹점.확장자 (날짜=사용일) - 가맹점 없으면 생략, 금액 쉼표 표기, 금지문자 정리 - db.approved_attachments 에 category/method/amount/merchant 추가 - 등록 폼: 사용일/분류/결제수단/금액/가맹점 필수 — 누락 시 경고창 + 포커스 (가맹점 input required, 금액 1원 이상) Co-Authored-By: Claude Opus 4.8 --- app/modules/expense/db.py | 2 +- app/modules/expense/router.py | 52 +++++++++++++++---- .../expense/templates/expense/index.html | 31 ++++++++++- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/app/modules/expense/db.py b/app/modules/expense/db.py index 39af638..5453a02 100644 --- a/app/modules/expense/db.py +++ b/app/modules/expense/db.py @@ -398,7 +398,7 @@ class ExpenseDBStore: """ SELECT a.id, a.item_id, a.owner, a.kind, a.filename, a.stored_path, a.content_type, a.uploaded_at, - i.spent_at + i.spent_at, i.category, i.method, i.amount, i.merchant FROM expense_attachments a JOIN expense_items i ON i.id = a.item_id WHERE i.status = ANY(%s) diff --git a/app/modules/expense/router.py b/app/modules/expense/router.py index 1dcc350..fd70b6b 100644 --- a/app/modules/expense/router.py +++ b/app/modules/expense/router.py @@ -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 diff --git a/app/modules/expense/templates/expense/index.html b/app/modules/expense/templates/expense/index.html index 64309bd..962f90d 100644 --- a/app/modules/expense/templates/expense/index.html +++ b/app/modules/expense/templates/expense/index.html @@ -69,7 +69,7 @@