From 0e461041064fda5e3119b81aa3209957b80679ca Mon Sep 17 00:00:00 2001 From: Macbook Date: Sat, 30 May 2026 01:28:58 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=8C=EB=A6=BC=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/TradingChart.tsx | 12 +++- .../TradeNotificationListRow.tsx | 46 ++++++------- .../TradeSignalMiniChart.tsx | 38 ++++------- frontend/src/styles/tradeNotificationList.css | 10 +++ frontend/src/utils/ChartManager.ts | 67 ++++++++++++++++--- 5 files changed, 116 insertions(+), 57 deletions(-) diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 04ddc88..9739bd9 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -358,6 +358,13 @@ const TradingChart: React.FC = ({ required = mgr.resetPaneHeights(wrapperH); } + if (paneLayoutClamp) { + try { + const w = wrapper.clientWidth; + if (w > 0 && wrapperH > 0) mgr.resize(w, wrapperH); + } catch { /* ok */ } + } + if (!paneLayoutClamp && required > wrapperH + 4) { container.style.height = `${required}px`; } else { @@ -730,7 +737,10 @@ const TradingChart: React.FC = ({ useEffect(() => { if (!containerRef.current) return; - const mgr = new ChartManager(containerRef.current, theme); + const mgr = new ChartManager(containerRef.current, theme, { + compactPaneLayout: paneLayoutClamp, + autoSize: !paneLayoutClamp, + }); managerRef.current = mgr; setChartMgr(mgr); if (!volumeVisibleRef.current) { diff --git a/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx b/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx index 0f90135..6fb8fdf 100644 --- a/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx +++ b/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx @@ -141,11 +141,11 @@ const TradeNotificationListRow: React.FC = ({ const chartExpanded = isListLayout && expandedChartKey != null; const chartActive = isListLayout && chartsEnabled && panelActive; - const signalActive = isListLayout && panelActive && !chartExpanded; + const signalCardEnabled = isListLayout && panelActive && !chartExpanded; const ticker = tickers?.get(item.market); const expandedIndicator = indicatorCards.find(c => c.id === expandedChartKey); - const signalSnapshotEnabled = isListLayout && panelActive && item.strategyId != null; + const signalSnapshotEnabled = signalCardEnabled && item.strategyId != null; const { snapshot, loading: signalLoading } = useTradeNotificationSignalSnapshot( item.market, item.strategyId, @@ -356,28 +356,26 @@ const TradeNotificationListRow: React.FC = ({ ) : ( <> - {signalActive && ( - - setExpandedChartKey('candle')} - /> - - )} + + setExpandedChartKey('candle')} + /> +
= ({ const marketRef = useRef(market); marketRef.current = market; + const syncChartLayout = useCallback(() => { + const wrap = canvasWrapRef.current; + const mgr = managerRef.current; + if (!wrap || !mgr?.hasMainSeries()) return; + const chartWrap = wrap.querySelector('.tv-chart-wrap'); + const w = chartWrap?.clientWidth ?? wrap.clientWidth; + const h = chartWrap?.clientHeight ?? wrap.clientHeight; + if (w <= 0 || h <= 0) return; + mgr.syncLayout(w, h); + }, []); + const handleCandlesReady = useCallback(() => { chartLiveReadyRef.current = true; const pending = pendingBarRef.current; @@ -57,12 +68,8 @@ const TradeSignalMiniChart: React.FC = ({ if (!pending || !mgr || barsMarketRef.current !== marketRef.current) return; pendingBarRef.current = null; mgr.updateBar(pending); - const wrap = canvasWrapRef.current; - if (wrap && wrap.clientWidth > 0 && wrap.clientHeight > 0) { - mgr.resetPaneHeights(wrap.clientHeight); - mgr.resize(wrap.clientWidth, wrap.clientHeight); - } - }, []); + syncChartLayout(); + }, [syncChartLayout]); const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => { if (barsMarketRef.current !== marketRef.current) return; @@ -107,17 +114,6 @@ const TradeSignalMiniChart: React.FC = ({ barsMarketRef.current = barsMarket; const barsReady = bars.length >= 2 && barsMarket === market; - const syncChartLayout = useCallback(() => { - const wrap = canvasWrapRef.current; - const mgr = managerRef.current; - 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(() => { chartLiveReadyRef.current = false; pendingBarRef.current = null; @@ -132,14 +128,8 @@ const TradeSignalMiniChart: React.FC = ({ const timers = [200, 600, 1200].map(delay => setTimeout(() => { if (!canvasWrapRef.current) return; - const h = canvasWrapRef.current.clientHeight; - if (h <= 0) return; - const w = canvasWrapRef.current.clientWidth; if (managerRef.current?.hasMainSeries()) { - if (w > 0) { - managerRef.current.resetPaneHeights(h); - managerRef.current.resize(w, h); - } + syncChartLayout(); } else if (!chartReloadTriggeredRef.current && delay >= 600 && barsReady) { chartReloadTriggeredRef.current = true; setChartReloadTick(t => t + 1); diff --git a/frontend/src/styles/tradeNotificationList.css b/frontend/src/styles/tradeNotificationList.css index c759c55..a9b4425 100644 --- a/frontend/src/styles/tradeNotificationList.css +++ b/frontend/src/styles/tradeNotificationList.css @@ -1160,6 +1160,16 @@ ul.tnl-list.tnl-list--grid { min-height: 300px; } +/* 그리드형 — 상세 메타와 투자전략 구분선 사이 여백 축소 */ +.tnl-summary-card--grid .tnl-summary-meta { + flex: 0 1 auto; +} + +.tnl-summary-card--grid .tnl-summary-strategy-field { + margin-top: -6px; + padding-top: 2px; +} + .tnl-row--grid.tnl-row--unread .tnl-summary-card { border-color: rgba(122, 162, 247, 0.45); } diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 7c3872f..b3b529c 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -258,13 +258,23 @@ export class ChartManager { /** applyPaneLayout / resetPaneHeights 에서 측정한 마지막 가용 높이(px) */ private _lastLayoutAvailableHeight?: number; - constructor(container: HTMLElement, theme: Theme) { + /** 알림 목록 미니 차트 등 고정 높이 — pane 최소 px·비율 완화 */ + private _compactPaneLayout = false; + + constructor( + container: HTMLElement, + theme: Theme, + options?: { autoSize?: boolean; compactPaneLayout?: boolean }, + ) { this.container = container; + this._compactPaneLayout = options?.compactPaneLayout ?? false; + const autoSize = options?.autoSize ?? !this._compactPaneLayout; const t = getTheme(theme); this.chart = createChart(container, { /* autoSize: true → LWC가 내부적으로 ResizeObserver를 사용해 컨테이너 크기 변경을 자동으로 처리. - 멀티 레이아웃에서 초기 렌더 시 컨테이너가 0 크기일 때도 차트가 정상 표시됨. */ - autoSize: true, + 멀티 레이아웃에서 초기 렌더 시 컨테이너가 0 크기일 때도 차트가 정상 표시됨. + compact(미니) 차트는 autoSize:false + syncLayout() 으로 피드백 루프 방지. */ + autoSize, layout: { background: { type: ColorType.Solid, color: t.bgColor }, textColor: t.textColor, @@ -764,10 +774,39 @@ export class ChartManager { /** 지표 추가·제거 후 pane index 동기화 + stretch 재배분 */ private _finalizeIndicatorPaneLayout(): void { - if (this._activeIndicatorPaneIndices().size === 0) return; this._removeOrphanSubPanes(); - this._resyncIndicatorPaneIndices(); - this.resetPaneHeights(this._lastLayoutAvailableHeight); + if (this._activeIndicatorPaneIndices().size > 0) { + this._resyncIndicatorPaneIndices(); + } else { + this._trimTrailingEmptySubPanes(); + } + const H = this._lastLayoutAvailableHeight ?? this.container.clientHeight; + this.resetPaneHeights(H > 0 ? H : undefined); + if (this._compactPaneLayout && H > 0) { + try { + const w = this.container.clientWidth; + if (w > 0) this.chart.resize(w, H); + } catch { /* ok */ } + } + this._notifyPaneLayout(); + } + + /** + * 고정 높이 미니 차트 — pane 비율 재계산 + LWC resize 를 한 번에 수행. + * (autoSize:false 일 때 외부 ResizeObserver 가 호출) + */ + syncLayout(width: number, height: number): void { + if (height > 0) this._lastLayoutAvailableHeight = height; + this._removeOrphanSubPanes(); + if (this._activeIndicatorPaneIndices().size > 0) { + this._resyncIndicatorPaneIndices(); + } else { + this._trimTrailingEmptySubPanes(); + } + this.resetPaneHeights(height); + try { + if (width > 0 && height > 0) this.chart.resize(width, height); + } catch { /* ok */ } this._notifyPaneLayout(); } @@ -3530,7 +3569,7 @@ export class ChartManager { const volumeShown = this._volumeVisible && !this._candleOnlyLayout; const VOL_FRAC = this._volumeFrac(H); - const MIN_IND_PX = 80; + const MIN_IND_PX = this._minIndPx(H); const ORPHAN_STRETCH = 0.0001; /** 보조지표 pane 은 동일 stretch weight → 항상 같은 높이 */ const IND_EQUAL_WEIGHT = 1; @@ -3569,14 +3608,26 @@ export class ChartManager { return totalRequired; } + private _minIndPx(H: number): number { + if (this._compactPaneLayout) { + return Math.min(56, Math.max(28, Math.round(H * 0.2))); + } + return 80; + } + private _paneHeightFractions( indicatorPaneCount: number, volFrac: number, + availableHeight?: number, ): { mainFrac: number; eachIndFrac: number } { const N = indicatorPaneCount; if (N === 0) { return { mainFrac: 1 - volFrac, eachIndFrac: 0 }; } + if (this._compactPaneLayout && N === 1 && availableHeight != null && availableHeight < 400) { + const mainFrac = 0.52; + return { mainFrac, eachIndFrac: Math.max(0.28, 1 - mainFrac - volFrac) }; + } if (N <= 3) { return { mainFrac: 0.5, eachIndFrac: (0.5 - volFrac) / N }; } @@ -3621,7 +3672,7 @@ export class ChartManager { ? this._lastLayoutAvailableHeight : this.container.clientHeight); const N = this._activeIndicatorPaneIndices().size; - const { mainFrac } = this._paneHeightFractions(N, this._volumeFrac(H)); + const { mainFrac } = this._paneHeightFractions(N, this._volumeFrac(H), H); return this._applyPaneStretchFactors(mainFrac, availableHeight); }