추세검색 화면 적용
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 추세검색 카드 — 인라인 캔들 + RSI/MACD/BB
|
||||
*/
|
||||
import React, {
|
||||
useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
} from 'react';
|
||||
import TradingChart from '../TradingChart';
|
||||
import { pinCandleWatch } from '../../utils/backendApi';
|
||||
import type {
|
||||
ChartMode, ChartType, Drawing, LegendData, OHLCVBar, Theme, Timeframe,
|
||||
} from '../../types';
|
||||
import { isUpbitMarket } from '../../utils/upbitApi';
|
||||
import { useChartRealtimeData, type ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||
import { useHistoryLoader } from '../../hooks/useHistoryLoader';
|
||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||
import type { ChartManager } from '../../utils/ChartManager';
|
||||
import { timeframeToCandleType } from '../../utils/chartCandleType';
|
||||
import { enrichIndicatorConfig } from '../../utils/indicatorRegistry';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
timeframe: string;
|
||||
theme?: Theme;
|
||||
chartRealtimeSource?: ChartRealtimeSource;
|
||||
chartSeriesPriceLabels?: boolean;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
const CHART_INDICATOR_TYPES = ['BollingerBands', 'RSI', 'MACD'] as const;
|
||||
|
||||
function toChartTimeframe(tf: string): Timeframe {
|
||||
if (tf === '1d') return '1D';
|
||||
if (tf === '1w') return '1W';
|
||||
if (tf === '1M') return '1M';
|
||||
return tf as Timeframe;
|
||||
}
|
||||
|
||||
const TrendSearchCardChart: React.FC<Props> = ({
|
||||
market,
|
||||
timeframe,
|
||||
theme = 'dark',
|
||||
chartRealtimeSource = 'BACKEND_STOMP',
|
||||
chartSeriesPriceLabels = true,
|
||||
}) => {
|
||||
const chartTf = toChartTimeframe(timeframe);
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
|
||||
const indicators = useMemo(
|
||||
() => CHART_INDICATOR_TYPES.map(type => {
|
||||
const base = enrichIndicatorConfig({
|
||||
id: `${type}-tsd-card`,
|
||||
type,
|
||||
params: { ...getParams(type) },
|
||||
});
|
||||
return {
|
||||
...base,
|
||||
plots: getVisualConfig(type)?.plots ?? base.plots,
|
||||
};
|
||||
}),
|
||||
[getParams, getVisualConfig],
|
||||
);
|
||||
|
||||
const managerRef = useRef<ChartManager | null>(null);
|
||||
const canvasWrapRef = useRef<HTMLDivElement | null>(null);
|
||||
const [chartReloadTick, setChartReloadTick] = useState(0);
|
||||
const chartReloadTriggeredRef = useRef(false);
|
||||
const pendingBarRef = useRef<OHLCVBar | null>(null);
|
||||
const barsMarketRef = useRef<string | null>(null);
|
||||
const chartLiveReadyRef = useRef(false);
|
||||
const marketRef = useRef(market);
|
||||
marketRef.current = market;
|
||||
|
||||
const handleCandlesReady = useCallback(() => {
|
||||
chartLiveReadyRef.current = true;
|
||||
const pending = pendingBarRef.current;
|
||||
const mgr = managerRef.current;
|
||||
if (!pending || !mgr || barsMarketRef.current !== marketRef.current) return;
|
||||
pendingBarRef.current = null;
|
||||
mgr.updateBar(pending);
|
||||
}, []);
|
||||
|
||||
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
|
||||
if (barsMarketRef.current !== marketRef.current) return;
|
||||
if (!chartLiveReadyRef.current || !managerRef.current) {
|
||||
pendingBarRef.current = bar;
|
||||
return;
|
||||
}
|
||||
if (append) managerRef.current.appendBar(bar);
|
||||
else managerRef.current.updateBar(bar);
|
||||
}, []);
|
||||
|
||||
const handleTickUpdate = useCallback((bar: OHLCVBar) => applyRealtimeBar(bar, false), [applyRealtimeBar]);
|
||||
const handleNewCandle = useCallback((bar: OHLCVBar) => applyRealtimeBar(bar, true), [applyRealtimeBar]);
|
||||
|
||||
const useUpbit = isUpbitMarket(market);
|
||||
|
||||
const { bars, barsMarket, isLoading } = useChartRealtimeData(
|
||||
market,
|
||||
chartTf,
|
||||
useMemo(() => ({
|
||||
onTickUpdate: handleTickUpdate,
|
||||
onNewCandle: handleNewCandle,
|
||||
enabled: useUpbit,
|
||||
source: chartRealtimeSource,
|
||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
|
||||
);
|
||||
|
||||
const { isLoadingMore } = useHistoryLoader({
|
||||
symbol: market,
|
||||
timeframe: chartTf,
|
||||
isUpbit: useUpbit,
|
||||
managerRef,
|
||||
});
|
||||
|
||||
barsMarketRef.current = barsMarket;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chartLiveReadyRef.current = false;
|
||||
pendingBarRef.current = null;
|
||||
chartReloadTriggeredRef.current = false;
|
||||
}, [market, chartTf]);
|
||||
|
||||
useEffect(() => {
|
||||
const timers = [100, 300, 800, 1500].map(delay =>
|
||||
setTimeout(() => {
|
||||
const wrap = canvasWrapRef.current;
|
||||
const mgr = managerRef.current;
|
||||
if (!wrap) return;
|
||||
const h = wrap.clientHeight;
|
||||
if (h <= 0) return;
|
||||
if (mgr?.hasMainSeries()) mgr.resetPaneHeights(h);
|
||||
else if (!chartReloadTriggeredRef.current && delay >= 800 && bars.length > 0) {
|
||||
chartReloadTriggeredRef.current = true;
|
||||
setChartReloadTick(t => t + 1);
|
||||
}
|
||||
}, delay),
|
||||
);
|
||||
return () => timers.forEach(clearTimeout);
|
||||
}, [market, chartTf, bars.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useUpbit) return;
|
||||
void pinCandleWatch(market, timeframeToCandleType(chartTf));
|
||||
}, [market, chartTf, useUpbit]);
|
||||
|
||||
return (
|
||||
<div className="vtd-card-chart-wrap">
|
||||
<div ref={canvasWrapRef} className="vtd-card-chart slot-chart-wrap">
|
||||
{isLoading && <div className="vtd-card-chart-loading">차트 로딩…</div>}
|
||||
{isLoadingMore && (
|
||||
<div className="chart-history-loading">
|
||||
<div className="loading-spinner" style={{ width: 14, height: 14 }} />
|
||||
<span>과거 데이터…</span>
|
||||
</div>
|
||||
)}
|
||||
<TradingChart
|
||||
key={`${market}-${chartTf}-${chartReloadTick}`}
|
||||
bars={bars}
|
||||
barsMarket={barsMarket}
|
||||
market={market}
|
||||
timeframe={chartTf}
|
||||
chartType={'candlestick' as ChartType}
|
||||
theme={theme}
|
||||
mode={'chart' as ChartMode}
|
||||
indicators={indicators}
|
||||
drawingTool="cursor"
|
||||
drawings={[] as Drawing[]}
|
||||
logScale={false}
|
||||
drawingsLocked
|
||||
drawingsVisible={false}
|
||||
displayTimezone={DEFAULT_DISPLAY_TIMEZONE}
|
||||
onCrosshair={noop as (d: LegendData | null) => void}
|
||||
onManagerReady={mgr => { managerRef.current = mgr; }}
|
||||
onAddDrawing={noop as (d: Drawing) => void}
|
||||
onCandlesReady={handleCandlesReady}
|
||||
magnifierEnabled={false}
|
||||
volumeVisible
|
||||
showHoverToolbar={false}
|
||||
seriesPriceLabelsEnabled={chartSeriesPriceLabels}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrendSearchCardChart;
|
||||
Reference in New Issue
Block a user