""" 공공데이터포털 — 한국천문연구원 특일정보 OpenAPI 클라이언트. 엔드포인트: getRestDeInfo (국경일/공휴일 — 임시공휴일 포함) 공식 문서: https://www.data.go.kr/data/15012690/openapi.do `holidays` 패키지가 누락하는 임시공휴일·근로자의 날 등을 정부 공인 데이터로 보강하기 위해 사용. 설계: - 네트워크 실패는 silent (None 반환) — 호출자가 fallback 처리 - API 키는 코드 내 박혀있으나 dev 본인 계정의 특일정보 API 한정 키 (50명 이내 사용 환경에서 일일 한도 1000회 충분) """ from __future__ import annotations import json import urllib.parse import urllib.request import urllib.error from typing import List, Dict, Optional # 공공데이터포털 dev 키 (특일정보 API 한정). # 노출 시 data.go.kr 마이페이지에서 즉시 폐기/재발급 가능. _SERVICE_KEY = 'fa419259319e31d2fcd4f959e65da817fe2f19894bff340a63889db7a8ffac93' _BASE = 'https://apis.data.go.kr/B090041/openapi/service/SpcdeInfoService' _USER_AGENT = 'ClockOutCalculator/2.10 (KASI special-day client)' def fetch_korean_holidays(year: int, timeout: int = 10) -> Optional[List[Dict]]: """해당 연도의 한국 공휴일 전체를 정부 API에서 받아 반환. Returns: 성공: [{'date': '2026-05-01', 'name': '근로자의 날', 'is_holiday': True}, ...] 실패: None (네트워크 오류, 인증 실패, 응답 파싱 실패 등) """ params = { 'serviceKey': _SERVICE_KEY, 'solYear': str(year), '_type': 'json', 'numOfRows': '100', 'pageNo': '1', } url = f"{_BASE}/getRestDeInfo?" + urllib.parse.urlencode(params) req = urllib.request.Request(url, headers={'User-Agent': _USER_AGENT}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: data = json.loads(resp.read()) except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, OSError, TimeoutError): return None return _parse_response(data) def _parse_response(data: Dict) -> Optional[List[Dict]]: """API 응답 JSON을 표준 형식으로 정규화. API 응답 패턴: - resultCode == '00' → 정상 - items.item: 단일 결과면 dict, 여러 개면 list - items가 빈 문자열일 때 (totalCount=0)도 정상으로 간주 """ try: response = data.get('response') or {} header = response.get('header') or {} if header.get('resultCode') != '00': return None body = response.get('body') or {} items_root = body.get('items') if not items_root: return [] # 그 해 공휴일 없음 (드물지만 정상 응답) item = items_root.get('item') if isinstance(items_root, dict) else None if item is None: return [] if isinstance(item, dict): item = [item] out = [] for entry in item: locdate = entry.get('locdate') name = entry.get('dateName') is_holiday = (entry.get('isHoliday') == 'Y') if not locdate or not name: continue # locdate: 20260501 (int 또는 str) ds = str(locdate) if len(ds) != 8 or not ds.isdigit(): continue iso = f"{ds[0:4]}-{ds[4:6]}-{ds[6:8]}" out.append({'date': iso, 'name': str(name), 'is_holiday': is_holiday}) return out except (AttributeError, TypeError, KeyError): return None def is_configured() -> bool: """키가 설정되어 있는지 (테스트/빈 키 환경 가드).""" return bool(_SERVICE_KEY) and len(_SERVICE_KEY) > 10