diff --git a/frontend/src/App.css b/frontend/src/App.css index 147ff4f..243529d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -29,7 +29,8 @@ } /* ── Dark Theme: 도쿄 나이트 (THEME_SETTINGS.md §2.2) ───────────── */ -.app.dark { +.app.dark, +html.theme-dark { --bg: #1a1b26; --bg1: #1a1b26; --bg2: #24283b; @@ -48,7 +49,8 @@ } /* ── Light Theme (THEME_SETTINGS.md §2.1) ──────────────────────── */ -.app.light { +.app.light, +html.theme-light { --bg: #f5f5f5; --bg1: #f5f5f5; --bg2: #ffffff; @@ -67,7 +69,8 @@ } /* ── Blue Theme: 딥 네이비 (THEME_SETTINGS.md §2.3) ─────────────── */ -.app.blue { +.app.blue, +html.theme-blue { --bg: #0c1929; --bg1: #0c1929; --bg2: #15273d; @@ -9412,32 +9415,32 @@ } /* ── 라이트 테마 오버라이드 ── */ -.app.light .tam-modal { +html.theme-light .tam-modal { background: #ffffff; border-color: rgba(25,118,210,0.35); color: #212121; box-shadow: 0 20px 60px rgba(0,0,0,0.18), 0 0 40px rgba(25,118,210,0.08); } -.app.light .tam-header { background: #f5f7fa; border-color: rgba(0,0,0,0.1); } -.app.light .tam-header-title { color: #1a237e; } -.app.light .tam-header-ko { color: #546e7a; } -.app.light .tam-close { color: #78909c; } -.app.light .tam-close:hover { background: rgba(0,0,0,0.06); color: #37474f; } -.app.light .tam-price-section { border-color: rgba(0,0,0,0.08); } -.app.light .tam-market-name { color: #212121; } -.app.light .tam-price-time { color: #78909c; } -.app.light .tam-strategy-card { background: #f5f7fa; border-color: rgba(0,0,0,0.1); } -.app.light .tam-strategy-label { color: #78909c; } -.app.light .tam-strategy-val { color: #212121; } -.app.light .tam-strategy-exec-val { color: #546e7a; } -.app.light .tam-field-label { color: #546e7a; } -.app.light .tam-pct-btn2 { background: #f0f3f8; border-color: rgba(0,0,0,0.12); color: #546e7a; } -.app.light .tam-pct-btn2:hover { background: #e3eaf6; } -.app.light .tam-price-input, -.app.light .tam-memo-input { background: #f5f7fa; border-color: rgba(0,0,0,0.14); color: #212121; } -.app.light .tam-market-btn { background: #f0f3f8; border-color: rgba(0,0,0,0.12); color: #546e7a; } -.app.light .tam-market-btn:hover { background: #e3eaf6; color: #212121; } -.app.light .tam-disclaimer { color: #90a4ae; } +html.theme-light .tam-header { background: #f5f7fa; border-color: rgba(0,0,0,0.1); } +html.theme-light .tam-header-title { color: #1a237e; } +html.theme-light .tam-header-ko { color: #546e7a; } +html.theme-light .tam-close { color: #78909c; } +html.theme-light .tam-close:hover { background: rgba(0,0,0,0.06); color: #37474f; } +html.theme-light .tam-price-section { border-color: rgba(0,0,0,0.08); } +html.theme-light .tam-market-name { color: #212121; } +html.theme-light .tam-price-time { color: #78909c; } +html.theme-light .tam-strategy-card { background: #f5f7fa; border-color: rgba(0,0,0,0.1); } +html.theme-light .tam-strategy-label { color: #78909c; } +html.theme-light .tam-strategy-val { color: #212121; } +html.theme-light .tam-strategy-exec-val { color: #546e7a; } +html.theme-light .tam-field-label { color: #546e7a; } +html.theme-light .tam-pct-btn2 { background: #f0f3f8; border-color: rgba(0,0,0,0.12); color: #546e7a; } +html.theme-light .tam-pct-btn2:hover { background: #e3eaf6; } +html.theme-light .tam-price-input, +html.theme-light .tam-memo-input { background: #f5f7fa; border-color: rgba(0,0,0,0.14); color: #212121; } +html.theme-light .tam-market-btn { background: #f0f3f8; border-color: rgba(0,0,0,0.12); color: #546e7a; } +html.theme-light .tam-market-btn:hover { background: #e3eaf6; color: #212121; } +html.theme-light .tam-disclaimer { color: #90a4ae; } /* ═══════════════════════════════════════════════════════════════════════════ Trade signal notifications (tsn- / tnl- / tmb-notify-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a37a868..6d0c9ad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -88,6 +88,7 @@ import LoginModal from './components/LoginModal'; 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 { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions'; import { firstAllowedTopMenu, normalizeRole } from './utils/permissions'; import { clearAdminUnlock } from './utils/adminUnlock'; @@ -991,6 +992,11 @@ function App() { saveState({ symbol, timeframe, chartType, theme, indicators, watchlist, alertPrices: alerts.map(a => a.price) }); }, [symbol, timeframe, chartType, theme, indicators, watchlist, alerts]); + // body 포털(알림 팝업 등) 테마 CSS 변수 동기화 + useEffect(() => { + syncDocumentTheme(theme); + }, [theme]); + // ── DB 워크스페이스 동기화 ───────────────────────────────────────────── const workspaceState = useMemo(() => ({ layoutId: layoutDef.id, @@ -1483,6 +1489,7 @@ function App() { {menuPage === 'paper' && ( = { BTC: '#f7931a', ETH: '#627eea', XRP: '#23292f', SOL: '#9945ff', @@ -28,6 +29,7 @@ type HistoryTab = 'open' | 'recent'; type RightTab = 'trade' | 'orderbook'; interface Props { + theme?: Theme; tickers?: Map; onGoChart?: (market: string) => void; refreshKey?: number; @@ -66,6 +68,7 @@ function coinIcon(code: string): string { } const PaperTradingPage: React.FC = ({ + theme = 'dark', tickers, refreshKey = 0, defaultMarket = 'KRW-BTC', @@ -264,7 +267,7 @@ const PaperTradingPage: React.FC = ({
- +
diff --git a/frontend/src/components/TradeSignalSnackbar.tsx b/frontend/src/components/TradeSignalSnackbar.tsx index 941d79f..aaf82b5 100644 --- a/frontend/src/components/TradeSignalSnackbar.tsx +++ b/frontend/src/components/TradeSignalSnackbar.tsx @@ -93,8 +93,17 @@ export const TradeSignalSnackbar: React.FC = ({ setPageIndex(p => Math.min(totalPages - 1, p + 1)); }, [totalPages]); + const dismissAll = useCallback(() => { + dismissAllToasts(); + setPageIndex(0); + positionsRef.current = {}; + }, [dismissAllToasts]); + const dismissCurrentPage = useCallback(() => { dismissToasts(pageItems.map(i => i.id)); + positionsRef.current = Object.fromEntries( + Object.entries(positionsRef.current).filter(([id]) => !pageItems.some(p => p.id === id)), + ); }, [dismissToasts, pageItems]); const handleDragStart = useCallback((e: React.PointerEvent, key: string) => { @@ -219,7 +228,7 @@ export const TradeSignalSnackbar: React.FC = ({