알림 시간봉 오류 수정
This commit is contained in:
@@ -838,8 +838,12 @@ function App() {
|
||||
const filtered = activeLiveSettings.filter(s => monitoredMarkets.includes(s.market));
|
||||
if (filtered.length > 0) return expandLiveStrategySubscriptions(filtered);
|
||||
if (marketSubscriptions.length > 0) return marketSubscriptions;
|
||||
return monitoredMarkets.map(m => ({ market: m, candleType: '1m' }));
|
||||
}, [monitoredMarkets, activeLiveSettings, marketSubscriptions]);
|
||||
const types = liveStrategyCandleTypes?.length ? liveStrategyCandleTypes : [];
|
||||
if (types.length > 0) {
|
||||
return monitoredMarkets.flatMap(m => types.map(candleType => ({ market: m, candleType })));
|
||||
}
|
||||
return [];
|
||||
}, [monitoredMarkets, activeLiveSettings, marketSubscriptions, liveStrategyCandleTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (monitoredMarkets.length === 0) {
|
||||
@@ -1683,6 +1687,7 @@ function App() {
|
||||
popupEnabled={appDefaults.tradeAlertPopup ?? true}
|
||||
soundEnabled={appDefaults.tradeAlertSoundEnabled ?? true}
|
||||
soundId={appDefaults.tradeAlertSound ?? 'bell'}
|
||||
settingsSessionKey={authUser?.userId ?? 0}
|
||||
>
|
||||
<VerificationIssueNotificationProvider
|
||||
enabled={canMenu('verification-board')}
|
||||
@@ -1696,13 +1701,6 @@ function App() {
|
||||
activeMarket={symbol}
|
||||
enabled={monitoredMarkets.length > 0}
|
||||
popupEnabled={appDefaults.tradeAlertPopup ?? true}
|
||||
strategyId={liveStrategySettings.strategyId ?? undefined}
|
||||
executionType={liveStrategySettings.executionType ?? 'CANDLE_CLOSE'}
|
||||
strategyName={liveStrategyName}
|
||||
strategyNameByMarket={liveStrategyNameByMarket}
|
||||
executionTypeByMarket={Object.fromEntries(
|
||||
activeLiveSettings.map(s => [s.market, s.executionType ?? 'CANDLE_CLOSE']),
|
||||
)}
|
||||
chartMarkersActive={menuPage === 'chart'}
|
||||
onMarkersChange={markers => {
|
||||
managerRef.current?.setLiveStrategyMarkers(markers, appDefaults.btShowPrice);
|
||||
|
||||
@@ -16,11 +16,6 @@ interface Props {
|
||||
activeMarket: string;
|
||||
enabled: boolean;
|
||||
popupEnabled: boolean;
|
||||
strategyId: number | undefined;
|
||||
executionType: string;
|
||||
strategyName: string | null;
|
||||
strategyNameByMarket?: Record<string, string>;
|
||||
executionTypeByMarket?: Record<string, string>;
|
||||
onMarkersChange: (markers: LiveMarker[]) => void;
|
||||
/** 차트 화면일 때만 마커를 차트에 반영 */
|
||||
chartMarkersActive: boolean;
|
||||
@@ -32,10 +27,6 @@ export const LiveSignalNotifier = forwardRef<LiveSignalNotifierHandle, Props>(fu
|
||||
activeMarket,
|
||||
enabled,
|
||||
popupEnabled: _popupEnabled,
|
||||
strategyName,
|
||||
executionType,
|
||||
strategyNameByMarket,
|
||||
executionTypeByMarket,
|
||||
onMarkersChange,
|
||||
chartMarkersActive,
|
||||
}, ref) {
|
||||
@@ -63,11 +54,10 @@ export const LiveSignalNotifier = forwardRef<LiveSignalNotifierHandle, Props>(fu
|
||||
signalType: marker.signal,
|
||||
price: marker.price,
|
||||
candleTime: marker.time,
|
||||
strategyName: strategyNameByMarket?.[marker.market] ?? strategyName,
|
||||
executionType: executionTypeByMarket?.[marker.market] ?? executionType,
|
||||
strategyId: marker.strategyId ?? undefined,
|
||||
executionType: marker.executionType ?? undefined,
|
||||
candleType: marker.candleType ?? '1m',
|
||||
});
|
||||
// STOMP만 수신·DB 저장 누락 시 배지 동기화
|
||||
window.setTimeout(() => { void refreshHistory(); }, 800);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -187,6 +187,7 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
}
|
||||
for (const n of prev) {
|
||||
if (hidden.has(n.id)) continue;
|
||||
// 서버 이력에 없는 STOMP-only 항목은 세션 전환 후 제거 (serverOnly가 아닐 때만 병합)
|
||||
if (!map.has(n.id)) map.set(n.id, n);
|
||||
else if (n.isRead) map.set(n.id, { ...map.get(n.id)!, isRead: true });
|
||||
}
|
||||
@@ -213,6 +214,7 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingsLoaded) return;
|
||||
setToastNotifications([]);
|
||||
void refreshHistory({ serverOnly: true, rawServerList: true });
|
||||
}, [refreshHistory, settingsLoaded, settingsSessionKey]);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { parseStompJson } from '../utils/stompMessage';
|
||||
import { subscribeStompTopic } from '../utils/stompChartBroker';
|
||||
import { signalBelongsToCurrentSession } from '../utils/tradeSignalOwnership';
|
||||
|
||||
export interface LiveMarker {
|
||||
time: number;
|
||||
@@ -16,6 +17,10 @@ export interface LiveMarker {
|
||||
price: number;
|
||||
market: string;
|
||||
candleType: string;
|
||||
strategyId?: number | null;
|
||||
userId?: number | null;
|
||||
deviceId?: string | null;
|
||||
executionType?: string | null;
|
||||
}
|
||||
|
||||
export interface MarketCandleSub {
|
||||
@@ -57,9 +62,17 @@ export function useLiveStrategyMarkers({
|
||||
|
||||
const pushMarker = useCallback((market: string, candleType: string, data: {
|
||||
time: number; close: number; signal: string;
|
||||
strategyId?: number | null;
|
||||
userId?: number | null;
|
||||
deviceId?: string | null;
|
||||
executionType?: string | null;
|
||||
}) => {
|
||||
if (data.signal !== 'BUY' && data.signal !== 'SELL') return;
|
||||
|
||||
if (!signalBelongsToCurrentSession({ userId: data.userId, deviceId: data.deviceId })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${market}:${candleType}:${data.time}:${data.signal}`;
|
||||
if (seenRef.current.has(key)) return;
|
||||
seenRef.current.add(key);
|
||||
@@ -70,6 +83,10 @@ export function useLiveStrategyMarkers({
|
||||
price: data.close,
|
||||
market,
|
||||
candleType,
|
||||
strategyId: data.strategyId,
|
||||
userId: data.userId,
|
||||
deviceId: data.deviceId,
|
||||
executionType: data.executionType,
|
||||
};
|
||||
|
||||
const prev = markersByMarketRef.current[market] ?? [];
|
||||
@@ -120,6 +137,10 @@ export function useLiveStrategyMarkers({
|
||||
close: number;
|
||||
signal?: string;
|
||||
candleType?: string;
|
||||
strategyId?: number | null;
|
||||
userId?: number | null;
|
||||
deviceId?: string | null;
|
||||
executionType?: string | null;
|
||||
}>(msg);
|
||||
if (!data?.signal || data.signal === 'NONE') return;
|
||||
const resolvedCandleType = data.candleType ?? ct;
|
||||
@@ -127,6 +148,10 @@ export function useLiveStrategyMarkers({
|
||||
time: data.time,
|
||||
close: data.close,
|
||||
signal: data.signal,
|
||||
strategyId: data.strategyId,
|
||||
userId: data.userId,
|
||||
deviceId: data.deviceId,
|
||||
executionType: data.executionType,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,7 +155,9 @@ export function updateStartCandleTypes(
|
||||
candleTypes: string[],
|
||||
): EditorConditionState {
|
||||
const types = normalizeCandleTypesList(candleTypes);
|
||||
const primary = types[0];
|
||||
// 조건 노드 left/rightCandleType 동기화는 "대표 분봉"에만 사용되므로,
|
||||
// multi 분봉에서 stale 1m 이 앞에 오는 경우 1m 로 동기화되지 않도록 non-1m 을 우선한다.
|
||||
const primary = types.find(ct => normalizeStartCandleType(ct) !== '1m') ?? types[0];
|
||||
const nextMeta = {
|
||||
...state.startMeta,
|
||||
[startId]: { candleTypes: types, candleType: primary },
|
||||
@@ -197,7 +199,13 @@ function normalizeCandleTypesList(types: string[]): string[] {
|
||||
out.push(ct);
|
||||
}
|
||||
}
|
||||
return out.length > 0 ? out : [DEFAULT_START_CANDLE];
|
||||
if (out.length === 0) return [DEFAULT_START_CANDLE];
|
||||
// 레거시/불일치로 multi 분봉에 stale 1m 이 섞인 경우가 있어,
|
||||
// multi 인데 non-1m 이 있으면 1m 을 기본적으로 제외한다.
|
||||
if (out.length > 1 && out.includes('1m') && out.some(ct => ct !== '1m')) {
|
||||
return out.filter(ct => ct !== '1m');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function readTimeframeCandleTypes(node: LogicNode): string[] {
|
||||
@@ -467,7 +475,16 @@ export function mergeStartMetaForLoad(
|
||||
};
|
||||
for (const [startId, meta] of Object.entries(decoded)) {
|
||||
if (stored[startId]) {
|
||||
const types = normalizeCandleTypesList(getStartCandleTypes(stored[startId]));
|
||||
const storedTypes = normalizeCandleTypesList(getStartCandleTypes(stored[startId]));
|
||||
const decodedTypes = normalizeCandleTypesList(getStartCandleTypes(meta));
|
||||
// flow_layout_json 에 stale 1m 이 남아 있는 경우가 있어,
|
||||
// DSL 에 1m 이 없고 non-1m 이 있으면 DSL 쪽을 우선한다.
|
||||
const shouldPreferDecoded =
|
||||
storedTypes.includes('1m')
|
||||
&& storedTypes.some(ct => ct !== '1m')
|
||||
&& !decodedTypes.includes('1m')
|
||||
&& decodedTypes.some(ct => ct !== '1m');
|
||||
const types = shouldPreferDecoded ? decodedTypes : storedTypes;
|
||||
merged[startId] = { candleTypes: types, candleType: types[0] };
|
||||
} else {
|
||||
const types = normalizeCandleTypesList(getStartCandleTypes(meta));
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* STOMP·실시간 시그널 소유자 검증 — 로그인 사용자는 user_id, 비로그인은 device_id 기준.
|
||||
*/
|
||||
export interface SignalOwnership {
|
||||
userId?: number | null;
|
||||
deviceId?: string | null;
|
||||
}
|
||||
|
||||
function readDeviceId(): string {
|
||||
return localStorage.getItem('gc_device_id') ?? '';
|
||||
}
|
||||
|
||||
function readUserId(): number | null {
|
||||
const raw = localStorage.getItem('gc_user_id');
|
||||
if (!raw) return null;
|
||||
const id = Number(raw);
|
||||
return Number.isFinite(id) && id > 0 ? id : null;
|
||||
}
|
||||
|
||||
export function getCurrentSignalSession(): { userId: number | null; deviceId: string } {
|
||||
return { userId: readUserId(), deviceId: readDeviceId() };
|
||||
}
|
||||
|
||||
/** STOMP 페이로드·DB 시그널이 현재 세션 소유인지 */
|
||||
export function signalBelongsToCurrentSession(owner: SignalOwnership): boolean {
|
||||
const { userId, deviceId } = getCurrentSignalSession();
|
||||
|
||||
if (owner.userId != null && owner.userId > 0) {
|
||||
return userId != null && userId === owner.userId;
|
||||
}
|
||||
|
||||
if (owner.deviceId != null && owner.deviceId !== '') {
|
||||
return userId == null && owner.deviceId === deviceId;
|
||||
}
|
||||
|
||||
// 소유자 메타 없음(레거시 브로드캐스트) — 타 사용자 시그널 유입 방지
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user