import json from pywebpush import WebPushException, webpush from sqlalchemy import select from sqlalchemy.orm import Session from app.config import settings from app.models.push_subscription import PushSubscription from app.schemas.push import PushSubscribeRequest def list_subscriptions(db: Session, user_id: int) -> list[PushSubscription]: return list(db.scalars(select(PushSubscription).where(PushSubscription.user_id == user_id))) def save_subscription( db: Session, user_id: int, data: PushSubscribeRequest, user_agent: str | None = None ) -> PushSubscription: existing = db.scalar(select(PushSubscription).where(PushSubscription.endpoint == data.endpoint)) if existing: existing.user_id = user_id # 같은 기기에서 다른 유저가 재구독하면 소유자를 갱신한다. existing.p256dh_key = data.keys.p256dh existing.auth_key = data.keys.auth db.commit() return existing sub = PushSubscription( user_id=user_id, endpoint=data.endpoint, p256dh_key=data.keys.p256dh, auth_key=data.keys.auth, user_agent=user_agent, ) db.add(sub) db.commit() db.refresh(sub) return sub def delete_subscription(db: Session, endpoint: str) -> None: existing = db.scalar(select(PushSubscription).where(PushSubscription.endpoint == endpoint)) if existing: db.delete(existing) db.commit() def send_to_user(db: Session, user_id: int, title: str, body: str, url: str = "/today") -> int: """해당 유저의 구독자에게만 알림을 보낸다. 만료된(410/404) 구독은 자동으로 삭제한다. 성공 발송 건수를 반환.""" payload = json.dumps({"title": title, "body": body, "url": url}, ensure_ascii=False) sent = 0 for sub in list_subscriptions(db, user_id): try: webpush( subscription_info={ "endpoint": sub.endpoint, "keys": {"p256dh": sub.p256dh_key, "auth": sub.auth_key}, }, data=payload, vapid_private_key=settings.vapid_private_key, vapid_claims={"sub": settings.vapid_subject}, ) sent += 1 except WebPushException as exc: status_code = exc.response.status_code if exc.response is not None else None if status_code in (404, 410): db.delete(sub) db.commit() # 그 외 오류(일시적 네트워크 문제 등)는 건너뛰고 다음 구독자에게 계속 발송한다. return sent