From 5871eb24659cf3c5df08651bb9a5efb02d3a2516 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 22 Jun 2026 16:47:20 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8D=B0=EC=8A=A4=ED=81=AC=ED=86=B1=20?= =?UTF-8?q?=EC=95=B1=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.css | 26 ++++++++++++++ frontend/src/App.tsx | 6 ++++ frontend/src/chart/useChartWorkspace.ts | 18 +++++++++- .../src/components/BacktestHistoryPage.tsx | 3 ++ frontend/src/components/DashboardPage.tsx | 3 ++ frontend/src/components/NotifyUiBindings.tsx | 1 + frontend/src/components/PaperTradingPage.tsx | 3 ++ .../src/components/StrategyEvaluationPage.tsx | 6 ++++ frontend/src/components/TopMenuBar.tsx | 33 +++++++++++++++++ .../components/TradeNotificationListPage.tsx | 6 ++++ frontend/src/components/TrendSearchPage.tsx | 3 ++ .../src/components/VerificationBoardPage.tsx | 3 ++ .../src/components/VirtualTradingPage.tsx | 3 ++ frontend/src/hooks/useChartRealtimeData.ts | 4 +++ frontend/src/hooks/useStompChartData.ts | 4 ++- frontend/src/hooks/useUpbitData.ts | 13 +++++-- frontend/src/utils/pageRefreshEvents.ts | 35 +++++++++++++++++++ 17 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 frontend/src/utils/pageRefreshEvents.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index 459959d..4f8ea43 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1155e91..f3f85e6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 ( saveAppDef({ tradeAlertPopupLayout: v })} onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })} onFullscreen={handleFullscreen} + onRefresh={isDesktop() ? handlePageRefresh : undefined} onOpenFloatingWidgets={showFloatingWidgets ? () => setFloatingLayoutPickerOpen(true) : undefined} floatingWidgetCount={floatingWidgetCount} /> diff --git a/frontend/src/chart/useChartWorkspace.ts b/frontend/src/chart/useChartWorkspace.ts index bc7d0e4..4da94ea 100644 --- a/frontend/src/chart/useChartWorkspace.ts +++ b/frontend/src/chart/useChartWorkspace.ts @@ -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(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개, 실시간 수신 후에도 변경 없음) diff --git a/frontend/src/components/BacktestHistoryPage.tsx b/frontend/src/components/BacktestHistoryPage.tsx index 3881d67..0e9d792 100644 --- a/frontend/src/components/BacktestHistoryPage.tsx +++ b/frontend/src/components/BacktestHistoryPage.tsx @@ -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); diff --git a/frontend/src/components/DashboardPage.tsx b/frontend/src/components/DashboardPage.tsx index 88e095c..25274a5 100644 --- a/frontend/src/components/DashboardPage.tsx +++ b/frontend/src/components/DashboardPage.tsx @@ -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 = ({ theme, onGoChart }) => { return () => window.clearInterval(id); }, [refresh]); + usePageRefresh('dashboard', refresh); + const mon = data?.systemMonitor; const app = data?.app as Record | undefined; const health = mon ? healthLevel(mon) : 'yellow'; diff --git a/frontend/src/components/NotifyUiBindings.tsx b/frontend/src/components/NotifyUiBindings.tsx index eba0009..45aee62 100644 --- a/frontend/src/components/NotifyUiBindings.tsx +++ b/frontend/src/components/NotifyUiBindings.tsx @@ -28,6 +28,7 @@ interface NotifyTopMenuBarProps { onFullscreen?: () => void; onOpenFloatingWidgets?: () => void; floatingWidgetCount?: number; + onRefresh?: () => void; } export const NotifyTopMenuBar: React.FC = props => { diff --git a/frontend/src/components/PaperTradingPage.tsx b/frontend/src/components/PaperTradingPage.tsx index 9202891..ecd1263 100644 --- a/frontend/src/components/PaperTradingPage.tsx +++ b/frontend/src/components/PaperTradingPage.tsx @@ -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 = ({ useEffect(() => { if (refreshKey > 0) void loadData(); }, [refreshKey, loadData]); + usePageRefresh('paper', () => { void loadData(); }); + useEffect(() => { if (initialLoading) return; setVirtualTargets(loadVirtualTargets()); diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx index be879df..d09d7f3 100644 --- a/frontend/src/components/StrategyEvaluationPage.tsx +++ b/frontend/src/components/StrategyEvaluationPage.tsx @@ -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); diff --git a/frontend/src/components/TopMenuBar.tsx b/frontend/src/components/TopMenuBar.tsx index c51ca82..a2ba4f5 100644 --- a/frontend/src/components/TopMenuBar.tsx +++ b/frontend/src/components/TopMenuBar.tsx @@ -50,6 +50,8 @@ interface TopMenuBarProps { onOpenFloatingWidgets?: () => void; /** 실행 중인 플로팅 위젯 창 수 */ floatingWidgetCount?: number; + /** 데스크톱 — 현재 화면 데이터 새로고침 */ + onRefresh?: () => void; /** 메뉴별 접근 허용 (없으면 전체 표시) */ menuPermissions?: Record; } @@ -116,6 +118,15 @@ const IcDesktopUpdate = () => ( ); +const IcRefresh = () => ( + + + + + + +); + const IcNotify = () => ( @@ -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({ {/* 우측 영역 */}
+ {desktopClient && onRefresh && ( + + )} + {onOpenFloatingWidgets && (