feat(cupang): 달력을 2개월(현재+다음 달) 뷰로 개편
- index 라우터가 두 달치 출고를 합쳐(중복 id 제거) 달 별 격자 2개를 생성 - 상단 바: ‹ › 이동 + 기간 제목 + [오늘], 달마다 머리글/월 출고 합계 배지 - 셀 UI 정리: 오늘=검은 원, 선택=링, 출고일=건수/센터수 태그, 타월은 흐리게 - 범례 추가, 1200px 이하에서는 두 달을 세로로 배치 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1126,3 +1126,137 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
|
||||
.cpg-prod-right .cpg-row-actions { flex-wrap: nowrap; gap: 4px; }
|
||||
.cpg-prod-right .cpg-row-actions .erp-btn { padding: 3px 8px; font-size: 12px; line-height: 1.5; }
|
||||
.cpg-prod-right .cpg-active-toggle { white-space: nowrap; }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
메인 달력 — 2개월(현재 달 + 다음 달) 월간 뷰
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
.cpg-cal-card { padding: 16px 18px 14px; }
|
||||
|
||||
/* 상단 바: ‹ › + 기간 제목 + 오늘 */
|
||||
.cpg-mcal-bar {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding-bottom: 12px; margin-bottom: 14px;
|
||||
border-bottom: 1px solid var(--color-subtle-ash);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cpg-mcal-nav { display: inline-flex; gap: 4px; }
|
||||
.cpg-mcal-navbtn {
|
||||
width: 32px; height: 32px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border: 1px solid var(--color-subtle-ash); border-radius: 8px;
|
||||
font-size: 17px; line-height: 1; color: var(--color-rich-black);
|
||||
text-decoration: none; background: #fff;
|
||||
transition: background .12s, border-color .12s;
|
||||
}
|
||||
.cpg-mcal-navbtn:hover { background: var(--color-ghost-gray); border-color: var(--color-midtone-gray); }
|
||||
.cpg-mcal-range {
|
||||
margin: 0; font-size: 19px; font-weight: 600; letter-spacing: -0.4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cpg-mcal-todaybtn {
|
||||
margin-left: auto;
|
||||
padding: 6px 14px; border-radius: 999px;
|
||||
border: 1px solid var(--color-subtle-ash); background: #fff;
|
||||
font-size: 13px; color: var(--color-rich-black); text-decoration: none;
|
||||
}
|
||||
.cpg-mcal-todaybtn:hover { background: var(--color-ghost-gray); border-color: var(--color-midtone-gray); }
|
||||
|
||||
/* 두 달 나란히 */
|
||||
.cpg-mcal-months {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 22px;
|
||||
flex: 1 1 auto; min-height: 0;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.cpg-mcal-months { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
.cpg-mcal { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
||||
|
||||
/* 달 머리글 */
|
||||
.cpg-mcal-h {
|
||||
display: flex; align-items: baseline; gap: 6px;
|
||||
padding: 0 2px 8px;
|
||||
}
|
||||
.cpg-mcal-mname { font-size: 15px; font-weight: 700; letter-spacing: -0.2px; }
|
||||
.cpg-mcal-myear { font-size: 12px; color: var(--color-midtone-gray); font-family: var(--font-geist-mono); }
|
||||
.cpg-mcal-total {
|
||||
margin-left: auto;
|
||||
font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
||||
background: var(--color-ghost-gray); color: var(--color-midtone-gray);
|
||||
}
|
||||
.cpg-mcal.is-current .cpg-mcal-total {
|
||||
background: var(--color-deep-black); color: #fff;
|
||||
}
|
||||
|
||||
/* 날짜 격자 */
|
||||
.cpg-mcal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
gap: 2px;
|
||||
flex: 1 1 auto; min-height: 0;
|
||||
}
|
||||
.cpg-mcal-wd {
|
||||
text-align: center; padding: 4px 0 6px;
|
||||
font-size: 11px; font-weight: 600; letter-spacing: .3px;
|
||||
color: var(--color-midtone-gray);
|
||||
border-bottom: 1px solid var(--color-subtle-ash);
|
||||
}
|
||||
.cpg-mcal-wd.is-sun { color: var(--color-callout-red); }
|
||||
.cpg-mcal-wd.is-sat { color: #2b5bc2; }
|
||||
|
||||
.cpg-mcal-cell {
|
||||
position: relative;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 3px;
|
||||
min-height: 52px; min-width: 0; padding: 5px 4px 6px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none; color: var(--color-rich-black);
|
||||
transition: background .12s, box-shadow .12s;
|
||||
}
|
||||
.cpg-mcal-cell:hover { background: var(--color-ghost-gray); }
|
||||
.cpg-mcal-day {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 24px; height: 24px; padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px; font-weight: 500; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.cpg-mcal-cell.is-red .cpg-mcal-day { color: var(--color-callout-red); }
|
||||
.cpg-mcal-cell.is-blue .cpg-mcal-day { color: #2b5bc2; }
|
||||
/* 이번 달이 아닌 날 — 흐리게 */
|
||||
.cpg-mcal-cell.is-out { opacity: .38; }
|
||||
/* 오늘 — 검은 원 */
|
||||
.cpg-mcal-cell.is-today .cpg-mcal-day {
|
||||
background: var(--color-deep-black); color: #fff; font-weight: 700;
|
||||
}
|
||||
/* 선택한 날 — 링 강조 */
|
||||
.cpg-mcal-cell.is-selected {
|
||||
background: var(--color-ghost-gray);
|
||||
box-shadow: inset 0 0 0 1.5px var(--color-deep-black);
|
||||
}
|
||||
|
||||
/* 출고 있는 날 — 건수/센터수 태그 */
|
||||
.cpg-mcal-tag {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
max-width: 100%; padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-deep-black); color: #fff;
|
||||
font-size: 11px; line-height: 1.6; white-space: nowrap;
|
||||
overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.cpg-mcal-tag > em { font-style: normal; color: #c9c9c9; font-size: 10px; }
|
||||
.cpg-mcal-cell.is-out .cpg-mcal-tag { background: var(--color-midtone-gray); }
|
||||
|
||||
/* 범례 */
|
||||
.cpg-mcal-legend {
|
||||
display: flex; gap: 16px; flex-wrap: wrap;
|
||||
margin-top: 12px; padding-top: 10px;
|
||||
border-top: 1px solid var(--color-subtle-ash);
|
||||
font-size: 11px; color: var(--color-midtone-gray);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cpg-mcal-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.cpg-mcal-legend i { width: 12px; height: 12px; border-radius: 999px; display: inline-block; }
|
||||
.cpg-lg-dot { background: var(--color-deep-black); }
|
||||
.cpg-lg-today { background: var(--color-deep-black); box-shadow: 0 0 0 2px #fff, 0 0 0 3px var(--color-subtle-ash); }
|
||||
.cpg-lg-sel { background: var(--color-ghost-gray); box-shadow: inset 0 0 0 1.5px var(--color-deep-black); }
|
||||
|
||||
Reference in New Issue
Block a user