89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
import React, { memo, useEffect, useRef, useState } from 'react';
|
|
import type { ChangeDir, TickerData } from '../../hooks/useMarketTicker';
|
|
import { coerceFiniteNumber, safePercentFromRate, safeToFixed } from '../../utils/safeFormat';
|
|
|
|
function fmtKrw(price: number | null | undefined | unknown): string {
|
|
const p = coerceFiniteNumber(price);
|
|
if (p == null) return '-';
|
|
if (p >= 1_000_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
|
|
if (p >= 1_000) return p.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
|
|
if (p >= 1) return p.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
|
|
if (p >= 0.01) return safeToFixed(p, 4, '-');
|
|
return safeToFixed(p, 8, '-');
|
|
}
|
|
|
|
function fmtPct(rate: number | null | undefined | unknown): string {
|
|
return safePercentFromRate(rate, 2).replace('—', '-');
|
|
}
|
|
|
|
const ChangeArrow = memo(function ChangeArrow({ change }: { change: ChangeDir }) {
|
|
if (change === 'RISE') return <span className="vtd-target-arrow vtd-target-arrow--rise">▲</span>;
|
|
if (change === 'FALL') return <span className="vtd-target-arrow vtd-target-arrow--fall">▼</span>;
|
|
return <span className="vtd-target-arrow vtd-target-arrow--even">─</span>;
|
|
});
|
|
|
|
interface Props {
|
|
market: string;
|
|
ticker?: TickerData;
|
|
/** 카드 메타 행 — 현재가만 간략 표시 */
|
|
compact?: boolean;
|
|
/** compact 시 "현재가" 라벨 표시 (기본 true) */
|
|
showPriceLabel?: boolean;
|
|
}
|
|
|
|
const VirtualTargetQuote: React.FC<Props> = ({ market, ticker, compact = false, showPriceLabel = true }) => {
|
|
const code = market.replace(/^KRW-/, '');
|
|
const change: ChangeDir = ticker?.change ?? 'EVEN';
|
|
const colorCls = change === 'RISE' ? 'vtd-target-up' : change === 'FALL' ? 'vtd-target-dn' : 'vtd-target-even';
|
|
|
|
const [flashClass, setFlashClass] = useState('');
|
|
const prevPriceRef = useRef<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
const price = ticker?.tradePrice ?? null;
|
|
if (price === null) return;
|
|
const prev = prevPriceRef.current;
|
|
if (prev !== null && prev !== price) {
|
|
const cls = price > prev ? 'vtd-target-quote--flash-rise' : 'vtd-target-quote--flash-fall';
|
|
setFlashClass(cls);
|
|
const t = setTimeout(() => setFlashClass(''), 700);
|
|
prevPriceRef.current = price;
|
|
return () => clearTimeout(t);
|
|
}
|
|
prevPriceRef.current = price;
|
|
}, [ticker?.tradePrice]);
|
|
|
|
if (compact) {
|
|
return (
|
|
<div className={`vtd-target-quote vtd-target-quote--compact ${flashClass}`.trim()} aria-label="현재가">
|
|
{showPriceLabel && <span className="vtd-card-price-label">현재가</span>}
|
|
<span className={`vtd-target-price ${colorCls}`}>
|
|
{ticker ? fmtKrw(ticker.tradePrice) : '-'}
|
|
</span>
|
|
<span className={`vtd-target-change ${colorCls}`}>
|
|
{ticker ? fmtPct(ticker.changeRate) : '-'}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`vtd-target-quote ${flashClass}`.trim()} aria-label="현재가 및 전일 대비">
|
|
<div className="vtd-target-quote-left">
|
|
<ChangeArrow change={change} />
|
|
<span className="vtd-target-quote-pair">{code}/KRW</span>
|
|
</div>
|
|
<div className="vtd-target-quote-right">
|
|
<span className={`vtd-target-price ${colorCls}`}>
|
|
{ticker ? fmtKrw(ticker.tradePrice) : '-'}
|
|
</span>
|
|
<span className={`vtd-target-change ${colorCls}`}>
|
|
{ticker ? fmtPct(ticker.changeRate) : '-'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default VirtualTargetQuote;
|