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;