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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user