change(cupang): 화면 문구 "박스" → "상자" 통일

템플릿·라우터 메시지·페이지 제목과 seed/DDL 기본값(쿠팡박스→쿠팡상자)까지
표기를 맞췄다. URL(/box-calc)과 코드 식별자(box_name 등)는 그대로.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 16:00:09 +09:00
parent 1c1ca69feb
commit 4a260cb161
14 changed files with 140 additions and 140 deletions
+29 -29
View File
@@ -154,7 +154,7 @@ async def index(request: Request) -> HTMLResponse:
# 선택일의 묶음 — 달력과 같은 기준(출고일)
sel_shipments = [s for s in shipments if s.get("ship_date") == sel]
# 오른쪽 상세: 센터별 상품 목록·수량·박스 수·출고방식
# 오른쪽 상세: 센터별 상품 목록·수량·상자 수·출고방식
for s in sel_shipments:
full = store.get_shipment(shipment_id=s["id"])
lines = (full.get("lines") if full else []) or []
@@ -334,16 +334,16 @@ async def export_shipments_xlsx(request: Request, date: str = "") -> Any:
# ════════════════════════════════════════════════════════════
@router.get("/new")
async def new_form(request: Request) -> RedirectResponse:
"""신규 등록 폼은 없앴다. 출고 묶음은 박스 계산의 [분배 확정] 으로만 만든다.
"""신규 등록 폼은 없앴다. 출고 묶음은 상자 계산의 [분배 확정] 으로만 만든다.
예전 링크/북마크가 404 나지 않도록 박스 계산으로 보낸다.
예전 링크/북마크가 404 나지 않도록 상자 계산으로 보낸다.
"""
return RedirectResponse(url="/cupang/box-calc", status_code=303)
@router.get("/{shipment_id:int}", response_class=HTMLResponse)
async def detail(request: Request, shipment_id: int) -> HTMLResponse:
"""출고 묶음 보기 — 박스 계산 화면과 같은 3열 구성(읽기 전용).
"""출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성(읽기 전용).
수정 기능은 없앴다. 잘못 만들었으면 달력에서 삭제하고 다시 확정한다.
"""
@@ -611,7 +611,7 @@ async def center_delete(
# ════════════════════════════════════════════════════════════
# 박스 입수량 관리
# 상자 입수량 관리
# ════════════════════════════════════════════════════════════
@router.get("/box-rules", response_class=HTMLResponse)
async def box_rules_page(request: Request) -> HTMLResponse:
@@ -629,8 +629,8 @@ async def box_rules_page(request: Request) -> HTMLResponse:
"user": user,
"is_admin": is_admin(user),
"nav_items": build_erp_nav(user, active="cupang"),
"page_title": "쿠팡 밀크런 — 박스 입수량",
"page_subtitle": "제품코드별 쿠팡박스 1박스당 입수량 설정",
"page_title": "쿠팡 밀크런 — 상자 입수량",
"page_subtitle": "제품코드별 쿠팡상자 1상자당 입수량 설정",
"box_rules": store.list_box_rules(include_inactive=True),
"products": store.list_products(),
"search_enabled": bool((_itemcode(request)) and _itemcode(request).enabled),
@@ -644,7 +644,7 @@ async def box_rule_upsert(
product_code: str = Form(...),
units_per_box: int = Form(...),
product_name_snapshot: str = Form(""),
box_name: str = Form("쿠팡박스"),
box_name: str = Form("쿠팡상자"),
memo: str = Form(""),
user: dict[str, Any] = Depends(_require_user),
) -> RedirectResponse:
@@ -681,7 +681,7 @@ async def box_rule_delete(
# ════════════════════════════════════════════════════════════
# 박스 계산기 — 제품명 + 수량 → 박스 수 / 남은 낱개
# 상자 계산기 — 제품명 + 수량 → 상자 수 / 남은 낱개
# 저장하지 않는 계산 전용 화면. 규칙은 cupang_box_rules 를 그대로 사용한다.
# ════════════════════════════════════════════════════════════
@router.get("/box-calc", response_class=HTMLResponse)
@@ -701,7 +701,7 @@ async def box_calc_page(request: Request) -> HTMLResponse:
"is_admin": is_admin(user),
"nav_items": build_erp_nav(user, active="cupang"),
"page_title": "쿠팡 밀크런 — 쿠팡 발주 업로드",
"page_subtitle": "쿠팡 발주 엑셀을 올리면 센터별 수량을 합산해 박스를 계산합니다.",
"page_subtitle": "쿠팡 발주 엑셀을 올리면 센터별 수량을 합산해 상자를 계산합니다.",
"box_rules": store.list_box_rules(),
# 센터 선택 드롭다운은 가나다순 (한글 음절은 코드포인트 순 = 가나다순)
"centers": sorted(store.list_centers(), key=lambda c: (c.get("name") or "")),
@@ -866,7 +866,7 @@ async def box_calc_upload(
if unknown_centers:
warnings.append("등록되지 않은 센터: " + ", ".join(sorted(unknown_centers)))
if no_rule:
warnings.append("박스 입수량 미설정: " + ", ".join(sorted(no_rule)))
warnings.append("상자 입수량 미설정: " + ", ".join(sorted(no_rule)))
uniq_dates = sorted(set(ship_dates))
@@ -927,7 +927,7 @@ async def box_calc_api(
payload: dict[str, Any] = Body(...),
user: dict[str, Any] = Depends(_require_user),
) -> JSONResponse:
"""[{product_code, quantity}] → 제품별 박스 계산 + 박스명별 합계.
"""[{product_code, quantity}] → 제품별 상자 계산 + 상자명별 합계.
클라이언트 계산을 신뢰하지 않고 store.compute_boxes 로 서버에서 계산한다.
"""
@@ -943,9 +943,9 @@ async def box_calc_api(
def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
"""[{product_code, quantity}] → 제품별 박스 + 자투리 혼합 박스 + 합계.
"""[{product_code, quantity}] → 제품별 상자 + 자투리 혼합 상자 + 합계.
박스 계산 화면(API)과 출고 상세 보기가 같은 결과를 쓰도록 한 곳에 둔다.
상자 계산 화면(API)과 출고 상세 보기가 같은 결과를 쓰도록 한 곳에 둔다.
"""
from .store import compute_boxes # noqa: WPS433
@@ -1003,12 +1003,12 @@ def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""제품별 자투리(remainder_units)를 같은 박스명끼리 모아 혼합 박스에 담는다.
"""제품별 자투리(remainder_units)를 같은 상자명끼리 모아 혼합 상자에 담는다.
- 한 박스의 용량을 1 로 두고, 제품 1개가 차지하는 부피를 1/units_per_box 로 본다.
(예: 쿠팡 2호 = 8개들이 → 1개 = 1/8 박스)
- 같은 박스명끼리만 섞는다. 서로 다른 입수량이 섞여도 부피 합으로 정확히 계산된다.
- 한 제품의 자투리가 두 박스에 나뉘어 담기는 것은 허용(그래야 박스 수가 최소).
- 한 상자의 용량을 1 로 두고, 제품 1개가 차지하는 부피를 1/units_per_box 로 본다.
(예: 쿠팡 2호 = 8개들이 → 1개 = 1/8 상자)
- 같은 상자명끼리만 섞는다. 서로 다른 입수량이 섞여도 부피 합으로 정확히 계산된다.
- 한 제품의 자투리가 두 상자에 나뉘어 담기는 것은 허용(그래야 상자 수가 최소).
- 오차 없이 계산하려고 float 대신 Fraction 을 쓴다.
"""
groups: dict[str, list[dict[str, Any]]] = {}
@@ -1019,7 +1019,7 @@ def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for box_name in sorted(groups):
# 자투리가 많은 제품부터 담아 박스 안 품목 수를 줄인다.
# 자투리가 많은 제품부터 담아 상자 안 품목 수를 줄인다.
items = sorted(groups[box_name], key=lambda r: -r["remainder_units"])
boxes: list[dict[str, Any]] = []
cur: list[dict[str, Any]] = []
@@ -1030,7 +1030,7 @@ def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
left = int(r["remainder_units"])
while left > 0:
take = min(left, int(free / unit))
if take == 0: # 남은 자리 없음 → 새 박스
if take == 0: # 남은 자리 없음 → 새 상자
boxes.append({"items": cur, "fill_percent": float(round((1 - free) * 100, 1))})
cur, free = [], Fraction(1)
continue
@@ -1245,9 +1245,9 @@ async def product_delete(
# 분배 확정 — 센터별 출고 묶음 생성
# ════════════════════════════════════════════════════════════
def _clean_box_plan(raw: Any) -> list[dict[str, Any]]:
"""화면이 보낸 박스 구성을 표시용으로만 정리해서 저장한다.
"""화면이 보낸 상자 구성을 표시용으로만 정리해서 저장한다.
수량·박스 수는 라인(제품 합계)이 정답이고, 이 값은 "어느 상자에 무엇이
수량·상자 수는 라인(제품 합계)이 정답이고, 이 값은 "어느 상자에 무엇이
담겼는지"를 보여주기 위한 스냅샷이다.
"""
if not isinstance(raw, list):
@@ -1305,7 +1305,7 @@ async def box_calc_confirm(
) -> JSONResponse:
"""{ship_date, centers:[{center_id, ship_method, boxes, items:[...]}]} → 센터마다 출고 묶음 1건 생성.
화면에서 보낸 박스 수는 요약 표시용이고, 라인의 박스 수는 저장 시
화면에서 보낸 상자 수는 요약 표시용이고, 라인의 상자 수는 저장 시
서버(store.compute_boxes)가 수량과 입수량으로 다시 계산한다.
"""
from .store import SHIP_METHODS as _METHODS # noqa: WPS433
@@ -1345,7 +1345,7 @@ async def box_calc_confirm(
status_code=400, detail=f"{center['name']} 의 출고방식을 선택하세요."
)
# 같은 제품이 여러 박스로 나뉘어 담겼을 수 있으므로 제품코드로 합친다.
# 같은 제품이 여러 상자로 나뉘어 담겼을 수 있으므로 제품코드로 합친다.
merged: dict[str, int] = {}
for it in raw.get("items") or []:
if not isinstance(it, dict):
@@ -1382,10 +1382,10 @@ async def box_calc_confirm(
except (TypeError, ValueError):
boxes = 0
pieces = sum(merged.values())
# 화면에서 고른 상자 종류별 박스 수(예: "쿠팡상자41"). 출고리스트 엑셀의 "출고" 칸.
# 화면에서 고른 상자 종류별 상자 수(예: "쿠팡상자41"). 출고리스트 엑셀의 "출고" 칸.
summary = str(raw.get("box_summary") or "").strip()
if not summary:
summary = f"{boxes}박스 · {pieces}" if boxes else f"{pieces}"
summary = f"{boxes}상자 · {pieces}" if boxes else f"{pieces}"
plans.append(
{
@@ -1417,7 +1417,7 @@ async def box_calc_confirm(
"outbound_summary": plan["summary"],
"worker": worker,
"status": "출고준비",
"memo": "박스 계산에서 분배 확정",
"memo": "상자 계산에서 분배 확정",
"box_plan": plan["box_plan"],
},
lines=plan["lines"],
@@ -1438,7 +1438,7 @@ async def box_calc_confirm(
# ════════════════════════════════════════════════════════════
# 박스 계산 임시 저장 (화면 상태 스냅샷)
# 상자 계산 임시 저장 (화면 상태 스냅샷)
# ════════════════════════════════════════════════════════════
@router.get("/api/box-calc/drafts")
async def box_calc_drafts_list(