From 032ec692909185cb54b0ce9e42b650910d221bad Mon Sep 17 00:00:00 2001 From: Macbook Date: Fri, 5 Jun 2026 22:24:41 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=ED=85=8C=EC=8A=A4=ED=8C=85=20?= =?UTF-8?q?=EC=BA=94=EB=93=A4=EC=B0=A8=ED=8A=B8=20=ED=95=98=EB=8B=A8?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EC=A1=B0=EC=A7=80=ED=91=9C=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/RightSidePanel.tsx | 31 ++++++++++++++----- frontend/src/components/TradingChart.tsx | 14 +++++++-- .../backtest/BacktestAnalysisChart.tsx | 8 +++-- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/RightSidePanel.tsx b/frontend/src/components/RightSidePanel.tsx index ff4ec88..f333ad1 100644 --- a/frontend/src/components/RightSidePanel.tsx +++ b/frontend/src/components/RightSidePanel.tsx @@ -85,12 +85,27 @@ const RightSidePanel: React.FC = ({ const isOpenControlled = openControlled !== undefined && onOpenChange !== undefined; const open = isOpenControlled ? !!openControlled : openInternal; const { trades: recentTrades, strength: tradeStrength } = useUpbitRecentTrades(market, open && wsFeedEnabled); - const setOpen = (next: boolean | ((p: boolean) => boolean)) => { - const cur = isOpenControlled ? !!openControlled : openInternal; - const value = typeof next === 'function' ? next(cur) : next; - if (isOpenControlled && onOpenChange) onOpenChange(value); - else setOpenInternal(value); - }; + + /** prop 최신값을 ref 로 유지 — setOpen 콜백이 stale closure 를 참조하지 않도록 */ + const openControlledRef = useRef(openControlled); + openControlledRef.current = openControlled; + const isOpenControlledRef = useRef(isOpenControlled); + isOpenControlledRef.current = isOpenControlled; + + /** + * useCallback 으로 안정적인 참조 유지. + * setOpenInternal 은 useState 가 보장하는 stable identity 이므로 deps 불필요. + * fillRequest useEffect 에서 deps 변경으로 인한 재실행을 방지한다. + */ + const setOpen = useCallback((next: boolean | ((p: boolean) => boolean)) => { + if (isOpenControlledRef.current && onOpenChange) { + const cur = !!openControlledRef.current; + const value = typeof next === 'function' ? next(cur) : next; + onOpenChange(value); + } else { + setOpenInternal(prev => typeof next === 'function' ? next(prev) : next); + } + }, [onOpenChange]); const activeTab = activeTabControlled ?? tabInternal; const setActiveTab = useCallback((tab: TradeRightPanelTab) => { @@ -102,7 +117,9 @@ const RightSidePanel: React.FC = ({ if (!fillRequest) return; setOpen(true); setActiveTab('trade'); - }, [fillRequest?.seq, fillRequest?.side, setOpen, setActiveTab]); + // setOpen / setActiveTab 은 안정적인 useCallback 참조이므로 deps 에서 제외 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fillRequest?.seq, fillRequest?.side]); const handleOrderbookRowClick = useCallback((price: number, rowType: 'ask' | 'bid') => { setActiveTab('trade'); diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index c047511..3809938 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -150,6 +150,8 @@ interface TradingChartProps { paneSeparatorOptions?: ChartPaneSeparatorOptions; /** true: pane 합산 높이로 chart-container 를 키우지 않고 래퍼 높이에 맞춤 (미니 차트·알림 목록) */ paneLayoutClamp?: boolean; + /** paneLayoutClamp=true 환경에서도 캔들/보조지표 비율 드래그 핸들을 표시 (백테스팅 차트 등) */ + showPaneResizeHandle?: boolean; /** 캔들+거래량 vs 보조 pane 높이 비율 (aux 0 이면 미적용) */ paneAreaRatio?: { candle: number; aux: number } | null; /** 보조지표 pane 레전드(이름·값 오버레이) 표시 (기본 true, 알림 미니차트는 false) */ @@ -207,6 +209,7 @@ const TradingChart: React.FC = ({ chartVisible = true, paneSeparatorOptions, paneLayoutClamp = false, + showPaneResizeHandle = false, paneAreaRatio = null, showPaneLegend = true, crosshairInfoVisible = true, @@ -250,6 +253,8 @@ const TradingChart: React.FC = ({ const candleOnlyModeRef = useRef(false); const paneAreaRatioRef = useRef(paneAreaRatio); paneAreaRatioRef.current = paneAreaRatio; + const paneLayoutClampRef = useRef(paneLayoutClamp); + paneLayoutClampRef.current = paneLayoutClamp; /** 사용자 드래그로 설정한 pane 비율 — prop 변경 시 초기화 */ const userDraggedRatioRef = useRef<{ candle: number; aux: number } | null>(null); const lastCrosshairPriceRef = useRef(null); @@ -390,8 +395,11 @@ const TradingChart: React.FC = ({ userDraggedRatioRef.current = { candle: newCandle, aux: newAux }; const required = mgr.resetPaneHeights(H); - // 스크롤 영역이 필요한 경우 컨테이너 확장 - const newContainerH = required > H + 4 ? required : H; + // paneLayoutClamp 모드: 컨테이너를 래퍼 높이에 고정 (overflow:hidden 으로 클립) + // 일반 모드: 보조지표 필요 시 컨테이너 확장하여 스크롤 영역 확보 + const newContainerH = paneLayoutClampRef.current + ? H + : (required > H + 4 ? required : H); if (container.style.height !== `${newContainerH}px`) { container.style.height = `${newContainerH}px`; } @@ -1698,7 +1706,7 @@ const TradingChart: React.FC = ({ className="chart-container" /> {/* 캔들 pane 하단 드래그 핸들 — 캔들/보조지표 높이 비율 조절 */} - {candlePaneBottomY != null && !candleOnlyMode && !paneLayoutClamp && chartPaintReady && ( + {candlePaneBottomY != null && !candleOnlyMode && (!paneLayoutClamp || showPaneResizeHandle) && chartPaintReady && (
= ({ let inds: IndicatorConfig[]; if (strategy) { inds = buildVirtualTradingChartIndicators(strategy, getParams, getVisualConfig); - // [수정] 오실레이터 필터 제거 — TradingChart sub-pane 에서 직접 렌더링 - // (기존: showOscillatorPanel 시 overlay 지표만 남기고 나머지는 SVG 패널로 분리) inds = inds.filter(i => i.timeframeVisibility?.[chartTimeframe] !== false); + // 전략 지표가 모두 캔들 오버레이(SMA/EMA/일목 등)인 경우 기본 오실레이터(RSI·MACD)를 추가. + // 기존 SVG 기반 StrategyOscillatorPanes 의 fallback 동작과 동일. + if (showOscillatorPanel && inds.every(i => isOverlayType(i.type))) { + inds = [...inds, ...defaultOscillatorIndicators(getParams)]; + } } else { inds = defaultOverlayIndicators(getParams); // 전략 미선택 시 기본 오실레이터(RSI, MACD)도 TradingChart sub-pane 으로 추가 @@ -477,6 +480,7 @@ const BacktestAnalysisChart: React.FC = ({ volumeVisible={false} paneSeparatorOptions={appDefaults.chartPaneSeparator} paneLayoutClamp + showPaneResizeHandle={auxPaneCount > 0} paneAreaRatio={paneAreaRatio} showPaneLegend crosshairInfoVisible