diff --git a/frontend/src/components/TradeSignalLabelOverlay.tsx b/frontend/src/components/TradeSignalLabelOverlay.tsx index d1cf160..28c2213 100644 --- a/frontend/src/components/TradeSignalLabelOverlay.tsx +++ b/frontend/src/components/TradeSignalLabelOverlay.tsx @@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'; import type { ChartManager } from '../utils/ChartManager'; import { layoutStackedSignalLabels, + resolveSignalBarTime, resolveSignalLabelAnchors, type DrawableSignalLabel, type TradeSignalLabelItem, @@ -10,6 +11,8 @@ import { interface Props { manager: ChartManager; + /** tv-chart-wrap 등 상위 — DrawingCanvas(z-index 10) 위에 표시 */ + getMountTarget?: () => HTMLElement | null; } const ARROW_OFFSET = 16; @@ -18,9 +21,10 @@ const FONT = '600 11px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif function resolveArrowY( mgr: ChartManager, item: TradeSignalLabelItem, + barTime: number, mainTopY: number, ): number | null { - const bar = mgr.getBarAtTime(item.time); + const bar = mgr.getBarAtTime(barTime); let paneY: number | null; if (bar) { paneY = item.isBuy ? mgr.priceToY(bar.low) : mgr.priceToY(bar.high); @@ -60,9 +64,10 @@ function renderTradeSignalLabels( const drawable: DrawableSignalLabel[] = []; for (const item of labels) { - const x = mgr.timeToX(item.time); + const barTime = resolveSignalBarTime(mgr, item.time); + const x = mgr.timeToX(barTime); if (x == null || x < -40 || x > cssW + 40) continue; - const arrowY = resolveArrowY(mgr, item, main.topY); + const arrowY = resolveArrowY(mgr, item, barTime, main.topY); if (arrowY == null) continue; drawable.push({ item, x, arrowY }); } @@ -94,35 +99,45 @@ function renderTradeSignalLabels( ctx.setLineDash([]); } -const TradeSignalLabelOverlay: React.FC = ({ manager }) => { +const TradeSignalLabelOverlay: React.FC = ({ manager, getMountTarget }) => { const canvasRef = useRef(null); const [portalTarget, setPortalTarget] = useState(null); const redrawRafRef = useRef(0); + const resolveMountTarget = useCallback((): HTMLElement | null => { + return getMountTarget?.() ?? manager.getContainer(); + }, [getMountTarget, manager]); + useEffect(() => { - const container = manager.getContainer(); + const container = resolveMountTarget(); if (container) { setPortalTarget(container); return; } - const tid = window.setTimeout(() => setPortalTarget(manager.getContainer()), 100); + const tid = window.setTimeout(() => setPortalTarget(resolveMountTarget()), 100); return () => clearTimeout(tid); - }, [manager]); + }, [resolveMountTarget]); const redraw = useCallback(() => { const canvas = canvasRef.current; - const container = manager.getContainer(); - if (!canvas || !container) return; + const chartContainer = manager.getContainer(); + const mountEl = resolveMountTarget(); + if (!canvas || !chartContainer || !mountEl) return; - const cssW = container.clientWidth; - const cssH = container.clientHeight; + const cssW = chartContainer.clientWidth; + const cssH = chartContainer.clientHeight; if (cssW <= 0 || cssH <= 0) return; + const chartRect = chartContainer.getBoundingClientRect(); + const mountRect = mountEl.getBoundingClientRect(); + canvas.style.left = `${chartRect.left - mountRect.left}px`; + canvas.style.top = `${chartRect.top - mountRect.top}px`; + canvas.style.width = `${cssW}px`; + canvas.style.height = `${cssH}px`; + const dpr = window.devicePixelRatio || 1; canvas.width = Math.round(cssW * dpr); canvas.height = Math.round(cssH * dpr); - canvas.style.width = `${cssW}px`; - canvas.style.height = `${cssH}px`; const ctx = canvas.getContext('2d'); if (!ctx) return; @@ -130,7 +145,7 @@ const TradeSignalLabelOverlay: React.FC = ({ manager }) => { ctx.clearRect(0, 0, cssW, cssH); renderTradeSignalLabels(ctx, manager, cssW, manager.getTradeSignalLabels()); - }, [manager]); + }, [manager, resolveMountTarget]); useEffect(() => { if (!portalTarget) return; @@ -153,17 +168,20 @@ const TradeSignalLabelOverlay: React.FC = ({ manager }) => { manager.subscribeTradeSignalLabels(scheduleRedraw), ]; - const container = manager.getContainer(); + const chartContainer = manager.getContainer(); const ro = new ResizeObserver(scheduleRedraw); - if (container) ro.observe(container); + if (chartContainer) ro.observe(chartContainer); + if (portalTarget !== chartContainer) ro.observe(portalTarget); - const scrollWrap = container?.closest('.tv-chart-wrap') as HTMLElement | null; + const scrollWrap = portalTarget.closest('.tv-chart-wrap') as HTMLElement | null; scrollWrap?.addEventListener('scroll', scheduleRedraw, { passive: true }); + window.addEventListener('scroll', scheduleRedraw, { passive: true, capture: true }); return () => { unsubs.forEach(u => u()); ro.disconnect(); scrollWrap?.removeEventListener('scroll', scheduleRedraw); + window.removeEventListener('scroll', scheduleRedraw, true); if (redrawRafRef.current) { cancelAnimationFrame(redrawRafRef.current); redrawRafRef.current = 0; @@ -180,9 +198,8 @@ const TradeSignalLabelOverlay: React.FC = ({ manager }) => { aria-hidden style={{ position: 'absolute', - inset: 0, pointerEvents: 'none', - zIndex: 8, + zIndex: 15, }} />, portalTarget, diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 4ec0760..bd7fe9f 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -1885,9 +1885,6 @@ const TradingChart: React.FC = ({ options={paneSeparatorOptions} /> )} - {chartMgr && chartPaintReady && ( - - )} {chartMgr && ( = ({ selectedDrawingId={selectedDrawingId} /> )} + {chartMgr && chartPaintReady && ( + wrapperRef.current} + /> + )} {/* PaneLegend: containerRef 를 상태에 저장해 React 렌더와 동기화 */} {chartMgr && showCandlePaneControls && !candleOnlyLocked && (