'use client'; import { useEffect, useRef, useState, useCallback } from 'react'; import { useSignalRContext } from '@/contexts/signalrProvider'; import { fetchApi } from '@/lib/utils/client'; import type { TickerData, TickerRestData, TickerMeta } from '@/types/crypto'; function restToTickerData(t: TickerRestData): TickerData { return { market: t.market, symbol: t.symbol, openingPrice: t.openingPrice, highPrice: t.highPrice, lowPrice: t.lowPrice, tradePrice: t.tradePrice, prevClosingPrice: t.prevClosingPrice, change: t.change, changePrice: t.changePrice, signedChangePrice: t.signedChangePrice, changeRate: t.changeRate, signedChangeRate: t.signedChangeRate, tradeVolume: t.tradeVolume, accTradeVolume: t.accTradeVolume, accTradeVolume24h: t.accTradeVolume24h, accTradePrice: t.accTradePrice, accTradePrice24h: t.accTradePrice24h, tradeDate: '', tradeTime: '', tradeTimestamp: 0, askBid: '', accAskVolume: 0, accBidVolume: 0, highest52WeekPrice: 0, highest52WeekDate: '', lowest52WeekPrice: 0, lowest52WeekDate: '', marketState: 'ACTIVE', delistingDate: null, marketWarning: '', timestamp: 0, streamType: '', }; } function extractMeta(t: TickerRestData): TickerMeta { return { korName: t.korName, engName: t.engName, logoImage: t.logoImage, }; } export default function useTickers(quoteMarket: string = 'KRW', initialTickers?: TickerRestData[]) { const { cryptoConnection, cryptoConnected } = useSignalRContext(); const [tickers, setTickers] = useState>(new Map()); const [meta, setMeta] = useState>(new Map()); const tickersRef = useRef>(new Map()); const metaRef = useRef>(new Map()); const updateTimerRef = useRef | null>(null); const subscribedQuoteRef = useRef(null); const initializedRef = useRef(false); // 초기 REST 데이터 로드 useEffect(() => { // initialTickers는 KRW일 때만 사용 (서버에서 KRW로 패칭) if (!initializedRef.current && initialTickers && initialTickers.length > 0 && quoteMarket === 'KRW') { const tickerMap = new Map(); const metaMap = new Map(); for (const t of initialTickers) { tickerMap.set(t.market, restToTickerData(t)); metaMap.set(t.market, extractMeta(t)); } tickersRef.current = tickerMap; metaRef.current = metaMap; setTickers(new Map(tickerMap)); setMeta(new Map(metaMap)); initializedRef.current = true; } else { loadTickers(quoteMarket); } }, [quoteMarket]); const loadTickers = async (quote: string) => { try { const res = await fetchApi(`/api/crypto/tickers?quote=${quote}`); if (res.success && res.data) { const tickerMap = new Map(); const metaMap = new Map(); for (const t of res.data) { tickerMap.set(t.market, restToTickerData(t)); metaMap.set(t.market, extractMeta(t)); } tickersRef.current = tickerMap; metaRef.current = metaMap; setTickers(new Map(tickerMap)); setMeta(new Map(metaMap)); initializedRef.current = true; } } catch (error) { console.error('Failed to load tickers:', error); } }; // 배치 업데이트 (250ms 간격) const scheduleUpdate = useCallback(() => { if (updateTimerRef.current) { return; } updateTimerRef.current = setTimeout(() => { setTickers(new Map(tickersRef.current)); updateTimerRef.current = null; }, 250); }, []); // SignalR 구독 useEffect(() => { if (!cryptoConnection || !cryptoConnected) { return; } const handleTicker = (ticker: TickerData) => { tickersRef.current.set(ticker.market, ticker); scheduleUpdate(); }; const handleTickers = (list: TickerData[]) => { for (const ticker of list) { tickersRef.current.set(ticker.market, ticker); } scheduleUpdate(); }; // 이전 구독 해제 if (subscribedQuoteRef.current && subscribedQuoteRef.current !== quoteMarket) { cryptoConnection.invoke('UnsubscribeTickers', subscribedQuoteRef.current).catch(() => {}); } cryptoConnection.on('ReceiveTicker', handleTicker); cryptoConnection.on('ReceiveTickers', handleTickers); cryptoConnection.invoke('SubscribeTickers', quoteMarket).catch(console.error); subscribedQuoteRef.current = quoteMarket; return () => { cryptoConnection.off('ReceiveTicker', handleTicker); cryptoConnection.off('ReceiveTickers', handleTickers); if (subscribedQuoteRef.current) { cryptoConnection.invoke('UnsubscribeTickers', subscribedQuoteRef.current).catch(() => {}); subscribedQuoteRef.current = null; } if (updateTimerRef.current) { clearTimeout(updateTimerRef.current); updateTimerRef.current = null; } }; }, [cryptoConnection, cryptoConnected, quoteMarket, scheduleUpdate]); return { tickers, meta }; }