guardia-itsm/routers/push_notify.py

147 lines
4.5 KiB
Python

"""미래 준비 — 모바일 푸시알림 디바이스 등록·발송·이력."""
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()]