feat(cupang): 달력 칸에 공휴일 이름 표시(대체공휴일 포함)

- holidays.py 를 이름까지 담도록 확장(holiday_name), is_holiday 는 이름 유무로 판정
- 대체공휴일은 원래 명칭 대신 "대체공휴일"로 표기, 겹치는 날은 "어린이날·부처님오신날"
- 달력 칸에 날짜 아래 빨강 소형 라벨 + title 툴팁, 칸 최소 높이 62→70px

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 02:52:59 +09:00
parent 15f181318b
commit b302ec0951
8 changed files with 86 additions and 45 deletions
+64 -37
View File
@@ -1,8 +1,9 @@
"""대한민국 공휴일 판정 (달력 색상용). """대한민국 공휴일 판정 + 이름 (달력 색상·표기용).
- 고정 양력 공휴일은 매년 동일 → 연도 무관 판정. - 고정 양력 공휴일은 매년 동일 → 연도 무관 판정.
- 음력 공휴일(설날/부처님오신날/추석)과 대체공휴일은 매년 달라짐 → - 음력 공휴일(설날/부처님오신날/추석)과 대체공휴일은 매년 달라짐 →
연도별 dict(`_LUNAR_AND_SUBSTITUTE`)에 명시. 새 연도는 KASI 발표값을 추가한다. 연도별 dict(`_LUNAR_AND_SUBSTITUTE`)에 명시. 새 연도는 KASI 발표값을 추가한다.
- 대체공휴일은 원래 공휴일 이름 대신 "대체공휴일"로 표기한다.
미수록 연도는 고정 양력 공휴일만 빨강 처리된다(음력/대체는 누락). 미수록 연도는 고정 양력 공휴일만 빨강 처리된다(음력/대체는 누락).
""" """
@@ -11,52 +12,78 @@ from __future__ import annotations
from datetime import date from datetime import date
# 매년 동일한 양력 공휴일 (month, day) SUBSTITUTE = "대체공휴일"
_FIXED_SOLAR: set[tuple[int, int]] = {
(1, 1), # 신정 # 매년 동일한 양력 공휴일 (month, day) -> 이름
(3, 1), # 삼일절 _FIXED_SOLAR: dict[tuple[int, int], str] = {
(5, 5), # 어린이날 (1, 1): "신정",
(6, 6), # 현충일 (3, 1): "삼일절",
(8, 15), # 광복절 (5, 5): "어린이날",
(10, 3), # 개천절 (6, 6): "현충일",
(10, 9), # 한글날 (8, 15): "광복절",
(12, 25), # 성탄절 (10, 3): "개천절",
(10, 9): "한글날",
(12, 25): "성탄절",
} }
# 연도별 음력 공휴일 + 대체공휴일 (ISO 날짜 문자열). KASI 발표 기준. # 연도별 음력 공휴일 + 대체공휴일 (ISO 날짜 -> 이름). KASI 발표 기준.
_LUNAR_AND_SUBSTITUTE: dict[int, set[str]] = { _LUNAR_AND_SUBSTITUTE: dict[int, dict[str, str]] = {
2025: { 2025: {
"2025-01-28", "2025-01-29", "2025-01-30", # 설날 연휴 "2025-01-28": "설날 연휴",
"2025-03-03", # 삼일절 대체(3/1 토) "2025-01-29": "설날",
"2025-05-06", # 부처님오신날 대체(5/5 겹침) "2025-01-30": "설날 연휴",
"2025-05-05", # 부처님오신날(어린이날과 동일일) "2025-03-03": SUBSTITUTE, # 삼일절(3/1 토)
"2025-10-06", "2025-10-07", "2025-10-08", # 추석 연휴 "2025-05-05": "부처님오신날", # 어린이날과 같은 날
"2025-10-08", # 추석 대체 가능 "2025-05-06": SUBSTITUTE, # 부처님오신날 겹침
"2025-10-06": "추석",
"2025-10-07": "추석 연휴",
"2025-10-08": SUBSTITUTE, # 추석 연휴 겹침
}, },
2026: { 2026: {
"2026-02-16", "2026-02-17", "2026-02-18", # 설날 연휴 (설날 2/17) "2026-02-16": "설날 연휴",
"2026-03-02", # 삼일절 대체 (3/1 일) "2026-02-17": "설날",
"2026-05-24", # 부처님오신날 (일) "2026-02-18": "설날 연휴",
"2026-05-25", # 부처님오신날 대체 "2026-03-02": SUBSTITUTE, # 삼일절(3/1 일)
"2026-08-17", # 광복절 대체 (8/15 토) "2026-05-24": "부처님오신날",
"2026-09-24", "2026-09-25", "2026-09-26", # 추석 연휴 (추석 9/25) "2026-05-25": SUBSTITUTE,
"2026-09-28", # 추석 대체 (9/26 토) "2026-08-17": SUBSTITUTE, # 광복절(8/15 토)
"2026-10-05", # 개천절 대체 (10/3 토) "2026-09-24": "추석 연휴",
"2026-09-25": "추석",
"2026-09-26": "추석 연휴",
"2026-09-28": SUBSTITUTE,
"2026-10-05": SUBSTITUTE, # 개천절(10/3 토)
}, },
2027: { 2027: {
"2027-02-06", "2027-02-07", "2027-02-08", # 설날 연휴 (설날 2/7) "2027-02-06": "설날 연휴",
"2027-02-09", # 설날 대체 (2/7 일) "2027-02-07": "설날",
"2027-05-13", # 부처님오신날 (목) "2027-02-08": "설날 연휴",
"2027-08-16", # 광복절 대체 (8/15 일) "2027-02-09": SUBSTITUTE,
"2027-09-14", "2027-09-15", "2027-09-16", # 추석 연휴 (추석 9/15) "2027-05-13": "부처님오신날",
"2027-10-04", # 개천절 대체 (10/3 일) "2027-08-16": SUBSTITUTE, # 광복절(8/15 일)
"2027-10-11", # 한글날 대체 (10/9 토) "2027-09-14": "추석 연휴",
"2027-09-15": "추석",
"2027-09-16": "추석 연휴",
"2027-10-04": SUBSTITUTE, # 개천절(10/3 일)
"2027-10-11": SUBSTITUTE, # 한글날(10/9 토)
}, },
} }
def holiday_name(d: date) -> str:
"""그날의 공휴일 이름. 공휴일이 아니면 빈 문자열.
한 날에 두 공휴일이 겹치면 "어린이날·부처님오신날" 처럼 이어 붙인다.
"""
names: list[str] = []
fixed = _FIXED_SOLAR.get((d.month, d.day))
if fixed:
names.append(fixed)
extra = _LUNAR_AND_SUBSTITUTE.get(d.year, {}).get(d.isoformat())
if extra and extra not in names:
names.append(extra)
return "·".join(names)
def is_holiday(d: date) -> bool: def is_holiday(d: date) -> bool:
"""공휴일(일요일 제외)이면 True. 일/토 색상은 요일로 따로 판정한다.""" """공휴일(일요일 제외)이면 True. 일/토 색상은 요일로 따로 판정한다."""
if (d.month, d.day) in _FIXED_SOLAR: return bool(holiday_name(d))
return True
return d.isoformat() in _LUNAR_AND_SUBSTITUTE.get(d.year, set())
+2 -1
View File
@@ -21,7 +21,7 @@ from app.timezone import today_kst
from fastapi import APIRouter, Body, Depends, File, Form, HTTPException, Request, UploadFile from fastapi import APIRouter, Body, Depends, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from .holidays import is_holiday from .holidays import holiday_name, is_holiday
from .store import SHIP_METHODS from .store import SHIP_METHODS
router = APIRouter(prefix="/cupang", tags=["cupang"]) router = APIRouter(prefix="/cupang", tags=["cupang"])
@@ -181,6 +181,7 @@ async def index(request: Request) -> HTMLResponse:
"is_sunday": d.weekday() == 6, "is_sunday": d.weekday() == 6,
"is_saturday": d.weekday() == 5, "is_saturday": d.weekday() == 5,
"is_holiday": is_holiday(d), "is_holiday": is_holiday(d),
"holiday_name": holiday_name(d),
"counts": counts.get(d.isoformat(), {}), "counts": counts.get(d.isoformat(), {}),
} }
for d in week for d in week
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903h" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903i" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903h" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903i" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903h" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903i" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -56,8 +56,11 @@
{% if cell.counts.ship %}has-ship{% 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 %}" {% 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 }}" href="/cupang/?year={{ year }}&month={{ month }}&date={{ cell.date }}"
title="{{ cell.date }}{% if cell.counts.ship %} — 출고 {{ cell.counts.ship }}건 / 센터 {{ cell.counts.centers }}곳{% endif %}"> title="{{ cell.date }}{% if cell.holiday_name %} — {{ cell.holiday_name }}{% endif %}{% if cell.counts.ship %} — 출고 {{ cell.counts.ship }}건 / 센터 {{ cell.counts.centers }}곳{% endif %}">
<span class="cpg-mcal-day">{{ cell.day }}</span> <span class="cpg-mcal-day">{{ cell.day }}</span>
{% if cell.holiday_name %}
<span class="cpg-mcal-holi">{{ cell.holiday_name }}</span>
{% endif %}
{% if cell.counts.ship %} {% if cell.counts.ship %}
<span class="cpg-mcal-tag">{{ cell.counts.ship }}건<em>{{ cell.counts.centers }}곳</em></span> <span class="cpg-mcal-tag">{{ cell.counts.ship }}건<em>{{ cell.counts.centers }}곳</em></span>
{% endif %} {% endif %}
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903h" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903i" />{% endblock %}
{% block content %} {% block content %}
<section class="cpg"> <section class="cpg">
@@ -1,6 +1,6 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903h" />{% endblock %} {% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260903i" />{% endblock %}
{% block content %} {% block content %}
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #} {# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
+11 -1
View File
@@ -1112,7 +1112,7 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
.cpg-mcal-cell { .cpg-mcal-cell {
position: relative; position: relative;
display: flex; flex-direction: column; align-items: center; gap: 4px; display: flex; flex-direction: column; align-items: center; gap: 4px;
min-height: 62px; min-width: 0; padding: 6px 4px 7px; min-height: 70px; min-width: 0; padding: 6px 4px 7px;
border: 1px solid var(--color-subtle-ash); border: 1px solid var(--color-subtle-ash);
border-radius: 8px; border-radius: 8px;
background: #fff; background: #fff;
@@ -1141,6 +1141,16 @@ body.cpg-dragging { cursor: grabbing; user-select: none; }
box-shadow: inset 0 0 0 1.5px var(--color-deep-black); box-shadow: inset 0 0 0 1.5px var(--color-deep-black);
} }
/* 공휴일 이름 — 날짜 아래 작은 빨강 글씨 */
.cpg-mcal-holi {
max-width: 100%;
font-size: 11px; font-weight: 700; line-height: 1.3;
color: var(--color-callout-red);
text-align: center;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cpg-mcal-cell.is-out .cpg-mcal-holi { color: #e3a59b; }
/* 출고 있는 날 — 건수/센터수 태그 */ /* 출고 있는 날 — 건수/센터수 태그 */
.cpg-mcal-tag { .cpg-mcal-tag {
display: inline-flex; align-items: center; gap: 4px; display: inline-flex; align-items: center; gap: 4px;