TradeHistory.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. 'use client';
  2. import { useEffect, useRef } from 'react';
  3. import { useCryptoContext } from '@/contexts/cryptoProvider';
  4. import useTrades from '@/hooks/useTrades';
  5. import './trade-history.scss';
  6. interface TradeSoundHandle {
  7. playTradeSound: (side: 'BID' | 'ASK', tradeVolume?: number) => void;
  8. muted: boolean;
  9. toggleMute: () => void;
  10. }
  11. interface TradeHistoryProps {
  12. tradeSound: TradeSoundHandle;
  13. }
  14. function formatPrice(price: number): string {
  15. if (price >= 1000) {
  16. return price.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
  17. }
  18. if (price >= 1) {
  19. return price.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
  20. }
  21. return price.toLocaleString('ko-KR', { maximumFractionDigits: 4 });
  22. }
  23. function formatSize(size: number): string {
  24. if (size >= 1) {
  25. return size.toFixed(4);
  26. }
  27. return size.toFixed(6);
  28. }
  29. function formatTime(timestamp: number): string {
  30. const date = new Date(timestamp);
  31. return date.toLocaleTimeString('ko-KR', {
  32. hour: '2-digit',
  33. minute: '2-digit',
  34. second: '2-digit',
  35. hour12: false,
  36. });
  37. }
  38. export default function TradeHistory({ tradeSound }: TradeHistoryProps) {
  39. const { selectedMarket, tickers } = useCryptoContext();
  40. const trades = useTrades(selectedMarket);
  41. const ticker = tickers.get(selectedMarket);
  42. const { playTradeSound, muted, toggleMute } = tradeSound;
  43. const prevTradeIdRef = useRef<number>(0);
  44. // 체결 시 효과음 재생
  45. useEffect(() => {
  46. if (trades.length === 0) return;
  47. const latest = trades[0];
  48. if (latest.sequentialId !== prevTradeIdRef.current && prevTradeIdRef.current !== 0) {
  49. playTradeSound(latest.askBid as 'BID' | 'ASK', latest.tradeVolume);
  50. }
  51. prevTradeIdRef.current = latest.sequentialId;
  52. }, [trades, playTradeSound]);
  53. // 체결 강도 계산: 매수 비율 (%)
  54. let tradeIntensity: number | null = null;
  55. let intensityClass = '';
  56. if (ticker) {
  57. const total = ticker.accAskVolume + ticker.accBidVolume;
  58. if (total > 0) {
  59. tradeIntensity = (ticker.accBidVolume / total) * 100;
  60. intensityClass = tradeIntensity >= 50 ? 'up' : 'down';
  61. }
  62. }
  63. return (
  64. <div className='trade-history'>
  65. <div className='trade-title'>
  66. <span className='trade-title-left'>
  67. <span>체결 내역</span>
  68. <button
  69. className={`sound-toggle ${muted ? 'muted' : 'active'}`}
  70. onClick={toggleMute}
  71. title={muted ? '효과음 켜기' : '효과음 끄기'}
  72. >
  73. {muted ? '🔇' : '🔊'}
  74. </button>
  75. </span>
  76. {tradeIntensity !== null && (
  77. <span className={`trade-intensity ${intensityClass}`}>
  78. 체결강도 {tradeIntensity.toFixed(1)}%
  79. </span>
  80. )}
  81. </div>
  82. <div className='trade-header'>
  83. <span>체결가</span>
  84. <span>체결량</span>
  85. <span>시간</span>
  86. </div>
  87. <div className='trade-body'>
  88. {trades.length === 0 ? (
  89. <div className='trade-loading'>준비 중...</div>
  90. ) : (
  91. trades.map((trade, i) => (
  92. <div
  93. key={`${trade.sequentialId}-${i}`}
  94. className={`trade-row ${trade.askBid === 'BID' ? 'buy' : 'sell'}`}
  95. >
  96. <span className='price'>{formatPrice(trade.tradePrice)}</span>
  97. <span className='size'>{formatSize(trade.tradeVolume)}</span>
  98. <span className='time'>{formatTime(trade.tradeTimestamp)}</span>
  99. </div>
  100. ))
  101. )}
  102. </div>
  103. </div>
  104. );
  105. }