feat(project): 프로젝트 달력에 2026~2030 대한민국 공휴일·대체공휴일 표시

vacation 모듈은 공휴일을 vacation_db 테이블로 관리하지만 휴가일수 계산용
으로 강하게 결합돼 있어(조사 결과 공용 provider 없음), project 달력은
"보여주기만" 하면 되므로 store.py 에 정적 KR_HOLIDAYS 표(날짜→이름)를
추가하는 방식으로 처리 — 새 DB 연결/마이그레이션 없음.

- 신정·설날·삼일절·어린이날·부처님오신날·현충일·광복절·추석·개천절·
  한글날·성탄절 + 실제 "관공서의 공휴일에 관한 규정" 대체공휴일 규칙
  그대로 반영(설날/추석/어린이날/삼일절/광복절/개천절/한글날/부처님오신
  날/성탄절 대상, 신정·현충일은 대체 없음). 근로자의 날·제헌절은 관공서
  공휴일이 아니라서 제외(vacation 모듈 2026년 기존 시드와 동일 기준).
  선거일 같은 임시공휴일은 범위 밖.
- 웹 검색으로 확보한 2027~2030년 데이터를 파이썬 요일 계산으로 전수
  검증(90건 전부 일치) — 날짜 오타 위험 제거.
- 달력 날짜 칸: 공휴일이면 일요일과 같은 빨간색 + 이름 라벨 표시,
  호버 시 툴팁(title)도 함께 제공.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 00:14:40 +09:00
