알림 전체닫기 오동작 수정
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -34,6 +34,7 @@ import {
|
|||||||
ensurePaperChartOverlays,
|
ensurePaperChartOverlays,
|
||||||
resolveStrategyPrimaryTimeframe,
|
resolveStrategyPrimaryTimeframe,
|
||||||
} from '../../utils/strategyToChartIndicators';
|
} from '../../utils/strategyToChartIndicators';
|
||||||
|
import type { ChartOverlayToggleKey } from '../../utils/chartOverlayVisibility';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
@@ -60,6 +61,11 @@ interface Props {
|
|||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
|
/** PaperOverlayVisibility → ChartManager 동기화용 (candle 항상 true) */
|
||||||
|
function paperVisToChartVis(pv: PaperOverlayVisibility) {
|
||||||
|
return { ma: pv.ma, bollinger: pv.bollinger, ichimoku: pv.ichimoku, candle: true };
|
||||||
|
}
|
||||||
|
|
||||||
let _indSeq = 0;
|
let _indSeq = 0;
|
||||||
function newIndId() {
|
function newIndId() {
|
||||||
_indSeq += 1;
|
_indSeq += 1;
|
||||||
@@ -116,10 +122,14 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [chartType] = useState<ChartType>('candlestick');
|
const [chartType] = useState<ChartType>('candlestick');
|
||||||
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
||||||
|
/** 개별 지표 숨김 ID 집합 (실시간 차트와 동일한 per-indicator 숨김 지원) */
|
||||||
|
const [hiddenIndicatorIds, setHiddenIndicatorIds] = useState<ReadonlySet<string>>(new Set());
|
||||||
const managerRef = useRef<ChartManager | null>(null);
|
const managerRef = useRef<ChartManager | null>(null);
|
||||||
|
const overlayVisibilityRef = useRef(overlayVisibility);
|
||||||
const logicalRangeUnsubRef = useRef<(() => void) | null>(null);
|
const logicalRangeUnsubRef = useRef<(() => void) | null>(null);
|
||||||
const viewportUnsubRef = useRef<(() => void) | null>(null);
|
const viewportUnsubRef = useRef<(() => void) | null>(null);
|
||||||
const loadMoreRefRef = useRef<MutableRefObject<() => void>>({ current: () => {} });
|
const loadMoreRefRef = useRef<MutableRefObject<() => void>>({ current: () => {} });
|
||||||
|
overlayVisibilityRef.current = overlayVisibility;
|
||||||
const signalsRef = useRef(signals);
|
const signalsRef = useRef(signals);
|
||||||
signalsRef.current = signals;
|
signalsRef.current = signals;
|
||||||
const onHistoryBarsLoadedRef = useRef(onHistoryBarsLoaded);
|
const onHistoryBarsLoadedRef = useRef(onHistoryBarsLoaded);
|
||||||
@@ -159,15 +169,55 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
}, [strategy, getParams, getVisualConfig, chartTimeframe, overlayVisibility]);
|
}, [strategy, getParams, getVisualConfig, chartTimeframe, overlayVisibility]);
|
||||||
|
|
||||||
const indicators = useMemo(() => {
|
const indicators = useMemo(() => {
|
||||||
if (!overlayVisibility) return baseIndicators;
|
let inds = overlayVisibility
|
||||||
return applyPaperOverlayVisibility(baseIndicators, overlayVisibility);
|
? applyPaperOverlayVisibility(baseIndicators, overlayVisibility)
|
||||||
}, [baseIndicators, overlayVisibility]);
|
: baseIndicators;
|
||||||
|
// 개별 지표 숨김 상태 반영 (per-indicator hidden toggle)
|
||||||
|
if (hiddenIndicatorIds.size > 0) {
|
||||||
|
inds = inds.map(ind =>
|
||||||
|
hiddenIndicatorIds.has(ind.id) ? { ...ind, hidden: true } : ind,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return inds;
|
||||||
|
}, [baseIndicators, overlayVisibility, hiddenIndicatorIds]);
|
||||||
|
|
||||||
const candleOverlayToggles = useMemo(() => {
|
const candleOverlayToggles = useMemo(() => {
|
||||||
if (!overlayVisibility || !onToggleOverlay) return undefined;
|
if (!overlayVisibility || !onToggleOverlay) return undefined;
|
||||||
return listPaperOverlayToggleItems(baseIndicators, overlayVisibility);
|
return listPaperOverlayToggleItems(baseIndicators, overlayVisibility);
|
||||||
}, [baseIndicators, overlayVisibility, onToggleOverlay]);
|
}, [baseIndicators, overlayVisibility, onToggleOverlay]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 캔들 오버레이 그룹 토글 — 실시간 차트와 동일하게 ChartManager 를 즉시 호출한 뒤
|
||||||
|
* 부모 state 도 업데이트한다.
|
||||||
|
*/
|
||||||
|
const handleToggleCandleOverlay = useCallback((key: ChartOverlayToggleKey) => {
|
||||||
|
const pKey = key as PaperOverlayToggleKey;
|
||||||
|
const prev = overlayVisibilityRef.current;
|
||||||
|
if (prev && pKey in prev) {
|
||||||
|
const newVisible = !prev[pKey];
|
||||||
|
managerRef.current?.setChartOverlayGroupVisible(key, newVisible);
|
||||||
|
}
|
||||||
|
onToggleOverlay?.(pKey);
|
||||||
|
}, [onToggleOverlay]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개별 보조지표 숨김/표시 — 실시간 차트와 동일한 per-indicator 토글
|
||||||
|
*/
|
||||||
|
const handleToggleIndicatorHidden = useCallback((id: string) => {
|
||||||
|
setHiddenIndicatorIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
const willHide = !next.has(id);
|
||||||
|
if (willHide) {
|
||||||
|
next.add(id);
|
||||||
|
} else {
|
||||||
|
next.delete(id);
|
||||||
|
}
|
||||||
|
// ChartManager 즉시 반영
|
||||||
|
managerRef.current?.toggleIndicatorVisible(id, !willHide);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const auxPaneCount = useMemo(
|
const auxPaneCount = useMemo(
|
||||||
() => countNonOverlayIndicatorPanes(indicators, isOverlayType),
|
() => countNonOverlayIndicatorPanes(indicators, isOverlayType),
|
||||||
[indicators],
|
[indicators],
|
||||||
@@ -284,6 +334,10 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
|
|
||||||
const onManagerReady = useCallback((mgr: ChartManager) => {
|
const onManagerReady = useCallback((mgr: ChartManager) => {
|
||||||
managerRef.current = mgr;
|
managerRef.current = mgr;
|
||||||
|
// 실시간 차트와 동일하게: 마운트 시 오버레이 가시성 ChartManager 에 동기화
|
||||||
|
if (overlayVisibilityRef.current) {
|
||||||
|
mgr.setChartOverlayVisibility(paperVisToChartVis(overlayVisibilityRef.current));
|
||||||
|
}
|
||||||
setupHistoryScroll(mgr);
|
setupHistoryScroll(mgr);
|
||||||
setLoadedBarCount(mgr.getRawBarsLength());
|
setLoadedBarCount(mgr.getRawBarsLength());
|
||||||
applyPaneRatio(mgr);
|
applyPaneRatio(mgr);
|
||||||
@@ -393,9 +447,8 @@ const BacktestAnalysisChart: React.FC<Props> = ({
|
|||||||
seriesPriceLabelsEnabled={priceLabels}
|
seriesPriceLabelsEnabled={priceLabels}
|
||||||
showHoverToolbar
|
showHoverToolbar
|
||||||
candleOverlayToggles={candleOverlayToggles}
|
candleOverlayToggles={candleOverlayToggles}
|
||||||
onToggleCandleOverlay={onToggleOverlay
|
onToggleCandleOverlay={onToggleOverlay ? handleToggleCandleOverlay : undefined}
|
||||||
? (key) => onToggleOverlay(key as PaperOverlayToggleKey)
|
onToggleIndicatorHidden={handleToggleIndicatorHidden}
|
||||||
: undefined}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!loading && !error && bars.length === 0 && (
|
{!loading && !error && bars.length === 0 && (
|
||||||
|
|||||||
@@ -160,8 +160,12 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
|||||||
const refreshHistory = useCallback(async (opts?: { serverOnly?: boolean; rawServerList?: boolean }) => {
|
const refreshHistory = useCallback(async (opts?: { serverOnly?: boolean; rawServerList?: boolean }) => {
|
||||||
try {
|
try {
|
||||||
const dtos = await loadTradeSignals();
|
const dtos = await loadTradeSignals();
|
||||||
/** dismiss 직후 비동기 완료 시 state readIds가 늦게 반영되는 것 방지 */
|
/**
|
||||||
const read = opts?.rawServerList ? new Set<string>() : loadReadIds();
|
* 항상 DB 캐시의 최신 readIds 사용.
|
||||||
|
* rawServerList=true 시 빈 Set을 쓰던 기존 코드는 새로고침 후 모든 알림이
|
||||||
|
* 미읽음으로 복원되는 버그를 유발했으므로 제거.
|
||||||
|
*/
|
||||||
|
const read = loadReadIds();
|
||||||
const hidden = opts?.rawServerList ? new Set<string>() : loadHiddenIds();
|
const hidden = opts?.rawServerList ? new Set<string>() : loadHiddenIds();
|
||||||
const items = dtos
|
const items = dtos
|
||||||
.map(d => dtoToItem(d, read))
|
.map(d => dtoToItem(d, read))
|
||||||
@@ -170,9 +174,16 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
|||||||
let newlyArrived: TradeNotificationItem[] = [];
|
let newlyArrived: TradeNotificationItem[] = [];
|
||||||
|
|
||||||
setAllNotifications(prev => {
|
setAllNotifications(prev => {
|
||||||
newlyArrived = items.filter(
|
/**
|
||||||
item => !item.isRead && !prev.some(p => p.id === item.id),
|
* rawServerList=true 는 앱 초기 로드(새로고침·로그인 직후)이므로
|
||||||
);
|
* 이미 존재하는 서버 이력을 실시간 신규 알림으로 취급하지 않는다.
|
||||||
|
* 실제 새 알림 판단은 STOMP addNotification 과 일반 refreshHistory 에서만 한다.
|
||||||
|
*/
|
||||||
|
if (!opts?.rawServerList) {
|
||||||
|
newlyArrived = items.filter(
|
||||||
|
item => !item.isRead && !prev.some(p => p.id === item.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (opts?.serverOnly || opts?.rawServerList) {
|
if (opts?.serverOnly || opts?.rawServerList) {
|
||||||
return items
|
return items
|
||||||
@@ -234,15 +245,12 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
|||||||
|
|
||||||
const addNotification = useCallback((signal: TradeSignalInfo & { dbId?: number }) => {
|
const addNotification = useCallback((signal: TradeSignalInfo & { dbId?: number }) => {
|
||||||
const id = makeId(signal);
|
const id = makeId(signal);
|
||||||
// 실시간 재수신 시 이전에 읽음 처리된 동일 키도 다시 미확인으로
|
/**
|
||||||
setReadIds(prev => {
|
* 이전에 읽음 처리(닫기)한 동일 키를 readIds 에서 제거하던 로직을 삭제.
|
||||||
if (!prev.has(id)) return prev;
|
* STOMP 재연결 시 동일 신호가 재수신되어도 사용자가 명시적으로 닫은 알림은
|
||||||
const next = new Set(prev);
|
* 다시 열리지 않아야 한다. 새로운 candleTime 을 가진 신호는 키가 달라
|
||||||
next.delete(id);
|
* readIds 에 없으므로 정상적으로 미읽음 처리된다.
|
||||||
saveReadIds(next);
|
*/
|
||||||
readIdsRef.current = next;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
const item: TradeNotificationItem = {
|
const item: TradeNotificationItem = {
|
||||||
...signal,
|
...signal,
|
||||||
id,
|
id,
|
||||||
@@ -311,28 +319,23 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const dismissAllToasts = useCallback(() => {
|
const dismissAllToasts = useCallback(() => {
|
||||||
const snapshot = toastNotificationsRef.current;
|
/**
|
||||||
if (snapshot.length === 0) return;
|
* 전체 닫기 = 현재 토스트 + allNotifications 의 모든 미읽음 항목을 읽음 처리.
|
||||||
const idSet = new Set(snapshot.map(n => n.id));
|
* 기존에는 toastNotifications 에 있는 항목만 처리해서, 토스트에 없는 미읽음 항목은
|
||||||
|
* 다음 refreshHistory 폴링 시 다시 팝업으로 나타나는 문제가 있었다.
|
||||||
setReadIds(r => {
|
*/
|
||||||
const next = new Set(r);
|
setAllNotifications(prev => {
|
||||||
idSet.forEach(i => next.add(i));
|
const nextRead = new Set(readIdsRef.current);
|
||||||
saveReadIds(next);
|
// 토스트 항목
|
||||||
readIdsRef.current = next;
|
toastNotificationsRef.current.forEach(n => nextRead.add(n.id));
|
||||||
return next;
|
// allNotifications 의 미읽음 항목 전체
|
||||||
|
prev.forEach(n => nextRead.add(n.id));
|
||||||
|
saveReadIds(nextRead);
|
||||||
|
readIdsRef.current = nextRead;
|
||||||
|
setReadIds(nextRead);
|
||||||
|
return prev.map(n => ({ ...n, isRead: true }));
|
||||||
});
|
});
|
||||||
setToastNotifications([]);
|
setToastNotifications([]);
|
||||||
setAllNotifications(all => {
|
|
||||||
const byId = new Map(all.map(n => [n.id, n]));
|
|
||||||
for (const n of snapshot) {
|
|
||||||
const existing = byId.get(n.id);
|
|
||||||
byId.set(n.id, existing ? { ...existing, isRead: true } : { ...n, isRead: true });
|
|
||||||
}
|
|
||||||
return [...byId.values()]
|
|
||||||
.sort((a, b) => b.receivedAt - a.receivedAt)
|
|
||||||
.slice(0, MAX_HISTORY);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const markAllAsRead = useCallback(() => {
|
const markAllAsRead = useCallback(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user