| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- 'use client';
- import { useEffect, useRef, useState, useCallback } from 'react';
- import { useCryptoContext } from '@/contexts/cryptoProvider';
- import './market-header.scss';
- 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 formatVolume(volume: number): string {
- if (volume >= 1_000_000_000) {
- return `${(volume / 1_000_000_000).toFixed(1)}B`;
- }
- if (volume >= 1_000_000) {
- return `${(volume / 1_000_000).toFixed(1)}M`;
- }
- if (volume >= 1_000) {
- return `${(volume / 1_000).toFixed(1)}K`;
- }
- return volume.toFixed(2);
- }
- const MAX_SPARKLINE_POINTS = 60;
- function VolumeSparkline({ value, market }: { value: number; market: string }) {
- const historyRef = useRef<number[]>([]);
- const prevMarketRef = useRef(market);
- const [open, setOpen] = useState(false);
- const wrapperRef = useRef<HTMLDivElement>(null);
- // 마켓 변경 시 히스토리 초기화
- if (market !== prevMarketRef.current) {
- historyRef.current = [];
- prevMarketRef.current = market;
- }
- useEffect(() => {
- if (value <= 0) return;
- const h = historyRef.current;
- h.push(value);
- if (h.length > MAX_SPARKLINE_POINTS) {
- h.shift();
- }
- }, [value]);
- // 외부 클릭 시 닫기
- const handleClickOutside = useCallback((e: MouseEvent) => {
- if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
- setOpen(false);
- }
- }, []);
- useEffect(() => {
- if (open) {
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }
- }, [open, handleClickOutside]);
- const history = historyRef.current;
- if (history.length < 2) return null;
- const min = Math.min(...history);
- const max = Math.max(...history);
- const range = max - min || 1;
- const w = 80;
- const h = 20;
- const points = history.map((v, i) => {
- const x = (i / (history.length - 1)) * w;
- const y = h - ((v - min) / range) * h;
- return `${x.toFixed(1)},${y.toFixed(1)}`;
- }).join(' ');
- const isUp = history[history.length - 1] >= history[history.length - 2];
- const color = isUp ? 'hsl(var(--crypto-up))' : 'hsl(var(--crypto-down))';
- return (
- <div className='volume-sparkline-wrapper' ref={wrapperRef}>
- <svg
- className='volume-sparkline'
- width={w}
- height={h}
- viewBox={`0 0 ${w} ${h}`}
- onClick={() => setOpen((prev) => !prev)}
- >
- <polyline
- points={points}
- fill='none'
- stroke={color}
- strokeWidth='1.5'
- strokeLinejoin='round'
- strokeLinecap='round'
- />
- </svg>
- {open && (
- <div className='volume-popover'>
- <span className='volume-popover-label'>거래량(24h)</span>
- <span className='volume-popover-value'>{formatVolume(value)} KRW</span>
- </div>
- )}
- </div>
- );
- }
- export default function MarketHeader() {
- const { selectedMarket, tickers } = useCryptoContext();
- const ticker = tickers.get(selectedMarket);
- if (!ticker) {
- return (
- <div className='market-header'>
- <div className='market-header-loading'>준비 중...</div>
- </div>
- );
- }
- const changeClass = ticker.change === 'RISE' ? 'up' : ticker.change === 'FALL' ? 'down' : 'neutral';
- const changeRate = (ticker.signedChangeRate * 100).toFixed(2);
- const changeSign = ticker.signedChangePrice >= 0 ? '+' : '';
- return (
- <div className='market-header'>
- <div className='market-title'>
- <span className='symbol'>{ticker.symbol}</span>
- <span className='pair'>/{selectedMarket.split('-')[0]}</span>
- </div>
- <div className={`market-price ${changeClass}`}>
- <span className='current-price'>{formatPrice(ticker.tradePrice)}</span>
- </div>
- <div className={`market-change ${changeClass}`}>
- <span>{changeSign}{formatPrice(ticker.signedChangePrice)}</span>
- <span>({changeSign}{changeRate}%)</span>
- </div>
- <div className='market-stats'>
- <div className='stat'>
- <span className='label'>고가</span>
- <span className='value up'>{formatPrice(ticker.highPrice)}</span>
- </div>
- <div className='stat'>
- <span className='label'>저가</span>
- <span className='value down'>{formatPrice(ticker.lowPrice)}</span>
- </div>
- <div className='stat volume-stat'>
- <span className='label'>거래량(24h)</span>
- <VolumeSparkline value={ticker.accTradePrice24h} market={selectedMarket} />
- </div>
- </div>
- </div>
- );
- }
|