useTickers.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. 'use client';
  2. import { useEffect, useRef, useState, useCallback } from 'react';
  3. import { useSignalRContext } from '@/contexts/signalrProvider';
  4. import { fetchApi } from '@/lib/utils/client';
  5. import type { TickerData, TickerRestData, TickerMeta } from '@/types/crypto';
  6. function restToTickerData(t: TickerRestData): TickerData {
  7. return {
  8. market: t.market,
  9. symbol: t.symbol,
  10. openingPrice: t.openingPrice,
  11. highPrice: t.highPrice,
  12. lowPrice: t.lowPrice,
  13. tradePrice: t.tradePrice,
  14. prevClosingPrice: t.prevClosingPrice,
  15. change: t.change,
  16. changePrice: t.changePrice,
  17. signedChangePrice: t.signedChangePrice,
  18. changeRate: t.changeRate,
  19. signedChangeRate: t.signedChangeRate,
  20. tradeVolume: t.tradeVolume,
  21. accTradeVolume: t.accTradeVolume,
  22. accTradeVolume24h: t.accTradeVolume24h,
  23. accTradePrice: t.accTradePrice,
  24. accTradePrice24h: t.accTradePrice24h,
  25. tradeDate: '',
  26. tradeTime: '',
  27. tradeTimestamp: 0,
  28. askBid: '',
  29. accAskVolume: 0,
  30. accBidVolume: 0,
  31. highest52WeekPrice: 0,
  32. highest52WeekDate: '',
  33. lowest52WeekPrice: 0,
  34. lowest52WeekDate: '',
  35. marketState: 'ACTIVE',
  36. delistingDate: null,
  37. marketWarning: '',
  38. timestamp: 0,
  39. streamType: '',
  40. };
  41. }
  42. function extractMeta(t: TickerRestData): TickerMeta {
  43. return {
  44. korName: t.korName,
  45. engName: t.engName,
  46. logoImage: t.logoImage,
  47. };
  48. }
  49. export default function useTickers(quoteMarket: string = 'KRW', initialTickers?: TickerRestData[]) {
  50. const { cryptoConnection, cryptoConnected } = useSignalRContext();
  51. const [tickers, setTickers] = useState<Map<string, TickerData>>(new Map());
  52. const [meta, setMeta] = useState<Map<string, TickerMeta>>(new Map());
  53. const tickersRef = useRef<Map<string, TickerData>>(new Map());
  54. const metaRef = useRef<Map<string, TickerMeta>>(new Map());
  55. const updateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  56. const subscribedQuoteRef = useRef<string | null>(null);
  57. const initializedRef = useRef(false);
  58. // 초기 REST 데이터 로드
  59. useEffect(() => {
  60. // initialTickers는 KRW일 때만 사용 (서버에서 KRW로 패칭)
  61. if (!initializedRef.current && initialTickers && initialTickers.length > 0 && quoteMarket === 'KRW') {
  62. const tickerMap = new Map<string, TickerData>();
  63. const metaMap = new Map<string, TickerMeta>();
  64. for (const t of initialTickers) {
  65. tickerMap.set(t.market, restToTickerData(t));
  66. metaMap.set(t.market, extractMeta(t));
  67. }
  68. tickersRef.current = tickerMap;
  69. metaRef.current = metaMap;
  70. setTickers(new Map(tickerMap));
  71. setMeta(new Map(metaMap));
  72. initializedRef.current = true;
  73. } else {
  74. loadTickers(quoteMarket);
  75. }
  76. }, [quoteMarket]);
  77. const loadTickers = async (quote: string) => {
  78. try {
  79. const res = await fetchApi<TickerRestData[]>(`/api/crypto/tickers?quote=${quote}`);
  80. if (res.success && res.data) {
  81. const tickerMap = new Map<string, TickerData>();
  82. const metaMap = new Map<string, TickerMeta>();
  83. for (const t of res.data) {
  84. tickerMap.set(t.market, restToTickerData(t));
  85. metaMap.set(t.market, extractMeta(t));
  86. }
  87. tickersRef.current = tickerMap;
  88. metaRef.current = metaMap;
  89. setTickers(new Map(tickerMap));
  90. setMeta(new Map(metaMap));
  91. initializedRef.current = true;
  92. }
  93. } catch (error) {
  94. console.error('Failed to load tickers:', error);
  95. }
  96. };
  97. // 배치 업데이트 (250ms 간격)
  98. const scheduleUpdate = useCallback(() => {
  99. if (updateTimerRef.current) {
  100. return;
  101. }
  102. updateTimerRef.current = setTimeout(() => {
  103. setTickers(new Map(tickersRef.current));
  104. updateTimerRef.current = null;
  105. }, 250);
  106. }, []);
  107. // SignalR 구독
  108. useEffect(() => {
  109. if (!cryptoConnection || !cryptoConnected) {
  110. return;
  111. }
  112. const handleTicker = (ticker: TickerData) => {
  113. tickersRef.current.set(ticker.market, ticker);
  114. scheduleUpdate();
  115. };
  116. const handleTickers = (list: TickerData[]) => {
  117. for (const ticker of list) {
  118. tickersRef.current.set(ticker.market, ticker);
  119. }
  120. scheduleUpdate();
  121. };
  122. // 이전 구독 해제
  123. if (subscribedQuoteRef.current && subscribedQuoteRef.current !== quoteMarket) {
  124. cryptoConnection.invoke('UnsubscribeTickers', subscribedQuoteRef.current).catch(() => {});
  125. }
  126. cryptoConnection.on('ReceiveTicker', handleTicker);
  127. cryptoConnection.on('ReceiveTickers', handleTickers);
  128. cryptoConnection.invoke('SubscribeTickers', quoteMarket).catch(console.error);
  129. subscribedQuoteRef.current = quoteMarket;
  130. return () => {
  131. cryptoConnection.off('ReceiveTicker', handleTicker);
  132. cryptoConnection.off('ReceiveTickers', handleTickers);
  133. if (subscribedQuoteRef.current) {
  134. cryptoConnection.invoke('UnsubscribeTickers', subscribedQuoteRef.current).catch(() => {});
  135. subscribedQuoteRef.current = null;
  136. }
  137. if (updateTimerRef.current) {
  138. clearTimeout(updateTimerRef.current);
  139. updateTimerRef.current = null;
  140. }
  141. };
  142. }, [cryptoConnection, cryptoConnected, quoteMarket, scheduleUpdate]);
  143. return { tickers, meta };
  144. }