가상매매 목록 실시간 연결안됨 문제수정

This commit is contained in:
Macbook
2026-06-07 19:06:59 +09:00
parent 5928599fd0
commit b294c2be5d
5 changed files with 82 additions and 14 deletions
@@ -14,6 +14,8 @@ import {
export type VirtualLiveStatus = 'idle' | 'connecting' | 'live' | 'disconnected';
const STALE_MS = 20_000;
/** useStompChartData 와 동일 — STOMP 수립 후 틱 없어도 실시간 파이프라인 준비 완료로 간주 */
const CONN_GRACE_MS = 800;
const TICK_CANDLE = '1m';
interface TargetRef {
@@ -30,6 +32,24 @@ function topicFor(market: string): string {
return `/sub/charts/${market}/${TICK_CANDLE}`;
}
function isCandlePayload(data: { time?: unknown; close?: unknown } | null): boolean {
if (!data) return false;
return typeof data.time === 'number' || typeof data.close === 'number';
}
/** 목록·카드에서 liveStatusByMarket 미등록 시 fallback */
export function resolveVirtualLiveStatusFallback(
market: string,
strategyId: number | null,
statusByMarket: Record<string, VirtualLiveStatus>,
running: boolean,
): VirtualLiveStatus {
const known = statusByMarket[market];
if (known) return known;
if (!strategyId) return 'idle';
return running ? 'connecting' : 'idle';
}
export interface VirtualTargetLiveState {
statusByMarket: Record<string, VirtualLiveStatus>;
/** STOMP 틱 수신 시각 (ms) — 카드 형광 플래시 트리거 */
@@ -46,6 +66,9 @@ export function useVirtualTargetLiveStatus(
const [lastTickAtByMarket, setLastTickAtByMarket] = useState<Record<string, number>>({});
const stateRef = useRef<Record<string, MarketLiveState>>({});
const stompConnectedRef = useRef(isStompChartConnected());
const stompConnectedAtRef = useRef<number | null>(
isStompChartConnected() ? Date.now() : null,
);
const patchMarket = (market: string, patch: Partial<MarketLiveState>) => {
const prev = stateRef.current[market] ?? { status: 'idle' as VirtualLiveStatus, lastTickAt: null };
@@ -66,12 +89,22 @@ export function useVirtualTargetLiveStatus(
return;
}
if (stompConnectedRef.current) {
const connectedAt = stompConnectedAtRef.current;
if (connectedAt != null && now - connectedAt >= CONN_GRACE_MS) {
patchMarket(market, { status: 'live' });
return;
}
patchMarket(market, { status: 'connecting' });
} else {
patchMarket(market, { status: 'disconnected' });
}
};
const targetsKey = targets
.filter(t => t.strategyId != null)
.map(t => `${t.market}:${t.strategyId}`)
.join('|');
useEffect(() => {
const activeMarkets = targets
.filter(t => t.strategyId != null)
@@ -112,17 +145,30 @@ export function useVirtualTargetLiveStatus(
const unsubs = activeMarkets.map(market => {
const topic = topicFor(market);
return subscribeStompTopic(topic, msg => {
const data = parseStompJson<{ time?: number }>(msg);
if (data?.time == null) return;
const data = parseStompJson<{ time?: number; close?: number }>(msg);
if (!isCandlePayload(data)) return;
patchMarket(market, { status: 'live', lastTickAt: Date.now() });
});
});
let graceTimer: ReturnType<typeof setTimeout> | null = null;
const scheduleGraceRecompute = () => {
if (graceTimer) clearTimeout(graceTimer);
if (!stompConnectedRef.current) return;
graceTimer = setTimeout(() => {
for (const market of activeMarkets) recomputeStatus(market);
}, CONN_GRACE_MS);
};
const offConn = subscribeStompConnection(next => {
stompConnectedRef.current = next;
stompConnectedAtRef.current = next ? Date.now() : null;
for (const market of activeMarkets) recomputeStatus(market);
if (next) scheduleGraceRecompute();
});
if (stompConnectedRef.current) scheduleGraceRecompute();
const staleId = window.setInterval(() => {
for (const market of activeMarkets) recomputeStatus(market);
}, 3000);
@@ -131,8 +177,9 @@ export function useVirtualTargetLiveStatus(
unsubs.forEach(u => u());
offConn();
clearInterval(staleId);
if (graceTimer) clearTimeout(graceTimer);
};
}, [targets, running, eagerConnect]);
}, [targetsKey, running, eagerConnect]);
return { statusByMarket: byMarket, lastTickAtByMarket };
}