실시간 차트 오류수정
This commit is contained in:
+10
-2
@@ -983,6 +983,11 @@ function App() {
|
|||||||
const barsMarket = useUpbit ? upbitBarsMarket : symbol;
|
const barsMarket = useUpbit ? upbitBarsMarket : symbol;
|
||||||
barsMarketRef.current = barsMarket;
|
barsMarketRef.current = barsMarket;
|
||||||
|
|
||||||
|
/** 종목·타임프레임 전환 시 이전 틱 버퍼 폐기 */
|
||||||
|
useEffect(() => {
|
||||||
|
pendingRealtimeBarRef.current = null;
|
||||||
|
}, [symbol, timeframe]);
|
||||||
|
|
||||||
// Upbit 초기 로딩 완료 시 stats 업데이트
|
// Upbit 초기 로딩 완료 시 stats 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useUpbit && upbitBars.length > 0) {
|
if (useUpbit && upbitBars.length > 0) {
|
||||||
@@ -1092,8 +1097,11 @@ function App() {
|
|||||||
noAutoSave: !dbLoaded,
|
noAutoSave: !dbLoaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 로그인·로그아웃 시에만 차트 재마운트 (boot/ready·심볼 변경은 TradingChart effect로 처리) */
|
/** 로그인·로그아웃·종목·타임프레임 변경 시 차트 재마운트 (종목 전환 직후 stale 데이터·중단된 reload 방지) */
|
||||||
const chartMountKey = useMemo(() => String(sessionKey), [sessionKey]);
|
const chartMountKey = useMemo(
|
||||||
|
() => `${sessionKey}:${symbol}:${timeframe}`,
|
||||||
|
[sessionKey, symbol, timeframe],
|
||||||
|
);
|
||||||
|
|
||||||
// DB에 워크스페이스 없음(204) → appDefaults 레이아웃 적용 후 저장 허용
|
// DB에 워크스페이스 없음(204) → appDefaults 레이아웃 적용 후 저장 허용
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -380,6 +380,10 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
// 데이터 로딩 완료 후 차트가 여전히 비어 있으면 1회만 재마운트 (data + blank guard)
|
// 데이터 로딩 완료 후 차트가 여전히 비어 있으면 1회만 재마운트 (data + blank guard)
|
||||||
const [reloadTick, setReloadTick] = useState(0);
|
const [reloadTick, setReloadTick] = useState(0);
|
||||||
const reloadTriggeredRef = useRef(false); // 재마운트는 최대 1회
|
const reloadTriggeredRef = useRef(false); // 재마운트는 최대 1회
|
||||||
|
useEffect(() => {
|
||||||
|
reloadTriggeredRef.current = false;
|
||||||
|
pendingRealtimeBarRef.current = null;
|
||||||
|
}, [symbol, timeframe]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checks = [200, 500, 1000, 2000].map(delay =>
|
const checks = [200, 500, 1000, 2000].map(delay =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -626,7 +630,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<TradingChart
|
<TradingChart
|
||||||
key={reloadTick}
|
key={`${symbol}:${timeframe}:${reloadTick}`}
|
||||||
bars={bars}
|
bars={bars}
|
||||||
barsMarket={barsMarket}
|
barsMarket={barsMarket}
|
||||||
market={symbol}
|
market={symbol}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import type { OHLCVBar, ChartType, Theme, IndicatorConfig, LegendData, Drawing,
|
|||||||
import { ChartManager } from '../utils/ChartManager';
|
import { ChartManager } from '../utils/ChartManager';
|
||||||
import { setIndicatorChartContext } from '../utils/indicatorRegistry';
|
import { setIndicatorChartContext } from '../utils/indicatorRegistry';
|
||||||
import { DISPLAY_COUNT } from '../hooks/useUpbitData';
|
import { DISPLAY_COUNT } from '../hooks/useUpbitData';
|
||||||
|
|
||||||
|
/** Upbit 실시간 — 초기 히스토리가 이 개수 미만이면 setData 하지 않음 (종목 전환 직후 1~2봉만 로드되는 깨짐 방지) */
|
||||||
|
const MIN_BARS_FOR_FULL_RELOAD = Math.min(50, Math.floor(DISPLAY_COUNT / 4));
|
||||||
import DrawingCanvas, { hitTestDrawing } from './DrawingCanvas';
|
import DrawingCanvas, { hitTestDrawing } from './DrawingCanvas';
|
||||||
import PaneLegend, { type PaneLegendProps } from './PaneLegend';
|
import PaneLegend, { type PaneLegendProps } from './PaneLegend';
|
||||||
import ChartHoverToolbar from './ChartHoverToolbar';
|
import ChartHoverToolbar from './ChartHoverToolbar';
|
||||||
@@ -274,6 +277,25 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
inds: IndicatorConfig[],
|
inds: IndicatorConfig[],
|
||||||
) => Promise<void>>(async () => {});
|
) => Promise<void>>(async () => {});
|
||||||
|
|
||||||
|
const queueFullReload = useCallback(() => {
|
||||||
|
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
|
||||||
|
recoveryTimerRef.current = window.setTimeout(() => {
|
||||||
|
recoveryTimerRef.current = null;
|
||||||
|
const m = managerRef.current;
|
||||||
|
const barData = barsRef.current;
|
||||||
|
const inds = indicatorsRef.current;
|
||||||
|
if (!m || barData.length === 0) return;
|
||||||
|
if (barsMarket !== undefined && barsMarket !== market) return;
|
||||||
|
if (market.startsWith('KRW-') && barData.length < MIN_BARS_FOR_FULL_RELOAD) return;
|
||||||
|
prevBarsKey.current = '';
|
||||||
|
reloadAllRef.current?.(
|
||||||
|
m, barData,
|
||||||
|
prevChartType.current, prevTheme.current, prevLogScale.current,
|
||||||
|
inds,
|
||||||
|
);
|
||||||
|
}, 80);
|
||||||
|
}, [market, barsMarket]);
|
||||||
|
|
||||||
const queueIndicatorRecovery = useCallback(() => {
|
const queueIndicatorRecovery = useCallback(() => {
|
||||||
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
|
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
|
||||||
recoveryTimerRef.current = window.setTimeout(() => {
|
recoveryTimerRef.current = window.setTimeout(() => {
|
||||||
@@ -329,7 +351,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
mgr.setData(newBars, ct, th);
|
mgr.setData(newBars, ct, th);
|
||||||
if (gen !== reloadGenRef.current) {
|
if (gen !== reloadGenRef.current) {
|
||||||
prevBarsKey.current = '';
|
prevBarsKey.current = '';
|
||||||
queueIndicatorRecovery();
|
queueFullReload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mgr.setTheme(th);
|
mgr.setTheme(th);
|
||||||
@@ -339,7 +361,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
for (const ind of inds) {
|
for (const ind of inds) {
|
||||||
if (gen !== reloadGenRef.current) {
|
if (gen !== reloadGenRef.current) {
|
||||||
prevBarsKey.current = '';
|
prevBarsKey.current = '';
|
||||||
queueIndicatorRecovery();
|
queueFullReload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await mgr.addIndicator(ind);
|
await mgr.addIndicator(ind);
|
||||||
@@ -347,7 +369,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
|
|
||||||
if (gen !== reloadGenRef.current) {
|
if (gen !== reloadGenRef.current) {
|
||||||
prevBarsKey.current = '';
|
prevBarsKey.current = '';
|
||||||
queueIndicatorRecovery();
|
queueFullReload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +382,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
reloadRetryRef.current += 1;
|
reloadRetryRef.current += 1;
|
||||||
await mgr.reloadIndicatorsOnly(inds);
|
await mgr.reloadIndicatorsOnly(inds);
|
||||||
if (gen !== reloadGenRef.current) {
|
if (gen !== reloadGenRef.current) {
|
||||||
queueIndicatorRecovery();
|
queueFullReload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,7 +414,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
|
|
||||||
if (gen !== reloadGenRef.current) {
|
if (gen !== reloadGenRef.current) {
|
||||||
prevBarsKey.current = '';
|
prevBarsKey.current = '';
|
||||||
queueIndicatorRecovery();
|
queueFullReload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +438,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
if (delay === 1400) onDataLoaded?.();
|
if (delay === 1400) onDataLoaded?.();
|
||||||
}, delay)
|
}, delay)
|
||||||
);
|
);
|
||||||
}, [applyPaneLayout, onDataLoaded, displayTimezone, timeframe, market, queueIndicatorRecovery]);
|
}, [applyPaneLayout, onDataLoaded, displayTimezone, timeframe, market, queueFullReload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadAllRef.current = reloadAll;
|
reloadAllRef.current = reloadAll;
|
||||||
@@ -740,6 +762,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
? true
|
? true
|
||||||
: barsMarket === market;
|
: barsMarket === market;
|
||||||
if (!barsReady) return;
|
if (!barsReady) return;
|
||||||
|
if (market.startsWith('KRW-') && bars.length < MIN_BARS_FOR_FULL_RELOAD) return;
|
||||||
|
|
||||||
// manager 준비 전 bars 가 먼저 도착한 경우(로그인 직후 등) 메인 시리즈 누락 방지
|
// manager 준비 전 bars 가 먼저 도착한 경우(로그인 직후 등) 메인 시리즈 누락 방지
|
||||||
if (!mgr.hasMainSeries()) {
|
if (!mgr.hasMainSeries()) {
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export function useStompChartData(
|
|||||||
}, [market, candleType, opts.enabled]);
|
}, [market, candleType, opts.enabled]);
|
||||||
|
|
||||||
const applyCandle = useCallback((data: StompCandlePayload, displayTf: Timeframe) => {
|
const applyCandle = useCallback((data: StompCandlePayload, displayTf: Timeframe) => {
|
||||||
|
if (!historyReadyRef.current) return;
|
||||||
const oneMin: OHLCVBar = {
|
const oneMin: OHLCVBar = {
|
||||||
time: data.time,
|
time: data.time,
|
||||||
open: data.open,
|
open: data.open,
|
||||||
|
|||||||
Reference in New Issue
Block a user