parent 0ba6182574
commit a7a294ce93
6 changed files with 95 additions and 5 deletions
+1
View File
@@ -285,6 +285,7 @@ def _build_task_calendar(
"is_today": d == today, "is_today": d == today,
"is_sunday": d.weekday() == 6, "is_sunday": d.weekday() == 6,
"is_saturday": d.weekday() == 5, "is_saturday": d.weekday() == 5,
"holiday_name": store.holiday_name(d.isoformat()),
} }
for d in week for d in week
] ]
+84
View File
@@ -49,6 +49,90 @@ COLOR_PALETTE: tuple[str, ...] = (
"#5a6772", # slate "#5a6772", # slate
) )
# ════════════════════════════════════════════════════════════
# 대한민국 국경일·공휴일(2026~2030) — 프로젝트 달력 표시 전용.
#
# vacation 모듈도 공휴일을 관리하지만(vacation_holidays 테이블) 그건
# 휴가 일수 계산용으로 vacation_db 에 강하게 결합돼 있고, 여기는 달력에
# 빨간 글씨로 "보여만" 주면 되는 용도라 별도 DB 연결 없이 정적 표로 둔다.
# 근로자의 날(5/1)·제헌절(7/17)은 관공서 공휴일이 아니라서 제외했다
# (vacation 모듈의 기존 2026년 시드와 동일 기준). 대체공휴일은 실제
# "관공서의 공휴일에 관한 규정" 대체공휴일 규정을 그대로 반영함(설날·
# 추석·어린이날·삼일절/광복절/개천절/한글날·부처님오신날·성탄절 대상,
# 신정·현충일은 대체 없음). 새 연도가 필요하면 이 표에 추가한다.
# ════════════════════════════════════════════════════════════
KR_HOLIDAYS: dict[str, str] = {
# 2026
"2026-01-01": "신정",
"2026-02-16": "설날 연휴", "2026-02-17": "설날", "2026-02-18": "설날 연휴",
"2026-03-01": "삼일절", "2026-03-02": "삼일절 대체",
"2026-05-05": "어린이날",
"2026-05-24": "부처님오신날", "2026-05-25": "부처님오신날 대체",
"2026-06-06": "현충일",
"2026-08-15": "광복절", "2026-08-17": "광복절 대체",
"2026-09-24": "추석 연휴", "2026-09-25": "추석", "2026-09-26": "추석 연휴",
"2026-09-28": "추석 대체",
"2026-10-03": "개천절", "2026-10-05": "개천절 대체",
"2026-10-09": "한글날",
"2026-12-25": "성탄절",
# 2027
"2027-01-01": "신정",
"2027-02-06": "설날 연휴", "2027-02-07": "설날", "2027-02-08": "설날 연휴",
"2027-02-09": "설날 대체",
"2027-03-01": "삼일절",
"2027-05-05": "어린이날",
"2027-05-13": "부처님오신날",
"2027-06-06": "현충일",
"2027-08-15": "광복절", "2027-08-16": "광복절 대체",
"2027-09-14": "추석 연휴", "2027-09-15": "추석", "2027-09-16": "추석 연휴",
"2027-10-03": "개천절", "2027-10-04": "개천절 대체",
"2027-10-09": "한글날", "2027-10-11": "한글날 대체",
"2027-12-25": "성탄절", "2027-12-27": "성탄절 대체",
# 2028
"2028-01-01": "신정",
"2028-01-26": "설날 연휴", "2028-01-27": "설날", "2028-01-28": "설날 연휴",
"2028-03-01": "삼일절",
"2028-05-02": "부처님오신날",
"2028-05-05": "어린이날",
"2028-06-06": "현충일",
"2028-08-15": "광복절",
"2028-10-02": "추석 연휴", "2028-10-03": "추석·개천절", "2028-10-04": "추석 연휴",
"2028-10-05": "개천절 대체",
"2028-10-09": "한글날",
"2028-12-25": "성탄절",
# 2029
"2029-01-01": "신정",
"2029-02-12": "설날 연휴", "2029-02-13": "설날", "2029-02-14": "설날 연휴",
"2029-03-01": "삼일절",
"2029-05-05": "어린이날", "2029-05-07": "어린이날 대체",
"2029-05-20": "부처님오신날", "2029-05-21": "부처님오신날 대체",
"2029-06-06": "현충일",
"2029-08-15": "광복절",
"2029-09-21": "추석 연휴", "2029-09-22": "추석", "2029-09-23": "추석 연휴",
"2029-09-24": "추석 대체",
"2029-10-03": "개천절",
"2029-10-09": "한글날",
"2029-12-25": "성탄절",
# 2030
"2030-01-01": "신정",
"2030-02-02": "설날 연휴", "2030-02-03": "설날", "2030-02-04": "설날 연휴",
"2030-02-05": "설날 대체",
"2030-03-01": "삼일절",
"2030-05-05": "어린이날", "2030-05-06": "어린이날 대체",
"2030-05-09": "부처님오신날",
"2030-06-06": "현충일",
"2030-08-15": "광복절",
"2030-09-11": "추석 연휴", "2030-09-12": "추석", "2030-09-13": "추석 연휴",
"2030-10-03": "개천절",
"2030-10-09": "한글날",
"2030-12-25": "성탄절",
}
def holiday_name(iso_date: str) -> str:
"""해당 날짜(YYYY-MM-DD)가 공휴일이면 이름을, 아니면 빈 문자열을 반환."""
return KR_HOLIDAYS.get(iso_date, "")
def norm_str(s: Any, *, lower: bool = False) -> str: def norm_str(s: Any, *, lower: bool = False) -> str:
out = str(s or "").strip() out = str(s or "").strip()
@@ -1,7 +1,7 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/project.css?v=20260916s" /> <link rel="stylesheet" href="/static/project.css?v=20260917a" />
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" /> <link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
{% endblock %} {% endblock %}
@@ -1,7 +1,7 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/project.css?v=20260916s" /> <link rel="stylesheet" href="/static/project.css?v=20260917a" />
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" /> <link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
{% endblock %} {% endblock %}
@@ -1,7 +1,7 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/project.css?v=20260916s" /> <link rel="stylesheet" href="/static/project.css?v=20260917a" />
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host --> <!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" /> <link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
<!-- 타임라인 vis-timeline — self-host --> <!-- 타임라인 vis-timeline — self-host -->
@@ -126,9 +126,10 @@
<div class="pj-week-days"> <div class="pj-week-days">
{% for cell in week.days %} {% for cell in week.days %}
<div class="pj-day {% if not cell.in_month %}pj-out{% endif %} {% if cell.is_today %}pj-today{% endif %}" <div class="pj-day {% if not cell.in_month %}pj-out{% endif %} {% if cell.is_today %}pj-today{% endif %}"
data-date="{{ cell.date }}"> data-date="{{ cell.date }}" {% if cell.holiday_name %}title="{{ cell.holiday_name }}"{% endif %}>
<span class="pj-day-num <span class="pj-day-num
{% if cell.is_sunday %}pj-red{% elif cell.is_saturday %}pj-blue{% endif %}">{{ cell.day }}</span> {% if cell.is_sunday or cell.holiday_name %}pj-red{% elif cell.is_saturday %}pj-blue{% endif %}">{{ cell.day }}</span>
{% if cell.holiday_name %}<span class="pj-day-holiday">{{ cell.holiday_name }}</span>{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
+4
View File
@@ -390,6 +390,10 @@
.pj-today .pj-day-num.pj-blue { background: #1d4ed8; } .pj-today .pj-day-num.pj-blue { background: #1d4ed8; }
.pj-red { color: #c22b10 !important; } .pj-red { color: #c22b10 !important; }
.pj-blue { color: #1d4ed8 !important; } .pj-blue { color: #1d4ed8 !important; }
.pj-day-holiday {
display: block; font-size: 10.5px; color: #c22b10; font-weight: 600;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 1px;
}
/* bar 오버레이 — 날짜 숫자 아래(top)부터 7열 그리드로 겹쳐 그림 */ /* bar 오버레이 — 날짜 숫자 아래(top)부터 7열 그리드로 겹쳐 그림 */
.pj-week-bars { position: absolute; left: 0; right: 0; top: 34px; bottom: 4px; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); grid-auto-rows: 22px; row-gap: 2px; pointer-events: none; } .pj-week-bars { position: absolute; left: 0; right: 0; top: 34px; bottom: 4px; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); grid-auto-rows: 22px; row-gap: 2px; pointer-events: none; }