"""PWA Web Push 알림 — pywebpush 라이브러리 활용. 환경변수: VAPID_PRIVATE_KEY: VAPID 개인키 (base64url 또는 PEM) VAPID_PUBLIC_KEY: VAPID 공개키 (브라우저 subscribe 시 사용) VAPID_CLAIMS_EMAIL: 관리자 이메일 (mailto: claim) """ from __future__ import annotations import json import logging import os from typing import Optional logger = logging.getLogger(__name__) VAPID_PRIVATE_KEY = os.getenv("VAPID_PRIVATE_KEY", "") VAPID_PUBLIC_KEY = os.getenv("VAPID_PUBLIC_KEY", "") VAPID_EMAIL = os.getenv("VAPID_CLAIMS_EMAIL", "admin@guardia.local") async def send_push( subscription_info: dict, title: str, body: str, url: str = "/", icon: str = "/static/icons/icon-192.png", ) -> bool: """단일 푸시 구독에 알림 전송. 성공 시 True.""" if not VAPID_PRIVATE_KEY: logger.debug("VAPID_PRIVATE_KEY 미설정 — 푸시 알림 스킵") return False try: from pywebpush import webpush, WebPushException webpush( subscription_info=subscription_info, data=json.dumps({ "title": title, "body": body, "url": url, "icon": icon, }), vapid_private_key=VAPID_PRIVATE_KEY, vapid_claims={"sub": f"mailto:{VAPID_EMAIL}"}, ) return True except Exception as e: logger.warning("푸시 알림 전송 실패: %s", str(e)[:100]) return False async def broadcast_push( title: str, body: str, url: str = "/", user_ids: Optional[list] = None, ) -> dict: """ 여러 사용자에게 Push 브로드캐스트. user_ids=None 이면 전체 구독자에게 발송. Returns: {"sent": int, "failed": int} """ from database import SessionLocal from models import PushSubscription from sqlalchemy import select sent = 0 failed = 0 try: async with SessionLocal() as db: q = select(PushSubscription) if user_ids: q = q.where(PushSubscription.user_id.in_(user_ids)) subs = (await db.execute(q)).scalars().all() for sub in subs: info = { "endpoint": sub.endpoint, "keys": { "p256dh": sub.p256dh, "auth": sub.auth, }, } ok = await send_push(info, title, body, url) if ok: sent += 1 else: failed += 1 except Exception as e: logger.error("브로드캐스트 푸시 오류: %s", e) return {"sent": sent, "failed": failed}