feat(cupang): 달력을 2개월(현재+다음 달) 뷰로 개편

- index 라우터가 두 달치 출고를 합쳐(중복 id 제거) 달 별 격자 2개를 생성
- 상단 바: ‹ › 이동 + 기간 제목 + [오늘], 달마다 머리글/월 출고 합계 배지
- 셀 UI 정리: 오늘=검은 원, 선택=링, 출고일=건수/센터수 태그, 타월은 흐리게
- 범례 추가, 1200px 이하에서는 두 달을 세로로 배치

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 12:20:41 +09:00
parent 0db54b6300
commit 1cf6ba385d
9 changed files with 240 additions and 63 deletions
+49 -28
View File
@@ -129,11 +129,18 @@ async def index(request: Request) -> HTMLResponse:
store, user = guard
year, month = _ym(request)
next_y, next_m = (year + 1, 1) if month == 12 else (year, month + 1)
prev_y, prev_m = (year - 1, 12) if month == 1 else (year, month - 1)
# 달력은 현재 달 + 다음 달 2개월을 함께 보여준다.
# 취소된 묶음은 달력·목록 어디에도 보이지 않는다(soft delete = 삭제로 취급).
shipments = [
s for s in store.list_shipments(year=year, month=month)
if s.get("status") != "취소"
]
merged: dict[Any, dict[str, Any]] = {}
for y, m in ((year, month), (next_y, next_m)):
for s in store.list_shipments(year=y, month=m):
if s.get("status") == "취소":
continue
merged[s["id"]] = s
shipments = list(merged.values())
# 달력 칸에는 출고 건수와 센터 수만 보여준다.
counts: dict[str, dict[str, Any]] = {}
@@ -147,11 +154,12 @@ async def index(request: Request) -> HTMLResponse:
for cell in counts.values():
cell["centers"] = len(cell["centers"])
# 선택 날짜 (기본: 오늘이 이번 달이면 오늘, 아니면 1일)
# 선택 날짜 (기본: 오늘이 보이는 두 달 안이면 오늘, 아니면 첫 달 1일)
sel = request.query_params.get("date") or ""
today = today_kst()
if not sel:
sel = today.isoformat() if (today.year == year and today.month == month) else f"{year:04d}-{month:02d}-01"
in_view = (today.year, today.month) in ((year, month), (next_y, next_m))
sel = today.isoformat() if in_view else f"{year:04d}-{month:02d}-01"
# 선택일의 묶음 — 달력과 같은 기준(출고일)
sel_shipments = [s for s in shipments if s.get("ship_date") == sel]
@@ -172,27 +180,39 @@ async def index(request: Request) -> HTMLResponse:
s["total_boxes"] = sum(it["boxes"] for it in s["items"])
cal = _calendar.Calendar(firstweekday=6) # 일요일 시작
weeks = cal.monthdatescalendar(year, month)
cal_weeks = [
[
{
"date": d.isoformat(),
"day": d.day,
"in_month": d.month == month,
"is_today": d == today,
"is_selected": d.isoformat() == sel,
"is_sunday": d.weekday() == 6,
"is_saturday": d.weekday() == 5,
"is_holiday": is_holiday(d),
"counts": counts.get(d.isoformat(), {}),
}
for d in week
]
for week in weeks
]
prev_y, prev_m = (year - 1, 12) if month == 1 else (year, month - 1)
next_y, next_m = (year + 1, 1) if month == 12 else (year, month + 1)
def _build_month(y: int, m: int) -> dict[str, Any]:
weeks = [
[
{
"date": d.isoformat(),
"day": d.day,
"in_month": d.month == m,
"is_today": d == today,
"is_selected": d.isoformat() == sel,
"is_sunday": d.weekday() == 6,
"is_saturday": d.weekday() == 5,
"is_holiday": is_holiday(d),
"counts": counts.get(d.isoformat(), {}),
}
for d in week
]
for week in cal.monthdatescalendar(y, m)
]
ship_total = sum(
int(c.get("ship") or 0)
for day, c in counts.items()
if day[:7] == f"{y:04d}-{m:02d}"
)
return {
"year": y,
"month": m,
"label": f"{y}{m}",
"weeks": weeks,
"ship_total": ship_total,
}
months = [_build_month(year, month), _build_month(next_y, next_m)]
return render_template(
request,
@@ -202,13 +222,14 @@ async def index(request: Request) -> HTMLResponse:
"is_admin": is_admin(user),
"nav_items": build_erp_nav(user, active="cupang"),
"page_title": "쿠팡 밀크런",
"page_subtitle": f"{year}{month}월 출고 일정",
"page_subtitle": f"{year}{month} · {next_y}{next_m} 출고 일정",
"year": year,
"month": month,
"prev_y": prev_y, "prev_m": prev_m,
"next_y": next_y, "next_m": next_m,
"today_iso": today.isoformat(),
"weekdays": ["", "", "", "", "", "", ""],
"cal_weeks": cal_weeks,
"months": months,
"selected_date": sel,
"sel_shipments": sel_shipments,
},