import React, { useState, useEffect, useCallback, lazy, Suspense } from 'react';
import { useIndicatorSettings } from './hooks/useIndicatorSettings';
import { getAppSettingsCache, useAppSettings } from './hooks/useAppSettings';
import { clampVirtualTargetMax } from './utils/virtualTargetLimits';
import { resolveTrendSearchAppSettings } from './utils/trendSearchAppSettings';
import TopMenuBar, { type MenuPage } from './components/TopMenuBar';
import { TradeNotificationProvider } from './contexts/TradeNotificationContext';
import { VerificationIssueNotificationProvider } from './contexts/VerificationIssueNotificationContext';
import { FormalLoginGate } from './components/FormalLoginGate';
import {
isFormalLogin,
isTradingMenuPage,
FORMAL_LOGIN_REQUIRED_MSG,
} from './utils/tradingAccess';
import { NotifyTopMenuBar } from './components/NotifyUiBindings';
import { AppNotificationLayer } from './components/AppNotificationLayer';
import type { Theme } from './types';
import type { ChartRealtimeSource } from './hooks/useChartRealtimeData';
import { useIsMobile } from './hooks/useMediaQuery';
import LoginModal from './components/LoginModal';
import AppDownloadModal from './components/AppDownloadModal';
import SplashScreen from './components/SplashScreen';
import { getAuthSession, setAuthSession, clearAuthSession, type AuthSession } from './utils/auth';
import { isAppEntered, setAppEntered, clearAppEntered } from './utils/appEntry';
import { syncDocumentTheme } from './utils/documentTheme';
import {
loadLocalTheme,
migrateDbThemeToLocalIfNeeded,
saveLocalTheme,
} from './utils/localThemeStorage';
import { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions';
import { firstAllowedTopMenu, normalizeRole, type SettingsCategoryId } from './utils/permissions';
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';
import { useChartWorkspaceContext } from './chart/ChartWorkspaceContext';
import './App.css';
import './styles/appPopup.css';
import './styles/paperDashboard.css';
import './styles/tradeRightPanel.css';
import './styles/virtualTradingDashboard.css';
import './styles/backtestDashboard.css';
const DashboardPage = lazy(() => import('./components/DashboardPage'));
const StrategyPage = lazy(() => import('./components/StrategyPage'));
const StrategyEditorPage = lazy(() => import('./components/StrategyEditorPage'));
const TradeNotificationListPage = lazy(() => import('./components/TradeNotificationListPage'));
const BacktestHistoryPage = lazy(() => import('./components/BacktestHistoryPage').then(m => ({ default: m.BacktestHistoryPage })));
const AnalysisReportPage = lazy(() => import('./components/analysisReport/AnalysisReportPage').then(m => ({ default: m.AnalysisReportPage })));
const SettingsPage = lazy(() => import('./components/SettingsPage'));
const PaperTradingPage = lazy(() => import('./components/PaperTradingPage'));
const VirtualTradingPage = lazy(() => import('./components/VirtualTradingPage'));
const TrendSearchPage = lazy(() => import('./components/TrendSearchPage'));
const VerificationBoardPage = lazy(() => import('./components/VerificationBoardPage'));
function PageLoadFallback() {
return
화면 로딩 중…
;
}
function AppMainContent({
theme,
onThemeToggle,
sessionKey,
formalLogin,
requireFormalLogin,
authUser,
guestMode,
loginOpen,
setLoginOpen,
appDownloadOpen,
setAppDownloadOpen,
menuPage,
setMenuPage,
guardedSetMenuPage,
settingsInitialCategory,
setSettingsInitialCategory,
verificationFocusIssueId,
setVerificationFocusIssueId,
menuPermissions,
canMenu,
appDefaults,
appSettingsLoaded,
saveAppDef,
getIndicatorParams,
saveIndicatorParams,
getIndicatorVisual,
saveIndicatorVisual,
saveAllIndicatorDefaults,
indicatorSettingsRevision,
handleLoginSuccess,
handleLogout,
}: {
theme: Theme;
onThemeToggle: () => void;
sessionKey: number;
formalLogin: boolean;
requireFormalLogin: () => void;
authUser: AuthSession | null;
guestMode: boolean;
loginOpen: boolean;
setLoginOpen: (v: boolean) => void;
appDownloadOpen: boolean;
setAppDownloadOpen: (v: boolean) => void;
menuPage: MenuPage;
setMenuPage: (p: MenuPage) => void;
guardedSetMenuPage: (p: MenuPage) => void;
settingsInitialCategory: SettingsCategoryId | undefined;
setSettingsInitialCategory: (c: SettingsCategoryId | undefined) => void;
verificationFocusIssueId: number | null;
setVerificationFocusIssueId: (id: number | null) => void;
menuPermissions: ReturnType['permissions'];
canMenu: ReturnType['can'];
appDefaults: ReturnType['defaults'];
appSettingsLoaded: boolean;
saveAppDef: ReturnType['save'];
getIndicatorParams: ReturnType['getParams'];
saveIndicatorParams: ReturnType['saveParams'];
getIndicatorVisual: ReturnType['getVisualConfig'];
saveIndicatorVisual: ReturnType['saveVisual'];
saveAllIndicatorDefaults: ReturnType['saveAllIndicatorDefaults'];
indicatorSettingsRevision: number;
handleLoginSuccess: (res: LoginResponse) => void;
handleLogout: () => void;
}) {
const isMobile = useIsMobile();
const {
symbol,
indicators,
goToMarketChart,
refreshPaperAccount,
handlePaperOrderFilled,
paperRefreshKey,
mode,
marketTickers,
settingsPageProps,
chartScreen,
} = useChartWorkspaceContext();
const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true;
const paperAutoTradeEnabled = appDefaults.paperAutoTradeEnabled ?? false;
const handleFullscreen = () => {
if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(() => {});
else document.exitFullscreen().catch(() => {});
};
return (
setAppDownloadOpen(true)}
authUser={authUser}
guestMode={guestMode}
menuPermissions={menuPermissions}
onLoginClick={() => setLoginOpen(true)}
onLogout={handleLogout}
tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
onTradeAlertPopupPosition={v => saveAppDef({ tradeAlertPopupPosition: v })}
onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
onFullscreen={handleFullscreen}
/>
setLoginOpen(false)}
onSuccess={handleLoginSuccess}
/>
{appDownloadOpen && (
setAppDownloadOpen(false)} />
)}
{menuPage === 'dashboard' && (
}>
{ goToMarketChart(m); setMenuPage('chart'); }} />
)}
{menuPage === 'strategy' && (
formalLogin
? (
}>
)
:
)}
{menuPage === 'strategy-editor' && (
formalLogin
? (
}>
)
:
)}
{menuPage === 'backtest' && (
formalLogin
? (
}>
)
:
)}
{menuPage === 'analysis-report' && (
formalLogin
? (
}>
)
:
)}
{menuPage === 'paper' && (
formalLogin ? (
}>
{
if (!formalLogin) { requireFormalLogin(); return; }
setSettingsInitialCategory('trading');
setMenuPage('settings');
}}
onGoChart={m => {
goToMarketChart(m);
setMenuPage('chart');
}}
/>
) : (
)
)}
{menuPage === 'virtual' && (
formalLogin ? (
}>
saveAppDef({ paperAutoTradeEnabled: v })}
onPaperOrderFilled={handlePaperOrderFilled}
/>
) : (
)
)}
{menuPage === 'trend-search' && (
}>
)}
{menuPage === 'verification-board' && (
}>
setVerificationFocusIssueId(null)}
/>
)}
{menuPage === 'notifications' && canMenu('notifications') && (
formalLogin ? (
}>
{
goToMarketChart(market);
setMenuPage('chart');
}}
/>
) : (
)
)}
{menuPage === 'settings' && canMenu('settings') && (
}>
saveAppDef({ btAutoPopup: v })}
btShowPrice={appDefaults.btShowPrice}
onBtShowPrice={v => saveAppDef({ btShowPrice: v })}
liveMarket={symbol}
tradeAlertPopup={appDefaults.tradeAlertPopup}
onTradeAlertPopup={v => saveAppDef({ tradeAlertPopup: v })}
tradeAlertSoundEnabled={appDefaults.tradeAlertSoundEnabled}
onTradeAlertSoundEnabled={v => saveAppDef({ tradeAlertSoundEnabled: v })}
tradeAlertSound={appDefaults.tradeAlertSound}
onTradeAlertSound={v => saveAppDef({ tradeAlertSound: v })}
tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
onTradeAlertPopupPosition={v => saveAppDef({ tradeAlertPopupPosition: v })}
tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
getIndicatorParams={getIndicatorParams}
saveIndicatorParams={saveIndicatorParams}
getIndicatorVisual={getIndicatorVisual}
saveIndicatorVisual={saveIndicatorVisual}
saveAllIndicatorDefaults={saveAllIndicatorDefaults}
indicatorSettingsRevision={indicatorSettingsRevision}
paperTradingEnabled={paperTradingEnabled}
onPaperTradingEnabled={v => saveAppDef({ paperTradingEnabled: v })}
paperInitialCapital={appDefaults.paperInitialCapital}
onPaperInitialCapital={v => saveAppDef({ paperInitialCapital: v })}
paperFeeRatePct={appDefaults.paperFeeRatePct}
onPaperFeeRatePct={v => saveAppDef({ paperFeeRatePct: v })}
paperSlippagePct={appDefaults.paperSlippagePct}
onPaperSlippagePct={v => saveAppDef({ paperSlippagePct: v })}
paperMinOrderKrw={appDefaults.paperMinOrderKrw}
onPaperMinOrderKrw={v => saveAppDef({ paperMinOrderKrw: v })}
paperAutoTradeEnabled={appDefaults.paperAutoTradeEnabled}
onPaperAutoTradeEnabled={v => saveAppDef({ paperAutoTradeEnabled: v })}
paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
onPaperAutoTradeBudgetPct={v => saveAppDef({ paperAutoTradeBudgetPct: v })}
paperOrderFillMode={appDefaults.paperOrderFillMode}
onPaperOrderFillMode={v => saveAppDef({ paperOrderFillMode: v })}
paperOrderAutoCancelPct={appDefaults.paperOrderAutoCancelPct}
onPaperOrderAutoCancelPct={v => saveAppDef({ paperOrderAutoCancelPct: v })}
virtualTargetMaxCount={appDefaults.virtualTargetMaxCount}
onVirtualTargetMaxCount={v => saveAppDef({ virtualTargetMaxCount: clampVirtualTargetMax(v) })}
trendSearchSettings={appDefaults.trendSearchSettings}
onTrendSearchSettingsChange={s => saveAppDef({
trendSearchSettings: resolveTrendSearchAppSettings(s),
})}
tradingMode={appDefaults.tradingMode}
onTradingMode={v => saveAppDef({ tradingMode: v })}
liveAutoTradeEnabled={appDefaults.liveAutoTradeEnabled}
onLiveAutoTradeEnabled={v => saveAppDef({ liveAutoTradeEnabled: v })}
hasUpbitKeys={appDefaults.hasUpbitKeys}
upbitAccessKeyMasked={appDefaults.upbitAccessKeyMasked}
onUpbitKeys={(access, secret) => {
const patch: Parameters[0] = {};
const a = access.trim();
const s = secret.trim();
const isMasked = (v: string) => v.startsWith('····') || v.startsWith('****');
if (a && !isMasked(a)) patch.upbitAccessKey = a;
if (s && !isMasked(s)) patch.upbitSecretKey = s;
if (Object.keys(patch).length) saveAppDef(patch);
}}
chartRealtimeSource={appDefaults.chartRealtimeSource}
onChartRealtimeSource={v => saveAppDef({ chartRealtimeSource: v })}
liveAutoTradeBudgetPct={appDefaults.liveAutoTradeBudgetPct}
onLiveAutoTradeBudgetPct={v => saveAppDef({ liveAutoTradeBudgetPct: v })}
fcmPushEnabled={appDefaults.fcmPushEnabled}
onFcmPushEnabled={v => saveAppDef({ fcmPushEnabled: v })}
verificationIssueNotify={appDefaults.verificationIssueNotify}
onVerificationIssueNotify={v => saveAppDef({ verificationIssueNotify: v })}
onFcmTest={async () => {
const { sendFcmTest } = await import('./utils/backendApi');
const r = await sendFcmTest();
window.alert(r?.available ? 'FCM 테스트 요청을 보냈습니다.' : 'FCM 서버가 비활성 상태입니다.');
}}
initialCategory={settingsInitialCategory}
{...settingsPageProps}
/>
)}
{chartScreen}
{
setVerificationFocusIssueId(issueId);
guardedSetMenuPage('verification-board');
}}
tradingMode={appDefaults.tradingMode}
hasUpbitKeys={appDefaults.hasUpbitKeys}
paperTradingEnabled={paperTradingEnabled}
liveAutoTradeBudgetPct={appDefaults.liveAutoTradeBudgetPct}
paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
onOrderDone={() => {
refreshPaperAccount(symbol);
handlePaperOrderFilled();
}}
/>
);
}
function App() {
const [sessionKey, setSessionKey] = useState(0);
const [appEntered, setAppEnteredState] = useState(() => isAppEntered());
const [authUser, setAuthUser] = useState(() =>
isAppEntered() ? getAuthSession() : null,
);
const [guestMode, setGuestMode] = useState(() =>
isAppEntered() && !getAuthSession(),
);
const formalLogin = isFormalLogin(authUser, guestMode);
const [loginOpen, setLoginOpen] = useState(false);
const requireFormalLogin = useCallback(() => {
window.alert(FORMAL_LOGIN_REQUIRED_MSG);
setLoginOpen(true);
}, []);
const [appDownloadOpen, setAppDownloadOpen] = useState(false);
const {
getParams: getIndicatorParams,
saveParams: saveIndicatorParams,
getVisualConfig: getIndicatorVisual,
saveVisual: saveIndicatorVisual,
saveAllIndicatorDefaults,
settingsRevision: indicatorSettingsRevision,
} = useIndicatorSettings(sessionKey);
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(loadLocalTheme);
const [menuPage, setMenuPage] = useState('chart');
const [settingsInitialCategory, setSettingsInitialCategory] = useState();
const [verificationFocusIssueId, setVerificationFocusIssueId] = useState(null);
useEffect(() => {
if (menuPage !== 'settings') setSettingsInitialCategory(undefined);
}, [menuPage]);
const enterMainApp = useCallback(() => {
setAppEntered();
setAppEnteredState(true);
}, []);
const handleLoginSuccess = useCallback((res: LoginResponse) => {
const session: AuthSession = {
userId: res.userId,
username: res.username,
displayName: res.displayName,
role: normalizeRole(res.role),
};
setAuthSession(session);
setAuthUser(session);
setGuestMode(false);
clearAdminUnlock();
invalidateAppSettingsCache();
invalidateIndicatorSettingsCache();
invalidateMenuPermissionsCache();
setSessionKey(k => k + 1);
enterMainApp();
}, [enterMainApp]);
const handleGuestEnter = useCallback(() => {
clearAuthSession();
setAuthUser(null);
setGuestMode(true);
clearAdminUnlock();
invalidateAppSettingsCache();
invalidateIndicatorSettingsCache();
invalidateMenuPermissionsCache();
setSessionKey(k => k + 1);
enterMainApp();
}, [enterMainApp]);
const handleLogout = useCallback(() => {
clearAuthSession();
setAuthUser(null);
setGuestMode(false);
clearAdminUnlock();
invalidateAppSettingsCache();
invalidateIndicatorSettingsCache();
invalidateMenuPermissionsCache();
setSessionKey(k => k + 1);
clearAppEntered();
setAppEnteredState(false);
setLoginOpen(false);
}, []);
const guardedSetMenuPage = useCallback((page: MenuPage) => {
if (!formalLogin && isTradingMenuPage(page)) {
requireFormalLogin();
return;
}
if (page === 'notifications' && !canMenu('notifications')) return;
if (page !== 'notifications' && !canMenu(page)) {
setMenuPage(firstAllowedTopMenu(menuPermissions));
return;
}
setMenuPage(page);
}, [canMenu, menuPermissions, formalLogin, requireFormalLogin]);
useEffect(() => {
if (!menuPermsLoaded) return;
if (menuPage === 'notifications') {
if (!canMenu('notifications')) setMenuPage(firstAllowedTopMenu(menuPermissions));
return;
}
if (!canMenu(menuPage)) {
setMenuPage(firstAllowedTopMenu(menuPermissions));
}
}, [menuPage, menuPermsLoaded, menuPermissions, canMenu]);
const handleThemeToggle = useCallback(() => {
setTheme(t => {
const next: Theme = t === 'dark' ? 'blue' : t === 'blue' ? 'light' : 'dark';
saveLocalTheme(next);
return next;
});
}, []);
useEffect(() => {
syncDocumentTheme(theme);
}, [theme]);
useEffect(() => {
if (!appSettingsLoaded) return;
setTheme(migrateDbThemeToLocalIfNeeded(getAppSettingsCache().defaultTheme));
}, [appSettingsLoaded]);
const openNotificationsPage = useCallback(
() => guardedSetMenuPage('notifications'),
[guardedSetMenuPage],
);
if (!appEntered) {
return (
);
}
return (
setAppDownloadOpen(true)}
>
);
}
export default App;