| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- '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<Map<string, number>>(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 (
- <div className='crypto-sidebar'>
- <div className='sidebar-tabs'>
- {QUOTE_TABS.map((tab) => (
- <button
- key={tab}
- type='button'
- className={`tab ${quoteMarket === tab ? 'active' : ''}`}
- onClick={() => setQuoteMarket(tab)}
- >
- {tab}
- </button>
- ))}
- </div>
- <div className='sidebar-search'>
- <input
- type='text'
- placeholder='코인 검색...'
- value={search}
- onChange={(e) => setSearch(e.target.value)}
- />
- </div>
- <div className='sidebar-list'>
- {sortedTickers.map((ticker) => {
- const tickerMeta = meta.get(ticker.market);
- return (
- <button
- key={ticker.market}
- type='button'
- className={`sidebar-item ${selectedMarket === ticker.market ? 'active' : ''}`}
- data-market={ticker.market}
- onClick={() => setSelectedMarket(ticker.market)}
- >
- <div className='item-info'>
- <span className='item-kor-name'>{tickerMeta?.korName ?? ticker.symbol}</span>
- <span className='item-symbol'>{ticker.symbol}</span>
- </div>
- <div className='item-price'>
- <span className={`price ${getChangeClass(ticker.change)}`}>
- {formatPrice(ticker.tradePrice)}
- </span>
- <span className={`change ${getChangeClass(ticker.change)}`}>
- {formatChangeRate(ticker.signedChangeRate)}
- </span>
- </div>
- </button>
- );
- })}
- {sortedTickers.length === 0 && (
- <div className='sidebar-empty'>검색 결과가 없습니다.</div>
- )}
- </div>
- </div>
- );
- }
|