You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
# backend/app/connectors/calendar/recurrence.py
|
|
# 반복 일정 표현을 iCalendar RRULE 문자열로 통일한다.
|
|
# - Google: recurrence 는 본래 ["RRULE:..."] 리스트 → 그대로 변환.
|
|
# - Outlook(Graph): recurrence 는 pattern/range 객체 → 양방향 변환.
|
|
# UI 가 제공하는 흔한 경우(매일/매주/평일/매월/매년 + 간격·횟수·종료일)를 지원한다.
|
|
|
|
_DAY_TO_GRAPH = {
|
|
"MO": "monday",
|
|
"TU": "tuesday",
|
|
"WE": "wednesday",
|
|
"TH": "thursday",
|
|
"FR": "friday",
|
|
"SA": "saturday",
|
|
"SU": "sunday",
|
|
}
|
|
_GRAPH_TO_DAY = {v: k for k, v in _DAY_TO_GRAPH.items()}
|
|
_WD_INDEX = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
|
|
|
|
|
def parse_rrule(rrule: str) -> dict:
|
|
"""'FREQ=WEEKLY;BYDAY=MO,WE;INTERVAL=2' → {FREQ:.., BYDAY:[..], INTERVAL:'2'}."""
|
|
out: dict = {}
|
|
for part in (rrule or "").replace("RRULE:", "").split(";"):
|
|
if "=" not in part:
|
|
continue
|
|
k, v = part.split("=", 1)
|
|
k = k.strip().upper()
|
|
if k == "BYDAY":
|
|
out[k] = [d.strip().upper() for d in v.split(",") if d.strip()]
|
|
else:
|
|
out[k] = v.strip()
|
|
return out
|
|
|
|
|
|
def rrule_to_google(rrule: str) -> list[str]:
|
|
"""Google Calendar recurrence 필드(리스트)."""
|
|
if not rrule:
|
|
return []
|
|
body = rrule if rrule.upper().startswith("RRULE:") else f"RRULE:{rrule}"
|
|
return [body]
|
|
|
|
|
|
def rrule_to_graph(rrule: str, date_iso: str) -> dict | None:
|
|
"""RRULE + 시작일(YYYY-MM-DD) → Microsoft Graph recurrence 객체."""
|
|
r = parse_rrule(rrule)
|
|
freq = r.get("FREQ", "").upper()
|
|
if not freq:
|
|
return None
|
|
interval = int(r.get("INTERVAL", "1") or 1)
|
|
pattern: dict = {"interval": interval}
|
|
if freq == "DAILY":
|
|
pattern["type"] = "daily"
|
|
elif freq == "WEEKLY":
|
|
pattern["type"] = "weekly"
|
|
days = r.get("BYDAY") or []
|
|
if not days and len(date_iso) >= 10:
|
|
from datetime import date
|
|
|
|
days = [_WD_INDEX[date.fromisoformat(date_iso[:10]).weekday()]]
|
|
pattern["daysOfWeek"] = [_DAY_TO_GRAPH[d] for d in days if d in _DAY_TO_GRAPH]
|
|
elif freq == "MONTHLY":
|
|
pattern["type"] = "absoluteMonthly"
|
|
pattern["dayOfMonth"] = int(date_iso[8:10]) if len(date_iso) >= 10 else 1
|
|
elif freq == "YEARLY":
|
|
pattern["type"] = "absoluteYearly"
|
|
pattern["month"] = int(date_iso[5:7]) if len(date_iso) >= 10 else 1
|
|
pattern["dayOfMonth"] = int(date_iso[8:10]) if len(date_iso) >= 10 else 1
|
|
else:
|
|
return None
|
|
rng: dict = {"type": "noEnd", "startDate": date_iso[:10]}
|
|
if r.get("COUNT"):
|
|
rng = {
|
|
"type": "numbered",
|
|
"startDate": date_iso[:10],
|
|
"numberOfOccurrences": int(r["COUNT"]),
|
|
}
|
|
elif r.get("UNTIL"):
|
|
until = r["UNTIL"][:8] # YYYYMMDD
|
|
if len(until) == 8:
|
|
rng = {
|
|
"type": "endDate",
|
|
"startDate": date_iso[:10],
|
|
"endDate": f"{until[:4]}-{until[4:6]}-{until[6:8]}",
|
|
}
|
|
return {"pattern": pattern, "range": rng}
|
|
|
|
|
|
def graph_to_rrule(recurrence: dict | None) -> str:
|
|
"""Graph recurrence 객체 → RRULE 문자열(표시·재동기화용, 베스트 에포트)."""
|
|
if not recurrence:
|
|
return ""
|
|
pat = recurrence.get("pattern") or {}
|
|
rng = recurrence.get("range") or {}
|
|
t = (pat.get("type") or "").lower()
|
|
freq = {
|
|
"daily": "DAILY",
|
|
"weekly": "WEEKLY",
|
|
"absolutemonthly": "MONTHLY",
|
|
"relativemonthly": "MONTHLY",
|
|
"absoluteyearly": "YEARLY",
|
|
"relativeyearly": "YEARLY",
|
|
}.get(t)
|
|
if not freq:
|
|
return ""
|
|
parts = [f"FREQ={freq}"]
|
|
if int(pat.get("interval", 1) or 1) > 1:
|
|
parts.append(f"INTERVAL={int(pat['interval'])}")
|
|
if freq == "WEEKLY" and pat.get("daysOfWeek"):
|
|
days = [_GRAPH_TO_DAY[d] for d in pat["daysOfWeek"] if d in _GRAPH_TO_DAY]
|
|
if days:
|
|
parts.append("BYDAY=" + ",".join(days))
|
|
if (rng.get("type") or "") == "numbered" and rng.get("numberOfOccurrences"):
|
|
parts.append(f"COUNT={int(rng['numberOfOccurrences'])}")
|
|
elif (rng.get("type") or "") == "endDate" and rng.get("endDate"):
|
|
parts.append("UNTIL=" + str(rng["endDate"]).replace("-", "") + "T000000Z")
|
|
return ";".join(parts)
|