Files
king 269e779129 feat(vacation): 휴가 관리 모듈 — 월간 달력 bar/연차·반차 신청/승인 워크플로/공휴일·연차 설정
- vacation_db 전용 저장소(VacationDBStore), VACATION_DB_URL 미설정 시 '설정 필요' 안내
- 월간 달력: 구글식 휴가 bar(주 단위 lane 배정), 일/공휴일 빨강·토요일 파랑
- 신청 폼: 종류/기간/시작·종료 구분(종일/오전/오후)/사유 + 예상 일수 미리보기
- 일수 계산은 서버(compute_days)에서 주말+공휴일(is_red) 제외 재계산
- 워크플로: 작성중/반려→제출→승인|반려, 취소 soft delete, 본인만 수정(작성중/반려)
- 권한: vacation(접근)/vacation_approver(승인), admin 통과. 설정은 admin 전용
- 공휴일(vacation_holidays) DB 기반 provider + settings CRUD, 연차잔여(vacation_balances)
- 엑셀 내보내기(openpyxl), scripts/sql/vacation_db_init.sql(멱등, 2026 공휴일 seed)
- main.py 등록, 메뉴 ready 전환, docs/.env.example/CLAUDE.md 갱신

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 08:20:20 +09:00

100 lines
3.2 KiB
Python

"""휴가 관리 모듈 상수 및 순수 계산 헬퍼.
- 데이터 저장은 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"