From e1ad1d626e919a9b128336ff630f4e8ff5a9415e Mon Sep 17 00:00:00 2001 From: Macbook Date: Fri, 29 May 2026 00:58:10 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=A4=EB=A7=A4=20=EC=8B=9C=EA=B7=B8?= =?UTF-8?q?=EB=84=90=20=EC=95=8C=EB=A6=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.css | 45 ++++++++++------ .../TradeNotificationSignalSummary.tsx | 2 +- .../TradeSignalMiniChart.tsx | 29 ++++++---- frontend/src/styles/tradeNotificationList.css | 54 ++++++++++++------- .../src/styles/virtualTradingDashboard.css | 26 +++------ 5 files changed, 94 insertions(+), 62 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index d76a035..fc3f85b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -35,6 +35,28 @@ --accent-muted: color-mix(in srgb, var(--accent) 22%, transparent); --popup-shadow: 0 8px 32px rgba(0, 0, 0, 0.55); --overlay-scrim: rgba(0, 0, 0, 0.55); + + /* 실시간 수신 형광 연두 — 가상매매·알림·차트 슬롯 공통 (얇은 외곽) */ + --gc-receive-border: #69f0ae; + --gc-receive-shadow: + 0 0 0 1px color-mix(in srgb, #69f0ae 92%, transparent), + 0 0 6px 2px #69f0ae50, + 0 0 14px 4px #b9f6ca26; + --gc-receive-shadow-inset: + 0 0 0 1px color-mix(in srgb, #69f0ae 92%, transparent), + 0 0 6px 2px #69f0ae50, + 0 0 14px 4px #b9f6ca26, + inset 0 0 28px color-mix(in srgb, #b9f6ca 10%, transparent); + --gc-receive-shadow-inset-light: + 0 0 0 1px color-mix(in srgb, #69f0ae 92%, transparent), + 0 0 6px 2px #69f0ae50, + 0 0 14px 4px #b9f6ca26, + inset 0 0 20px color-mix(in srgb, #b9f6ca 8%, transparent); + --gc-receive-panel-inset: inset 0 0 16px color-mix(in srgb, #b9f6ca 6%, transparent); + --gc-receive-dot-shadow: + 0 0 4px 2px #69f0ae, + 0 0 10px 4px #69f0ae55, + 0 0 16px 6px #b9f6ca1a; } /* ── Dark Theme: 도쿄 나이트 (THEME_SETTINGS.md §2.2) ───────────── */ @@ -537,11 +559,8 @@ html.theme-blue { .ws-dot.receiving { animation: none !important; /* idle 애니메이션 일시 중단 */ background: #b9f6ca !important; /* 밝은 연두 */ - transform: scale(1.4); - box-shadow: - 0 0 6px 3px #69f0ae, /* 내부 glow */ - 0 0 14px 6px #69f0ae66, /* 외부 glow */ - 0 0 24px 10px #b9f6ca22; /* 넓은 음영 */ + transform: scale(1.35); + box-shadow: var(--gc-receive-dot-shadow); } @keyframes ws-idle-lime { @@ -4634,12 +4653,8 @@ html.theme-blue { /* 실시간 수신 — 가상투자 vtd-card--receiving 와 동일 형광 연두 */ .multi-slot-wrap:has(.chart-slot--receiving), .chart-slot--compact.chart-slot--receiving { - border-color: #69f0ae !important; - box-shadow: - 0 0 0 1px #69f0ae, - 0 0 10px 4px #69f0ae66, - 0 0 22px 8px #b9f6ca33, - inset 0 0 48px color-mix(in srgb, #b9f6ca 14%, transparent); + border-color: var(--gc-receive-border) !important; + box-shadow: var(--gc-receive-shadow-inset); transition: box-shadow 0.15s ease-out, border-color 0.15s ease-out; } @@ -4654,14 +4669,12 @@ html.theme-blue { .chart-slot--compact.chart-slot--receiving.active { box-shadow: - 0 0 0 1px #69f0ae, - 0 0 10px 4px #69f0ae66, - 0 0 22px 8px #b9f6ca33, - inset 0 0 0 1px color-mix(in srgb, #69f0ae 55%, var(--accent, #2962ff)); + var(--gc-receive-shadow), + inset 0 0 0 1px color-mix(in srgb, #69f0ae 50%, var(--accent, #2962ff)); } .chart-slot--compact.chart-slot--receiving .slot-chart-wrap--compact { - box-shadow: inset 0 0 24px color-mix(in srgb, #b9f6ca 8%, transparent); + box-shadow: var(--gc-receive-panel-inset); } .slot-header--compact { diff --git a/frontend/src/components/tradeNotification/TradeNotificationSignalSummary.tsx b/frontend/src/components/tradeNotification/TradeNotificationSignalSummary.tsx index 349ddbb..30d4c56 100644 --- a/frontend/src/components/tradeNotification/TradeNotificationSignalSummary.tsx +++ b/frontend/src/components/tradeNotification/TradeNotificationSignalSummary.tsx @@ -80,7 +80,7 @@ const TradeNotificationSignalSummary: React.FC = ({ headVariant={embedded ? 'inline-quote' : 'default'} theme={theme} ticker={ticker} - chartLiveReceiveHighlight + chartLiveReceiveHighlight={!embedded} readOnlyStrategyLabel={strategyLabel} /> ); diff --git a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx index e21bc35..df49106 100644 --- a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx +++ b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx @@ -58,7 +58,10 @@ const TradeSignalMiniChart: React.FC = ({ pendingBarRef.current = null; mgr.updateBar(pending); const wrap = canvasWrapRef.current; - if (wrap && wrap.clientHeight > 0) mgr.resetPaneHeights(wrap.clientHeight); + if (wrap && wrap.clientWidth > 0 && wrap.clientHeight > 0) { + mgr.resetPaneHeights(wrap.clientHeight); + mgr.resize(wrap.clientWidth, wrap.clientHeight); + } }, []); const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => { @@ -104,11 +107,15 @@ const TradeSignalMiniChart: React.FC = ({ barsMarketRef.current = barsMarket; const barsReady = bars.length >= 2 && barsMarket === market; - const applyPaneHeights = useCallback(() => { + const syncChartLayout = useCallback(() => { const wrap = canvasWrapRef.current; const mgr = managerRef.current; - if (!wrap || wrap.clientHeight <= 0 || !mgr?.hasMainSeries()) return; - mgr.resetPaneHeights(wrap.clientHeight); + if (!wrap || !mgr?.hasMainSeries()) return; + const w = wrap.clientWidth; + const h = wrap.clientHeight; + if (w <= 0 || h <= 0) return; + mgr.resetPaneHeights(h); + mgr.resize(w, h); }, []); useLayoutEffect(() => { @@ -127,8 +134,12 @@ const TradeSignalMiniChart: React.FC = ({ if (!canvasWrapRef.current) return; const h = canvasWrapRef.current.clientHeight; if (h <= 0) return; + const w = canvasWrapRef.current.clientWidth; if (managerRef.current?.hasMainSeries()) { - managerRef.current.resetPaneHeights(h); + if (w > 0) { + managerRef.current.resetPaneHeights(h); + managerRef.current.resize(w, h); + } } else if (!chartReloadTriggeredRef.current && delay >= 600 && barsReady) { chartReloadTriggeredRef.current = true; setChartReloadTick(t => t + 1); @@ -136,13 +147,13 @@ const TradeSignalMiniChart: React.FC = ({ }, delay), ); - const ro = new ResizeObserver(() => applyPaneHeights()); + const ro = new ResizeObserver(() => syncChartLayout()); ro.observe(wrap); return () => { timers.forEach(clearTimeout); ro.disconnect(); }; - }, [market, timeframe, enabled, barsReady, applyPaneHeights, fillHeight]); + }, [market, timeframe, enabled, barsReady, syncChartLayout, fillHeight]); useEffect(() => { if (!enabled || !barsReady) return; @@ -154,9 +165,9 @@ const TradeSignalMiniChart: React.FC = ({ useEffect(() => { if (!enabled) return; - const t = setTimeout(() => applyPaneHeights(), 80); + const t = setTimeout(() => syncChartLayout(), 80); return () => clearTimeout(t); - }, [fillHeight, enabled, applyPaneHeights]); + }, [fillHeight, enabled, syncChartLayout]); useEffect(() => { if (!enabled || !useUpbit) return; diff --git a/frontend/src/styles/tradeNotificationList.css b/frontend/src/styles/tradeNotificationList.css index 4dafc4f..dd529da 100644 --- a/frontend/src/styles/tradeNotificationList.css +++ b/frontend/src/styles/tradeNotificationList.css @@ -2,11 +2,8 @@ /* 실시간 WS/STOMP 수신 — 목록 행(.tnl-row) 외곽 아웃라인만 형광 연두 (내부 inset 없음) */ .tnl-row.tnl-row--receiving { - border-color: #69f0ae !important; - box-shadow: - 0 0 0 1px #69f0ae, - 0 0 10px 4px #69f0ae66, - 0 0 22px 8px #b9f6ca33; + border-color: var(--gc-receive-border) !important; + box-shadow: var(--gc-receive-shadow); transition: box-shadow 0.15s ease-out, border-color 0.15s ease-out; } @@ -36,6 +33,20 @@ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #7aa2f7) 35%, transparent); } +/* 수신 시 신호 상세(vtd-card) — 연두 테두리·inset 제거 (목록 행 외곽만 유지) */ +.tnl-row.tnl-row--receiving .tnl-signal-slot .vtd-card--receiving, +.tnl-row.tnl-row--receiving .tnl-signal-slot .vtd-card.vtd-card--live { + border-color: color-mix(in srgb, var(--accent, #3f7ef5) 40%, var(--se-border, var(--border))) !important; + box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #3f7ef5) 12%, transparent) !important; + background: var(--se-panel-card-bg, var(--bg2)) !important; +} + +.tnl-row.tnl-row--receiving .tnl-signal-slot .vtd-card--receiving .vtd-sig-panel, +.tnl-row.tnl-row--receiving .tnl-signal-slot .vtd-card--receiving .vtd-card-chart-panel, +.tnl-row.tnl-row--receiving .tnl-signal-slot .vtd-sig-panel-wrap--receiving .vtd-sig-panel { + box-shadow: none !important; +} + .tnl-row--gallery { display: block; width: 100%; @@ -206,12 +217,12 @@ height: 100%; } -/* 신호상세 — 가상매매 vtd-card 슬롯 (이퀄라이저·히트맵·신호등 최소 너비 확보) */ +/* 신호상세 — 가상매매 vtd-card 슬롯 (차트보다 좁게 · 나머지는 캔들·지표) */ .tnl-signal-slot { - flex: 1.35 1 300px; + flex: 1.05 1 272px; width: auto; - min-width: min(100%, 300px); - max-width: 42%; + min-width: min(100%, 272px); + max-width: 34%; height: 280px; max-height: 280px; display: flex; @@ -313,8 +324,9 @@ } .tnl-charts-track > .tnl-signal-slot { - flex: 1.35 1 300px; - min-width: min(100%, 300px); + flex: 1.05 1 272px; + min-width: min(100%, 272px); + max-width: 34%; } /* 가상매매 투자대상 카드(vtd)와 동일한 3단 구성 */ @@ -622,7 +634,7 @@ } .tnl-charts-track > .tnl-chart-card { - flex: 1 1 0; + flex: 1.12 1 0; width: 0; min-width: 0; max-width: none; @@ -685,13 +697,16 @@ .tnl-mini-chart-canvas { position: relative; - flex: 0 0 236px; - height: 236px; - max-height: 236px; + flex: 1 1 auto; + width: 100%; + min-width: 0; min-height: 0; + height: 100%; + max-height: 100%; display: flex; flex-direction: column; overflow: hidden; + box-sizing: border-box; } .tnl-mini-chart-canvas--fill { @@ -721,14 +736,17 @@ } .tnl-mini-chart-placeholder { - flex: 0 0 236px; - height: 236px; - max-height: 236px; + flex: 1 1 auto; + width: 100%; + min-height: 120px; + height: 100%; + max-height: 100%; display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--text3); + box-sizing: border-box; } .tnl-mini-chart-placeholder--fill { diff --git a/frontend/src/styles/virtualTradingDashboard.css b/frontend/src/styles/virtualTradingDashboard.css index 56e3e40..4715a47 100644 --- a/frontend/src/styles/virtualTradingDashboard.css +++ b/frontend/src/styles/virtualTradingDashboard.css @@ -720,11 +720,8 @@ } .vtd-focus-wrap--receiving { - border-color: #69f0ae !important; - box-shadow: - 0 0 0 1px #69f0ae, - 0 0 10px 4px #69f0ae66, - inset 0 0 32px color-mix(in srgb, #b9f6ca 12%, transparent); + border-color: var(--gc-receive-border) !important; + box-shadow: var(--gc-receive-shadow-inset-light); } .vtd-focus-head { @@ -917,7 +914,7 @@ } .vtd-sig-panel-wrap--receiving .vtd-sig-panel { - box-shadow: inset 0 0 24px color-mix(in srgb, #b9f6ca 8%, transparent); + box-shadow: var(--gc-receive-panel-inset); } .vtd-focus-sig-meta { @@ -1211,12 +1208,8 @@ /* 실시간 데이터 수신 — 실시간 차트 ws-dot.receiving 과 동일 형광 연두 음영 */ .vtd-card--receiving { - border-color: #69f0ae !important; - box-shadow: - 0 0 0 1px #69f0ae, - 0 0 10px 4px #69f0ae66, - 0 0 22px 8px #b9f6ca33, - inset 0 0 48px color-mix(in srgb, #b9f6ca 14%, transparent); + border-color: var(--gc-receive-border) !important; + box-shadow: var(--gc-receive-shadow-inset); background: linear-gradient( 165deg, color-mix(in srgb, #b9f6ca 18%, var(--vtd-sig-card-gradient)) 0%, @@ -1228,7 +1221,7 @@ .vtd-card--receiving .vtd-sig-panel, .vtd-card--receiving .vtd-card-chart-panel { - box-shadow: inset 0 0 24px color-mix(in srgb, #b9f6ca 8%, transparent); + box-shadow: var(--gc-receive-panel-inset); } .vtd-card-head { @@ -1535,11 +1528,8 @@ .vtd-live-badge-dot.receiving { animation: none !important; background: #b9f6ca !important; - transform: scale(1.35); - box-shadow: - 0 0 6px 3px #69f0ae, - 0 0 14px 6px #69f0ae66, - 0 0 24px 10px #b9f6ca22; + transform: scale(1.3); + box-shadow: var(--gc-receive-dot-shadow); } .vtd-live-badge--receiving {