From 1cf6ba385d3e3d9d4941b0838f8db76c461e5fd0 Mon Sep 17 00:00:00 2001 From: king Date: Tue, 1 Sep 2026 12:20:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(cupang):=20=EB=8B=AC=EB=A0=A5=EC=9D=84=202?= =?UTF-8?q?=EA=B0=9C=EC=9B=94(=ED=98=84=EC=9E=AC+=EB=8B=A4=EC=9D=8C=20?= =?UTF-8?q?=EB=8B=AC)=20=EB=B7=B0=EB=A1=9C=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index 라우터가 두 달치 출고를 합쳐(중복 id 제거) 달 별 격자 2개를 생성 - 상단 바: ‹ › 이동 + 기간 제목 + [오늘], 달마다 머리글/월 출고 합계 배지 - 셀 UI 정리: 오늘=검은 원, 선택=링, 출고일=건수/센터수 태그, 타월은 흐리게 - 범례 추가, 1200px 이하에서는 두 달을 세로로 배치 Co-Authored-By: Claude Opus 5 --- app/modules/cupang/router.py | 77 ++++++---- .../cupang/templates/cupang/box_calc.html | 2 +- .../cupang/templates/cupang/box_rules.html | 2 +- .../cupang/templates/cupang/centers.html | 2 +- .../cupang/templates/cupang/detail.html | 2 +- app/modules/cupang/templates/cupang/form.html | 2 +- .../cupang/templates/cupang/index.html | 80 +++++++---- .../cupang/templates/cupang/products.html | 2 +- app/static/cupang.css | 134 ++++++++++++++++++ 9 files changed, 240 insertions(+), 63 deletions(-) diff --git a/app/modules/cupang/router.py b/app/modules/cupang/router.py index a340a6b..5970561 100644 --- a/app/modules/cupang/router.py +++ b/app/modules/cupang/router.py @@ -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, }, diff --git a/app/modules/cupang/templates/cupang/box_calc.html b/app/modules/cupang/templates/cupang/box_calc.html index 4aa30d1..6599bcf 100644 --- a/app/modules/cupang/templates/cupang/box_calc.html +++ b/app/modules/cupang/templates/cupang/box_calc.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/box_rules.html b/app/modules/cupang/templates/cupang/box_rules.html index 2d11001..a3d3944 100644 --- a/app/modules/cupang/templates/cupang/box_rules.html +++ b/app/modules/cupang/templates/cupang/box_rules.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/centers.html b/app/modules/cupang/templates/cupang/centers.html index 2aa770b..b019e16 100644 --- a/app/modules/cupang/templates/cupang/centers.html +++ b/app/modules/cupang/templates/cupang/centers.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/detail.html b/app/modules/cupang/templates/cupang/detail.html index b803c4c..59eb144 100644 --- a/app/modules/cupang/templates/cupang/detail.html +++ b/app/modules/cupang/templates/cupang/detail.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/form.html b/app/modules/cupang/templates/cupang/form.html index c8216b9..f34958b 100644 --- a/app/modules/cupang/templates/cupang/form.html +++ b/app/modules/cupang/templates/cupang/form.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/modules/cupang/templates/cupang/index.html b/app/modules/cupang/templates/cupang/index.html index 09511bc..ae72478 100644 --- a/app/modules/cupang/templates/cupang/index.html +++ b/app/modules/cupang/templates/cupang/index.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
@@ -19,40 +19,62 @@
- +
-
- -

{{ year }}년 {{ month }}월

- +
+
+ + +
+

{{ months[0].label }} – {{ months[1].label }}

+ 오늘
-
- {% for wd in weekdays %} -
{{ wd }}
- {% endfor %} +
+ {% for m in months %} +
+
+ {{ m.month }}월 + {{ m.year }} + {% if m.ship_total %} + 출고 {{ m.ship_total }}건 + {% endif %} +
- {% for week in cal_weeks %} - {% for cell in week %} - - {{ cell.day }} - - {% if cell.counts.ship %} - 출고 {{ cell.counts.ship }}건 - 센터 {{ cell.counts.centers }}곳 - {% endif %} - - - {% endfor %} +
+ {% for wd in weekdays %} +
{{ wd }}
+ {% endfor %} + + {% for week in m.weeks %} + {% for cell in week %} + + {{ cell.day }} + {% if cell.counts.ship %} + {{ cell.counts.ship }}건{{ cell.counts.centers }}곳 + {% endif %} + + {% endfor %} + {% endfor %} +
+
{% endfor %}
+ +
+ 출고 있는 날 + 오늘 + 선택한 날 +
diff --git a/app/modules/cupang/templates/cupang/products.html b/app/modules/cupang/templates/cupang/products.html index 37792a2..d1708a4 100644 --- a/app/modules/cupang/templates/cupang/products.html +++ b/app/modules/cupang/templates/cupang/products.html @@ -1,6 +1,6 @@ {% extends "erp_base.html" %} -{% block head_extra %}{% endblock %} +{% block head_extra %}{% endblock %} {% block content %}
diff --git a/app/static/cupang.css b/app/static/cupang.css index cfbd83b..a12da18 100644 --- a/app/static/cupang.css +++ b/app/static/cupang.css @@ -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); }