데스크톱 앱 리프레시 기능 추가
This commit is contained in:
@@ -12664,6 +12664,32 @@ html.theme-light .tam-disclaimer { color: #90a4ae; }
|
||||
color: var(--accent, #8b5cf6);
|
||||
}
|
||||
|
||||
/* TopMenuBar 새로고침 (데스크톱) */
|
||||
.tmb-refresh-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tmb-refresh-btn:hover {
|
||||
background: var(--bg4);
|
||||
color: var(--accent, #2196f3);
|
||||
}
|
||||
.tmb-refresh-btn.busy {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
}
|
||||
.tmb-refresh-btn.busy svg {
|
||||
animation: tmb-update-spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
/* TopMenuBar 전체화면 버튼 */
|
||||
.tmb-fullscreen-btn {
|
||||
display: flex;
|
||||
|
||||
@@ -38,6 +38,7 @@ import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
||||
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
||||
import { isDesktop } from './utils/platform';
|
||||
import { syncDesktopMainWindowTitle } from './utils/desktopBridge';
|
||||
import { dispatchPageRefresh } from './utils/pageRefreshEvents';
|
||||
import { writeDesktopSync } from './utils/desktopSync';
|
||||
|
||||
const LAST_MENU_KEY = 'gc_last_menu';
|
||||
@@ -185,6 +186,10 @@ function AppMainContent({
|
||||
else document.exitFullscreen().catch(() => {});
|
||||
};
|
||||
|
||||
const handlePageRefresh = useCallback(() => {
|
||||
dispatchPageRefresh(menuPage);
|
||||
}, [menuPage]);
|
||||
|
||||
return (
|
||||
<VerificationIssueNotificationProvider
|
||||
enabled={canMenu('verification-board')}
|
||||
@@ -209,6 +214,7 @@ function AppMainContent({
|
||||
onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
|
||||
onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
|
||||
onFullscreen={handleFullscreen}
|
||||
onRefresh={isDesktop() ? handlePageRefresh : undefined}
|
||||
onOpenFloatingWidgets={showFloatingWidgets ? () => setFloatingLayoutPickerOpen(true) : undefined}
|
||||
floatingWidgetCount={floatingWidgetCount}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,7 @@ import { normalizeTimezone, setDisplayTimezone, useDisplayTimezone } from '../ut
|
||||
import { calcStats, type PriceStats } from '../utils/calculations';
|
||||
import { saveState, DEFAULT_MAIN_CHART_STYLE } from '../utils/storage';
|
||||
import { useWorkspacePersist } from '../hooks/useWorkspacePersist';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import type { WatchlistItem } from '../utils/backendApi';
|
||||
import {
|
||||
addWatchlistItem, removeWatchlistItem,
|
||||
@@ -620,6 +621,7 @@ export function useChartWorkspace({
|
||||
const [paperCashBalance, setPaperCashBalance] = useState(0);
|
||||
const [paperCoinQty, setPaperCoinQty] = useState(0);
|
||||
const [paperRefreshKey, setPaperRefreshKey] = useState(0);
|
||||
const [chartRefreshToken, setChartRefreshToken] = useState(0);
|
||||
|
||||
const handlePaperOrderFilled = useCallback(() => {
|
||||
setPaperRefreshKey(k => k + 1);
|
||||
@@ -679,6 +681,19 @@ export function useChartWorkspace({
|
||||
invalidateRuns: btInvalidateRuns,
|
||||
} = useBacktest();
|
||||
const [btSettings, setBtSettings] = useState<BacktestSettingsDto>(DEFAULT_BACKTEST_SETTINGS);
|
||||
|
||||
usePageRefresh('chart', useCallback(() => {
|
||||
setChartRefreshToken(t => t + 1);
|
||||
void refreshAllTickers();
|
||||
void refreshPaperAccount(symbol);
|
||||
btInvalidateRuns();
|
||||
}, [refreshAllTickers, refreshPaperAccount, symbol, btInvalidateRuns]));
|
||||
|
||||
usePageRefresh('paper', useCallback(() => {
|
||||
setPaperRefreshKey(k => k + 1);
|
||||
void refreshAllTickers();
|
||||
}, [refreshAllTickers]));
|
||||
|
||||
const [showBtSettings, setShowBtSettings] = useState(false);
|
||||
const [showBtResult, setShowBtResult] = useState(false);
|
||||
const [btStrategyName, setBtStrategyName] = useState('전략');
|
||||
@@ -1160,7 +1175,8 @@ export function useChartWorkspace({
|
||||
onNewCandle: handleNewCandle,
|
||||
enabled: chartScreenActive && useUpbit && chartConfigReady,
|
||||
source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
|
||||
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartConfigReady, chartRealtimeSource]),
|
||||
refreshToken: chartRefreshToken,
|
||||
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartConfigReady, chartRealtimeSource, chartRefreshToken]),
|
||||
);
|
||||
|
||||
// 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../utils/backendApi';
|
||||
import type { Theme, Timeframe } from '../types';
|
||||
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import { buildEquityFromSignals } from '../utils/backtestEquity';
|
||||
import {
|
||||
formatChartTypeKo,
|
||||
@@ -210,6 +211,8 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) {
|
||||
|
||||
useEffect(() => { void fetchAll(); }, [fetchAll]);
|
||||
|
||||
usePageRefresh('backtest', () => { void fetchAll(); });
|
||||
|
||||
useEffect(() => {
|
||||
const onTradesChanged = () => { void refreshLiveTrades(); };
|
||||
window.addEventListener(PAPER_TRADES_CHANGED_EVENT, onTradesChanged);
|
||||
|
||||
@@ -16,6 +16,7 @@ import DashboardSegmentBar from './dashboard/DashboardSegmentBar';
|
||||
import DashboardSparkChart from './dashboard/DashboardSparkChart';
|
||||
import DashboardIndicatorBar from './dashboard/DashboardIndicatorBar';
|
||||
import DashboardSystemCard from './dashboard/DashboardSystemCard';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import '../styles/strategyEditorTheme.css';
|
||||
import '../styles/dashboardCommand.css';
|
||||
|
||||
@@ -110,6 +111,8 @@ const DashboardPage: React.FC<Props> = ({ theme, onGoChart }) => {
|
||||
return () => window.clearInterval(id);
|
||||
}, [refresh]);
|
||||
|
||||
usePageRefresh('dashboard', refresh);
|
||||
|
||||
const mon = data?.systemMonitor;
|
||||
const app = data?.app as Record<string, unknown> | undefined;
|
||||
const health = mon ? healthLevel(mon) : 'yellow';
|
||||
|
||||
@@ -28,6 +28,7 @@ interface NotifyTopMenuBarProps {
|
||||
onFullscreen?: () => void;
|
||||
onOpenFloatingWidgets?: () => void;
|
||||
floatingWidgetCount?: number;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
export const NotifyTopMenuBar: React.FC<NotifyTopMenuBarProps> = props => {
|
||||
|
||||
@@ -35,6 +35,7 @@ import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
|
||||
import { loadVirtualSession, loadVirtualTargets } from '../utils/virtualTradingStorage';
|
||||
import { resolveVirtualTargetStrategyId } from '../utils/virtualTargetStrategy';
|
||||
import { formatVirtualTargetOptionLabel } from '../utils/virtualTargetNames';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
|
||||
type HistoryTab = 'open' | 'recent' | 'ledger';
|
||||
type CenterTab = 'invest' | 'chart';
|
||||
@@ -152,6 +153,8 @@ const PaperTradingPage: React.FC<Props> = ({
|
||||
|
||||
useEffect(() => { if (refreshKey > 0) void loadData(); }, [refreshKey, loadData]);
|
||||
|
||||
usePageRefresh('paper', () => { void loadData(); });
|
||||
|
||||
useEffect(() => {
|
||||
if (initialLoading) return;
|
||||
setVirtualTargets(loadVirtualTargets());
|
||||
|
||||
@@ -28,6 +28,7 @@ import StrategyEvaluationStrategyPanel from './strategyEvaluation/StrategyEvalua
|
||||
import StrategyEvaluationIndicatorPalettePanel from './strategyEvaluation/StrategyEvaluationIndicatorPalettePanel';
|
||||
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
||||
import StrategyEvaluationMarketTab from './strategyEvaluation/StrategyEvaluationMarketTab';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
|
||||
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
||||
import { readStoredSize, readStoredBool, storeBool, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
|
||||
@@ -375,6 +376,11 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
void refreshStrategies();
|
||||
}, [refreshStrategies]);
|
||||
|
||||
usePageRefresh('strategy-evaluation', () => {
|
||||
void refreshStrategies();
|
||||
setParamsRevision(v => v + 1);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedStrategyId) {
|
||||
setSelectedStrategy(null);
|
||||
|
||||
@@ -50,6 +50,8 @@ interface TopMenuBarProps {
|
||||
onOpenFloatingWidgets?: () => void;
|
||||
/** 실행 중인 플로팅 위젯 창 수 */
|
||||
floatingWidgetCount?: number;
|
||||
/** 데스크톱 — 현재 화면 데이터 새로고침 */
|
||||
onRefresh?: () => void;
|
||||
/** 메뉴별 접근 허용 (없으면 전체 표시) */
|
||||
menuPermissions?: Record<string, boolean>;
|
||||
}
|
||||
@@ -116,6 +118,15 @@ const IcDesktopUpdate = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcRefresh = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M2 8a6 6 0 0 1 10.2-4.2"/>
|
||||
<polyline points="12,2 12,5.5 8.5,5.5"/>
|
||||
<path d="M14 8a6 6 0 0 1-10.2 4.2"/>
|
||||
<polyline points="4,14 4,10.5 7.5,10.5"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcNotify = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M8 1.5a4.5 4.5 0 0 0-4.5 4.5c0 3.5-1 4.5-1.5 4.5h12c-.5 0-1.5-1-1.5-4.5A4.5 4.5 0 0 0 8 1.5z"/>
|
||||
@@ -293,10 +304,12 @@ export const TopMenuBar = memo(function TopMenuBar({
|
||||
onFullscreen,
|
||||
onOpenFloatingWidgets,
|
||||
floatingWidgetCount = 0,
|
||||
onRefresh,
|
||||
menuPermissions,
|
||||
}: TopMenuBarProps) {
|
||||
const [isFullscreen, setIsFullscreen] = useState(() => !!document.fullscreenElement);
|
||||
const [desktopUpdateOpen, setDesktopUpdateOpen] = useState(false);
|
||||
const [refreshBusy, setRefreshBusy] = useState(false);
|
||||
const desktopClient = isDesktop();
|
||||
const appVersion = useAppTitleBarVersion();
|
||||
|
||||
@@ -340,6 +353,26 @@ export const TopMenuBar = memo(function TopMenuBar({
|
||||
|
||||
{/* 우측 영역 */}
|
||||
<div className="tmb-right">
|
||||
{desktopClient && onRefresh && (
|
||||
<button
|
||||
type="button"
|
||||
className={`tmb-refresh-btn${refreshBusy ? ' busy' : ''}`}
|
||||
onClick={() => {
|
||||
if (refreshBusy) return;
|
||||
setRefreshBusy(true);
|
||||
try {
|
||||
onRefresh();
|
||||
} finally {
|
||||
window.setTimeout(() => setRefreshBusy(false), 600);
|
||||
}
|
||||
}}
|
||||
title="현재 화면 새로고침"
|
||||
aria-label="현재 화면 새로고침"
|
||||
>
|
||||
<IcRefresh />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{onOpenFloatingWidgets && (
|
||||
<div className="tmb-floating-widget-wrap">
|
||||
<button
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type StrategyDto,
|
||||
} from '../utils/backendApi';
|
||||
import { prefetchNotificationStrategies, resolveNotificationStrategy } from '../utils/resolveNotificationStrategy';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import { normalizeMarketCode } from '../utils/strategyHydrate';
|
||||
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
|
||||
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
||||
@@ -182,6 +183,11 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
||||
void refreshPaperData();
|
||||
}, [refreshPaperData]);
|
||||
|
||||
usePageRefresh('notifications', () => {
|
||||
void refreshHistory();
|
||||
void refreshPaperData();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type TrendSearchAppSettings,
|
||||
} from '../utils/trendSearchAppSettings';
|
||||
import { autoAddTrendSearchTargets } from '../utils/trendSearchAutoAddTargets';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import '../styles/trendSearchDashboard.css';
|
||||
import '../styles/virtualTradingDashboard.css';
|
||||
|
||||
@@ -136,6 +137,8 @@ const TrendSearchPage: React.FC<Props> = ({
|
||||
}
|
||||
}, [applyResults]);
|
||||
|
||||
usePageRefresh('trend-search', () => { void loadFromDb(); });
|
||||
|
||||
const runScan = useCallback(async () => {
|
||||
setSearching(true);
|
||||
try {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type VerificationIssueDto,
|
||||
} from '../utils/verificationBoardApi';
|
||||
import { prepareAttachmentsForUpload } from '../utils/verificationAttachmentFiles';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import '../styles/verificationBoard.css';
|
||||
|
||||
interface Props {
|
||||
@@ -79,6 +80,8 @@ const VerificationBoardPage: React.FC<Props> = ({
|
||||
void reload(stageFilter);
|
||||
}, [stageFilter]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
usePageRefresh('verification-board', () => { void reload(stageFilter); });
|
||||
|
||||
useEffect(() => {
|
||||
if (focusIssueId == null) return;
|
||||
setSelectedId(focusIssueId);
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import type { VirtualCardDisplayMode } from './virtual/VirtualTargetCard';
|
||||
import { useVirtualIndicatorSnapshots } from '../hooks/useVirtualIndicatorSnapshots';
|
||||
import { PAPER_TRADES_CHANGED_EVENT } from '../utils/paperTradeEvents';
|
||||
import { usePageRefresh } from '../utils/pageRefreshEvents';
|
||||
import { useVirtualTargetLiveStatus } from '../hooks/useVirtualTargetLiveStatus';
|
||||
import type { ChartRealtimeSource } from '../hooks/useChartRealtimeData';
|
||||
import {
|
||||
@@ -261,6 +262,8 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
if (opts?.focusHistory) setRightTab('history');
|
||||
}, [onPaperOrderFilled]);
|
||||
|
||||
usePageRefresh('virtual', () => { void refreshPaperData(); });
|
||||
|
||||
const handleGlobalDisplayModeChange = useCallback((mode: VirtualCardDisplayMode) => {
|
||||
setGlobalDisplayMode(mode);
|
||||
}, []);
|
||||
|
||||
@@ -12,6 +12,8 @@ interface Options {
|
||||
/** 직접 지정할 페치 봉 수 — UPBIT_DIRECT 전용 (deepObvHistory·WARMUP_COUNT 무시) */
|
||||
fetchCount?: number;
|
||||
source?: ChartRealtimeSource;
|
||||
/** 증가 시 초기 캔들 REST 재로드 (화면 새로고침) */
|
||||
refreshToken?: number;
|
||||
}
|
||||
|
||||
export function useChartRealtimeData(
|
||||
@@ -25,6 +27,7 @@ export function useChartRealtimeData(
|
||||
onNewCandle: opts.onNewCandle,
|
||||
enabled: opts.enabled !== false && useStomp,
|
||||
deepObvHistory: opts.deepObvHistory,
|
||||
refreshToken: opts.refreshToken,
|
||||
});
|
||||
const upbit = useUpbitData(market, timeframe, {
|
||||
onTickUpdate: opts.onTickUpdate,
|
||||
@@ -32,6 +35,7 @@ export function useChartRealtimeData(
|
||||
enabled: opts.enabled !== false && !useStomp,
|
||||
deepObvHistory: opts.deepObvHistory,
|
||||
fetchCount: opts.fetchCount,
|
||||
refreshToken: opts.refreshToken,
|
||||
});
|
||||
return useStomp ? stomp : upbit;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ interface Options {
|
||||
enabled?: boolean;
|
||||
/** OBV 누적 정확도 — 더 많은 초기 봉 로드 */
|
||||
deepObvHistory?: boolean;
|
||||
/** 화면 새로고침 — REST 초기 로드 재실행 */
|
||||
refreshToken?: number;
|
||||
}
|
||||
|
||||
interface Return {
|
||||
@@ -141,7 +143,7 @@ export function useStompChartData(
|
||||
setIsLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [market, candleType, timeframe, opts.enabled, opts.deepObvHistory]);
|
||||
}, [market, candleType, timeframe, opts.enabled, opts.deepObvHistory, opts.refreshToken]);
|
||||
|
||||
const syncBarsToReactIfNeeded = useCallback((marketId: string) => {
|
||||
if (!bootstrapFromStompRef.current) return;
|
||||
|
||||
@@ -28,6 +28,8 @@ interface UpbitDataOptions {
|
||||
deepObvHistory?: boolean;
|
||||
/** 직접 지정할 페치 봉 수 (deepObvHistory·WARMUP_COUNT 무시) */
|
||||
fetchCount?: number;
|
||||
/** 화면 새로고침 — REST 초기 로드 재실행 */
|
||||
refreshToken?: number;
|
||||
}
|
||||
|
||||
interface UseUpbitDataReturn {
|
||||
@@ -73,6 +75,7 @@ export function useUpbitData(
|
||||
// ── 1. REST API: 초기 캔들 로딩 (표시용 + 워밍업 합산) ─────────────────
|
||||
// 이전 market 값을 기억해 종목 변경 시에만 캐시를 무효화한다.
|
||||
const prevMarketRef = useRef<string>('');
|
||||
const prevRefreshTokenRef = useRef(opts.refreshToken ?? 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (opts.enabled === false) return;
|
||||
@@ -85,15 +88,19 @@ export function useUpbitData(
|
||||
setLatestBar(null);
|
||||
barsRef.current = [];
|
||||
|
||||
const refreshToken = opts.refreshToken ?? 0;
|
||||
const bustCache = refreshToken > 0 && refreshToken !== prevRefreshTokenRef.current;
|
||||
prevRefreshTokenRef.current = refreshToken;
|
||||
|
||||
// 종목(market)이 변경된 경우에는 캐시를 무효화해 신규 요청을 강제한다.
|
||||
// 타임프레임만 바뀐 경우도 캐시 키가 다르므로 자동으로 새 요청이 발생한다.
|
||||
const marketChanged = prevMarketRef.current !== '' && prevMarketRef.current !== market;
|
||||
prevMarketRef.current = market;
|
||||
if (bustCache) invalidateMarketCache(market);
|
||||
|
||||
const fetchCount = opts.fetchCount
|
||||
?? (opts.deepObvHistory ? OBV_DEEP_HISTORY : DISPLAY_COUNT + WARMUP_COUNT);
|
||||
|
||||
fetchUpbitCandlesCached(market, timeframe, fetchCount, marketChanged)
|
||||
fetchUpbitCandlesCached(market, timeframe, fetchCount, marketChanged || bustCache)
|
||||
.then(newBars => {
|
||||
if (cancelled) return;
|
||||
barsRef.current = newBars;
|
||||
@@ -109,7 +116,7 @@ export function useUpbitData(
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [market, timeframe, opts.enabled, opts.deepObvHistory, opts.fetchCount]);
|
||||
}, [market, timeframe, opts.enabled, opts.deepObvHistory, opts.fetchCount, opts.refreshToken]);
|
||||
|
||||
// ── 2. WebSocket: 실시간 체결 구독 ────────────────────────────────────
|
||||
const handleMessage = useCallback((msg: UpbitMessage) => {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { MenuPage } from '../components/TopMenuBar';
|
||||
|
||||
export const PAGE_REFRESH_EVENT = 'gc_page_refresh';
|
||||
|
||||
export type PageRefreshDetail = {
|
||||
page: MenuPage;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
export function dispatchPageRefresh(page: MenuPage): void {
|
||||
window.dispatchEvent(new CustomEvent<PageRefreshDetail>(PAGE_REFRESH_EVENT, {
|
||||
detail: { page, ts: Date.now() },
|
||||
}));
|
||||
}
|
||||
|
||||
/** 현재 메뉴(또는 지정 목록)에서 새로고침 이벤트 수신 */
|
||||
export function usePageRefresh(
|
||||
page: MenuPage | readonly MenuPage[],
|
||||
onRefresh: () => void | Promise<void>,
|
||||
): void {
|
||||
const handlerRef = useRef(onRefresh);
|
||||
handlerRef.current = onRefresh;
|
||||
|
||||
useEffect(() => {
|
||||
const pages = Array.isArray(page) ? page : [page];
|
||||
const listener = (e: Event) => {
|
||||
const detail = (e as CustomEvent<PageRefreshDetail>).detail;
|
||||
if (!detail || !pages.includes(detail.page)) return;
|
||||
void handlerRef.current();
|
||||
};
|
||||
window.addEventListener(PAGE_REFRESH_EVENT, listener);
|
||||
return () => window.removeEventListener(PAGE_REFRESH_EVENT, listener);
|
||||
}, [page]);
|
||||
}
|
||||
Reference in New Issue
Block a user