알림팝업 전체닫기 오류 수정
This commit is contained in:
+27
-24
@@ -29,7 +29,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Dark Theme: 도쿄 나이트 (THEME_SETTINGS.md §2.2) ───────────── */
|
/* ── Dark Theme: 도쿄 나이트 (THEME_SETTINGS.md §2.2) ───────────── */
|
||||||
.app.dark {
|
.app.dark,
|
||||||
|
html.theme-dark {
|
||||||
--bg: #1a1b26;
|
--bg: #1a1b26;
|
||||||
--bg1: #1a1b26;
|
--bg1: #1a1b26;
|
||||||
--bg2: #24283b;
|
--bg2: #24283b;
|
||||||
@@ -48,7 +49,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Light Theme (THEME_SETTINGS.md §2.1) ──────────────────────── */
|
/* ── Light Theme (THEME_SETTINGS.md §2.1) ──────────────────────── */
|
||||||
.app.light {
|
.app.light,
|
||||||
|
html.theme-light {
|
||||||
--bg: #f5f5f5;
|
--bg: #f5f5f5;
|
||||||
--bg1: #f5f5f5;
|
--bg1: #f5f5f5;
|
||||||
--bg2: #ffffff;
|
--bg2: #ffffff;
|
||||||
@@ -67,7 +69,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Blue Theme: 딥 네이비 (THEME_SETTINGS.md §2.3) ─────────────── */
|
/* ── Blue Theme: 딥 네이비 (THEME_SETTINGS.md §2.3) ─────────────── */
|
||||||
.app.blue {
|
.app.blue,
|
||||||
|
html.theme-blue {
|
||||||
--bg: #0c1929;
|
--bg: #0c1929;
|
||||||
--bg1: #0c1929;
|
--bg1: #0c1929;
|
||||||
--bg2: #15273d;
|
--bg2: #15273d;
|
||||||
@@ -9412,32 +9415,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── 라이트 테마 오버라이드 ── */
|
/* ── 라이트 테마 오버라이드 ── */
|
||||||
.app.light .tam-modal {
|
html.theme-light .tam-modal {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-color: rgba(25,118,210,0.35);
|
border-color: rgba(25,118,210,0.35);
|
||||||
color: #212121;
|
color: #212121;
|
||||||
box-shadow: 0 20px 60px rgba(0,0,0,0.18), 0 0 40px rgba(25,118,210,0.08);
|
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); }
|
html.theme-light .tam-header { background: #f5f7fa; border-color: rgba(0,0,0,0.1); }
|
||||||
.app.light .tam-header-title { color: #1a237e; }
|
html.theme-light .tam-header-title { color: #1a237e; }
|
||||||
.app.light .tam-header-ko { color: #546e7a; }
|
html.theme-light .tam-header-ko { color: #546e7a; }
|
||||||
.app.light .tam-close { color: #78909c; }
|
html.theme-light .tam-close { color: #78909c; }
|
||||||
.app.light .tam-close:hover { background: rgba(0,0,0,0.06); color: #37474f; }
|
html.theme-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); }
|
html.theme-light .tam-price-section { border-color: rgba(0,0,0,0.08); }
|
||||||
.app.light .tam-market-name { color: #212121; }
|
html.theme-light .tam-market-name { color: #212121; }
|
||||||
.app.light .tam-price-time { color: #78909c; }
|
html.theme-light .tam-price-time { color: #78909c; }
|
||||||
.app.light .tam-strategy-card { background: #f5f7fa; border-color: rgba(0,0,0,0.1); }
|
html.theme-light .tam-strategy-card { background: #f5f7fa; border-color: rgba(0,0,0,0.1); }
|
||||||
.app.light .tam-strategy-label { color: #78909c; }
|
html.theme-light .tam-strategy-label { color: #78909c; }
|
||||||
.app.light .tam-strategy-val { color: #212121; }
|
html.theme-light .tam-strategy-val { color: #212121; }
|
||||||
.app.light .tam-strategy-exec-val { color: #546e7a; }
|
html.theme-light .tam-strategy-exec-val { color: #546e7a; }
|
||||||
.app.light .tam-field-label { color: #546e7a; }
|
html.theme-light .tam-field-label { color: #546e7a; }
|
||||||
.app.light .tam-pct-btn2 { background: #f0f3f8; border-color: rgba(0,0,0,0.12); color: #546e7a; }
|
html.theme-light .tam-pct-btn2 { background: #f0f3f8; border-color: rgba(0,0,0,0.12); color: #546e7a; }
|
||||||
.app.light .tam-pct-btn2:hover { background: #e3eaf6; }
|
html.theme-light .tam-pct-btn2:hover { background: #e3eaf6; }
|
||||||
.app.light .tam-price-input,
|
html.theme-light .tam-price-input,
|
||||||
.app.light .tam-memo-input { background: #f5f7fa; border-color: rgba(0,0,0,0.14); color: #212121; }
|
html.theme-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; }
|
html.theme-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; }
|
html.theme-light .tam-market-btn:hover { background: #e3eaf6; color: #212121; }
|
||||||
.app.light .tam-disclaimer { color: #90a4ae; }
|
html.theme-light .tam-disclaimer { color: #90a4ae; }
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
Trade signal notifications (tsn- / tnl- / tmb-notify-)
|
Trade signal notifications (tsn- / tnl- / tmb-notify-)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import LoginModal from './components/LoginModal';
|
|||||||
import SplashScreen from './components/SplashScreen';
|
import SplashScreen from './components/SplashScreen';
|
||||||
import { getAuthSession, setAuthSession, clearAuthSession, type AuthSession } from './utils/auth';
|
import { getAuthSession, setAuthSession, clearAuthSession, type AuthSession } from './utils/auth';
|
||||||
import { isAppEntered, setAppEntered, clearAppEntered } from './utils/appEntry';
|
import { isAppEntered, setAppEntered, clearAppEntered } from './utils/appEntry';
|
||||||
|
import { syncDocumentTheme } from './utils/documentTheme';
|
||||||
import { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions';
|
import { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions';
|
||||||
import { firstAllowedTopMenu, normalizeRole } from './utils/permissions';
|
import { firstAllowedTopMenu, normalizeRole } from './utils/permissions';
|
||||||
import { clearAdminUnlock } from './utils/adminUnlock';
|
import { clearAdminUnlock } from './utils/adminUnlock';
|
||||||
@@ -991,6 +992,11 @@ function App() {
|
|||||||
saveState({ symbol, timeframe, chartType, theme, indicators, watchlist, alertPrices: alerts.map(a => a.price) });
|
saveState({ symbol, timeframe, chartType, theme, indicators, watchlist, alertPrices: alerts.map(a => a.price) });
|
||||||
}, [symbol, timeframe, chartType, theme, indicators, watchlist, alerts]);
|
}, [symbol, timeframe, chartType, theme, indicators, watchlist, alerts]);
|
||||||
|
|
||||||
|
// body 포털(알림 팝업 등) 테마 CSS 변수 동기화
|
||||||
|
useEffect(() => {
|
||||||
|
syncDocumentTheme(theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
// ── DB 워크스페이스 동기화 ─────────────────────────────────────────────
|
// ── DB 워크스페이스 동기화 ─────────────────────────────────────────────
|
||||||
const workspaceState = useMemo(() => ({
|
const workspaceState = useMemo(() => ({
|
||||||
layoutId: layoutDef.id,
|
layoutId: layoutDef.id,
|
||||||
@@ -1483,6 +1489,7 @@ function App() {
|
|||||||
|
|
||||||
{menuPage === 'paper' && (
|
{menuPage === 'paper' && (
|
||||||
<PaperTradingPage
|
<PaperTradingPage
|
||||||
|
theme={theme}
|
||||||
tickers={marketTickers}
|
tickers={marketTickers}
|
||||||
refreshKey={paperRefreshKey}
|
refreshKey={paperRefreshKey}
|
||||||
defaultMarket={symbol}
|
defaultMarket={symbol}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { TradeOrderFillRequest } from '../types';
|
|||||||
import PaperMiniChart from './paper/PaperMiniChart';
|
import PaperMiniChart from './paper/PaperMiniChart';
|
||||||
import PaperIndicatorPanel from './paper/PaperIndicatorPanel';
|
import PaperIndicatorPanel from './paper/PaperIndicatorPanel';
|
||||||
import PaperCompactOrderbook from './paper/PaperCompactOrderbook';
|
import PaperCompactOrderbook from './paper/PaperCompactOrderbook';
|
||||||
|
import type { Theme } from '../types';
|
||||||
|
|
||||||
const COIN_COLORS: Record<string, string> = {
|
const COIN_COLORS: Record<string, string> = {
|
||||||
BTC: '#f7931a', ETH: '#627eea', XRP: '#23292f', SOL: '#9945ff',
|
BTC: '#f7931a', ETH: '#627eea', XRP: '#23292f', SOL: '#9945ff',
|
||||||
@@ -28,6 +29,7 @@ type HistoryTab = 'open' | 'recent';
|
|||||||
type RightTab = 'trade' | 'orderbook';
|
type RightTab = 'trade' | 'orderbook';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
theme?: Theme;
|
||||||
tickers?: Map<string, { tradePrice: number | null }>;
|
tickers?: Map<string, { tradePrice: number | null }>;
|
||||||
onGoChart?: (market: string) => void;
|
onGoChart?: (market: string) => void;
|
||||||
refreshKey?: number;
|
refreshKey?: number;
|
||||||
@@ -66,6 +68,7 @@ function coinIcon(code: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PaperTradingPage: React.FC<Props> = ({
|
const PaperTradingPage: React.FC<Props> = ({
|
||||||
|
theme = 'dark',
|
||||||
tickers,
|
tickers,
|
||||||
refreshKey = 0,
|
refreshKey = 0,
|
||||||
defaultMarket = 'KRW-BTC',
|
defaultMarket = 'KRW-BTC',
|
||||||
@@ -264,7 +267,7 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
|
|
||||||
<main className="ptd-col ptd-col--center">
|
<main className="ptd-col ptd-col--center">
|
||||||
<div className="ptd-card ptd-chart-card">
|
<div className="ptd-card ptd-chart-card">
|
||||||
<PaperMiniChart market={selectedMarket} />
|
<PaperMiniChart market={selectedMarket} theme={theme} />
|
||||||
<PaperIndicatorPanel market={selectedMarket} />
|
<PaperIndicatorPanel market={selectedMarket} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -93,8 +93,17 @@ export const TradeSignalSnackbar: React.FC<Props> = ({
|
|||||||
setPageIndex(p => Math.min(totalPages - 1, p + 1));
|
setPageIndex(p => Math.min(totalPages - 1, p + 1));
|
||||||
}, [totalPages]);
|
}, [totalPages]);
|
||||||
|
|
||||||
|
const dismissAll = useCallback(() => {
|
||||||
|
dismissAllToasts();
|
||||||
|
setPageIndex(0);
|
||||||
|
positionsRef.current = {};
|
||||||
|
}, [dismissAllToasts]);
|
||||||
|
|
||||||
const dismissCurrentPage = useCallback(() => {
|
const dismissCurrentPage = useCallback(() => {
|
||||||
dismissToasts(pageItems.map(i => i.id));
|
dismissToasts(pageItems.map(i => i.id));
|
||||||
|
positionsRef.current = Object.fromEntries(
|
||||||
|
Object.entries(positionsRef.current).filter(([id]) => !pageItems.some(p => p.id === id)),
|
||||||
|
);
|
||||||
}, [dismissToasts, pageItems]);
|
}, [dismissToasts, pageItems]);
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: React.PointerEvent, key: string) => {
|
const handleDragStart = useCallback((e: React.PointerEvent, key: string) => {
|
||||||
@@ -219,7 +228,7 @@ export const TradeSignalSnackbar: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="tsn-dismiss-all"
|
className="tsn-dismiss-all"
|
||||||
onClick={dismissAllToasts}
|
onClick={dismissAll}
|
||||||
title={`대기 중인 알림 ${toastNotifications.length}건 전체 닫기`}
|
title={`대기 중인 알림 ${toastNotifications.length}건 전체 닫기`}
|
||||||
>
|
>
|
||||||
전체닫기 ({toastNotifications.length})
|
전체닫기 ({toastNotifications.length})
|
||||||
|
|||||||
@@ -99,10 +99,10 @@ const BacktestAssetChart: React.FC<Props> = memo(({ curve, markers }) => {
|
|||||||
|
|
||||||
{[0.25, 0.5, 0.75].map(r => {
|
{[0.25, 0.5, 0.75].map(r => {
|
||||||
const y = pad.t + innerH * (1 - r);
|
const y = pad.t + innerH * (1 - r);
|
||||||
return <line key={r} x1={pad.l} y1={y} x2={W - pad.r} y2={y} stroke="rgba(201,162,39,0.07)" strokeWidth="1" />;
|
return <line key={r} x1={pad.l} y1={y} x2={W - pad.r} y2={y} className="btd-chart-grid" />;
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<text x={pad.l - 8} y={pad.t - 6} textAnchor="start" fill="#6272a4" fontSize="8">자본금</text>
|
<text x={pad.l - 8} y={pad.t - 6} textAnchor="start" className="btd-chart-label" fontSize="8">자본금</text>
|
||||||
|
|
||||||
{chart.yTicks.map(v => (
|
{chart.yTicks.map(v => (
|
||||||
<text
|
<text
|
||||||
@@ -111,7 +111,7 @@ const BacktestAssetChart: React.FC<Props> = memo(({ curve, markers }) => {
|
|||||||
y={chart.toY(v)}
|
y={chart.toY(v)}
|
||||||
textAnchor="end"
|
textAnchor="end"
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
fill="#6272a4"
|
className="btd-chart-label"
|
||||||
fontSize="8.5"
|
fontSize="8.5"
|
||||||
>
|
>
|
||||||
{fmtY(v)}
|
{fmtY(v)}
|
||||||
@@ -124,7 +124,7 @@ const BacktestAssetChart: React.FC<Props> = memo(({ curve, markers }) => {
|
|||||||
key={i}
|
key={i}
|
||||||
d={seg.d}
|
d={seg.d}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={seg.up ? '#22c55e' : '#ef4444'}
|
className={seg.up ? 'btd-chart-line-up' : 'btd-chart-line-down'}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -152,7 +152,7 @@ const BacktestAssetChart: React.FC<Props> = memo(({ curve, markers }) => {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{chart.xLabels.map((l, i) => (
|
{chart.xLabels.map((l, i) => (
|
||||||
<text key={i} x={l.x} y={H - 5} textAnchor="middle" fill="#6272a4" fontSize="8">
|
<text key={i} x={l.x} y={H - 5} textAnchor="middle" className="btd-chart-label" fontSize="8">
|
||||||
{l.label}
|
{l.label}
|
||||||
</text>
|
</text>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,44 +1,84 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { createChart, CandlestickSeries, type IChartApi, type ISeriesApi, type Time, ColorType } from 'lightweight-charts';
|
import { createChart, CandlestickSeries, type IChartApi, type ISeriesApi, type Time, ColorType } from 'lightweight-charts';
|
||||||
import type { Timeframe } from '../../types';
|
import type { Theme, Timeframe } from '../../types';
|
||||||
import { fetchUpbitCandles } from '../../utils/upbitApi';
|
import { fetchUpbitCandles } from '../../utils/upbitApi';
|
||||||
|
|
||||||
const TF_OPTIONS: Timeframe[] = ['1m', '5m', '15m', '1h', '4h', '1D'];
|
const TF_OPTIONS: Timeframe[] = ['1m', '5m', '15m', '1h', '4h', '1D'];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
market: string;
|
market: string;
|
||||||
|
theme?: Theme;
|
||||||
wsLabel?: string;
|
wsLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PaperMiniChart: React.FC<Props> = ({ market, wsLabel = 'WebSocket <100ms' }) => {
|
function readChartTheme(container: HTMLElement | null) {
|
||||||
|
const root = container?.closest('.app') ?? document.documentElement;
|
||||||
|
const s = getComputedStyle(root);
|
||||||
|
const v = (name: string, fallback: string) => s.getPropertyValue(name).trim() || fallback;
|
||||||
|
return {
|
||||||
|
bgColor: v('--bg3', '#1f2335'),
|
||||||
|
textColor: v('--text', '#c0caf5'),
|
||||||
|
gridColor: v('--sep', 'rgba(122, 162, 247, 0.15)'),
|
||||||
|
borderColor: v('--border', 'rgba(122, 162, 247, 0.15)'),
|
||||||
|
upColor: v('--up', '#ff6b6b'),
|
||||||
|
downColor: v('--down', '#4dabf7'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaperMiniChart: React.FC<Props> = ({ market, theme = 'dark', wsLabel = 'WebSocket <100ms' }) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const chartRef = useRef<IChartApi | null>(null);
|
const chartRef = useRef<IChartApi | null>(null);
|
||||||
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
|
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
|
||||||
const [timeframe, setTimeframe] = useState<Timeframe>('1h');
|
const [timeframe, setTimeframe] = useState<Timeframe>('1h');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
const applyTheme = useCallback((chart: IChartApi, series: ISeriesApi<'Candlestick'>) => {
|
||||||
if (!containerRef.current) return undefined;
|
const t = readChartTheme(containerRef.current);
|
||||||
const chart = createChart(containerRef.current, {
|
chart.applyOptions({
|
||||||
layout: {
|
layout: {
|
||||||
background: { type: ColorType.Solid, color: '#121520' },
|
background: { type: ColorType.Solid, color: t.bgColor },
|
||||||
textColor: '#9aa5ce',
|
textColor: t.textColor,
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
vertLines: { color: 'rgba(122,162,247,0.06)' },
|
vertLines: { color: t.gridColor },
|
||||||
horzLines: { color: 'rgba(122,162,247,0.06)' },
|
horzLines: { color: t.gridColor },
|
||||||
},
|
},
|
||||||
rightPriceScale: { borderColor: 'rgba(122,162,247,0.12)' },
|
rightPriceScale: { borderColor: t.borderColor },
|
||||||
timeScale: { borderColor: 'rgba(122,162,247,0.12)' },
|
timeScale: { borderColor: t.borderColor },
|
||||||
|
});
|
||||||
|
series.applyOptions({
|
||||||
|
upColor: t.upColor,
|
||||||
|
downColor: t.downColor,
|
||||||
|
borderUpColor: t.upColor,
|
||||||
|
borderDownColor: t.downColor,
|
||||||
|
wickUpColor: t.upColor,
|
||||||
|
wickDownColor: t.downColor,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return undefined;
|
||||||
|
const t = readChartTheme(containerRef.current);
|
||||||
|
const chart = createChart(containerRef.current, {
|
||||||
|
layout: {
|
||||||
|
background: { type: ColorType.Solid, color: t.bgColor },
|
||||||
|
textColor: t.textColor,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: { color: t.gridColor },
|
||||||
|
horzLines: { color: t.gridColor },
|
||||||
|
},
|
||||||
|
rightPriceScale: { borderColor: t.borderColor },
|
||||||
|
timeScale: { borderColor: t.borderColor },
|
||||||
crosshair: { mode: 1 },
|
crosshair: { mode: 1 },
|
||||||
});
|
});
|
||||||
const series = chart.addSeries(CandlestickSeries, {
|
const series = chart.addSeries(CandlestickSeries, {
|
||||||
upColor: '#22c55e',
|
upColor: t.upColor,
|
||||||
downColor: '#ef4444',
|
downColor: t.downColor,
|
||||||
borderUpColor: '#22c55e',
|
borderUpColor: t.upColor,
|
||||||
borderDownColor: '#ef4444',
|
borderDownColor: t.downColor,
|
||||||
wickUpColor: '#22c55e',
|
wickUpColor: t.upColor,
|
||||||
wickDownColor: '#ef4444',
|
wickDownColor: t.downColor,
|
||||||
});
|
});
|
||||||
chartRef.current = chart;
|
chartRef.current = chart;
|
||||||
seriesRef.current = series;
|
seriesRef.current = series;
|
||||||
@@ -61,6 +101,12 @@ const PaperMiniChart: React.FC<Props> = ({ market, wsLabel = 'WebSocket <100ms'
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chartRef.current && seriesRef.current) {
|
||||||
|
applyTheme(chartRef.current, seriesRef.current);
|
||||||
|
}
|
||||||
|
}, [theme, applyTheme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import React, {
|
|||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { TradeSignalInfo } from '../components/TradeAlertModal';
|
import type { TradeSignalInfo } from '../components/TradeAlertModal';
|
||||||
@@ -121,6 +122,8 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
|||||||
const alertSoundId = normalizeTradeAlertSoundId(soundId) as TradeAlertSoundId;
|
const alertSoundId = normalizeTradeAlertSoundId(soundId) as TradeAlertSoundId;
|
||||||
const [readIds, setReadIds] = useState<Set<string>>(() => loadReadIds());
|
const [readIds, setReadIds] = useState<Set<string>>(() => loadReadIds());
|
||||||
const [toastNotifications, setToastNotifications] = useState<TradeNotificationItem[]>([]);
|
const [toastNotifications, setToastNotifications] = useState<TradeNotificationItem[]>([]);
|
||||||
|
const toastNotificationsRef = useRef(toastNotifications);
|
||||||
|
toastNotificationsRef.current = toastNotifications;
|
||||||
const [allNotifications, setAllNotifications] = useState<TradeNotificationItem[]>([]);
|
const [allNotifications, setAllNotifications] = useState<TradeNotificationItem[]>([]);
|
||||||
const [detailSignal, setDetailSignal] = useState<TradeSignalInfo | null>(null);
|
const [detailSignal, setDetailSignal] = useState<TradeSignalInfo | null>(null);
|
||||||
const [detailCentered, setDetailCentered] = useState(false);
|
const [detailCentered, setDetailCentered] = useState(false);
|
||||||
@@ -247,20 +250,10 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const dismissAllToasts = useCallback(() => {
|
const dismissAllToasts = useCallback(() => {
|
||||||
setToastNotifications(prev => {
|
const ids = toastNotificationsRef.current.map(n => n.id);
|
||||||
const ids = prev.map(n => n.id);
|
if (ids.length === 0) return;
|
||||||
setReadIds(r => {
|
dismissToasts(ids);
|
||||||
const next = new Set(r);
|
}, [dismissToasts]);
|
||||||
ids.forEach(i => next.add(i));
|
|
||||||
saveReadIds(next);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
setAllNotifications(all =>
|
|
||||||
all.map(n => (ids.includes(n.id) ? { ...n, isRead: true } : n)),
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const markAllAsRead = useCallback(() => {
|
const markAllAsRead = useCallback(() => {
|
||||||
setAllNotifications(prev => {
|
setAllNotifications(prev => {
|
||||||
|
|||||||
@@ -287,28 +287,44 @@
|
|||||||
background: rgba(0, 0, 0, 0.45);
|
background: rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 라이트 테마 */
|
/* 라이트 테마 — body 포털(html.theme-light) · 앱 내부(.app.light) 공통 */
|
||||||
|
html.theme-light .app-popup-shell,
|
||||||
.app.light .app-popup-shell,
|
.app.light .app-popup-shell,
|
||||||
|
html.theme-light .tam-modal,
|
||||||
.app.light .tam-modal,
|
.app.light .tam-modal,
|
||||||
|
html.theme-light .lsp-modal,
|
||||||
.app.light .lsp-modal,
|
.app.light .lsp-modal,
|
||||||
|
html.theme-light .gc-popup-panel,
|
||||||
.app.light .gc-popup-panel,
|
.app.light .gc-popup-panel,
|
||||||
|
html.theme-light .bts-modal,
|
||||||
.app.light .bts-modal,
|
.app.light .bts-modal,
|
||||||
|
html.theme-light .brm-modal,
|
||||||
.app.light .brm-modal,
|
.app.light .brm-modal,
|
||||||
|
html.theme-light .ism-dialog,
|
||||||
.app.light .ism-dialog,
|
.app.light .ism-dialog,
|
||||||
|
html.theme-light .login-modal,
|
||||||
.app.light .login-modal {
|
.app.light .login-modal {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-color: rgba(25, 118, 210, 0.35);
|
border-color: rgba(25, 118, 210, 0.35);
|
||||||
color: #212121;
|
color: #212121;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.18), 0 0 40px rgba(25, 118, 210, 0.08);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.18), 0 0 40px rgba(25, 118, 210, 0.08);
|
||||||
}
|
}
|
||||||
|
html.theme-light .app-popup-header,
|
||||||
.app.light .app-popup-header,
|
.app.light .app-popup-header,
|
||||||
|
html.theme-light .gc-popup-header,
|
||||||
.app.light .gc-popup-header {
|
.app.light .gc-popup-header {
|
||||||
background: linear-gradient(180deg, rgba(25, 118, 210, 0.1) 0%, #f5f7fa 100%);
|
background: linear-gradient(180deg, rgba(25, 118, 210, 0.1) 0%, #f5f7fa 100%);
|
||||||
border-color: rgba(0, 0, 0, 0.1);
|
border-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
html.theme-light .app-popup-header-title,
|
||||||
.app.light .app-popup-header-title,
|
.app.light .app-popup-header-title,
|
||||||
|
html.theme-light .gc-popup-title,
|
||||||
.app.light .gc-popup-title { color: #1a237e; }
|
.app.light .gc-popup-title { color: #1a237e; }
|
||||||
|
html.theme-light .app-popup-header-ko,
|
||||||
.app.light .app-popup-header-ko { color: #546e7a; }
|
.app.light .app-popup-header-ko { color: #546e7a; }
|
||||||
|
html.theme-light .app-popup-card,
|
||||||
.app.light .app-popup-card { background: #f5f7fa; border-color: rgba(0, 0, 0, 0.1); }
|
.app.light .app-popup-card { background: #f5f7fa; border-color: rgba(0, 0, 0, 0.1); }
|
||||||
|
html.theme-light .app-popup-input,
|
||||||
.app.light .app-popup-input,
|
.app.light .app-popup-input,
|
||||||
|
html.theme-light .app-popup-select,
|
||||||
.app.light .app-popup-select { background: #f0f3f8; border-color: rgba(0, 0, 0, 0.12); color: #212121; }
|
.app.light .app-popup-select { background: #f0f3f8; border-color: rgba(0, 0, 0, 0.12); color: #212121; }
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
/* 백테스팅 대시보드 (btd-*) — 금색 포인트 다크 테마 */
|
/* 백테스팅 대시보드 (btd-*) — 앱 테마 변수 + 금색 포인트 */
|
||||||
|
|
||||||
.btd-page {
|
.btd-page {
|
||||||
|
--btd-gold: #c9a227;
|
||||||
|
--btd-gold-light: #e8c547;
|
||||||
|
--btd-gold-dark: #b8860b;
|
||||||
|
--btd-gold-border: rgba(201, 162, 39, 0.18);
|
||||||
|
--btd-gold-border-strong: rgba(201, 162, 39, 0.35);
|
||||||
|
--btd-gold-bg: rgba(201, 162, 39, 0.08);
|
||||||
|
--btd-gold-bg-hover: rgba(201, 162, 39, 0.14);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100vh - var(--tmb-h, 46px));
|
height: calc(100vh - var(--tmb-h, 46px));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0a0e14;
|
background: var(--bg);
|
||||||
color: #c8d0e0;
|
color: var(--text);
|
||||||
font-family: var(--font, 'Noto Sans KR', sans-serif);
|
font-family: var(--font, 'Noto Sans KR', sans-serif);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app.light .btd-page,
|
||||||
|
html.theme-light .btd-page {
|
||||||
|
--btd-gold-border: rgba(184, 134, 11, 0.28);
|
||||||
|
--btd-gold-border-strong: rgba(184, 134, 11, 0.45);
|
||||||
|
--btd-gold-bg: rgba(201, 162, 39, 0.1);
|
||||||
|
--btd-gold-bg-hover: rgba(201, 162, 39, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 좌측 타임라인 사이드바 (~23%) ── */
|
/* ── 좌측 타임라인 사이드바 (~23%) ── */
|
||||||
.btd-sidebar {
|
.btd-sidebar {
|
||||||
width: 23%;
|
width: 23%;
|
||||||
@@ -17,8 +33,8 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-right: 1px solid rgba(201, 162, 39, 0.15);
|
border-right: 1px solid var(--btd-gold-border);
|
||||||
background: #0d1219;
|
background: var(--bg2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +43,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 12px 10px;
|
padding: 12px 12px 10px;
|
||||||
border-bottom: 1px solid rgba(201, 162, 39, 0.12);
|
border-bottom: 1px solid var(--btd-gold-border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +51,7 @@
|
|||||||
width: 3px;
|
width: 3px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: linear-gradient(180deg, #e8c547, #b8860b);
|
background: linear-gradient(180deg, var(--btd-gold-light), var(--btd-gold-dark));
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +60,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #f0e6c8;
|
color: var(--text);
|
||||||
letter-spacing: -0.2px;
|
letter-spacing: -0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,16 +73,16 @@
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid rgba(201, 162, 39, 0.2);
|
border: 1px solid var(--btd-gold-border);
|
||||||
background: rgba(201, 162, 39, 0.06);
|
background: var(--btd-gold-bg);
|
||||||
color: #c9a227;
|
color: var(--btd-gold);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.btd-icon-btn:hover:not(:disabled) { background: rgba(201, 162, 39, 0.14); }
|
.btd-icon-btn:hover:not(:disabled) { background: var(--btd-gold-bg-hover); }
|
||||||
.btd-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
.btd-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||||
.btd-icon-btn--danger { color: #ef4444; border-color: rgba(239, 68, 68, 0.3); }
|
.btd-icon-btn--danger { color: #ef4444; border-color: rgba(239, 68, 68, 0.3); }
|
||||||
|
|
||||||
@@ -78,7 +94,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
@@ -121,32 +137,32 @@
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #1a2030;
|
background: var(--bg3);
|
||||||
border: 2px solid rgba(201, 162, 39, 0.45);
|
border: 2px solid rgba(201, 162, 39, 0.45);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-timeline-item--active .btd-timeline-node {
|
.btd-timeline-item--active .btd-timeline-node {
|
||||||
background: #c9a227;
|
background: var(--btd-gold);
|
||||||
border-color: #e8c547;
|
border-color: var(--btd-gold-light);
|
||||||
box-shadow: 0 0 8px rgba(201, 162, 39, 0.5);
|
box-shadow: 0 0 8px rgba(201, 162, 39, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-timeline-card {
|
.btd-timeline-card {
|
||||||
background: #121820;
|
background: var(--bg3);
|
||||||
border: 1px solid rgba(201, 162, 39, 0.12);
|
border: 1px solid var(--btd-gold-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-timeline-item:hover .btd-timeline-card {
|
.btd-timeline-item:hover .btd-timeline-card {
|
||||||
border-color: rgba(201, 162, 39, 0.28);
|
border-color: var(--btd-gold-border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-timeline-item--active .btd-timeline-card {
|
.btd-timeline-item--active .btd-timeline-card {
|
||||||
border-color: rgba(201, 162, 39, 0.65);
|
border-color: rgba(201, 162, 39, 0.65);
|
||||||
box-shadow: 0 0 0 1px rgba(201, 162, 39, 0.2), 0 4px 16px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 0 0 1px rgba(201, 162, 39, 0.2), 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-timeline-top {
|
.btd-timeline-top {
|
||||||
@@ -160,7 +176,7 @@
|
|||||||
.btd-timeline-name {
|
.btd-timeline-name {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #eef2ff;
|
color: var(--text);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +185,7 @@
|
|||||||
.btd-timeline-date {
|
.btd-timeline-date {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +215,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #0a0e14;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-dashboard {
|
.btd-dashboard {
|
||||||
@@ -219,7 +235,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.btd-empty span { font-size: 48px; opacity: 0.35; }
|
.btd-empty span { font-size: 48px; opacity: 0.35; }
|
||||||
@@ -237,7 +253,7 @@
|
|||||||
width: 4px;
|
width: 4px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: linear-gradient(180deg, #e8c547, #b8860b);
|
background: linear-gradient(180deg, var(--btd-gold-light), var(--btd-gold-dark));
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +261,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #f0e6c8;
|
color: var(--text);
|
||||||
letter-spacing: -0.2px;
|
letter-spacing: -0.2px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
@@ -253,11 +269,10 @@
|
|||||||
.btd-section-sub {
|
.btd-section-sub {
|
||||||
margin: 1px 0 0;
|
margin: 1px 0 0;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KPI ~18% | 차트 ~44% | 테이블 ~32% (헤더 포함) */
|
|
||||||
.btd-section--kpi {
|
.btd-section--kpi {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
@@ -286,8 +301,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btd-kpi-card {
|
.btd-kpi-card {
|
||||||
background: #121820;
|
background: var(--bg2);
|
||||||
border: 1px solid rgba(201, 162, 39, 0.18);
|
border: 1px solid var(--btd-gold-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -295,7 +310,7 @@
|
|||||||
|
|
||||||
.btd-kpi-label {
|
.btd-kpi-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #9aa5ce;
|
color: var(--text2);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -303,19 +318,19 @@
|
|||||||
.btd-kpi-value {
|
.btd-kpi-value {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: #fff;
|
color: var(--text);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
}
|
}
|
||||||
.btd-kpi-value.up { color: #22c55e; }
|
.btd-kpi-value.up { color: #22c55e; }
|
||||||
.btd-kpi-value.down { color: #ef4444; }
|
.btd-kpi-value.down { color: #ef4444; }
|
||||||
.btd-kpi-value.gold { color: #c9a227; }
|
.btd-kpi-value.gold { color: var(--btd-gold); }
|
||||||
.btd-kpi-value.neutral { color: #e2e8f0; }
|
.btd-kpi-value.neutral { color: var(--text2); }
|
||||||
|
|
||||||
.btd-kpi-desc {
|
.btd-kpi-desc {
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +344,7 @@
|
|||||||
.btd-kpi-icon {
|
.btd-kpi-icon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
color: #c9a227;
|
color: var(--btd-gold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-kpi-dual {
|
.btd-kpi-dual {
|
||||||
@@ -352,8 +367,8 @@
|
|||||||
.btd-chart-card {
|
.btd-chart-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background: #121820;
|
background: var(--bg2);
|
||||||
border: 1px solid rgba(201, 162, 39, 0.18);
|
border: 1px solid var(--btd-gold-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 4px 8px 2px;
|
padding: 4px 8px 2px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -373,12 +388,24 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btd-chart-grid {
|
||||||
|
stroke: var(--sep);
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-chart-label {
|
||||||
|
fill: var(--text3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btd-chart-line-up { stroke: #22c55e; }
|
||||||
|
.btd-chart-line-down { stroke: #ef4444; }
|
||||||
|
|
||||||
/* ── 거래 내역 테이블 ── */
|
/* ── 거래 내역 테이블 ── */
|
||||||
.btd-table-card {
|
.btd-table-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background: #121820;
|
background: var(--bg2);
|
||||||
border: 1px solid rgba(201, 162, 39, 0.18);
|
border: 1px solid var(--btd-gold-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -399,7 +426,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btd-table thead {
|
.btd-table thead {
|
||||||
background: rgba(201, 162, 39, 0.1);
|
background: var(--btd-gold-bg);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -410,19 +437,19 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #c9a227;
|
color: var(--btd-gold);
|
||||||
border-bottom: 1px solid rgba(201, 162, 39, 0.2);
|
border-bottom: 1px solid var(--btd-gold-border);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-table td {
|
.btd-table td {
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
border-bottom: 1px solid var(--sep);
|
||||||
color: #c8d0e0;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-table tbody tr:hover td {
|
.btd-table tbody tr:hover td {
|
||||||
background: rgba(201, 162, 39, 0.04);
|
background: var(--btd-gold-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-table tbody tr:last-child td {
|
.btd-table tbody tr:last-child td {
|
||||||
@@ -430,19 +457,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btd-td-time {
|
.btd-td-time {
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btd-table td.buy { color: #3b82f6; font-weight: 700; }
|
.btd-table td.buy { color: var(--down, #3b82f6); font-weight: 700; }
|
||||||
.btd-table td.sell { color: #ef4444; font-weight: 700; }
|
.btd-table td.sell { color: var(--up, #ef4444); font-weight: 700; }
|
||||||
.btd-table td.up { color: #22c55e; font-weight: 700; }
|
.btd-table td.up { color: #22c55e; font-weight: 700; }
|
||||||
.btd-table td.down { color: #ef4444; font-weight: 700; }
|
.btd-table td.down { color: #ef4444; font-weight: 700; }
|
||||||
|
|
||||||
.btd-table-empty {
|
.btd-table-empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
padding: 24px !important;
|
padding: 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +479,11 @@
|
|||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.btd-page { flex-direction: column; height: auto; overflow: auto; }
|
.btd-page { flex-direction: column; height: auto; overflow: auto; }
|
||||||
.btd-sidebar { width: 100%; max-height: 240px; border-right: none; border-bottom: 1px solid rgba(201, 162, 39, 0.15); }
|
.btd-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 240px;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--btd-gold-border);
|
||||||
|
}
|
||||||
.btd-kpi-grid { grid-template-columns: 1fr 1fr; }
|
.btd-kpi-grid { grid-template-columns: 1fr 1fr; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* 모의투자 대시보드 (ptd-) — 전체 화면 고정 레이아웃 */
|
/* 모의투자 대시보드 (ptd-) — 전체 화면 고정 레이아웃, 앱 테마 변수 연동 */
|
||||||
.ptd-page {
|
.ptd-page {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #0d1117;
|
background: var(--bg);
|
||||||
color: #c0caf5;
|
color: var(--text);
|
||||||
padding: 8px 12px 10px;
|
padding: 8px 12px 10px;
|
||||||
font-family: var(--font, 'Noto Sans KR', sans-serif);
|
font-family: var(--font, 'Noto Sans KR', sans-serif);
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 320px;
|
min-height: 320px;
|
||||||
color: #9aa5ce;
|
color: var(--text2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ptd-topbar {
|
.ptd-topbar {
|
||||||
@@ -26,18 +26,18 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.ptd-title { margin: 0; font-size: 18px; font-weight: 800; color: #fff; }
|
.ptd-title { margin: 0; font-size: 18px; font-weight: 800; color: var(--text); }
|
||||||
.ptd-topbar-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
.ptd-topbar-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
||||||
.ptd-btn {
|
.ptd-btn {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid rgba(122,162,247,0.2);
|
border: 1px solid var(--border);
|
||||||
background: #1e222d;
|
background: var(--bg2);
|
||||||
color: #c0caf5;
|
color: var(--text);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.ptd-btn:hover { background: #252a38; }
|
.ptd-btn:hover { background: var(--bg3); }
|
||||||
.ptd-btn--danger { border-color: rgba(239,68,68,0.4); color: #ef4444; }
|
.ptd-btn--danger { border-color: rgba(239,68,68,0.4); color: #ef4444; }
|
||||||
|
|
||||||
.ptd-banner {
|
.ptd-banner {
|
||||||
@@ -68,13 +68,13 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #1e222d;
|
background: var(--bg2);
|
||||||
border: 1px solid rgba(122,162,247,0.12);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.ptd-metric-icon { font-size: 16px; line-height: 1; }
|
.ptd-metric-icon { font-size: 16px; line-height: 1; }
|
||||||
.ptd-metric-title { font-size: 10px; color: #6272a4; margin-bottom: 2px; }
|
.ptd-metric-title { font-size: 10px; color: var(--text3); margin-bottom: 2px; }
|
||||||
.ptd-metric-value { font-size: 17px; font-weight: 800; line-height: 1.1; }
|
.ptd-metric-value { font-size: 17px; font-weight: 800; line-height: 1.1; color: var(--text); }
|
||||||
.ptd-metric-sub { font-size: 9px; color: #6272a4; margin-top: 2px; }
|
.ptd-metric-sub { font-size: 9px; color: var(--text3); margin-top: 2px; }
|
||||||
.ptd-metric--up .ptd-metric-value { color: #22c55e; }
|
.ptd-metric--up .ptd-metric-value { color: #22c55e; }
|
||||||
.ptd-metric--down .ptd-metric-value { color: #ef4444; }
|
.ptd-metric--down .ptd-metric-value { color: #ef4444; }
|
||||||
|
|
||||||
@@ -101,8 +101,8 @@
|
|||||||
.ptd-col--right { min-width: 0; }
|
.ptd-col--right { min-width: 0; }
|
||||||
|
|
||||||
.ptd-card {
|
.ptd-card {
|
||||||
background: #1e222d;
|
background: var(--bg2);
|
||||||
border: 1px solid rgba(122,162,247,0.12);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -111,22 +111,22 @@
|
|||||||
padding: 8px 10px 0;
|
padding: 8px 10px 0;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #9aa5ce;
|
color: var(--text2);
|
||||||
}
|
}
|
||||||
.ptd-asset-card { padding-bottom: 8px; }
|
.ptd-asset-card { padding-bottom: 8px; }
|
||||||
.ptd-asset-total {
|
.ptd-asset-total {
|
||||||
padding: 4px 10px 0;
|
padding: 4px 10px 0;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.ptd-asset-total span { font-size: 12px; color: #6272a4; font-weight: 600; }
|
.ptd-asset-total span { font-size: 12px; color: var(--text3); font-weight: 600; }
|
||||||
.ptd-asset-ret {
|
.ptd-asset-ret {
|
||||||
padding: 2px 10px 0;
|
padding: 2px 10px 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
.ptd-asset-sub { padding: 4px 10px 0; font-size: 10px; color: #6272a4; }
|
.ptd-asset-sub { padding: 4px 10px 0; font-size: 10px; color: var(--text3); }
|
||||||
|
|
||||||
.ptd-table {
|
.ptd-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -136,12 +136,12 @@
|
|||||||
.ptd-table th, .ptd-table td {
|
.ptd-table th, .ptd-table td {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid rgba(122,162,247,0.08);
|
border-bottom: 1px solid var(--sep);
|
||||||
}
|
}
|
||||||
.ptd-table th { color: #6272a4; font-weight: 600; }
|
.ptd-table th { color: var(--text3); font-weight: 600; }
|
||||||
.ptd-table tbody tr { cursor: pointer; }
|
.ptd-table tbody tr { cursor: pointer; }
|
||||||
.ptd-table tbody tr:hover { background: rgba(63,126,245,0.06); }
|
.ptd-table tbody tr:hover { background: var(--bg3); }
|
||||||
.ptd-row--sel { background: rgba(63,126,245,0.1); }
|
.ptd-row--sel { background: var(--bg3); }
|
||||||
.ptd-coin {
|
.ptd-coin {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -154,8 +154,8 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
.ptd-muted { color: #6272a4; text-align: center; padding: 12px; font-size: 11px; }
|
.ptd-muted { color: var(--text3); text-align: center; padding: 12px; font-size: 11px; }
|
||||||
.ptd-time { color: #6272a4; white-space: nowrap; }
|
.ptd-time { color: var(--text3); white-space: nowrap; }
|
||||||
|
|
||||||
.ptd-donut-card { padding-bottom: 8px; }
|
.ptd-donut-card { padding-bottom: 8px; }
|
||||||
.ptd-donut-wrap {
|
.ptd-donut-wrap {
|
||||||
@@ -175,20 +175,20 @@
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
margin: 14px auto;
|
margin: 14px auto;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #1e222d;
|
background: var(--bg2);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #c0caf5;
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.ptd-donut-legend {
|
.ptd-donut-legend {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #9aa5ce;
|
color: var(--text2);
|
||||||
}
|
}
|
||||||
.ptd-donut-legend li { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }
|
.ptd-donut-legend li { display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }
|
||||||
.ptd-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
.ptd-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.ptd-chart-title { font-size: 12px; font-weight: 700; }
|
.ptd-chart-title { font-size: 12px; font-weight: 700; color: var(--text); }
|
||||||
.ptd-ws-badge {
|
.ptd-ws-badge {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
inset: 40% 0 auto;
|
inset: 40% 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.ptd-tf-row {
|
.ptd-tf-row {
|
||||||
@@ -248,16 +248,16 @@
|
|||||||
.ptd-tf-btn {
|
.ptd-tf-btn {
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid rgba(122,162,247,0.15);
|
border: 1px solid var(--border);
|
||||||
background: #121520;
|
background: var(--bg3);
|
||||||
color: #9aa5ce;
|
color: var(--text2);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.ptd-tf-btn--active {
|
.ptd-tf-btn--active {
|
||||||
border-color: #3f7ef5;
|
border-color: var(--accent);
|
||||||
color: #3f7ef5;
|
color: var(--accent);
|
||||||
background: rgba(63,126,245,0.12);
|
background: var(--bg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ptd-indicators {
|
.ptd-indicators {
|
||||||
@@ -265,16 +265,16 @@
|
|||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 0 10px 8px;
|
padding: 0 10px 8px;
|
||||||
border-top: 1px solid rgba(122,162,247,0.08);
|
border-top: 1px solid var(--sep);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.ptd-ind-panel {
|
.ptd-ind-panel {
|
||||||
background: #121520;
|
background: var(--bg3);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
}
|
}
|
||||||
.ptd-ind-label { font-size: 9px; color: #6272a4; margin-bottom: 2px; }
|
.ptd-ind-label { font-size: 9px; color: var(--text3); margin-bottom: 2px; }
|
||||||
.ptd-ind-svg { width: 100%; height: 36px; display: block; }
|
.ptd-ind-svg { width: 100%; height: 36px; display: block; }
|
||||||
|
|
||||||
/* 우측 패널 — 매매/호가 탭 */
|
/* 우측 패널 — 매매/호가 탭 */
|
||||||
@@ -283,14 +283,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background: #1e222d;
|
background: var(--bg2);
|
||||||
border: 1px solid rgba(122,162,247,0.12);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.ptd-tabs--main {
|
.ptd-tabs--main {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #121520;
|
background: var(--bg3);
|
||||||
}
|
}
|
||||||
.ptd-tabs--main .ptd-tab {
|
.ptd-tabs--main .ptd-tab {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -320,11 +320,11 @@
|
|||||||
.ptd-order-card .top-panel { padding: 4px 2px; background: transparent; }
|
.ptd-order-card .top-panel { padding: 4px 2px; background: transparent; }
|
||||||
.ptd-order-card .top-field { margin-bottom: 4px; }
|
.ptd-order-card .top-field { margin-bottom: 4px; }
|
||||||
.ptd-order-card .top-submit--buy { background: #22c55e !important; border-color: #22c55e !important; }
|
.ptd-order-card .top-submit--buy { background: #22c55e !important; border-color: #22c55e !important; }
|
||||||
.ptd-order-card .top-submit--sell { background: #3f7ef5 !important; border-color: #3f7ef5 !important; }
|
.ptd-order-card .top-submit--sell { background: var(--accent) !important; border-color: var(--accent) !important; }
|
||||||
|
|
||||||
.ptd-ob {
|
.ptd-ob {
|
||||||
background: #1e222d;
|
background: var(--bg2);
|
||||||
border: 1px solid rgba(122,162,247,0.12);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -339,8 +339,8 @@
|
|||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #9aa5ce;
|
color: var(--text2);
|
||||||
border-bottom: 1px solid rgba(122,162,247,0.08);
|
border-bottom: 1px solid var(--sep);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.ptd-ob-body {
|
.ptd-ob-body {
|
||||||
@@ -378,15 +378,15 @@
|
|||||||
.ptd-ob-row--bid .ptd-ob-bar { background: #22c55e; left: 0; right: auto; }
|
.ptd-ob-row--bid .ptd-ob-bar { background: #22c55e; left: 0; right: auto; }
|
||||||
.ptd-ob-row--ask .ptd-ob-price { color: #ef4444; position: relative; z-index: 1; }
|
.ptd-ob-row--ask .ptd-ob-price { color: #ef4444; position: relative; z-index: 1; }
|
||||||
.ptd-ob-row--bid .ptd-ob-price { color: #22c55e; position: relative; z-index: 1; }
|
.ptd-ob-row--bid .ptd-ob-price { color: #22c55e; position: relative; z-index: 1; }
|
||||||
.ptd-ob-size { position: relative; z-index: 1; color: #6272a4; }
|
.ptd-ob-size { position: relative; z-index: 1; color: var(--text3); }
|
||||||
.ptd-ob-mid {
|
.ptd-ob-mid {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: var(--text);
|
||||||
border-top: 1px solid rgba(122,162,247,0.08);
|
border-top: 1px solid var(--sep);
|
||||||
border-bottom: 1px solid rgba(122,162,247,0.08);
|
border-bottom: 1px solid var(--sep);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ptd-history-card {
|
.ptd-history-card {
|
||||||
@@ -402,17 +402,17 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
.ptd-tabs { display: flex; border-bottom: 1px solid rgba(122,162,247,0.1); flex-shrink: 0; }
|
.ptd-tabs { display: flex; border-bottom: 1px solid var(--sep); flex-shrink: 0; }
|
||||||
.ptd-tab {
|
.ptd-tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #6272a4;
|
color: var(--text3);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.ptd-tab.active { color: #3f7ef5; font-weight: 700; border-bottom: 2px solid #3f7ef5; }
|
.ptd-tab.active { color: var(--accent); font-weight: 700; border-bottom: 2px solid var(--accent); }
|
||||||
.ptd-table--compact { font-size: 9px; }
|
.ptd-table--compact { font-size: 9px; }
|
||||||
.ptd-table--compact th,
|
.ptd-table--compact th,
|
||||||
.ptd-table--compact td { padding: 5px 6px; }
|
.ptd-table--compact td { padding: 5px 6px; }
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Theme } from '../types';
|
||||||
|
|
||||||
|
const HTML_THEME_CLASSES = ['theme-dark', 'theme-light', 'theme-blue'] as const;
|
||||||
|
|
||||||
|
/** body 포털(알림 팝업 등)에서도 테마 CSS 변수가 적용되도록 html 클래스 동기화 */
|
||||||
|
export function syncDocumentTheme(theme: Theme): void {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const html = document.documentElement;
|
||||||
|
html.classList.remove(...HTML_THEME_CLASSES);
|
||||||
|
html.classList.add(`theme-${theme}`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user