실시간 차트 우측 상단 주가, 등락율 현재가로 반영
This commit is contained in:
@@ -6,6 +6,11 @@ import type { WsStatus } from '../hooks/useUpbitData';
|
||||
import type { ChartMode, IndicatorConfig, LegendData } from '../types';
|
||||
import type { ChartLegendVisibility } from '../types/chartLegend';
|
||||
import { formatPrice } from '../utils/dataGenerator';
|
||||
import {
|
||||
formatChartLiveChangePct,
|
||||
formatChartLivePrice,
|
||||
type ChartLiveQuote,
|
||||
} from '../utils/chartLiveQuote';
|
||||
import { getIndicatorDef, getIndicatorChartTitle } from '../utils/indicatorRegistry';
|
||||
import { formatBbLegendLabel } from '../utils/bollingerConfig';
|
||||
|
||||
@@ -28,13 +33,10 @@ export interface ChartLegendBarProps {
|
||||
useUpbit: boolean;
|
||||
wsStatus: WsStatus;
|
||||
mode: ChartMode;
|
||||
/** 크로스헤어·OHLC 범례용 (없으면 quoteBar) */
|
||||
/** 크로스헤어 OHLC·지표 값 */
|
||||
currentBar: LegendData | null;
|
||||
/** 종목명 옆 시세·등락 — 실시간 현재가만 (크로스헤어 미반영) */
|
||||
quoteBar?: LegendData | null;
|
||||
isUp: boolean;
|
||||
dailyChange: number;
|
||||
dailyChangePct: number;
|
||||
/** 우측 현재가·등락 (티커/최신 봉, 크로스헤어 무관) */
|
||||
liveQuote: ChartLiveQuote;
|
||||
indicators: IndicatorConfig[];
|
||||
legend: LegendData | null;
|
||||
}
|
||||
@@ -46,86 +48,107 @@ export const ChartLegendBar: React.FC<ChartLegendBarProps> = ({
|
||||
useUpbit,
|
||||
wsStatus,
|
||||
mode,
|
||||
currentBar,
|
||||
quoteBar: quoteBarProp,
|
||||
isUp,
|
||||
dailyChange,
|
||||
dailyChangePct,
|
||||
currentBar: ohlcBar,
|
||||
liveQuote,
|
||||
indicators,
|
||||
legend,
|
||||
}) => {
|
||||
const ohlcBar = currentBar;
|
||||
const quoteBar = quoteBarProp ?? currentBar;
|
||||
const showOhlc = ohlcBar && (v.open || v.high || v.low || v.close || v.change);
|
||||
const ohlcUp = ohlcBar ? ohlcBar.close >= ohlcBar.open : true;
|
||||
const showOhlc = ohlcBar && (v.open || v.high || v.low || v.close);
|
||||
const showLiveQuote = (v.close || v.change) && liveQuote.price != null;
|
||||
const showAny =
|
||||
v.symbol || v.timeframe || (useUpbit && v.liveStatus) || (mode === 'trading' && v.tradingBadge)
|
||||
|| showOhlc || (v.indicators && indicators.length > 0);
|
||||
|| showLiveQuote || showOhlc || (v.indicators && indicators.length > 0);
|
||||
|
||||
if (!showAny) return null;
|
||||
|
||||
return (
|
||||
<div className="tv-legend">
|
||||
{v.symbol && (
|
||||
<span className="tv-legend-sym">
|
||||
<span className="tv-legend-dot" style={{ background: isUp ? 'var(--up)' : 'var(--down)' }} />
|
||||
{symbol}
|
||||
</span>
|
||||
)}
|
||||
{v.timeframe && <span className="tv-legend-tf">· {timeframe}</span>}
|
||||
{useUpbit && v.liveStatus && <WsBadge status={wsStatus} isUpbit={true} />}
|
||||
{mode === 'trading' && v.tradingBadge && <span className="tv-legend-badge trading">✏</span>}
|
||||
{showOhlc && (
|
||||
<span className="tv-legend-ohlcv">
|
||||
{v.open && ohlcBar && (
|
||||
<span>O <b className={isUp ? 'up' : 'down'}>{formatPrice(ohlcBar.open)}</b></span>
|
||||
)}
|
||||
{v.high && ohlcBar && (
|
||||
<span>H <b className="up">{formatPrice(ohlcBar.high)}</b></span>
|
||||
)}
|
||||
{v.low && ohlcBar && (
|
||||
<span>L <b className="down">{formatPrice(ohlcBar.low)}</b></span>
|
||||
)}
|
||||
{v.close && quoteBar && (
|
||||
<span>C <b className={isUp ? 'up' : 'down'}>{formatPrice(quoteBar.close)}</b></span>
|
||||
)}
|
||||
{v.change && (
|
||||
<span className={`tv-legend-chg ${isUp ? 'up' : 'down'}`}>
|
||||
{isUp ? '▲' : '▼'} {formatPrice(Math.abs(dailyChange))} ({Math.abs(dailyChangePct).toFixed(2)}%)
|
||||
<div className="tv-legend-top">
|
||||
<div className="tv-legend-meta">
|
||||
{useUpbit && v.liveStatus && <WsBadge status={wsStatus} isUpbit={true} />}
|
||||
{v.symbol && (
|
||||
<span className="tv-legend-sym">
|
||||
<span
|
||||
className="tv-legend-dot"
|
||||
style={{ background: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}
|
||||
/>
|
||||
{symbol}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{v.indicators && indicators.map(ind => {
|
||||
const def = getIndicatorDef(ind.type);
|
||||
const vals = legend?.indicatorValues?.[ind.type];
|
||||
const plots = ind.plots ?? def?.plots ?? [];
|
||||
const label = ind.type === 'BollingerBands'
|
||||
? formatBbLegendLabel(ind.params ?? {})
|
||||
: getIndicatorChartTitle(ind.type);
|
||||
const fmtVal = (n: number) =>
|
||||
Math.abs(n) >= 1000
|
||||
? n.toLocaleString('ko-KR', { maximumFractionDigits: 1 })
|
||||
: n.toFixed(2);
|
||||
return def ? (
|
||||
<span key={ind.id} className="tv-legend-ind">
|
||||
<span className="tv-legend-ind-name">{label}</span>
|
||||
{v.indicatorValues && vals && vals.length > 0 && (
|
||||
<span className="tv-legend-ind-vals">
|
||||
{vals.map((n, i) =>
|
||||
Number.isFinite(n) ? (
|
||||
<b
|
||||
key={i}
|
||||
style={{ color: (plots[i]?.color ?? def.plots?.[i]?.color ?? '#42A5F5').slice(0, 7) }}
|
||||
>
|
||||
{fmtVal(n)}
|
||||
</b>
|
||||
) : null,
|
||||
{showLiveQuote && (
|
||||
<div className="tv-legend-quote" aria-label="실시간 현재가">
|
||||
{v.close && (
|
||||
<span className={`tv-legend-quote-price ${liveQuote.isUp ? 'up' : 'down'}`}>
|
||||
{formatChartLivePrice(liveQuote.price)}
|
||||
</span>
|
||||
)}
|
||||
{v.change && liveQuote.changePct != null && (
|
||||
<span className={`tv-legend-quote-chg ${liveQuote.isUp ? 'up' : 'down'}`}>
|
||||
{formatChartLiveChangePct(liveQuote.changePct)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{v.timeframe && <span className="tv-legend-tf">· {timeframe}</span>}
|
||||
{mode === 'trading' && v.tradingBadge && (
|
||||
<span className="tv-legend-badge trading">✏</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(showOhlc || (v.indicators && indicators.length > 0)) && (
|
||||
<div className="tv-legend-bottom">
|
||||
{showOhlc && (
|
||||
<span className="tv-legend-ohlcv">
|
||||
{v.open && (
|
||||
<span>O <b className={ohlcUp ? 'up' : 'down'}>{formatPrice(ohlcBar!.open)}</b></span>
|
||||
)}
|
||||
{v.high && (
|
||||
<span>H <b className="up">{formatPrice(ohlcBar!.high)}</b></span>
|
||||
)}
|
||||
{v.low && (
|
||||
<span>L <b className="down">{formatPrice(ohlcBar!.low)}</b></span>
|
||||
)}
|
||||
{v.close && (
|
||||
<span>C <b className={ohlcUp ? 'up' : 'down'}>{formatPrice(ohlcBar!.close)}</b></span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{v.indicators && indicators.map(ind => {
|
||||
const def = getIndicatorDef(ind.type);
|
||||
const vals = legend?.indicatorValues?.[ind.type];
|
||||
const plots = ind.plots ?? def?.plots ?? [];
|
||||
const label = ind.type === 'BollingerBands'
|
||||
? formatBbLegendLabel(ind.params ?? {})
|
||||
: getIndicatorChartTitle(ind.type);
|
||||
const fmtVal = (n: number) =>
|
||||
Math.abs(n) >= 1000
|
||||
? n.toLocaleString('ko-KR', { maximumFractionDigits: 1 })
|
||||
: n.toFixed(2);
|
||||
return def ? (
|
||||
<span key={ind.id} className="tv-legend-ind">
|
||||
<span className="tv-legend-ind-name">{label}</span>
|
||||
{v.indicatorValues && vals && vals.length > 0 && (
|
||||
<span className="tv-legend-ind-vals">
|
||||
{vals.map((n, i) =>
|
||||
Number.isFinite(n) ? (
|
||||
<b
|
||||
key={i}
|
||||
style={{
|
||||
color: (plots[i]?.color ?? def.plots?.[i]?.color ?? '#42A5F5').slice(0, 7),
|
||||
}}
|
||||
>
|
||||
{fmtVal(n)}
|
||||
</b>
|
||||
) : null,
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : null;
|
||||
})}
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,12 @@ import MainChartSettingsModal from './MainChartSettingsModal';
|
||||
import { MarketSearchPanel } from './MarketSearchPanel';
|
||||
import { generateBars, formatPrice } from '../utils/dataGenerator';
|
||||
import { getKoreanName } from '../utils/marketNameCache';
|
||||
import type { TickerData } from '../hooks/useMarketTicker';
|
||||
import {
|
||||
formatChartLiveChangePct,
|
||||
formatChartLivePrice,
|
||||
resolveChartLiveQuote,
|
||||
} from '../utils/chartLiveQuote';
|
||||
import { DEFAULT_MAIN_CHART_STYLE } from '../utils/storage';
|
||||
import { enrichIndicatorConfig, getIndicatorDef } from '../utils/indicatorRegistry';
|
||||
import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from '../utils/smaConfig';
|
||||
@@ -180,6 +186,8 @@ export interface ChartSlotProps {
|
||||
chartVisible?: boolean;
|
||||
/** pane 구분선 */
|
||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||
/** 업비트 실시간 티커 맵 (슬롯 심볼별 현재가·등락) */
|
||||
marketTickers?: Map<string, TickerData>;
|
||||
}
|
||||
|
||||
const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot({
|
||||
@@ -204,6 +212,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
chartLiveReceiveHighlight = true,
|
||||
chartVisible = true,
|
||||
chartPaneSeparator,
|
||||
marketTickers,
|
||||
}, ref) {
|
||||
// ── 전역 지표 파라미터 + 시각 설정 (DB 동기화) ────────────────────────
|
||||
const { getParams, saveParams, getVisualConfig, saveVisual } = useIndicatorSettings();
|
||||
@@ -557,10 +566,11 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
? (latestBar ?? (bars.length > 0 ? bars[bars.length - 1] : null))
|
||||
: (bars.length > 0 ? bars[bars.length - 1] : null);
|
||||
const quoteBar = lastKnownBar;
|
||||
const isUp = quoteBar ? quoteBar.close >= quoteBar.open : true;
|
||||
const prevClose = bars.length > 1 ? bars[bars.length - 2].close : quoteBar?.open ?? 0;
|
||||
const dailyChange = quoteBar ? quoteBar.close - prevClose : 0;
|
||||
const dailyChangePct = prevClose > 0 ? dailyChange / prevClose * 100 : 0;
|
||||
const slotTicker = marketTickers?.get(symbol) ?? null;
|
||||
const liveQuote = useMemo(
|
||||
() => resolveChartLiveQuote(slotTicker, quoteBar, bars),
|
||||
[slotTicker, quoteBar, bars],
|
||||
);
|
||||
|
||||
const receiveSignal = useMemo(() => {
|
||||
if (!useUpbit || wsStatus !== 'connected' || !latestBar) return null;
|
||||
@@ -671,8 +681,8 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
}, [compactMode, onActivate, showMarket]);
|
||||
|
||||
const displayKoName = compactKoName(symbol);
|
||||
const trendUp = compactMode && quoteBar != null && dailyChangePct >= 0;
|
||||
const trendDown = compactMode && quoteBar != null && dailyChangePct < 0;
|
||||
const trendUp = compactMode && liveQuote.price != null && liveQuote.isUp;
|
||||
const trendDown = compactMode && liveQuote.price != null && !liveQuote.isUp;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -708,20 +718,22 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
<span className="slot-compact-name-text">{displayKoName}</span>
|
||||
</button>
|
||||
|
||||
{quoteBar && (
|
||||
{liveQuote.price != null && (
|
||||
<div className="slot-compact-quote" aria-label="현재가">
|
||||
<span
|
||||
className="slot-compact-price"
|
||||
style={{ color: isUp ? 'var(--up)' : 'var(--down)' }}
|
||||
style={{ color: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}
|
||||
>
|
||||
{formatPrice(quoteBar.close)}
|
||||
</span>
|
||||
<span
|
||||
className="slot-compact-change"
|
||||
style={{ color: isUp ? 'var(--up)' : 'var(--down)' }}
|
||||
>
|
||||
{isUp ? '+' : '-'}{Math.abs(dailyChangePct).toFixed(2)}%
|
||||
{formatChartLivePrice(liveQuote.price).replace(' KRW', '')}
|
||||
</span>
|
||||
{liveQuote.changePct != null && (
|
||||
<span
|
||||
className="slot-compact-change"
|
||||
style={{ color: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}
|
||||
>
|
||||
{formatChartLiveChangePct(liveQuote.changePct)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -772,12 +784,14 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{quoteBar && (
|
||||
<span className="slot-price" style={{ color: isUp ? 'var(--up)' : 'var(--down)' }}>
|
||||
{formatPrice(quoteBar.close)}
|
||||
<span className="slot-change" style={{ color: isUp ? 'var(--up)' : 'var(--down)' }}>
|
||||
{isUp ? '▲' : '▼'} {Math.abs(dailyChangePct).toFixed(2)}%
|
||||
</span>
|
||||
{liveQuote.price != null && (
|
||||
<span className="slot-price" style={{ color: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}>
|
||||
{formatChartLivePrice(liveQuote.price).replace(' KRW', '')}
|
||||
{liveQuote.changePct != null && (
|
||||
<span className="slot-change" style={{ color: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}>
|
||||
{formatChartLiveChangePct(liveQuote.changePct)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{useUpbit && <WsBadge status={wsStatus} isUpbit={true} />}
|
||||
|
||||
Reference in New Issue
Block a user