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,
},
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901f" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901g" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901f" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901g" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901f" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901g" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901f" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901g" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901f" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901g" />{% endblock %}
{% block content %}
<section class="cpg">
+51 -29
View File
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901f" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901g" />{% endblock %}
{% block content %}
<section class="cpg">
@@ -19,40 +19,62 @@
</div>
<div class="cpg-layout">
<!-- ── 왼쪽: 월간 달력 ── -->
<!-- ── 왼쪽: 2개월 월간 달력 ── -->
<div class="erp-card cpg-cal-card">
<div class="cpg-cal-head">
<a class="erp-btn erp-btn-outline cpg-nav-btn"
href="/cupang/?year={{ prev_y }}&month={{ prev_m }}"></a>
<h2 class="cpg-cal-title">{{ year }}년 {{ month }}월</h2>
<a class="erp-btn erp-btn-outline cpg-nav-btn"
href="/cupang/?year={{ next_y }}&month={{ next_m }}"></a>
<div class="cpg-mcal-bar">
<div class="cpg-mcal-nav">
<a class="cpg-mcal-navbtn" title="이전 달"
href="/cupang/?year={{ prev_y }}&month={{ prev_m }}"></a>
<a class="cpg-mcal-navbtn" title="다음 달"
href="/cupang/?year={{ next_y }}&month={{ next_m }}"></a>
</div>
<h2 class="cpg-mcal-range">{{ months[0].label }} {{ months[1].label }}</h2>
<a class="cpg-mcal-todaybtn" href="/cupang/">오늘</a>
</div>
<div class="cpg-cal-grid">
{% for wd in weekdays %}
<div class="cpg-cal-wd {% if loop.index0 == 0 %}cpg-sun{% elif loop.index0 == 6 %}cpg-sat{% endif %}">{{ wd }}</div>
{% endfor %}
<div class="cpg-mcal-months">
{% for m in months %}
<section class="cpg-mcal{% if loop.first %} is-current{% endif %}">
<header class="cpg-mcal-h">
<span class="cpg-mcal-mname">{{ m.month }}월</span>
<span class="cpg-mcal-myear">{{ m.year }}</span>
{% if m.ship_total %}
<span class="cpg-mcal-total">출고 {{ m.ship_total }}건</span>
{% endif %}
</header>
{% for week in cal_weeks %}
{% for cell in week %}
<a class="cpg-cal-cell
{% if not cell.in_month %}cpg-out{% endif %}
{% if cell.is_today %}cpg-today{% endif %}
{% if cell.is_selected %}cpg-selected{% endif %}
{% if cell.is_sunday or cell.is_holiday %}cpg-red{% elif cell.is_saturday %}cpg-blue{% endif %}"
href="/cupang/?year={{ year }}&month={{ month }}&date={{ cell.date }}">
<span class="cpg-cal-day">{{ cell.day }}</span>
<span class="cpg-cal-badges">
{% if cell.counts.ship %}
<span class="erp-badge erp-badge-inverse cpg-mini">출고 {{ cell.counts.ship }}건</span>
<span class="erp-badge erp-badge-neutral cpg-mini">센터 {{ cell.counts.centers }}곳</span>
{% endif %}
</span>
</a>
{% endfor %}
<div class="cpg-mcal-grid">
{% for wd in weekdays %}
<div class="cpg-mcal-wd {% if loop.index0 == 0 %}is-sun{% elif loop.index0 == 6 %}is-sat{% endif %}">{{ wd }}</div>
{% endfor %}
{% for week in m.weeks %}
{% for cell in week %}
<a class="cpg-mcal-cell
{% if not cell.in_month %}is-out{% endif %}
{% if cell.is_today %}is-today{% endif %}
{% if cell.is_selected %}is-selected{% endif %}
{% if cell.counts.ship %}has-ship{% endif %}
{% if cell.is_sunday or cell.is_holiday %}is-red{% elif cell.is_saturday %}is-blue{% endif %}"
href="/cupang/?year={{ year }}&month={{ month }}&date={{ cell.date }}"
title="{{ cell.date }}{% if cell.counts.ship %} — 출고 {{ cell.counts.ship }}건 / 센터 {{ cell.counts.centers }}곳{% endif %}">
<span class="cpg-mcal-day">{{ cell.day }}</span>
{% if cell.counts.ship %}
<span class="cpg-mcal-tag">{{ cell.counts.ship }}건<em>{{ cell.counts.centers }}곳</em></span>
{% endif %}
</a>
{% endfor %}
{% endfor %}
</div>
</section>
{% endfor %}
</div>
<div class="cpg-mcal-legend">
<span><i class="cpg-lg-dot"></i>출고 있는 날</span>
<span><i class="cpg-lg-today"></i>오늘</span>
<span><i class="cpg-lg-sel"></i>선택한 날</span>
</div>
</div>
<!-- ── 오른쪽: 선택일 출고 묶음 리스트 ── -->
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901f" />{% endblock %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901g" />{% endblock %}
{% block content %}
<section class="cpg">