'use client'; import { useState, useMemo, useEffect, useRef } from 'react'; import { useCryptoContext } from '@/contexts/cryptoProvider'; import useTickers from '@/hooks/useTickers'; import type { TickerRestData } from '@/types/crypto'; import './sidebar.scss'; const QUOTE_TABS = ['KRW', 'BTC', 'USDT'] as const; type Props = { initialTickers?: TickerRestData[]; }; function formatPrice(price: number): string { if (price >= 1000) { return price.toLocaleString('ko-KR', { maximumFractionDigits: 0 }); } if (price >= 1) { return price.toLocaleString('ko-KR', { maximumFractionDigits: 2 }); } return price.toLocaleString('ko-KR', { maximumFractionDigits: 4 }); } function formatChangeRate(rate: number): string { const pct = (rate * 100).toFixed(2); return rate >= 0 ? `+${pct}%` : `${pct}%`; } function getChangeClass(change: string): string { if (change === 'RISE') return 'up'; if (change === 'FALL') return 'down'; return 'neutral'; } export default function CryptoSidebar({ initialTickers }: Props) { const { selectedMarket, setSelectedMarket, quoteMarket, setQuoteMarket, setTickers: setContextTickers, setTickerMeta: setContextMeta } = useCryptoContext(); const { tickers, meta } = useTickers(quoteMarket, quoteMarket === 'KRW' ? initialTickers : undefined); const [search, setSearch] = useState(''); const prevPricesRef = useRef>(new Map()); // tickers를 context에 공유 useEffect(() => { setContextTickers(tickers); }, [tickers, setContextTickers]); useEffect(() => { setContextMeta(meta); }, [meta, setContextMeta]); const sortedTickers = useMemo(() => { const arr = Array.from(tickers.values()); const keyword = search.toLowerCase().trim(); const filtered = keyword ? arr.filter((t) => { const m = meta.get(t.market); return t.symbol.toLowerCase().includes(keyword) || t.market.toLowerCase().includes(keyword) || (m?.korName && m.korName.includes(keyword)); }) : arr; return filtered.sort((a, b) => b.accTradePrice24h - a.accTradePrice24h); }, [tickers, search, meta]); // 시세 변동 보더 플래시 애니메이션 useEffect(() => { for (const [market, ticker] of tickers) { const prev = prevPricesRef.current.get(market); if (prev !== undefined && prev !== ticker.tradePrice) { const direction = ticker.tradePrice > prev ? 'up' : 'down'; const el = document.querySelector(`[data-market="${market}"]`) as HTMLElement | null; if (el) { el.classList.remove('flash-up', 'flash-down'); void el.offsetWidth; // force reflow el.classList.add(`flash-${direction}`); } } prevPricesRef.current.set(market, ticker.tradePrice); } }, [tickers]); return (
{QUOTE_TABS.map((tab) => ( ))}
setSearch(e.target.value)} />
{sortedTickers.map((ticker) => { const tickerMeta = meta.get(ticker.market); return ( ); })} {sortedTickers.length === 0 && (
검색 결과가 없습니다.
)}
); }