From 227db1e6072439a086ea805c01b5cf8e946b82d6 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 1 Jun 2026 01:50:54 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=A1=B0=EC=A7=80=ED=91=9C=20?= =?UTF-8?q?=ED=95=A9=EC=B9=98=EA=B8=B0,=20=EB=B6=84=EB=A6=AC,=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=ED=95=98=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=98=A4?= =?UTF-8?q?=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 | 43 ++++++++----- frontend/src/utils/ChartManager.ts | 78 +++++++++++++++++++++--- frontend/src/utils/indicatorChartSync.ts | 14 +++++ package-lock.json | 10 --- 4 files changed, 110 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 47c0db5..be20d54 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -1268,9 +1268,7 @@ const TradingChart: React.FC = ({ const currSK = styleKey(indicators); const needsRecalc = prevPK !== currPK; - const isReorderOnly = needsRecalc - && sortedParamKey(indicators) === sortedParamKey(prevList) - && currSK === prevSK; + const chartChange = classifyIndicatorChartChange(prevList, indicators); const prevMap = new Map(prevList.map(i => [i.id, i])); const removedIds = prevList @@ -1283,7 +1281,17 @@ const TradingChart: React.FC = ({ return singleIndParamKey(p) !== singleIndParamKey(c); }); - if (!needsRecalc && prevSK !== currSK) { + if ( + chartChange.mode === 'reorder-only' + || chartChange.mode === 'layout-changed' + ) { + /** 순서·병합·분리 — 설정에 맞게 sub-pane 전체 재배치 */ + indicatorSyncInFlightRef.current = true; + reloadIndicatorsWithCover(mgr, indicators, () => { + syncIndicatorTrackingRefs(indicators); + indicatorSyncInFlightRef.current = false; + }); + } else if (!needsRecalc && prevSK !== currSK) { for (const ind of indicators) { mgr.applyIndicatorStyle(ind); } @@ -1336,7 +1344,13 @@ const TradingChart: React.FC = ({ ) { const savedLR = mgr.getVisibleLogicalRange(); indicatorSyncInFlightRef.current = true; - void mgr.addIndicatorsBatch(addedConfigs).then(() => { + /** 복사 등 목록 중간 삽입 — pane 순서를 맞추려면 전체 재로드 */ + const insertIdx = addedConfigs.length === 1 + ? indicators.findIndex(i => i.id === addedConfigs[0].id) + : -1; + const reloadForPaneOrder = insertIdx >= 0 && insertIdx < indicators.length - 1; + + const finishAdd = () => { afterIndicatorPaneMutation(mgr); mgr.notifyPaneLayoutChanged(); restoreLogicalRange(mgr, savedLR); @@ -1349,7 +1363,13 @@ const TradingChart: React.FC = ({ setTimeout(() => mgr.notifyPaneLayoutChanged(), ms); }); })); - }); + }; + + if (reloadForPaneOrder) { + reloadIndicatorsWithCover(mgr, indicators, finishAdd); + } else { + void mgr.addIndicatorsBatch(addedConfigs).then(finishAdd); + } } else if ( paramsChangedConfigs.length > 0 && removedIds.length === 0 @@ -1373,19 +1393,12 @@ const TradingChart: React.FC = ({ setPaneLegendPaused(false); flushPendingIndicatorResync(mgr, syncGen); }); - } else if (isReorderOnly) { + } else if (chartChange.mode === 'full') { reloadIndicatorsWithCover(mgr, indicators, () => { syncIndicatorTrackingRefs(indicators); }); } else { - const change = classifyIndicatorChartChange(prevList, indicators); - if (change.mode === 'full') { - reloadIndicatorsWithCover(mgr, indicators, () => { - syncIndicatorTrackingRefs(indicators); - }); - } else { - syncIndicatorTrackingRefs(indicators); - } + syncIndicatorTrackingRefs(indicators); } } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 68b37bb..b9bbc0f 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -34,7 +34,7 @@ import { normalizeChartTimeFormat, } from './chartTimeFormat'; import { DEFAULT_DISPLAY_TIMEZONE } from './timezone'; -import { sortIndicatorsForPaneLoad } from './indicatorPaneMerge'; +import { getPaneHostId, sortIndicatorsForPaneLoad } from './indicatorPaneMerge'; import { calculateIndicator, enrichIndicatorConfig, getIndicatorDef, getHLineLabel, type PlotData, type MarkerData } from './indicatorRegistry'; import { IchimokuCloudPlugin, type IchimokuCloudPoint } from './IchimokuCloudPlugin'; @@ -808,6 +808,8 @@ export class ChartManager { private _finalizeIndicatorPaneLayout(): void { this._removeOrphanSubPanes(); if (this._activeIndicatorPaneIndices().size > 0) { + this._resyncIndicatorPaneIndices(); + this._splitCollidingIndicatorPanes(); this._resyncIndicatorPaneIndices(); this._applyAllSeriesPriceFormats(); } else { @@ -929,6 +931,8 @@ export class ChartManager { this._removeOrphanSubPanes(); this._trimTrailingEmptySubPanes(); this._resyncIndicatorPaneIndices(); + this._splitCollidingIndicatorPanes(); + this._resyncIndicatorPaneIndices(); this.resetPaneHeights(this._lastLayoutAvailableHeight); this._notifyPaneLayout(); } @@ -1420,21 +1424,71 @@ export class ChartManager { private _collectUsedSubPaneIndices(): Set { const indices = new Set(); for (const entry of this.indicators.values()) { - let found = false; + let pane = -1; for (const series of entry.seriesList) { const pi = this._readSeriesPaneIndex(series); - if (pi >= 2) { - indices.add(pi); - found = true; - } + if (pi >= 2) pane = Math.max(pane, pi); } - if (!found && entry.paneIndex != null && entry.paneIndex >= 2) { - indices.add(entry.paneIndex); + if (pane < 2 && entry.paneIndex != null && entry.paneIndex >= 2) { + pane = entry.paneIndex; } + if (pane >= 2) indices.add(pane); } return indices; } + private _indicatorConfigsFromMap(): IndicatorConfig[] { + return Array.from(this.indicators.values()).map(e => e.config); + } + + /** + * 병합(mergedWith)이 아닌 서로 다른 호스트가 같은 sub-pane에 붙어 있으면 분리. + * entry.paneIndex 와 실제 시리즈 pane 불일치·지표 제거 후 재추가 시 겹침 방지. + */ + private _splitCollidingIndicatorPanes(): void { + const configs = this._indicatorConfigsFromMap(); + const hostsByPane = new Map(); + + for (const [id, entry] of this.indicators) { + const hostId = getPaneHostId(id, configs); + if (id !== hostId) continue; + + const def = getIndicatorDef(entry.type); + if (def?.overlay || def?.returnsMarkers || entry.seriesList.length === 0) continue; + + let pi = -1; + for (const series of entry.seriesList) { + pi = Math.max(pi, this._readSeriesPaneIndex(series)); + } + if (pi < 2 && entry.paneIndex != null && entry.paneIndex >= 2) pi = entry.paneIndex; + if (pi < 2) continue; + + const list = hostsByPane.get(pi) ?? []; + if (!list.includes(hostId)) list.push(hostId); + hostsByPane.set(pi, list); + } + + let changed = false; + for (const hostIds of hostsByPane.values()) { + if (hostIds.length <= 1) continue; + for (let i = 1; i < hostIds.length; i++) { + const hostId = hostIds[i]; + const target = this._getNextSubPane(); + for (const [mid, ment] of this.indicators) { + if (getPaneHostId(mid, configs) !== hostId) continue; + for (const series of ment.seriesList) { + try { + series.moveToPane(target); + } catch { /* ok */ } + } + ment.paneIndex = target; + } + changed = true; + } + } + if (changed) this._removeOrphanSubPanes(); + } + private _getNextSubPane(): number { const used = this._collectUsedSubPaneIndices(); let pane = 2; @@ -1815,9 +1869,13 @@ export class ChartManager { this._reapplyAllPatternMarkers(); this._removeExtraSubPanes(); - for (const cfg of configs) { + const sorted = sortIndicatorsForPaneLoad(configs); + for (const cfg of sorted) { if (this._indicatorLoadStale(loadGen)) break; - await this.addIndicator(cfg); + await this.addIndicator(cfg, { skipLayout: true }); + } + if (!this._indicatorLoadStale(loadGen)) { + this._finalizeIndicatorPaneLayout(); } } diff --git a/frontend/src/utils/indicatorChartSync.ts b/frontend/src/utils/indicatorChartSync.ts index 1f03b92..fd35f8b 100644 --- a/frontend/src/utils/indicatorChartSync.ts +++ b/frontend/src/utils/indicatorChartSync.ts @@ -1,4 +1,5 @@ import type { IndicatorConfig } from '../types'; +import { layoutKey } from './indicatorPaneMerge'; export type IndicatorChartSyncMode = | 'none' @@ -6,6 +7,7 @@ export type IndicatorChartSyncMode = | 'add-only' | 'params-changed' | 'reorder-only' + | 'layout-changed' | 'full'; export interface IndicatorChartChange { @@ -62,6 +64,9 @@ export function classifyIndicatorChartChange( }; if (removedIds.length === 0 && added.length === 0 && paramsChangedIds.length === 0) { + if (layoutKey(prev) !== layoutKey(curr)) { + return { ...empty, mode: 'layout-changed' }; + } const sameSet = prev.length === curr.length && prev.every(p => currMap.has(p.id)); if (sameSet && prev.map(i => i.id).join(',') !== curr.map(i => i.id).join(',')) { @@ -70,6 +75,15 @@ export function classifyIndicatorChartChange( return empty; } + /** 병합·분리 등 mergedWith 변경 — params 키에도 잡히지만 pane 재배치가 필요 */ + if ( + removedIds.length === 0 + && added.length === 0 + && layoutKey(prev) !== layoutKey(curr) + ) { + return { ...empty, mode: 'layout-changed', paramsChangedIds }; + } + if (removedIds.length > 0 && added.length === 0 && paramsChangedIds.length === 0) { return { ...empty, mode: 'remove-only' }; } diff --git a/package-lock.json b/package-lock.json index a3852c9..4e4991b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "@capacitor/haptics": "^7.0.0", "@capacitor/ios": "^7.0.0", "@capacitor/preferences": "^7.0.0", - "@capacitor/push-notifications": "^7.0.0", "@capacitor/splash-screen": "^7.0.0", "@capacitor/status-bar": "^7.0.0", "@goldenchart/shared": "*", @@ -472,15 +471,6 @@ "@capacitor/core": ">=7.0.0" } }, - "node_modules/@capacitor/push-notifications": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-7.0.6.tgz", - "integrity": "sha512-zAhbHpdbc15ImuVGgoFwUZsKI+jjGxy/oO5mgfYKUx8Xl2OskndzhL79PYsCPjyGLbqUR9NRUZJthV9auVT3nw==", - "license": "MIT", - "peerDependencies": { - "@capacitor/core": ">=7.0.0" - } - }, "node_modules/@capacitor/splash-screen": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-7.0.5.tgz",