v2.10.0: \uc815\ubd80 \ud2b9\uc77c\uc815\ubcf4 API \uc5f0\ub3d9 + \uc77c \uc790\ub3d9 \ub3d9\uae30\ud654

\uacf5\uacf5\ub370\uc774\ud130\ud3ec\ud138 \ud55c\uad6d\ucc9c\ubb38\uc5f0\uad6c\uc6d0 \ud2b9\uc77c\uc815\ubcf4 API\ub85c \uc784\uc2dc\uacf5\ud734\uc77c\uae4c\uc9c0
\uc815\ubd80 \uacf5\uc778 \ub370\uc774\ud130\ub85c \ubcf4\uac15. holidays \ud328\ud0a4\uc9c0\ub294 fallback.

- utils/holiday_api.py: getRestDeInfo \uc5d4\ub4dc\ud3ec\uc778\ud2b8 + \uc751\ub2f5 \ud30c\uc11c (\ub2e8\uc77c/\ub2e4\uc218 item)
- Database.add_korean_holidays_from_api(year) + add_korean_holidays_auto fallback chain
- migrate_v290_holidays_auto_sync: \uc77c 1\ud68c \ubc31\uadf8\ub77c\uc6b4\ub4dc \ub3d9\uae30\ud654
  (sentinel holidays_synced_date, daemon thread, CLOCKOUT_DISABLE_HOLIDAY_SYNC env var)
- Settings UI \uc548\ub0b4\ubb38 \uc5c5\ub370\uc774\ud2b8

Tests: tests/test_holiday_api.py 14\uac1c + conftest.py + 175\u2192189 pytest \uc804\ubd80 green
\ud1b5\ud569 \uc2dc\ub098\ub9ac\uc624 53/53 green

\uc8fc\uc758: \ud0a4 \ud65c\uc6a9\uae30\uac04 \uc2dc\uc791 \uc9c1\ud6c4 (2026-05-01) propagation \uc73c\ub85c 401 \uac00\ub2a5,
fallback \uacbd\ub85c\uac00 \ud574\ub2f9 \uc0ac\ub840 \ucee4\ubc84 \u2014 \uadfc\ub85c\uc790\uc758 \ub0a0 \ud3ec\ud568 22\uac1c \ud734\uc77c \uc790\ub3d9 \ub4f1\ub85d \ud655\uc778
This commit is contained in:
KINDNICK
2026-05-01 13:51:33 +09:00
parent 47296dd35b
commit 97dd4e39f7
7 changed files with 386 additions and 12 deletions
+81 -9
View File
@@ -89,10 +89,54 @@ class Database:
self.migrate_v271_work_records_indexes()
self.migrate_v280_achievements_columns()
self.migrate_v280_hire_date()
self.migrate_v290_holidays_auto_sync()
# 기본 설정 초기화
self.init_default_settings()
def migrate_v290_holidays_auto_sync(self) -> None:
"""일 1회 한국 공휴일 자동 동기화 (백그라운드).
Sentinel: settings['holidays_synced_date'] = 'YYYY-MM-DD' (오늘 날짜).
값이 오늘과 같으면 스킵 — 즉 같은 날 여러 번 켜도 호출 1회.
매일 호출하므로 정부가 임시공휴일 발표하면 다음 날 자동 반영.
일일 한도 10000회, 사용자 50명 × 1회 = 0.5% 소비.
실제 동기화는 백그라운드 스레드에서 — 부트스트랩이 네트워크에 묶이지 않음.
실패는 silent, 다음 실행 시 재시도.
테스트 환경에서는 CLOCKOUT_DISABLE_HOLIDAY_SYNC=1 로 비활성화.
"""
import os
if os.environ.get('CLOCKOUT_DISABLE_HOLIDAY_SYNC'):
return
from datetime import datetime as _dt
import threading
try:
today = _dt.now().date().isoformat()
sentinel = self.get_setting('holidays_synced_date', '')
if sentinel == today:
return
except Exception:
return
cur_year = _dt.now().year
def _worker():
try:
# 새 연결로 작업 (sqlite3 connection은 thread-affine)
from core.database import Database
db = Database(self.db_path)
added = db.add_korean_holidays_auto(cur_year, include_next_year=True)
if added >= 0:
db.set_setting('holidays_synced_date', today)
except Exception:
pass
t = threading.Thread(target=_worker, daemon=True, name='holiday-sync')
t.start()
def _create_tables(self, conn) -> None:
"""init_database()에서 호출되는 CREATE TABLE 묶음. conn은 호출자가 관리."""
cursor = conn.cursor()
@@ -1713,6 +1757,32 @@ class Database:
for date, name in fixed_holidays:
self.add_holiday(date, name, is_recurring=True)
def add_korean_holidays_from_api(self, year: int) -> int:
"""공공데이터포털 특일정보 API로 한국 공휴일 등록 (정부 공인).
임시공휴일 + 근로자의 날 등 holidays 패키지가 놓치는 항목까지 포함.
네트워크 실패 시 -1 반환 → 호출자 fallback.
Returns:
추가된 공휴일 개수 (기존 등록과 중복은 제외). 실패 시 -1.
"""
try:
from utils.holiday_api import fetch_korean_holidays
except ImportError:
return -1
items = fetch_korean_holidays(year)
if items is None:
return -1
added = 0
for it in items:
if not it.get('is_holiday'):
continue
date_str = it['date']
if not self.is_holiday(date_str):
self.add_holiday(date_str, it['name'], is_recurring=False)
added += 1
return added
def add_korean_holidays_auto(self, year: int, include_next_year: bool = False) -> int:
"""`holidays` 패키지로 음력 명절 포함 한국 공휴일 자동 등록.
@@ -1738,30 +1808,32 @@ class Database:
Returns:
추가된 공휴일 개수. 패키지 미설치 시 -1.
"""
try:
import holidays as _holidays
except ImportError:
return -1
years_to_add = [year]
if include_next_year:
years_to_add.append(year + 1)
added = 0
for y in years_to_add:
# 1차: 정부 API (임시공휴일 포함, 가장 정확)
api_count = self.add_korean_holidays_from_api(y)
if api_count >= 0:
added += api_count
# API가 응답했으면 근로자의 날도 포함되어 있음. 끝.
continue
# 2차 fallback: holidays 패키지
try:
import holidays as _holidays
kr = _holidays.country_holidays('KR', years=y)
except Exception:
continue # 패키지 내부 오류는 해당 연도만 스킵
continue # 둘 다 실패면 해당 연도만 스킵
for d, name in kr.items():
date_str = d.isoformat()
if not self.is_holiday(date_str):
self.add_holiday(date_str, name, is_recurring=False)
added += 1
# holidays.KR이 누락하는 한국 노동자 휴일 보강.
# 근로자의 날(5/1)은 공식 '공휴일'은 아니지만 대부분 회사가 휴무.
# 패키지 버전마다 포함 여부가 달라서 명시적 추가.
# holidays.KR이 누락하는 근로자의 날 명시적 보강
extra = [(f"{y}-05-01", "근로자의 날")]
for date_str, name in extra:
if not self.is_holiday(date_str):
+1 -1
View File
@@ -4,4 +4,4 @@
릴리스 시 이 값을 올린 후 git tag → push.
CHANGELOG.md의 최상단 항목과 일치시킬 것.
"""
__version__ = '2.9.0'
__version__ = '2.10.0'