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; }