매매 시그널 알림 화면 수정
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 매매 시그널 알림 행 — 캔들·지표 미니 차트 카드
|
||||
*/
|
||||
import React, {
|
||||
useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
} from 'react';
|
||||
import TradingChart from '../TradingChart';
|
||||
import type { IndicatorConfig } from '../../types';
|
||||
import type {
|
||||
ChartMode, ChartType, Drawing, LegendData, OHLCVBar, Theme, Timeframe,
|
||||
} from '../../types';
|
||||
import { isUpbitMarket } from '../../utils/upbitApi';
|
||||
import { useChartRealtimeData } from '../../hooks/useChartRealtimeData';
|
||||
import { useHistoryLoader } from '../../hooks/useHistoryLoader';
|
||||
import type { ChartManager } from '../../utils/ChartManager';
|
||||
import { pinCandleWatch } from '../../utils/backendApi';
|
||||
import { timeframeToCandleType } from '../../utils/chartCandleType';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
timeframe: Timeframe;
|
||||
theme?: Theme;
|
||||
indicators?: IndicatorConfig[];
|
||||
enabled?: boolean;
|
||||
/** 전체보기 모드 — 부모 높이에 맞춤 */
|
||||
fillHeight?: boolean;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
market,
|
||||
timeframe,
|
||||
theme = 'dark',
|
||||
indicators = [],
|
||||
enabled = true,
|
||||
fillHeight = false,
|
||||
}) => {
|
||||
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 wrap = canvasWrapRef.current;
|
||||
if (wrap && wrap.clientHeight > 0) mgr.resetPaneHeights(wrap.clientHeight);
|
||||
}, []);
|
||||
|
||||
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,
|
||||
timeframe,
|
||||
useMemo(() => ({
|
||||
onTickUpdate: handleTickUpdate,
|
||||
onNewCandle: handleNewCandle,
|
||||
enabled: enabled && useUpbit,
|
||||
source: 'BACKEND_STOMP' as const,
|
||||
}), [handleTickUpdate, handleNewCandle, enabled, useUpbit]),
|
||||
);
|
||||
|
||||
const { isLoadingMore } = useHistoryLoader({
|
||||
symbol: market,
|
||||
timeframe,
|
||||
isUpbit: useUpbit && enabled,
|
||||
managerRef,
|
||||
});
|
||||
|
||||
barsMarketRef.current = barsMarket;
|
||||
const barsReady = bars.length >= 2 && barsMarket === market;
|
||||
|
||||
const applyPaneHeights = useCallback(() => {
|
||||
const wrap = canvasWrapRef.current;
|
||||
const mgr = managerRef.current;
|
||||
if (!wrap || wrap.clientHeight <= 0 || !mgr?.hasMainSeries()) return;
|
||||
mgr.resetPaneHeights(wrap.clientHeight);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chartLiveReadyRef.current = false;
|
||||
pendingBarRef.current = null;
|
||||
chartReloadTriggeredRef.current = false;
|
||||
}, [market, timeframe, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const wrap = canvasWrapRef.current;
|
||||
if (!wrap) return;
|
||||
|
||||
const timers = [200, 600, 1200].map(delay =>
|
||||
setTimeout(() => {
|
||||
if (!canvasWrapRef.current) return;
|
||||
const h = canvasWrapRef.current.clientHeight;
|
||||
if (h <= 0) return;
|
||||
if (managerRef.current?.hasMainSeries()) {
|
||||
managerRef.current.resetPaneHeights(h);
|
||||
} else if (!chartReloadTriggeredRef.current && delay >= 600 && barsReady) {
|
||||
chartReloadTriggeredRef.current = true;
|
||||
setChartReloadTick(t => t + 1);
|
||||
}
|
||||
}, delay),
|
||||
);
|
||||
|
||||
const ro = new ResizeObserver(() => applyPaneHeights());
|
||||
ro.observe(wrap);
|
||||
return () => {
|
||||
timers.forEach(clearTimeout);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, [market, timeframe, enabled, barsReady, applyPaneHeights, fillHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !barsReady) return;
|
||||
if (!managerRef.current?.hasMainSeries() && !chartReloadTriggeredRef.current) {
|
||||
chartReloadTriggeredRef.current = true;
|
||||
setChartReloadTick(t => t + 1);
|
||||
}
|
||||
}, [enabled, barsReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const t = setTimeout(() => applyPaneHeights(), 80);
|
||||
return () => clearTimeout(t);
|
||||
}, [fillHeight, enabled, applyPaneHeights]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !useUpbit) return;
|
||||
void pinCandleWatch(market, timeframeToCandleType(timeframe));
|
||||
if (timeframeToCandleType(timeframe) !== '1m') {
|
||||
void pinCandleWatch(market, '1m');
|
||||
}
|
||||
}, [market, timeframe, enabled, useUpbit]);
|
||||
|
||||
if (!enabled) {
|
||||
return (
|
||||
<div className={`tnl-mini-chart-placeholder${fillHeight ? ' tnl-mini-chart-placeholder--fill' : ''}`}>
|
||||
차트 대기
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={canvasWrapRef}
|
||||
className={[
|
||||
'tnl-mini-chart-canvas',
|
||||
'slot-chart-wrap',
|
||||
fillHeight ? 'tnl-mini-chart-canvas--fill' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{isLoading && <div className="tnl-mini-chart-loading">로딩…</div>}
|
||||
{isLoadingMore && (
|
||||
<div className="chart-history-loading">
|
||||
<div className="loading-spinner" style={{ width: 12, height: 12 }} />
|
||||
</div>
|
||||
)}
|
||||
<TradingChart
|
||||
key={`${market}-${timeframe}-${chartReloadTick}-${indicators.map(i => i.id).join(',')}`}
|
||||
chartVisible
|
||||
bars={bars}
|
||||
barsMarket={barsMarket}
|
||||
market={market}
|
||||
timeframe={timeframe}
|
||||
chartType={'candlestick' as ChartType}
|
||||
theme={theme}
|
||||
mode={'chart' as ChartMode}
|
||||
indicators={indicators}
|
||||
drawingTool="cursor"
|
||||
drawings={[] as Drawing[]}
|
||||
logScale={false}
|
||||
drawingsLocked
|
||||
drawingsVisible={false}
|
||||
onCrosshair={noop as (d: LegendData | null) => void}
|
||||
onManagerReady={mgr => { managerRef.current = mgr; }}
|
||||
onAddDrawing={noop as (d: Drawing) => void}
|
||||
onCandlesReady={handleCandlesReady}
|
||||
magnifierEnabled={false}
|
||||
volumeVisible={false}
|
||||
showHoverToolbar={false}
|
||||
seriesPriceLabelsEnabled={false}
|
||||
paneLayoutClamp
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeSignalMiniChart;
|
||||
Reference in New Issue
Block a user