"""휴가 관리 모듈 상수 및 순수 계산 헬퍼. - 데이터 저장은 vacation_db(PostgreSQL) 전용이다(`db.py`). 운영 데이터가 JSON 과 DB 로 갈라지는 것을 막기 위해 JSON 폴백을 두지 않는다. VACATION_DB_URL 미설정 시 라우터가 "설정 필요" 안내 페이지를 보여준다. - 이 모듈에는 상수와 순수 계산 헬퍼(휴가일수 계산)만 둔다. 공휴일 집합은 db.py 가 vacation_holidays 에서 읽어 넘겨준다(holiday provider 분리). """ from __future__ import annotations from datetime import date, timedelta from typing import Iterable # 휴가 종류 (한글 라벨 그대로 저장) VACATION_TYPES: tuple[str, ...] = ( "연차", "오전반차", "오후반차", "병가", "경조", "대체휴무", "기타", ) # 신청 상태 워크플로 STATUSES: tuple[str, ...] = ("작성중", "제출", "승인", "반려", "취소") # 시작/종료일 구분 HALVES: tuple[str, ...] = ("full", "am", "pm") HALF_LABELS: dict[str, str] = {"full": "종일", "am": "오전", "pm": "오후"} # 수정/삭제(취소) 가능한 상태 — 본인 편집 허용 EDITABLE_STATUSES: tuple[str, ...] = ("작성중", "반려") # 반차 성격의 휴가 종류 (단일 일자 0.5일 강제) _HALF_DAY_TYPES: dict[str, str] = {"오전반차": "am", "오후반차": "pm"} def _daterange(start: date, end: date) -> Iterable[date]: cur = start while cur <= end: yield cur cur += timedelta(days=1) def is_working_day(d: date, holidays: set[str]) -> bool: """주말(토/일)과 공휴일(holidays: ISO 날짜 집합)을 제외하면 근무일.""" if d.weekday() >= 5: # 5=토, 6=일 return False return d.isoformat() not in holidays def compute_days( *, start_date: date, end_date: date, start_half: str = "full", end_half: str = "full", vacation_type: str = "연차", holidays: set[str] | None = None, ) -> float: """휴가 일수 계산. 서버에서 항상 이 함수로 재계산한다(클라이언트 신뢰 금지). - 주말 + 공휴일(is_red) 제외. - 오전/오후 반차는 0.5일. - 같은 날 + 반차면 0.5일. - 여러 날에서 시작/종료가 반차면 시작일/종료일 각각 0.5 차감. - 휴가 종류가 오전반차/오후반차면 단일 일자 0.5일로 강제. """ holidays = holidays or set() if end_date < start_date: return 0.0 # 반차 종류는 단일 일자 0.5 if vacation_type in _HALF_DAY_TYPES: if is_working_day(start_date, holidays): return 0.5 return 0.0 working = [d for d in _daterange(start_date, end_date) if is_working_day(d, holidays)] n = len(working) if n == 0: return 0.0 if start_date == end_date: return 0.5 if start_half in ("am", "pm") else 1.0 total = float(n) if start_half in ("am", "pm") and is_working_day(start_date, holidays): total -= 0.5 if end_half in ("am", "pm") and is_working_day(end_date, holidays): total -= 0.5 return max(0.0, total) def normalize_half(value: str | None) -> str: v = (value or "full").strip().lower() return v if v in HALVES else "full"