feat(cupang): 센터 중복 등록 알림 + 목록 지역별 그룹화
추가 시 같은 이름이 있으면 조용히 재활성되던 동작(ON CONFLICT DO UPDATE) 때문에 사용자가 중복을 알 수 없었다. 등록 전에 먼저 조회해서 이미 있으면 만들지 않고 알림(활성/비활성 구분)을 띄운다. 목록은 29개가 가로 4열로 흩어져 훑기 어려웠다. 센터명 앞 지역 접두사로 묶고(인천14 → "인천") CSS 멀티컬럼으로 한 열을 위→아래로 읽게 바꿨다. 숫자는 자연 정렬(인천4 < 인천14). 행 액션은 hover/focus 시에만 노출해 이름이 먼저 눈에 들어오게 했다. - 라우터: _center_group / _center_sort_key / _group_centers 추가 - 검색은 그룹 단위로 접히고 결과 0건이면 안내 표시 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,10 @@ from __future__ import annotations
|
||||
|
||||
import calendar as _calendar
|
||||
import json
|
||||
import re
|
||||
from fractions import Fraction
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.timezone import today_kst
|
||||
|
||||
@@ -408,6 +410,34 @@ async def hard_delete(
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 입고센터 관리
|
||||
# ════════════════════════════════════════════════════════════
|
||||
_CENTER_PREFIX_RE = re.compile(r"^\D*")
|
||||
|
||||
|
||||
def _center_group(name: str) -> str:
|
||||
"""센터명 앞부분(숫자 전까지)을 지역 그룹 키로 쓴다. 예) 인천14 → 인천."""
|
||||
name = (name or "").strip()
|
||||
prefix = _CENTER_PREFIX_RE.match(name).group(0).strip()
|
||||
return prefix or name or "기타"
|
||||
|
||||
|
||||
def _center_sort_key(name: str) -> list[Any]:
|
||||
"""숫자를 숫자로 비교하는 자연 정렬. 예) 인천4 < 인천14."""
|
||||
parts = re.split(r"(\d+)", (name or "").strip())
|
||||
return [(1, int(p), "") if p.isdigit() else (0, 0, p.lower()) for p in parts]
|
||||
|
||||
|
||||
def _group_centers(centers: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
buckets: dict[str, list[dict[str, Any]]] = {}
|
||||
for c in centers:
|
||||
buckets.setdefault(_center_group(c["name"]), []).append(c)
|
||||
groups = []
|
||||
for key, items in buckets.items():
|
||||
items.sort(key=lambda c: _center_sort_key(c["name"]))
|
||||
groups.append({"name": key, "items": items, "count": len(items)})
|
||||
groups.sort(key=lambda g: _center_sort_key(g["name"]))
|
||||
return groups
|
||||
|
||||
|
||||
@router.get("/centers", response_class=HTMLResponse)
|
||||
async def centers_page(request: Request) -> HTMLResponse:
|
||||
from app.main import build_erp_nav, render_template # noqa: WPS433
|
||||
@@ -417,7 +447,10 @@ async def centers_page(request: Request) -> HTMLResponse:
|
||||
if not isinstance(guard, tuple):
|
||||
return guard
|
||||
store, user = guard
|
||||
centers = sorted(store.list_centers(include_inactive=True), key=lambda c: c["name"])
|
||||
centers = sorted(
|
||||
store.list_centers(include_inactive=True),
|
||||
key=lambda c: _center_sort_key(c["name"]),
|
||||
)
|
||||
return render_template(
|
||||
request,
|
||||
"cupang/centers.html",
|
||||
@@ -428,6 +461,9 @@ async def centers_page(request: Request) -> HTMLResponse:
|
||||
"page_title": "쿠팡 밀크런 — 입고센터 관리",
|
||||
"page_subtitle": "센터명 등록 · 수정 · 활성/비활성 · 삭제.",
|
||||
"centers": centers,
|
||||
"center_groups": _group_centers(centers),
|
||||
"flash": request.query_params.get("msg", ""),
|
||||
"flash_name": request.query_params.get("name", ""),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -442,11 +478,32 @@ async def center_create(
|
||||
store = _store(request)
|
||||
if store is None:
|
||||
raise HTTPException(status_code=503, detail="cupang_db 미설정")
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return RedirectResponse(url="/cupang/centers", status_code=303)
|
||||
|
||||
# 같은 이름이 이미 있으면 새로 만들지 않고 알림만 돌려준다.
|
||||
existing = next(
|
||||
(
|
||||
c
|
||||
for c in store.list_centers(include_inactive=True)
|
||||
if (c.get("name") or "").strip().lower() == name.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if existing is not None:
|
||||
msg = "dup" if existing.get("active") else "dup_inactive"
|
||||
return RedirectResponse(
|
||||
url=f"/cupang/centers?msg={msg}&name={quote(name)}", status_code=303
|
||||
)
|
||||
|
||||
try:
|
||||
store.create_center(name=name, sort_order=sort_order)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return RedirectResponse(url="/cupang/centers", status_code=303)
|
||||
return RedirectResponse(
|
||||
url=f"/cupang/centers?msg=added&name={quote(name)}", status_code=303
|
||||
)
|
||||
|
||||
|
||||
@router.post("/centers/{center_id}/edit")
|
||||
|
||||
Reference in New Issue
Block a user