'use client'; import { useEffect, useMemo, useState } from 'react'; type OddsPayload = { matchId: string; market: string; bookmaker: string; odds: number; }; type MatchEventPayload = { matchId: string; type: 'goal' | 'yellow' | 'red' | 'substitution'; minute: number; detail: string; }; type WSMessage = | ({ eventType: 'odds'; payload: OddsPayload }) | ({ eventType: 'event'; payload: MatchEventPayload }) | Record; export function useLiveMatchData(matchId: string | null) { const [odds, setOdds] = useState([]); const [events, setEvents] = useState([]); const wsBase = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000/ws/matches'; useEffect(() => { if (!matchId) { return undefined; } let isMounted = true; const ws = new WebSocket(`${wsBase}/${matchId}`); ws.onopen = () => { if (!isMounted) return; ws.send(JSON.stringify({ action: 'subscribe', matchId })); }; ws.onmessage = (event) => { let message: WSMessage; try { message = JSON.parse(event.data); } catch { return; } if (message.eventType === 'odds') { const payload = message.payload as OddsPayload; setOdds((prev) => { const next = [...prev.filter((item) => item.market !== payload.market), payload]; return next; }); return; } if (message.eventType === 'event') { const payload = message.payload as MatchEventPayload; setEvents((prev) => [payload, ...prev].slice(0, 80)); } }; ws.onerror = () => { console.error('WebSocket 錯誤', matchId); }; return () => { isMounted = false; ws.close(1000); }; }, [matchId, wsBase]); const liveState = useMemo( () => ({ odds, events, }), [odds, events], ); return liveState; }