From 0d14fd4ad29af8c2e88bb800c0b40bee36a1ce2e Mon Sep 17 00:00:00 2001 From: Macbook Date: Sun, 21 Jun 2026 23:50:04 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EC=A7=80=ED=91=9C=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=EC=98=81=EC=97=AD=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=ED=99=95=EB=8C=80=20=EC=B6=95=EC=86=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/TradingChart.tsx | 3 +- frontend/src/utils/ChartManager.ts | 65 ++++++++++++++---------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 5ac640c..54d8d0a 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -1219,7 +1219,8 @@ const TradingChart: React.FC = ({ e.preventDefault(); e.stopPropagation(); - m.zoomPriceAxisByWheel(chartX, chartY, e.deltaY); + const delta = Math.abs(e.deltaY) >= Math.abs(e.deltaX) ? e.deltaY : e.deltaX; + m.zoomPriceAxisByWheel(chartX, chartY, delta); }; container.addEventListener('wheel', onWheelCapture, { capture: true, passive: false }); diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 6172830..d2b8fbd 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -417,7 +417,7 @@ export class ChartManager { // pressedMouseMove: TradingChart 에서 커스텀 패닝 처리 (드로잉·클릭과 충돌 방지) // horzTouchDrag: 보조 pane 터치 드래그가 메인 timeScale 과 어긋나는 현상 방지 — TradingChart 커스텀 패닝만 사용 handleScroll: { mouseWheel: true, pressedMouseMove: false, horzTouchDrag: false, vertTouchDrag: false }, - handleScale: { axisPressedMouseMove: true, axisDoubleClickReset: true, mouseWheel: true, pinch: true }, + handleScale: { axisPressedMouseMove: true, axisDoubleClickReset: true, mouseWheel: false, pinch: true }, }); this._applyDisplayTimezoneOptions(); @@ -4120,15 +4120,15 @@ export class ChartManager { const paneIndex = this.getPaneIndexAtChartY(chartY); const scaleW = this.getPriceScaleWidth(paneIndex); if (scaleW <= 0) return false; - return chartX >= this.container.clientWidth - scaleW; + const minSub = this._minIndicatorSubPane(); + const hitW = paneIndex >= minSub ? Math.max(scaleW, 72) : scaleW; + return chartX >= this.container.clientWidth - hitW; } /** pane 우측 가격축(값 라벨) 너비 — PaneLegend 버튼 배치용 */ getPriceScaleWidth(paneIndex: number): number { try { - const ps = paneIndex === 1 - ? this.chart.priceScale('volume', 1) - : this.chart.priceScale('right', paneIndex); + const ps = this._getPriceScaleForPane(paneIndex); const scaleW = ps.width(); return scaleW > 0 ? scaleW : 56; } catch { @@ -4136,6 +4136,14 @@ export class ChartManager { } } + /** pane 1 = 볼륨(표시 시) 또는 첫 보조지표(볼륨 숨김) */ + private _getPriceScaleForPane(paneIndex: number) { + if (paneIndex === 1 && this._volumePaneShown()) { + return this.chart.priceScale('volume', 1); + } + return this.chart.priceScale('right', paneIndex); + } + /** 캔들 pane(0) 플롯 영역 너비 (우측 가격축 제외) */ private _candlePlotWidth(): number { const scaleW = this.getPriceScaleWidth(0); @@ -4279,9 +4287,11 @@ export class ChartManager { /** 우측 가격축 라벨 영역 휠 — 해당 pane(캔들·거래량·보조지표) 세로 줌 */ zoomPriceAxisByWheel(chartX: number, chartY: number, deltaY: number): boolean { - if (deltaY === 0 || !this.isOnPriceAxis(chartX, chartY)) return false; + if (!this.isOnPriceAxis(chartX, chartY)) return false; const paneIndex = this.getPaneIndexAtChartY(chartY); - return this._zoomPanePriceScaleByWheel(paneIndex, deltaY, chartY); + const delta = deltaY !== 0 ? deltaY : 0; + if (delta === 0) return true; + return this._zoomPanePriceScaleByWheel(paneIndex, delta, chartY); } /** @@ -4299,24 +4309,14 @@ export class ChartManager { return this._zoomPanePriceScaleByWheel(paneIndex, deltaY, anchorChartY); } - private _getPriceScaleForPane(paneIndex: number) { - return paneIndex === 1 - ? this.chart.priceScale('volume', 1) - : this.chart.priceScale('right', paneIndex); - } - private _getRepresentativeSeriesForPane(paneIndex: number): ISeriesApi | null { if (paneIndex === 0) return this.mainSeries; - if (paneIndex === 1 && this.volumeSeries) { - try { - const vis = (this.volumeSeries.options() as { visible?: boolean }).visible; - if (vis !== false) return this.volumeSeries; - } catch { - return this.volumeSeries; - } + if (paneIndex === 1 && this._volumePaneShown() && this.volumeSeries) { + return this.volumeSeries; } for (const entry of this.indicators.values()) { - if (entry.paneIndex === paneIndex && entry.seriesList.length > 0) { + const entryPane = entry.paneIndex ?? this._readSeriesPaneIndex(entry.seriesList[0]) ?? -1; + if (entryPane === paneIndex && entry.seriesList.length > 0) { return entry.seriesList[0]; } } @@ -4324,12 +4324,17 @@ export class ChartManager { return null; } - private _readPanePriceRange(paneIndex: number): { from: number; to: number } | null { - if (paneIndex === 0) return this._readMainPriceRange(); + private _snapshotPaneAutoscaleRange(paneIndex: number): { from: number; to: number } | null { + const ps = this._getPriceScaleForPane(paneIndex); + try { + const existing = ps.getVisibleRange(); + if (existing) return { from: existing.from, to: existing.to }; + } catch { /* fall through */ } try { - const existing = this._getPriceScaleForPane(paneIndex).getVisibleRange(); - if (existing) return { from: existing.from, to: existing.to }; + ps.setAutoScale(true); + const ranged = ps.getVisibleRange(); + if (ranged) return { from: ranged.from, to: ranged.to }; } catch { /* fall through */ } const series = this._getRepresentativeSeriesForPane(paneIndex); @@ -4342,6 +4347,11 @@ export class ChartManager { return { from: Math.min(pTop, pBot), to: Math.max(pTop, pBot) }; } + private _readPanePriceRange(paneIndex: number): { from: number; to: number } | null { + if (paneIndex === 0) return this._readMainPriceRange(); + return this._snapshotPaneAutoscaleRange(paneIndex); + } + private _preparePaneManualPriceScale(paneIndex: number): void { if (paneIndex === 0) { if (!this.mainSeries) return; @@ -4354,14 +4364,15 @@ export class ChartManager { this._getPriceScaleForPane(paneIndex).setAutoScale(false); } catch { /* ok */ } - if (paneIndex === 1 && this.volumeSeries) { + if (paneIndex === 1 && this._volumePaneShown() && this.volumeSeries) { try { this.volumeSeries.applyOptions({ autoscaleInfoProvider: undefined }); } catch { /* ok */ } } for (const entry of this.indicators.values()) { - if (entry.paneIndex !== paneIndex) continue; + const entryPane = entry.paneIndex ?? this._readSeriesPaneIndex(entry.seriesList[0]) ?? -1; + if (entryPane !== paneIndex) continue; for (const series of entry.seriesList) { try { series.applyOptions({ autoscaleInfoProvider: undefined });