frontend 속도 개선
This commit is contained in:
+112
-46
@@ -34,14 +34,33 @@ import { clearAdminUnlock } from './utils/adminUnlock';
|
||||
import type { LoginResponse } from './utils/backendApi';
|
||||
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
|
||||
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
|
||||
import { ChartWorkspaceRoot } from './chart/ChartWorkspaceRoot';
|
||||
|
||||
const LAST_MENU_KEY = 'gc_last_menu';
|
||||
const CHART_ONLY_INITIAL: ReadonlySet<MenuPage> = new Set(['dashboard', 'settings', 'verification-board']);
|
||||
|
||||
function loadLastMenu(): MenuPage {
|
||||
try {
|
||||
const saved = localStorage.getItem(LAST_MENU_KEY) as MenuPage | null;
|
||||
// 가상매매·투자관리 등 로그인이 필요한 메뉴는 첫 로드 시 복원하지 않음
|
||||
if (saved && saved !== 'chart') return saved;
|
||||
} catch { /* localStorage 불가 */ }
|
||||
return 'chart';
|
||||
}
|
||||
|
||||
function saveLastMenu(page: MenuPage): void {
|
||||
try { localStorage.setItem(LAST_MENU_KEY, page); } catch { /* no-op */ }
|
||||
}
|
||||
import { useChartWorkspaceContext } from './chart/ChartWorkspaceContext';
|
||||
|
||||
const ChartWorkspaceRoot = lazy(() =>
|
||||
import('./chart/ChartWorkspaceRoot').then(m => ({ default: m.ChartWorkspaceRoot }))
|
||||
);
|
||||
import './App.css';
|
||||
import './styles/appPopup.css';
|
||||
import './styles/paperDashboard.css';
|
||||
import './styles/tradeRightPanel.css';
|
||||
import './styles/virtualTradingDashboard.css';
|
||||
import './styles/backtestDashboard.css';
|
||||
// virtualTradingDashboard.css는 VirtualTradingPage.tsx에서 import
|
||||
// paperDashboard.css는 PaperTradingPage.tsx에서 import
|
||||
// tradeRightPanel.css는 ChartWorkspaceView.tsx에서 import
|
||||
// backtestDashboard.css는 BacktestHistoryPage.tsx, AnalysisReportPage.tsx에서 import
|
||||
// appPopup.css는 AppPopup.tsx에서 import
|
||||
|
||||
const DashboardPage = lazy(() => import('./components/DashboardPage'));
|
||||
const StrategyPage = lazy(() => import('./components/StrategyPage'));
|
||||
@@ -91,6 +110,8 @@ function AppMainContent({
|
||||
indicatorSettingsRevision,
|
||||
handleLoginSuccess,
|
||||
handleLogout,
|
||||
goToMarketChart,
|
||||
chartSlot,
|
||||
}: {
|
||||
theme: Theme;
|
||||
onThemeToggle: () => void;
|
||||
@@ -123,19 +144,21 @@ function AppMainContent({
|
||||
indicatorSettingsRevision: number;
|
||||
handleLoginSuccess: (res: LoginResponse) => void;
|
||||
handleLogout: () => void;
|
||||
/** 차트로 이동하는 App 레벨 콜백 */
|
||||
goToMarketChart: (market: string) => void;
|
||||
/** 차트 화면 슬롯 (lazy ChartWorkspaceRoot) */
|
||||
chartSlot: React.ReactNode;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const {
|
||||
symbol,
|
||||
indicators,
|
||||
goToMarketChart,
|
||||
refreshPaperAccount,
|
||||
handlePaperOrderFilled,
|
||||
paperRefreshKey,
|
||||
mode,
|
||||
marketTickers,
|
||||
settingsPageProps,
|
||||
chartScreen,
|
||||
} = useChartWorkspaceContext();
|
||||
|
||||
const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true;
|
||||
@@ -405,7 +428,7 @@ function AppMainContent({
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{chartScreen}
|
||||
{chartSlot}
|
||||
|
||||
<AppNotificationLayer
|
||||
menuPage={menuPage}
|
||||
@@ -451,6 +474,48 @@ function App() {
|
||||
}, []);
|
||||
const [appDownloadOpen, setAppDownloadOpen] = useState(false);
|
||||
|
||||
const { defaults: appDefaults, isLoaded: appSettingsLoaded, save: saveAppDef } = useAppSettings(sessionKey);
|
||||
const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey);
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(loadLocalTheme);
|
||||
const [menuPage, setMenuPage] = useState<MenuPage>(loadLastMenu);
|
||||
const [settingsInitialCategory, setSettingsInitialCategory] = useState<SettingsCategoryId | undefined>();
|
||||
const [verificationFocusIssueId, setVerificationFocusIssueId] = useState<number | null>(null);
|
||||
|
||||
// 차트 lazy mount 관련 상태: 처음 chart 메뉴 진입 시 마운트, 이후 유지
|
||||
const [chartEverActivated, setChartEverActivated] = useState(() => loadLastMenu() === 'chart');
|
||||
const [pendingChartMarket, setPendingChartMarket] = useState<string | null>(null);
|
||||
|
||||
// menuPage 변경 시 localStorage 저장 + chart 활성화 추적
|
||||
const setMenuPagePersist = useCallback((page: MenuPage) => {
|
||||
saveLastMenu(page);
|
||||
setMenuPage(page);
|
||||
if (page === 'chart') setChartEverActivated(true);
|
||||
}, []);
|
||||
|
||||
// 외부(대시보드·알림 등)에서 특정 마켓으로 차트 이동 요청
|
||||
const handleGoToMarketChart = useCallback((market: string) => {
|
||||
setPendingChartMarket(market);
|
||||
setMenuPagePersist('chart');
|
||||
setChartEverActivated(true);
|
||||
}, [setMenuPagePersist]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuPage === 'chart') setChartEverActivated(true);
|
||||
}, [menuPage]);
|
||||
|
||||
// 지표 설정: 차트·전략·알림 관련 페이지 첫 진입 시 로드 (이후 캐시 유지)
|
||||
const INDICATOR_SETTINGS_PAGES: MenuPage[] = ['chart', 'strategy-editor', 'notifications', 'strategy', 'backtest', 'analysis-report', 'settings'];
|
||||
const [indicatorSettingsEnabled, setIndicatorSettingsEnabled] = useState(
|
||||
() => INDICATOR_SETTINGS_PAGES.includes(menuPage),
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!indicatorSettingsEnabled && INDICATOR_SETTINGS_PAGES.includes(menuPage)) {
|
||||
setIndicatorSettingsEnabled(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [menuPage, indicatorSettingsEnabled]);
|
||||
|
||||
const {
|
||||
getParams: getIndicatorParams,
|
||||
saveParams: saveIndicatorParams,
|
||||
@@ -458,20 +523,12 @@ function App() {
|
||||
saveVisual: saveIndicatorVisual,
|
||||
saveAllIndicatorDefaults,
|
||||
settingsRevision: indicatorSettingsRevision,
|
||||
} = useIndicatorSettings(sessionKey);
|
||||
} = useIndicatorSettings(sessionKey, indicatorSettingsEnabled);
|
||||
const getParams = getIndicatorParams;
|
||||
const saveParams = saveIndicatorParams;
|
||||
const getVisualConfig = getIndicatorVisual;
|
||||
const saveVisual = saveIndicatorVisual;
|
||||
|
||||
const { defaults: appDefaults, isLoaded: appSettingsLoaded, save: saveAppDef } = useAppSettings(sessionKey);
|
||||
const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey);
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(loadLocalTheme);
|
||||
const [menuPage, setMenuPage] = useState<MenuPage>('chart');
|
||||
const [settingsInitialCategory, setSettingsInitialCategory] = useState<SettingsCategoryId | undefined>();
|
||||
const [verificationFocusIssueId, setVerificationFocusIssueId] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuPage !== 'settings') setSettingsInitialCategory(undefined);
|
||||
}, [menuPage]);
|
||||
@@ -532,22 +589,22 @@ function App() {
|
||||
}
|
||||
if (page === 'notifications' && !canMenu('notifications')) return;
|
||||
if (page !== 'notifications' && !canMenu(page)) {
|
||||
setMenuPage(firstAllowedTopMenu(menuPermissions));
|
||||
setMenuPagePersist(firstAllowedTopMenu(menuPermissions));
|
||||
return;
|
||||
}
|
||||
setMenuPage(page);
|
||||
}, [canMenu, menuPermissions, formalLogin, requireFormalLogin]);
|
||||
setMenuPagePersist(page);
|
||||
}, [canMenu, menuPermissions, formalLogin, requireFormalLogin, setMenuPagePersist]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuPermsLoaded) return;
|
||||
if (menuPage === 'notifications') {
|
||||
if (!canMenu('notifications')) setMenuPage(firstAllowedTopMenu(menuPermissions));
|
||||
if (!canMenu('notifications')) setMenuPagePersist(firstAllowedTopMenu(menuPermissions));
|
||||
return;
|
||||
}
|
||||
if (!canMenu(menuPage)) {
|
||||
setMenuPage(firstAllowedTopMenu(menuPermissions));
|
||||
setMenuPagePersist(firstAllowedTopMenu(menuPermissions));
|
||||
}
|
||||
}, [menuPage, menuPermsLoaded, menuPermissions, canMenu]);
|
||||
}, [menuPage, menuPermsLoaded, menuPermissions, canMenu, setMenuPagePersist]);
|
||||
|
||||
const handleThemeToggle = useCallback(() => {
|
||||
setTheme(t => {
|
||||
@@ -580,32 +637,40 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
// 차트 슬롯: 처음 chart 페이지 진입 시 마운트, 이후 visible 토글
|
||||
const chartSlot = chartEverActivated ? (
|
||||
<Suspense fallback={null}>
|
||||
<ChartWorkspaceRoot
|
||||
visible={menuPage === 'chart'}
|
||||
pendingChartMarket={pendingChartMarket}
|
||||
onPendingChartMarketConsumed={() => setPendingChartMarket(null)}
|
||||
menuPage={menuPage}
|
||||
sessionKey={sessionKey}
|
||||
formalLogin={formalLogin}
|
||||
requireFormalLogin={requireFormalLogin}
|
||||
theme={theme}
|
||||
onThemeToggle={handleThemeToggle}
|
||||
appDefaults={appDefaults as unknown as import('./utils/backendApi').AppSettingsDto}
|
||||
appSettingsLoaded={appSettingsLoaded}
|
||||
saveAppDef={saveAppDef}
|
||||
getParams={getParams}
|
||||
saveParams={saveParams}
|
||||
getVisualConfig={getVisualConfig}
|
||||
saveVisual={saveVisual}
|
||||
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
|
||||
indicatorSettingsRevision={indicatorSettingsRevision}
|
||||
onOpenNotificationsPage={openNotificationsPage}
|
||||
onOpenAppDownload={() => setAppDownloadOpen(true)}
|
||||
/>
|
||||
</Suspense>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<TradeNotificationProvider
|
||||
popupEnabled={appDefaults.tradeAlertPopup ?? true}
|
||||
soundEnabled={appDefaults.tradeAlertSoundEnabled ?? true}
|
||||
soundId={appDefaults.tradeAlertSound ?? 'bell'}
|
||||
settingsSessionKey={authUser?.userId ?? 0}
|
||||
>
|
||||
<ChartWorkspaceRoot
|
||||
visible={menuPage === 'chart'}
|
||||
menuPage={menuPage}
|
||||
sessionKey={sessionKey}
|
||||
formalLogin={formalLogin}
|
||||
requireFormalLogin={requireFormalLogin}
|
||||
theme={theme}
|
||||
onThemeToggle={handleThemeToggle}
|
||||
appDefaults={appDefaults as unknown as import('./utils/backendApi').AppSettingsDto}
|
||||
appSettingsLoaded={appSettingsLoaded}
|
||||
saveAppDef={saveAppDef}
|
||||
getParams={getParams}
|
||||
saveParams={saveParams}
|
||||
getVisualConfig={getVisualConfig}
|
||||
saveVisual={saveVisual}
|
||||
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
|
||||
indicatorSettingsRevision={indicatorSettingsRevision}
|
||||
onOpenNotificationsPage={openNotificationsPage}
|
||||
onOpenAppDownload={() => setAppDownloadOpen(true)}
|
||||
>
|
||||
<AppMainContent
|
||||
theme={theme}
|
||||
@@ -620,7 +685,7 @@ function App() {
|
||||
appDownloadOpen={appDownloadOpen}
|
||||
setAppDownloadOpen={setAppDownloadOpen}
|
||||
menuPage={menuPage}
|
||||
setMenuPage={setMenuPage}
|
||||
setMenuPage={setMenuPagePersist}
|
||||
guardedSetMenuPage={guardedSetMenuPage}
|
||||
settingsInitialCategory={settingsInitialCategory}
|
||||
setSettingsInitialCategory={setSettingsInitialCategory}
|
||||
@@ -639,8 +704,9 @@ function App() {
|
||||
indicatorSettingsRevision={indicatorSettingsRevision}
|
||||
handleLoginSuccess={handleLoginSuccess}
|
||||
handleLogout={handleLogout}
|
||||
goToMarketChart={handleGoToMarketChart}
|
||||
chartSlot={chartSlot}
|
||||
/>
|
||||
</ChartWorkspaceRoot>
|
||||
</TradeNotificationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import React, { createContext, useContext } from 'react';
|
||||
import type { ChartMode, IndicatorConfig } from '../types';
|
||||
import type { TickerData } from '../hooks/useMarketTicker';
|
||||
import type { SettingsPageChartBindings } from './useChartWorkspace';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
||||
import { DEFAULT_CHART_TIME_FORMAT } from '../utils/chartTimeFormat';
|
||||
import { DEFAULT_TRADE_ALERT_TIME_FORMAT } from '../utils/tradeAlertTimeFormat';
|
||||
|
||||
export interface ChartWorkspaceSharedState {
|
||||
symbol: string;
|
||||
@@ -21,12 +24,66 @@ export interface ChartWorkspaceSharedState {
|
||||
settingsPageProps: SettingsPageChartBindings;
|
||||
}
|
||||
|
||||
export interface ChartWorkspaceContextValue extends ChartWorkspaceSharedState {
|
||||
/** 실시간 차트 화면 — `.app` flex 레이아웃 안에서 렌더해야 좌·우 패널·메뉴가 정상 표시됨 */
|
||||
chartScreen: React.ReactNode;
|
||||
}
|
||||
export type ChartWorkspaceContextValue = ChartWorkspaceSharedState;
|
||||
|
||||
const ChartWorkspaceContext = createContext<ChartWorkspaceContextValue | null>(null);
|
||||
const NULL_SETTINGS_PAGE_PROPS: SettingsPageChartBindings = {
|
||||
magnetMode: 'off',
|
||||
onMagnetMode: () => {},
|
||||
liveStrategies: [],
|
||||
onClearLiveMarkers: () => {},
|
||||
liveSettings: { market: '', strategyId: null, isLiveCheck: false, executionType: 'CANDLE_CLOSE', positionMode: 'LONG_ONLY' },
|
||||
onLiveSettingsChange: () => {},
|
||||
watchlistCount: 0,
|
||||
monitoredMarkets: [],
|
||||
virtualDriven: false,
|
||||
chartIndicators: [],
|
||||
onAddIndicatorToChart: () => {},
|
||||
onRemoveIndicatorFromChart: () => {},
|
||||
onIndicatorListOrderChange: () => {},
|
||||
chartCandleAreaPriceLabels: false,
|
||||
onChartCandleAreaPriceLabels: () => {},
|
||||
chartSeriesPriceLabels: false,
|
||||
onChartSeriesPriceLabels: () => {},
|
||||
chartVolumeVisible: true,
|
||||
onChartVolumeVisible: () => {},
|
||||
chartLiveReceiveHighlight: true,
|
||||
onChartLiveReceiveHighlight: () => {},
|
||||
chartLegendOptions: undefined,
|
||||
onChartLegendOptionsChange: () => {},
|
||||
chartCrosshairInfoVisible: true,
|
||||
onChartCrosshairInfoVisible: () => {},
|
||||
chartHoverToolbarVisible: true,
|
||||
onChartHoverToolbarVisible: () => {},
|
||||
chartPaneSeparator: undefined,
|
||||
onChartPaneSeparatorChange: () => {},
|
||||
onPaperAccountReset: async () => {},
|
||||
onDisplayTimezoneChange: () => {},
|
||||
onChartTimeFormatChange: () => {},
|
||||
onTradeAlertTimeFormatChange: () => {},
|
||||
displayTimezone: DEFAULT_DISPLAY_TIMEZONE,
|
||||
chartTimeFormat: DEFAULT_CHART_TIME_FORMAT,
|
||||
tradeAlertTimeFormat: DEFAULT_TRADE_ALERT_TIME_FORMAT,
|
||||
};
|
||||
|
||||
export const NULL_CHART_WORKSPACE_CONTEXT: ChartWorkspaceContextValue = {
|
||||
symbol: 'KRW-BTC',
|
||||
indicators: [],
|
||||
goToMarketChart: () => {},
|
||||
refreshPaperAccount: async () => {},
|
||||
paperCashBalance: 0,
|
||||
paperCoinQty: 0,
|
||||
handlePaperOrderFilled: () => {},
|
||||
paperRefreshKey: 0,
|
||||
mode: 'chart',
|
||||
priorityMarkets: [],
|
||||
marketPanelOpen: false,
|
||||
marketTickers: new Map(),
|
||||
marketLoading: false,
|
||||
usdRate: 0,
|
||||
settingsPageProps: NULL_SETTINGS_PAGE_PROPS,
|
||||
};
|
||||
|
||||
const ChartWorkspaceContext = createContext<ChartWorkspaceContextValue>(NULL_CHART_WORKSPACE_CONTEXT);
|
||||
|
||||
export function ChartWorkspaceProvider({
|
||||
value,
|
||||
@@ -43,9 +100,5 @@ export function ChartWorkspaceProvider({
|
||||
}
|
||||
|
||||
export function useChartWorkspaceContext(): ChartWorkspaceContextValue {
|
||||
const ctx = useContext(ChartWorkspaceContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useChartWorkspaceContext must be used within ChartWorkspaceProvider');
|
||||
}
|
||||
return ctx;
|
||||
return useContext(ChartWorkspaceContext);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo, useEffect, useRef } from 'react';
|
||||
import { LiveSignalNotifier } from '../components/LiveSignalNotifier';
|
||||
import { ChartWorkspaceProvider } from './ChartWorkspaceContext';
|
||||
import { ChartWorkspaceView } from './ChartWorkspaceView';
|
||||
@@ -7,12 +7,14 @@ import type { ChartWorkspaceBridge } from './chartWorkspaceBridge';
|
||||
|
||||
export interface ChartWorkspaceRootProps extends UseChartWorkspaceParams {
|
||||
visible: boolean;
|
||||
children: React.ReactNode;
|
||||
/** 외부에서 이동 요청된 마켓 (알림·대시보드 등에서 차트로 이동 시) */
|
||||
pendingChartMarket?: string | null;
|
||||
onPendingChartMarketConsumed?: () => void;
|
||||
}
|
||||
|
||||
export const ChartWorkspaceRoot = forwardRef<ChartWorkspaceBridge, ChartWorkspaceRootProps>(
|
||||
function ChartWorkspaceRoot(props, ref) {
|
||||
const { visible, children, ...workspaceParams } = props;
|
||||
const { visible, pendingChartMarket, onPendingChartMarketConsumed, ...workspaceParams } = props;
|
||||
const {
|
||||
bridge,
|
||||
contextValue,
|
||||
@@ -23,19 +25,31 @@ export const ChartWorkspaceRoot = forwardRef<ChartWorkspaceBridge, ChartWorkspac
|
||||
|
||||
useImperativeHandle(ref, () => bridge, [bridge]);
|
||||
|
||||
// 외부에서 마켓 이동 요청 처리
|
||||
const pendingRef = useRef(pendingChartMarket);
|
||||
pendingRef.current = pendingChartMarket;
|
||||
const consumedRef = useRef(onPendingChartMarketConsumed);
|
||||
consumedRef.current = onPendingChartMarketConsumed;
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingChartMarket) {
|
||||
bridge.goToMarket(pendingChartMarket);
|
||||
consumedRef.current?.();
|
||||
}
|
||||
// bridge.goToMarket is stable (useCallback), pendingChartMarket is the trigger
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pendingChartMarket]);
|
||||
|
||||
const providerValue = useMemo(() => ({
|
||||
...contextValue,
|
||||
chartScreen: (
|
||||
<div style={{ display: visible ? 'contents' : 'none' }}>
|
||||
<ChartWorkspaceView {...viewProps} />
|
||||
</div>
|
||||
),
|
||||
}), [contextValue, visible, viewProps]);
|
||||
}), [contextValue]);
|
||||
|
||||
return (
|
||||
<ChartWorkspaceProvider value={providerValue}>
|
||||
<LiveSignalNotifier ref={liveNotifierRef} {...liveNotifierProps} />
|
||||
{children}
|
||||
<div style={{ display: visible ? 'contents' : 'none' }}>
|
||||
<ChartWorkspaceView {...viewProps} />
|
||||
</div>
|
||||
</ChartWorkspaceProvider>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '../styles/tradeRightPanel.css';
|
||||
import React from 'react';
|
||||
import DrawingToolbar from '../components/DrawingToolbar';
|
||||
import BottomBar from '../components/BottomBar';
|
||||
|
||||
@@ -1175,6 +1175,8 @@ export function useChartWorkspace({
|
||||
|
||||
// ── Watchlist 가격 맵 ──────────────────────────────────────────────────
|
||||
const priceMap = useMemo(() => {
|
||||
// 시세 패널이 열려 있고 ticker가 활성 상태일 때만 계산 (데모 심볼용)
|
||||
if (!showMarketPanel || !tickerFeedActive) return {} as Record<string, { price: number; changePct: number }>;
|
||||
const map: Record<string, { price: number; changePct: number }> = {};
|
||||
for (const sym of watchlist) {
|
||||
const b = isUpbitMarket(sym) ? [] : generateBars(sym, '1D');
|
||||
@@ -1183,7 +1185,7 @@ export function useChartWorkspace({
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [watchlist]);
|
||||
}, [watchlist, showMarketPanel, tickerFeedActive]);
|
||||
|
||||
|
||||
// ── DB 워크스페이스 동기화 ─────────────────────────────────────────────
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* AppPopup — TradeAlertModal(tam-) 기준 공통 팝업 셸
|
||||
* 신규 팝업은 이 컴포넌트를 사용하세요.
|
||||
*/
|
||||
import '../styles/appPopup.css';
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* BacktestHistoryPage — Golden Analysis Command Center (참조 UI 3열)
|
||||
*/
|
||||
import '../styles/backtestDashboard.css';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
loadBacktestResults,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 모의투자 대시보드 — 가상매매와 동일 3열 레이아웃(좌·중앙·우 접기/리사이즈)
|
||||
*/
|
||||
import '../styles/paperDashboard.css';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
loadPaperSummary,
|
||||
|
||||
@@ -250,6 +250,7 @@ export const TopMenuBar = memo(function TopMenuBar({
|
||||
type="button"
|
||||
className={`tmb-nav-btn ${activePage === page ? 'tmb-nav-btn--active' : ''}`}
|
||||
onClick={() => onPage(page)}
|
||||
onMouseEnter={page === 'chart' ? () => { void import('../chart/ChartWorkspaceRoot'); } : undefined}
|
||||
>
|
||||
<span className="tmb-nav-icon">{icon}</span>
|
||||
<span className="tmb-nav-label">{label}</span>
|
||||
|
||||
@@ -120,7 +120,11 @@ export function invalidateIndicatorSettingsCache() {
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function useIndicatorSettings(sessionKey = 0) {
|
||||
/**
|
||||
* @param sessionKey 로그인 세션 키 (변경 시 캐시 무효화 및 재로드)
|
||||
* @param enabled false 이면 DB 로드를 건너뜀 — 차트·전략편집·알림 페이지 진입 시에만 true
|
||||
*/
|
||||
export function useIndicatorSettings(sessionKey = 0, enabled = true) {
|
||||
const [settingsRevision, setSettingsRevision] = useState(0);
|
||||
const [settingsLoaded, setSettingsLoaded] = useState(
|
||||
() => _cache !== null && _visualCache !== null,
|
||||
@@ -130,6 +134,8 @@ export function useIndicatorSettings(sessionKey = 0) {
|
||||
useEffect(() => {
|
||||
const unsub = subscribeIndicatorSettings(() => setSettingsRevision(r => r + 1));
|
||||
|
||||
if (!enabled) return unsub;
|
||||
|
||||
const cacheWarm = _loadedSessionKey === sessionKey
|
||||
&& _cache !== null
|
||||
&& _visualCache !== null;
|
||||
@@ -157,7 +163,7 @@ export function useIndicatorSettings(sessionKey = 0) {
|
||||
setSettingsRevision(r => r + 1);
|
||||
});
|
||||
return unsub;
|
||||
}, [sessionKey]);
|
||||
}, [sessionKey, enabled]);
|
||||
|
||||
// ── 파라미터 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user