가상투자 메뉴 기능 구현
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 가상투자 — 종목별 STOMP 실시간 데이터 수신 상태
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { pinCandleWatch } from '../utils/backendApi';
|
||||
import { parseStompJson } from '../utils/stompMessage';
|
||||
import {
|
||||
isStompChartConnected,
|
||||
subscribeStompConnection,
|
||||
subscribeStompTopic,
|
||||
} from '../utils/stompChartBroker';
|
||||
|
||||
/** idle=미시작, connecting=연결 중, live=틱 수신 중, disconnected=끊김 */
|
||||
export type VirtualLiveStatus = 'idle' | 'connecting' | 'live' | 'disconnected';
|
||||
|
||||
const STALE_MS = 20_000;
|
||||
const TICK_CANDLE = '1m';
|
||||
|
||||
interface TargetRef {
|
||||
market: string;
|
||||
strategyId: number | null;
|
||||
}
|
||||
|
||||
interface MarketLiveState {
|
||||
status: VirtualLiveStatus;
|
||||
lastTickAt: number | null;
|
||||
}
|
||||
|
||||
function topicFor(market: string): string {
|
||||
return `/sub/charts/${market}/${TICK_CANDLE}`;
|
||||
}
|
||||
|
||||
export function useVirtualTargetLiveStatus(
|
||||
targets: TargetRef[],
|
||||
running: boolean,
|
||||
): Record<string, VirtualLiveStatus> {
|
||||
const [byMarket, setByMarket] = useState<Record<string, VirtualLiveStatus>>({});
|
||||
const stateRef = useRef<Record<string, MarketLiveState>>({});
|
||||
const stompConnectedRef = useRef(isStompChartConnected());
|
||||
|
||||
const patchMarket = (market: string, patch: Partial<MarketLiveState>) => {
|
||||
const prev = stateRef.current[market] ?? { status: 'idle' as VirtualLiveStatus, lastTickAt: null };
|
||||
const next: MarketLiveState = { ...prev, ...patch };
|
||||
stateRef.current[market] = next;
|
||||
setByMarket(cur => ({ ...cur, [market]: next.status }));
|
||||
};
|
||||
|
||||
const recomputeStatus = (market: string) => {
|
||||
const s = stateRef.current[market];
|
||||
if (!s || !running) return;
|
||||
const now = Date.now();
|
||||
if (s.lastTickAt != null && now - s.lastTickAt <= STALE_MS) {
|
||||
if (s.status !== 'live') patchMarket(market, { status: 'live' });
|
||||
return;
|
||||
}
|
||||
if (stompConnectedRef.current) {
|
||||
patchMarket(market, { status: 'connecting' });
|
||||
} else {
|
||||
patchMarket(market, { status: 'disconnected' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!running) {
|
||||
stateRef.current = {};
|
||||
setByMarket({});
|
||||
return;
|
||||
}
|
||||
|
||||
const activeMarkets = targets
|
||||
.filter(t => t.strategyId != null)
|
||||
.map(t => t.market);
|
||||
|
||||
for (const market of activeMarkets) {
|
||||
if (!stateRef.current[market]) {
|
||||
stateRef.current[market] = { status: 'connecting', lastTickAt: null };
|
||||
setByMarket(cur => ({ ...cur, [market]: 'connecting' }));
|
||||
}
|
||||
void pinCandleWatch(market, TICK_CANDLE);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(stateRef.current)) {
|
||||
if (!activeMarkets.includes(key)) {
|
||||
delete stateRef.current[key];
|
||||
setByMarket(cur => {
|
||||
const next = { ...cur };
|
||||
delete next[key];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const unsubs = activeMarkets.map(market => {
|
||||
const topic = topicFor(market);
|
||||
return subscribeStompTopic(topic, msg => {
|
||||
const data = parseStompJson<{ time?: number }>(msg);
|
||||
if (data?.time == null) return;
|
||||
patchMarket(market, { status: 'live', lastTickAt: Date.now() });
|
||||
});
|
||||
});
|
||||
|
||||
const offConn = subscribeStompConnection(next => {
|
||||
stompConnectedRef.current = next;
|
||||
for (const market of activeMarkets) recomputeStatus(market);
|
||||
});
|
||||
|
||||
const staleId = window.setInterval(() => {
|
||||
for (const market of activeMarkets) recomputeStatus(market);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(u => u());
|
||||
offConn();
|
||||
clearInterval(staleId);
|
||||
};
|
||||
}, [targets, running]);
|
||||
|
||||
return byMarket;
|
||||
}
|
||||
Reference in New Issue
Block a user