/** * 업비트 최근 체결 (호가창 좌측 체결 목록용) */ import { useState, useEffect, useRef, useCallback } from 'react'; export interface RecentTrade { price: number; volume: number; side: 'ask' | 'bid'; // ask=매도체결, bid=매수체결 time: number; } const MAX_TRADES = 12; interface UpbitTradeRaw { market: string; trade_price: number; trade_volume: number; ask_bid: 'ASK' | 'BID'; timestamp: number; } interface UpbitTradeWs { type: string; code: string; trade_price: number; trade_volume: number; ask_bid: 'ASK' | 'BID'; timestamp: number; } export function useUpbitRecentTrades(market: string) { const [trades, setTrades] = useState([]); const [strength, setStrength] = useState(null); const wsRef = useRef(null); const mountedRef = useRef(true); const pushTrade = useCallback((t: RecentTrade) => { setTrades(prev => { const next = [t, ...prev].slice(0, MAX_TRADES); const buyVol = next.filter(x => x.side === 'bid').reduce((s, x) => s + x.volume, 0); const sellVol = next.filter(x => x.side === 'ask').reduce((s, x) => s + x.volume, 0); if (sellVol > 0) setStrength((buyVol / sellVol) * 100); else if (buyVol > 0) setStrength(100); return next; }); }, []); const fetchSnapshot = useCallback(async () => { try { const res = await fetch(`/upbit-api/v1/trades/ticks?market=${market}&count=${MAX_TRADES}`); if (!res.ok) return; const data: UpbitTradeRaw[] = await res.json(); const list: RecentTrade[] = data.map(d => ({ price: d.trade_price, volume: d.trade_volume, side: d.ask_bid === 'ASK' ? 'ask' : 'bid', time: d.timestamp, })); setTrades(list); const buyVol = list.filter(x => x.side === 'bid').reduce((s, x) => s + x.volume, 0); const sellVol = list.filter(x => x.side === 'ask').reduce((s, x) => s + x.volume, 0); if (sellVol > 0) setStrength((buyVol / sellVol) * 100); } catch { /* ignore */ } }, [market]); useEffect(() => { mountedRef.current = true; setTrades([]); setStrength(null); fetchSnapshot(); const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'; const ws = new WebSocket(`${proto}://${window.location.host}/upbit-ws`); ws.binaryType = 'arraybuffer'; wsRef.current = ws; ws.onopen = () => { ws.send(JSON.stringify([ { ticket: `tr-${market}-${Date.now()}` }, { type: 'trade', codes: [market] }, ])); }; ws.onmessage = async (e: MessageEvent) => { if (!mountedRef.current) return; try { let text: string; if (typeof e.data === 'string') text = e.data; else if (e.data instanceof ArrayBuffer) text = new TextDecoder().decode(e.data); else if (e.data instanceof Blob) text = await e.data.text(); else return; const msg: UpbitTradeWs = JSON.parse(text); if (msg.type === 'trade' && msg.code === market) { pushTrade({ price: msg.trade_price, volume: msg.trade_volume, side: msg.ask_bid === 'ASK' ? 'ask' : 'bid', time: msg.timestamp, }); } } catch { /* ignore */ } }; return () => { mountedRef.current = false; if (wsRef.current) { wsRef.current.onopen = null; wsRef.current.onmessage = null; wsRef.current.onclose = null; wsRef.current.close(); wsRef.current = null; } }; }, [market, fetchSnapshot, pushTrade]); return { trades, strength }; }