Files
goldenChart/frontend/src/components/virtual/VirtualTargetQuote.tsx
T
2026-05-29 00:46:20 +09:00

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;