diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index aeb5a04..832a8d4 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -568,6 +568,102 @@ async def box_rule_delete( return RedirectResponse(url="/cupang/box-rules", status_code=303) +# ════════════════════════════════════════════════════════════ +# 박스 계산기 — 제품명 + 수량 → 박스 수 / 남은 낱개 +# 저장하지 않는 계산 전용 화면. 규칙은 cupang_box_rules 를 그대로 사용한다. +# ════════════════════════════════════════════════════════════ +@router.get("/box-calc", response_class=HTMLResponse) +async def box_calc_page(request: Request) -> HTMLResponse: + from app.main import build_erp_nav, render_template # noqa: WPS433 + from app.store import is_admin # noqa: WPS433 + + guard = _guard(request) + if not isinstance(guard, tuple): + return guard + store, user = guard + return render_template( + request, + "cupang/box_calc.html", + { + "user": user, + "is_admin": is_admin(user), + "nav_items": build_erp_nav(user, active="cupang"), + "page_title": "쿠팡 밀크런 — 박스 계산", + "page_subtitle": "제품명과 수량을 넣으면 박스 수와 남은 낱개를 계산합니다.", + "box_rules": store.list_box_rules(), + }, + ) + + +@router.post("/api/box-calc") +async def box_calc_api( + request: Request, + payload: dict[str, Any] = Body(...), + user: dict[str, Any] = Depends(_require_user), +) -> JSONResponse: + """[{product_code, quantity}] → 제품별 박스 계산 + 박스명별 합계. + + 클라이언트 계산을 신뢰하지 않고 store.compute_boxes 로 서버에서 계산한다. + """ + from .store import compute_boxes # noqa: WPS433 + + store = _store(request) + if store is None: + raise HTTPException(status_code=503, detail="cupang_db 미설정") + + raw_items = payload.get("items") + if not isinstance(raw_items, list): + raise HTTPException(status_code=400, detail="items 는 배열이어야 합니다.") + + rules = {r["product_code"]: r for r in store.list_box_rules()} + + results: list[dict[str, Any]] = [] + totals: dict[str, dict[str, Any]] = {} + for raw in raw_items: + if not isinstance(raw, dict): + continue + code = str(raw.get("product_code") or "").strip() + if not code: + continue + try: + qty = int(raw.get("quantity") or 0) + except (TypeError, ValueError): + qty = 0 + qty = max(qty, 0) + + rule = rules.get(code) + upb = rule["units_per_box"] if rule else None + calc = compute_boxes(qty, upb) + box_name = (rule or {}).get("box_name") or "" + results.append( + { + "product_code": code, + "product_name": (rule or {}).get("product_name_snapshot") or code, + "box_name": box_name, + "units_per_box": calc["units_per_box"], + "quantity": qty, + "configured": calc["configured"], + "full_boxes": calc["full_boxes"], + "remainder_units": calc["remainder_units"], + "required_boxes": calc["required_boxes"], + } + ) + if calc["configured"]: + agg = totals.setdefault( + box_name, {"box_name": box_name, "full_boxes": 0, "required_boxes": 0, "remainder_units": 0} + ) + agg["full_boxes"] += calc["full_boxes"] + agg["required_boxes"] += calc["required_boxes"] + agg["remainder_units"] += calc["remainder_units"] + + return JSONResponse( + { + "results": results, + "totals": sorted(totals.values(), key=lambda t: t["box_name"]), + } + ) + + # ════════════════════════════════════════════════════════════ # 설정 — 제품명 카탈로그 관리 (itemcode_db 에서 등록) # ════════════════════════════════════════════════════════════ diff --git a/app/modules/cupang/templates/cupang/box_calc.html b/app/modules/cupang/templates/cupang/box_calc.html new file mode 100644 index 0000000..bcde74e --- /dev/null +++ b/app/modules/cupang/templates/cupang/box_calc.html @@ -0,0 +1,225 @@ +{% extends "erp_base.html" %} + +{% block head_extra %}{% endblock %} + +{% block content %} +
+ +
+ ◀◀ 달력 + 박스 입수량 설정 +
+ + {% if not box_rules %} +
+

+ 등록된 박스 입수량 규칙이 없습니다. + 박스 입수량 설정에서 먼저 제품별 입수량을 등록하세요. +

+
+ {% else %} + +
+
+

박스 계산

+ 수량을 고치고 다시 계산하면 결과가 갱신됩니다. +
+ +
+ + + + + + + + + + + + + + +
제품명수량박스명입수량박스 수남은 낱개총 필요 박스동작
+
+ +
+ + + + +
+
+ + + + + + {% endif %} + +
+{% endblock %} diff --git a/app/modules/cupang/templates/cupang/index.html b/app/modules/cupang/templates/cupang/index.html index 1cc50dc..a835994 100644 --- a/app/modules/cupang/templates/cupang/index.html +++ b/app/modules/cupang/templates/cupang/index.html @@ -13,6 +13,7 @@ 제품명 설정 입고센터 관리 박스 입수량 설정 + 박스 계산
diff --git a/app/static/cupang.css b/app/static/cupang.css index 2131c52..c52413e 100644 --- a/app/static/cupang.css +++ b/app/static/cupang.css @@ -326,3 +326,12 @@ } .cpg-sort.is-asc .cpg-sort-ind::after { content: "▲"; opacity: 1; } .cpg-sort.is-desc .cpg-sort-ind::after { content: "▼"; opacity: 1; } + +/* 박스 계산기 */ +.cpg-calc-table .cpg-calc-num { text-align: right; white-space: nowrap; } +.cpg-calc-table th.cpg-calc-num { text-align: right; } +.cpg-calc-table .cpg-calc-name { min-width: 240px; } +.cpg-calc-table .cpg-calc-qty { width: 96px; text-align: right; } +.cpg-calc-row.cpg-calc-warn { background: color-mix(in srgb, var(--color-callout-red) 8%, transparent); } +.cpg-calc-actions { margin-top: 12px; gap: 8px; align-items: center; } +.cpg-calc-sum { margin-top: 16px; }