"""미래 준비 — 모바일 푸시알림 디바이스 등록·발송·이력.""" from __future__ import annotations import logging from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy import select, desc from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user, require_admin_role from database import get_db from models import User, PushDevice, PushLog logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/push", tags=["미래준비-푸시알림"]) class DeviceRegister(BaseModel): token: str platform: str = "android" class PushSend(BaseModel): title: str body: str category: str = "general" user_ids: list[int] = [] async def _send_to_device(device: PushDevice, title: str, body: str, category: str) -> bool: """FCM/Expo 푸시 발송 (온프레미스: 메신저 WebSocket으로 대체).""" try: import httpx async with httpx.AsyncClient(timeout=5) as c: r = await c.post("http://127.0.0.1:9001/api/messenger/push", json={ "token": device.token, "platform": device.platform, "title": title, "body": body, "category": category, }) return r.status_code == 200 except Exception as e: logger.warning("푸시 발송 실패 device=%s: %s", device.id, e) return False @router.post("/register") async def register_device( req: DeviceRegister, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """디바이스 토큰 등록 (중복이면 갱신).""" existing = await db.execute( select(PushDevice).where(PushDevice.token == req.token).limit(1) ) device = existing.scalar_one_or_none() if device: device.active = True device.last_used_at = datetime.utcnow() else: device = PushDevice( user_id=user.id, token=req.token, platform=req.platform, registered_at=datetime.utcnow(), ) db.add(device) await db.commit() await db.refresh(device) return {"ok": True, "device_id": device.id} @router.delete("/register/{token}") async def unregister_device( token: str, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """디바이스 토큰 비활성화.""" row = await db.execute( select(PushDevice).where(PushDevice.token == token, PushDevice.user_id == user.id).limit(1) ) device = row.scalar_one_or_none() if not device: raise HTTPException(404, "디바이스 없음") device.active = False await db.commit() return {"ok": True} @router.post("/send") async def send_push( req: PushSend, db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role), ): """관리자 푸시알림 발송.""" q = select(PushDevice).where(PushDevice.active == True) if req.user_ids: q = q.where(PushDevice.user_id.in_(req.user_ids)) rows = await db.execute(q) devices = rows.scalars().all() sent = 0 for device in devices: success = await _send_to_device(device, req.title, req.body, req.category) log = PushLog( device_id=device.id, title=req.title, body=req.body, category=req.category, success=success, sent_at=datetime.utcnow(), ) db.add(log) if success: sent += 1 await db.commit() return {"ok": True, "sent": sent, "total": len(devices)} @router.get("/logs") async def push_logs( limit: int = 50, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): """푸시 발송 이력.""" rows = await db.execute( select(PushLog).order_by(desc(PushLog.sent_at)).limit(limit) ) return [{ "id": l.id, "title": l.title, "body": l.body[:100], "category": l.category, "success": l.success, "sent_at": l.sent_at, } for l in rows.scalars().all()] @router.get("/devices") async def list_devices( db: AsyncSession = Depends(get_db), user: User = Depends(require_admin_role), ): """등록된 디바이스 목록.""" rows = await db.execute( select(PushDevice).where(PushDevice.active == True).order_by(desc(PushDevice.registered_at)) ) return [{ "id": d.id, "user_id": d.user_id, "platform": d.platform, "registered_at": d.registered_at, "last_used_at": d.last_used_at, } for d in rows.scalars().all()]