guardia-messenger/hooks/useAnomalyAlert.ts

70 lines
1.9 KiB
TypeScript

/**
* useAnomalyAlert (#22) — 이상 탐지 푸시 훅
*
* 1분마다 GET /api/metrics/anomalies?severity=high 폴링.
* 이전에 보지 못한 새 이상 감지 시 Alert.alert로 알림.
* (expo-notifications 플러그인 등록 없이 인앱 Alert 사용 → EAS 빌드 안전)
*/
import { useEffect, useRef, useState } from 'react'
import { Alert, AppState } from 'react-native'
import { API_BASE } from '../constants/Config'
import { authFetch } from '../utils/auth'
interface Anomaly {
id: number | string
title?: string
metric?: string
server?: string
severity?: string
detected_at?: string
}
const POLL_MS = 60_000
export function useAnomalyAlert(enabled = true) {
const seen = useRef<Set<string>>(new Set())
const [latest, setLatest] = useState<Anomaly[]>([])
useEffect(() => {
if (!enabled) return
let alive = true
async function poll() {
if (AppState.currentState !== 'active') return
try {
const res = await authFetch(`${API_BASE}/api/metrics/anomalies?severity=high`)
if (!res.ok) return
const d = await res.json()
const list: Anomaly[] = Array.isArray(d) ? d : d.items ?? d.anomalies ?? []
if (!alive) return
setLatest(list)
const fresh = list.filter(a => !seen.current.has(String(a.id)))
fresh.forEach(a => seen.current.add(String(a.id)))
if (fresh.length > 0) {
const first = fresh[0]
const more = fresh.length > 1 ? `${fresh.length - 1}` : ''
Alert.alert(
'⚠️ 이상 징후 감지',
`${first.title ?? first.metric ?? '메트릭 이상'}${first.server ? ` (${first.server})` : ''}${more}`,
)
}
} catch {
/* 폴링 실패 무시 */
}
}
poll()
const id = setInterval(poll, POLL_MS)
return () => {
alive = false
clearInterval(id)
}
}, [enabled])
return { latest }
}
export default useAnomalyAlert