feat: implement Phase 3 automation workers

- Add automation_state table for incremental sync (last UID, last check)
- Add law_monitor worker: 국가법령정보센터 API → NAS/DB/CalDAV VTODO
  (LAW_OC 승인 대기 중, 코드 완성)
- Add mailplus_archive worker: IMAP(993) → .eml NAS save + DB + SMTP
  notification (imaplib via asyncio.to_thread, timeout=30)
- Add daily_digest worker: PostgreSQL/pipeline stats → Markdown + SMTP
  (documents, law changes, email, queue errors, inbox backlog)
- Add CalDAV VTODO helper and SMTP email helper to core/utils.py
- Wire 3 cron jobs in APScheduler (law@07:00, mail@07:00+18:00,
  digest@20:00) with timezone=Asia/Seoul

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-02 15:24:50 +09:00
parent a5312c044b
commit 31d5498f8d
7 changed files with 652 additions and 1 deletions

View File

@@ -44,3 +44,81 @@ def count_log_errors(log_path: str) -> int:
return sum(1 for line in f if "[ERROR]" in line)
except FileNotFoundError:
return 0
# ─── CalDAV 헬퍼 ───
def create_caldav_todo(
caldav_url: str,
username: str,
password: str,
title: str,
description: str = "",
due_days: int = 7,
) -> str | None:
"""Synology Calendar에 VTODO 생성, UID 반환"""
import uuid
from datetime import datetime, timedelta, timezone
import caldav
try:
client = caldav.DAVClient(url=caldav_url, username=username, password=password)
principal = client.principal()
calendars = principal.calendars()
if not calendars:
return None
calendar = calendars[0]
uid = str(uuid.uuid4())
due = datetime.now(timezone.utc) + timedelta(days=due_days)
due_str = due.strftime("%Y%m%dT%H%M%SZ")
vtodo = f"""BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VTODO
UID:{uid}
SUMMARY:{title}
DESCRIPTION:{description}
DUE:{due_str}
STATUS:NEEDS-ACTION
PRIORITY:5
END:VTODO
END:VCALENDAR"""
calendar.save_event(vtodo)
return uid
except Exception as e:
logging.getLogger("caldav").error(f"CalDAV VTODO 생성 실패: {e}")
return None
# ─── SMTP 헬퍼 ───
def send_smtp_email(
host: str,
port: int,
username: str,
password: str,
subject: str,
body: str,
to_addr: str | None = None,
):
"""Synology MailPlus SMTP로 이메일 발송"""
import smtplib
from email.mime.text import MIMEText
to_addr = to_addr or username
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = username
msg["To"] = to_addr
try:
with smtplib.SMTP_SSL(host, port, timeout=30) as server:
server.login(username, password)
server.send_message(msg)
except Exception as e:
logging.getLogger("smtp").error(f"SMTP 발송 실패: {e}")