From 1dc4cfc0970023bf0a91508791617486e4acc049 Mon Sep 17 00:00:00 2001 From: king Date: Mon, 31 Aug 2026 17:13:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(cupang):=20=EB=8B=AC=EB=A0=A5=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=97=90=EC=84=9C=20=EC=B6=9C=EA=B3=A0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(=EA=B1=B4=EB=B3=84=20/=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 분배 확정으로 만든 출고를 지우려면 상세 페이지까지 들어가야 했다. 달력에서 날짜를 고른 뒤 오른쪽 카드에서 바로 지울 수 있게 한다. 센터가 여러 건일 때를 위해 목록 헤더에 "이 날짜 전체 삭제" 도 둔다. 둘 다 가운데 확인 팝업을 거친다. 삭제는 기존 정책대로 status='취소' soft delete 이고, 대신 달력·목록에서 취소 건을 걸러내 사용자에게는 삭제로 보이게 했다(기록은 DB 에 남는다). - router: /{id}/delete 에 next 폼값(내부 경로만 허용) 추가, POST /cupang/day-delete 신설 - index.html: 카드 헤더에 삭제 버튼, 앵커 안에 버튼이 들어가지 않게 구조 정리 --- app/modules/cupang/router.py | 49 ++++++++++- .../cupang/templates/cupang/box_calc.html | 2 +- .../cupang/templates/cupang/box_rules.html | 2 +- .../cupang/templates/cupang/centers.html | 2 +- .../cupang/templates/cupang/detail.html | 2 +- app/modules/cupang/templates/cupang/form.html | 2 +- .../cupang/templates/cupang/index.html | 85 ++++++++++++++++++- .../cupang/templates/cupang/products.html | 2 +- app/static/cupang.css | 15 ++++ 9 files changed, 149 insertions(+), 12 deletions(-) diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index 8ae27ae..7405682 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -129,7 +129,11 @@ async def index(request: Request) -> HTMLResponse: store, user = guard year, month = _ym(request) - shipments = store.list_shipments(year=year, month=month) + # 취소된 묶음은 달력·목록 어디에도 보이지 않는다(soft delete = 삭제로 취급). + shipments = [ + s for s in store.list_shipments(year=year, month=month) + if s.get("status") != "취소" + ] # 달력 칸에는 출고 건수와 센터 수만 보여준다. counts: dict[str, dict[str, Any]] = {} @@ -340,10 +344,19 @@ async def update( return RedirectResponse(url=f"/cupang/{shipment_id}", status_code=303) +def _safe_next(raw: str, fallback: str) -> str: + """열린 리다이렉트 방지 — /cupang/ 안쪽 경로만 허용.""" + nxt = (raw or "").strip() + if nxt.startswith("/cupang/") and "//" not in nxt[1:]: + return nxt + return fallback + + @router.post("/{shipment_id:int}/delete") async def delete( request: Request, shipment_id: int, + next: str = Form(""), user: dict[str, Any] = Depends(_require_user), ) -> RedirectResponse: """운영 안전: 기본은 status='취소' soft delete.""" @@ -354,7 +367,9 @@ async def delete( store.soft_delete(shipment_id=shipment_id) except KeyError: raise HTTPException(status_code=404, detail="출고 묶음을 찾을 수 없습니다.") - return RedirectResponse(url=f"/cupang/{shipment_id}", status_code=303) + return RedirectResponse( + url=_safe_next(next, f"/cupang/{shipment_id}"), status_code=303 + ) @router.post("/{shipment_id:int}/hard-delete") @@ -374,6 +389,36 @@ async def hard_delete( return RedirectResponse(url="/cupang/", status_code=303) +@router.post("/day-delete") +async def day_delete( + request: Request, + date: str = Form(...), + next: str = Form(""), + user: dict[str, Any] = Depends(_require_user), +) -> RedirectResponse: + """선택한 출고일의 묶음을 한 번에 취소 처리한다(soft delete).""" + store = _store(request) + if store is None: + raise HTTPException(status_code=503, detail="cupang_db 미설정") + + day = (date or "").strip() + try: + _date.fromisoformat(day) + except ValueError: + raise HTTPException(status_code=400, detail="날짜 형식이 올바르지 않습니다.") + + for ship in store.list_shipments(date_from=day, date_to=day): + if ship.get("status") == "취소": + continue + try: + store.soft_delete(shipment_id=ship["id"]) + except KeyError: + continue + return RedirectResponse( + url=_safe_next(next, f"/cupang/?date={day}"), status_code=303 + ) + + # ════════════════════════════════════════════════════════════ # 입고센터 관리 # ════════════════════════════════════════════════════════════ diff --git a/app/modules/cupang/templates/cupang/box_calc.html b/app/modules/cupang/templates/cupang/box_calc.html index cd220a3..ebfc39e 100644 --- a/app/modules/cupang/templates/cupang/box_calc.html +++ b/app/modules/cupang/templates/cupang/box_calc.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/box_rules.html b/app/modules/cupang/templates/cupang/box_rules.html index db9d195..8f3f934 100644 --- a/app/modules/cupang/templates/cupang/box_rules.html +++ b/app/modules/cupang/templates/cupang/box_rules.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/centers.html b/app/modules/cupang/templates/cupang/centers.html index 2fbba99..5d9830f 100644 --- a/app/modules/cupang/templates/cupang/centers.html +++ b/app/modules/cupang/templates/cupang/centers.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/detail.html b/app/modules/cupang/templates/cupang/detail.html index c40a34e..6fc87cf 100644 --- a/app/modules/cupang/templates/cupang/detail.html +++ b/app/modules/cupang/templates/cupang/detail.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/form.html b/app/modules/cupang/templates/cupang/form.html index dd607b5..ab52fc7 100644 --- a/app/modules/cupang/templates/cupang/form.html +++ b/app/modules/cupang/templates/cupang/form.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/index.html b/app/modules/cupang/templates/cupang/index.html index b67d753..698a67b 100644 --- a/app/modules/cupang/templates/cupang/index.html +++ b/app/modules/cupang/templates/cupang/index.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
@@ -60,16 +60,22 @@

{{ selected_date }} 출고

{{ sel_shipments|length }}건 + {% if sel_shipments %} + + {% endif %}
{% if sel_shipments %} - + {% endfor %} @@ -94,7 +100,78 @@ + + + + +
{% endblock %} +{% block scripts %} + +{% endblock %} + diff --git a/app/modules/cupang/templates/cupang/products.html b/app/modules/cupang/templates/cupang/products.html index 1f1b343..b2fac22 100644 --- a/app/modules/cupang/templates/cupang/products.html +++ b/app/modules/cupang/templates/cupang/products.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/static/cupang.css b/app/static/cupang.css index eef83cd..14df58c 100644 --- a/app/static/cupang.css +++ b/app/static/cupang.css @@ -836,6 +836,21 @@ body.cpg-dragging { cursor: grabbing; user-select: none; } .cpg-ship-iqty { color: var(--color-midtone-gray); } .cpg-ship-ibox { font-weight: 600; } +/* 달력 오른쪽 — 삭제 버튼 / 확인 팝업 */ +.cpg-list-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.cpg-list-head > h2 { flex: 0 0 auto; } +.cpg-list-head > .erp-muted { flex: 1 1 auto; } +.cpg-day-del { flex: 0 0 auto; } +.cpg-ship-name { + flex: 1 1 auto; min-width: 0; + font-size: 14px; font-weight: 700; text-decoration: none; color: inherit; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.cpg-ship-name:hover { text-decoration: underline; } +.cpg-del-box { width: min(400px, calc(100vw - 32px)); } +.cpg-del-msg { margin: 0; font-size: 14px; line-height: 1.5; word-break: keep-all; } +.cpg-del-act { display: flex; gap: 6px; justify-content: flex-end; margin-top: 4px; } + /* ③ 헤더의 분배 확정 버튼 */ .cpg-dist-head-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .cpg-dist-head-bar > .erp-muted { flex: 1 1 auto; min-width: 0; }