frontend 속도 개선

This commit is contained in:
Macbook
2026-06-11 00:14:14 +09:00
parent 0ff1992b64
commit 54fdc3e77a
15 changed files with 522 additions and 71 deletions
+112 -46
View File
@@ -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>
);
}