'use client'; import { useState, useMemo, useEffect, useRef, useCallback } from 'react'; import { useCryptoContext } from '@/contexts/cryptoProvider'; import useTickers from '@/hooks/useTickers'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCaretUp, faCaretDown } from '@fortawesome/free-solid-svg-icons'; import { formatPrice, formatChangeRate, getChangeClass, formatVolumeMillions } from '@/lib/utils/crypto'; import type { TickerRestData } from '@/types/crypto'; import './mobile-coin-list.scss'; const QUOTE_TABS = ['KRW', 'BTC', 'USDT'] as const; type SortKey = 'name' | 'price' | 'change' | 'volume'; type SortDir = 'asc' | 'desc'; type NameMode = 'kor' | 'eng'; type Props = { initialTickers?: TickerRestData[]; onSelectCoin: (market: string) => void; }; export default function MobileCoinList({ initialTickers, onSelectCoin }: 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 [sortKey, setSortKey] = useState(null); const [sortDir, setSortDir] = useState('desc'); const [nameMode, setNameMode] = useState('kor'); const prevPricesRef = useRef>(new Map()); const handleSort = useCallback((key: SortKey) => { if (key === 'name' && sortKey === 'name') { setNameMode((prev) => prev === 'kor' ? 'eng' : 'kor'); } else if (sortKey === key) { setSortDir((prev) => prev === 'desc' ? 'asc' : 'desc'); } else { setSortKey(key); setSortDir(key === 'name' ? 'asc' : 'desc'); } }, [sortKey]); 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; if (!sortKey) { return filtered.sort((a, b) => b.accTradePrice24h - a.accTradePrice24h); } const dir = sortDir === 'asc' ? 1 : -1; return filtered.sort((a, b) => { if (sortKey === 'name') { const aName = nameMode === 'kor' ? (meta.get(a.market)?.korName ?? a.symbol) : a.symbol; const bName = nameMode === 'kor' ? (meta.get(b.market)?.korName ?? b.symbol) : b.symbol; return dir * aName.localeCompare(bName, nameMode === 'kor' ? 'ko' : 'en'); } if (sortKey === 'price') { return dir * (a.tradePrice - b.tradePrice); } if (sortKey === 'change') { return dir * (a.signedChangeRate - b.signedChangeRate); } return dir * (a.accTradePrice24h - b.accTradePrice24h); }); }, [tickers, search, meta, sortKey, sortDir, nameMode]); // 시세 변동 보더 플래시 애니메이션 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-mcl-market="${market}"]`) as HTMLElement | null; if (el) { el.classList.remove('flash-up', 'flash-down'); void el.offsetWidth; el.classList.add(`flash-${direction}`); } } prevPricesRef.current.set(market, ticker.tradePrice); } }, [tickers]); const handleCoinClick = useCallback((market: string) => { setSelectedMarket(market); onSelectCoin(market); }, [setSelectedMarket, onSelectCoin]); return (
{QUOTE_TABS.map((tab) => ( ))}
setSearch(e.target.value)} />
{sortedTickers.map((ticker) => { const tickerMeta = meta.get(ticker.market); return ( ); })} {sortedTickers.length === 0 && (
검색 결과가 없습니다.
)}
); }