From ba6695eb9c90e2281d366c6c15bef885fb72502f Mon Sep 17 00:00:00 2001 From: Macbook Date: Sat, 30 May 2026 01:06:55 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=80=ED=91=9C=EC=B6=94=EA=B0=80=EC=8B=9C?= =?UTF-8?q?=20=EA=B2=B9=EC=B9=A8=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/TradingChart.tsx | 41 +++++++++++ frontend/src/utils/ChartManager.ts | 93 +++++++++++++++--------- 2 files changed, 101 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 2dc8fa0..04ddc88 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -441,6 +441,11 @@ const TradingChart: React.FC = ({ try { await mgr.addIndicatorsBatch(missing); afterIndicatorPaneMutation(mgr); + mgr.notifyPaneLayoutChanged(); + requestAnimationFrame(() => requestAnimationFrame(() => { + afterIndicatorPaneMutation(mgr); + mgr.notifyPaneLayoutChanged(); + })); } finally { indicatorSyncInFlightRef.current = false; } @@ -1178,10 +1183,38 @@ const TradingChart: React.FC = ({ indicatorSyncInFlightRef.current = true; mgr.removeIndicators(removedIds); afterIndicatorPaneMutation(mgr); + mgr.notifyPaneLayoutChanged(); restoreLogicalRange(mgr, savedLR); syncIndicatorTrackingRefs(indicators); indicatorSyncInFlightRef.current = false; + requestAnimationFrame(() => requestAnimationFrame(() => { + afterIndicatorPaneMutation(mgr); + mgr.notifyPaneLayoutChanged(); + })); void repairMissingIndicators(mgr, indicators); + } else if ( + addedConfigs.length > 0 + && removedIds.length > 0 + && paramsChangedConfigs.length === 0 + ) { + /** 지표 교체(제거+추가 동시) — CCI 제거 후 재추가 등 orphan pane·pane index 어긋남 방지 */ + const savedLR = mgr.getVisibleLogicalRange(); + indicatorSyncInFlightRef.current = true; + mgr.removeIndicators(removedIds); + void mgr.addIndicatorsBatch(addedConfigs).then(() => { + afterIndicatorPaneMutation(mgr); + mgr.notifyPaneLayoutChanged(); + restoreLogicalRange(mgr, savedLR); + syncIndicatorTrackingRefs(indicators); + indicatorSyncInFlightRef.current = false; + requestAnimationFrame(() => requestAnimationFrame(() => { + afterIndicatorPaneMutation(mgr); + mgr.notifyPaneLayoutChanged(); + [50, 150, 350].forEach(ms => { + setTimeout(() => mgr.notifyPaneLayoutChanged(), ms); + }); + })); + }); } else if ( addedConfigs.length > 0 && removedIds.length === 0 @@ -1191,9 +1224,17 @@ const TradingChart: React.FC = ({ indicatorSyncInFlightRef.current = true; void mgr.addIndicatorsBatch(addedConfigs).then(() => { afterIndicatorPaneMutation(mgr); + mgr.notifyPaneLayoutChanged(); restoreLogicalRange(mgr, savedLR); syncIndicatorTrackingRefs(indicators); indicatorSyncInFlightRef.current = false; + requestAnimationFrame(() => requestAnimationFrame(() => { + afterIndicatorPaneMutation(mgr); + mgr.notifyPaneLayoutChanged(); + [50, 150, 350].forEach(ms => { + setTimeout(() => mgr.notifyPaneLayoutChanged(), ms); + }); + })); }); } else if ( paramsChangedConfigs.length > 0 diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 0294fa6..7c3872f 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -652,6 +652,9 @@ export class ChartManager { return; } this.indicators.set(config.id, { id: config.id, type: config.type, seriesList, seriesMeta: ichiMeta, paneIndex: pane, alertLines: [], hlineRefs: [], config, cloudPlugin }); + const ichiPane = this._readSeriesPaneIndex(seriesList[0]); + const ichiEntry = this.indicators.get(config.id); + if (ichiEntry && ichiPane >= 2) ichiEntry.paneIndex = ichiPane; this.applyIndicatorStyle(config); return; } @@ -738,6 +741,8 @@ export class ChartManager { id: config.id, type: config.type, seriesList, seriesMeta, paneIndex: pane, alertLines: [], hlineRefs, fillPrimitive, config, }; + const actualPane = this._readSeriesPaneIndex(seriesList[0]); + if (actualPane >= 2) entry.paneIndex = actualPane; if (this._indicatorLoadStale(dataGenAtStart) || this.indicators.has(config.id)) { this._discardIndicatorSeries(seriesList, { fillPrimitive }); return; @@ -751,25 +756,28 @@ export class ChartManager { } catch (e) { console.error(`[Indicator] ${config.type} error:`, e); } finally { - if ( - !options?.skipLayout - && this._activeIndicatorPaneIndices().size > 0 - ) { - this.resetPaneHeights(this._lastLayoutAvailableHeight); + if (!options?.skipLayout) { + this._finalizeIndicatorPaneLayout(); } } } + /** 지표 추가·제거 후 pane index 동기화 + stretch 재배분 */ + private _finalizeIndicatorPaneLayout(): void { + if (this._activeIndicatorPaneIndices().size === 0) return; + this._removeOrphanSubPanes(); + this._resyncIndicatorPaneIndices(); + this.resetPaneHeights(this._lastLayoutAvailableHeight); + this._notifyPaneLayout(); + } + /** 여러 지표 추가 후 pane 레이아웃을 한 번만 갱신 */ async addIndicatorsBatch(configs: IndicatorConfig[]): Promise { for (const config of configs) { await this.addIndicator(config, { skipLayout: true }); } - if (configs.length > 0 && this._activeIndicatorPaneIndices().size > 0) { - this._removeOrphanSubPanes(); - this._resyncIndicatorPaneIndices(); - this.resetPaneHeights(this._lastLayoutAvailableHeight); - this._notifyPaneLayout(); + if (configs.length > 0) { + this._finalizeIndicatorPaneLayout(); } } @@ -831,11 +839,7 @@ export class ChartManager { if (this._indicatorLoadStale(loadGen)) return; - // 시리즈 제거 후 남은 빈 sub-pane 제거 (상단 orphan pane → 레이블·그래프 어긋남 방지) - this._removeOrphanSubPanes(); - // 빈 pane stretch 정리 + 캔들 pane 비율 복구 - this.resetPaneHeights(); - this._notifyPaneLayout(); + this._finalizeIndicatorPaneLayout(); } removeIndicator(id: string): void { @@ -849,7 +853,9 @@ export class ChartManager { if (this._detachIndicatorEntry(id)) any = true; } if (!any) return; + this._removeOrphanSubPanes(); this._trimTrailingEmptySubPanes(); + this._resyncIndicatorPaneIndices(); this.resetPaneHeights(this._lastLayoutAvailableHeight); this._notifyPaneLayout(); } @@ -876,12 +882,7 @@ export class ChartManager { } if (this._indicatorLoadStale(loadGen)) return; - this._removeOrphanSubPanes(); - this._resyncIndicatorPaneIndices(); - if (this._activeIndicatorPaneIndices().size > 0) { - this.resetPaneHeights(this._lastLayoutAvailableHeight); - this._notifyPaneLayout(); - } + this._finalizeIndicatorPaneLayout(); } /** @@ -1332,8 +1333,36 @@ export class ChartManager { return [...historical, ...future]; } + private _readSeriesPaneIndex(series: ISeriesApi | undefined): number { + if (!series) return -1; + try { + return series.getPane().paneIndex(); + } catch { + return -1; + } + } + + /** LWC 시리즈가 실제로 붙어 있는 sub-pane index (2+) */ + private _collectUsedSubPaneIndices(): Set { + const indices = new Set(); + for (const entry of this.indicators.values()) { + let found = false; + for (const series of entry.seriesList) { + const pi = this._readSeriesPaneIndex(series); + if (pi >= 2) { + indices.add(pi); + found = true; + } + } + if (!found && entry.paneIndex != null && entry.paneIndex >= 2) { + indices.add(entry.paneIndex); + } + } + return indices; + } + private _getNextSubPane(): number { - const used = new Set(Array.from(this.indicators.values()).map(e => e.paneIndex).filter(Boolean)); + const used = this._collectUsedSubPaneIndices(); let pane = 2; while (used.has(pane)) pane++; return pane; @@ -2091,7 +2120,7 @@ export class ChartManager { let y = 0; for (let i = 0; i < paneCount; i++) { const h = Math.round((panes[i].getStretchFactor() / totalFactor) * chartAreaH); - result.push({ paneIndex: i, topY: y, height: h }); + result.push({ paneIndex: panes[i].paneIndex(), topY: y, height: h }); y += h + (i < paneCount - 1 ? SEP_H : 0); } return result; @@ -2673,12 +2702,7 @@ export class ChartManager { /** 실제 보조지표가 붙어 있는 pane index 집합 (2+) */ private _activeIndicatorPaneIndices(): Set { - const indices = new Set(); - for (const entry of this.indicators.values()) { - const p = entry.paneIndex; - if (p != null && p >= 2) indices.add(p); - } - return indices; + return this._collectUsedSubPaneIndices(); } /** pane 2+ 빈 sub-pane 제거 — reloadIndicatorsOnly 전용 (모든 지표 시리즈 제거 후) */ @@ -2713,11 +2737,13 @@ export class ChartManager { /** 활성 지표가 없는 맨 끝 sub-pane 만 제거 — 단일 지표 제거 시 나머지 pane 유지 */ private _trimTrailingEmptySubPanes(): void { + const active = this._activeIndicatorPaneIndices(); for (;;) { const panes = this.chart.panes(); if (panes.length <= 2) return; const last = panes.length - 1; - if (this._activeIndicatorPaneIndices().has(last)) return; + const paneIndex = panes[last].paneIndex(); + if (active.has(paneIndex)) return; try { this.chart.removePane(last); } catch { @@ -3524,11 +3550,12 @@ export class ChartManager { : ORPHAN_STRETCH; for (let i = 0; i < panes.length; i++) { - if (i === 0) { + const paneIndex = panes[i].paneIndex(); + if (paneIndex === 0) { panes[i].setStretchFactor(mainWeight); - } else if (i === 1) { + } else if (paneIndex === 1) { panes[i].setStretchFactor(volumeShown ? volWeight : ORPHAN_STRETCH); - } else if (activeIndPanes.has(i)) { + } else if (activeIndPanes.has(paneIndex)) { panes[i].setStretchFactor(indWeight); } else { panes[i].setStretchFactor(ORPHAN_STRETCH);