diff --git a/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java b/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java index f351454..3f9fc4a 100644 --- a/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java +++ b/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java @@ -122,6 +122,11 @@ public class GcAppSettings { @Builder.Default private Boolean chartCrosshairInfoVisible = true; + /** 세로 크로스헤어와 지표 선 교차점 점 표시 (기본 true) */ + @Column(name = "chart_crosshair_markers_visible", nullable = false) + @Builder.Default + private Boolean chartCrosshairMarkersVisible = true; + /** 차트 마우스오버 줌·이동 플로팅 툴바(− + ⌐↔ ‹ ›) 표시 (기본 true) */ @Column(name = "chart_hover_toolbar_visible", nullable = false) @Builder.Default diff --git a/backend/src/main/java/com/goldenchart/service/AppSettingsService.java b/backend/src/main/java/com/goldenchart/service/AppSettingsService.java index 4825916..565a3cc 100644 --- a/backend/src/main/java/com/goldenchart/service/AppSettingsService.java +++ b/backend/src/main/java/com/goldenchart/service/AppSettingsService.java @@ -100,6 +100,8 @@ public class AppSettingsService { Boolean.parseBoolean(d.get("chartLiveReceiveHighlight").toString())); if (d.containsKey("chartCrosshairInfoVisible")) s.setChartCrosshairInfoVisible( Boolean.parseBoolean(d.get("chartCrosshairInfoVisible").toString())); + if (d.containsKey("chartCrosshairMarkersVisible")) s.setChartCrosshairMarkersVisible( + Boolean.parseBoolean(d.get("chartCrosshairMarkersVisible").toString())); if (d.containsKey("chartHoverToolbarVisible")) s.setChartHoverToolbarVisible( Boolean.parseBoolean(d.get("chartHoverToolbarVisible").toString())); if (d.containsKey("chartLegendOptions")) s.setChartLegendOptionsJson(toJson(d.get("chartLegendOptions"))); @@ -219,6 +221,7 @@ public class AppSettingsService { m.put("chartVolumeVisible", s.getChartVolumeVisible() != null ? s.getChartVolumeVisible() : true); m.put("chartLiveReceiveHighlight", s.getChartLiveReceiveHighlight() != null ? s.getChartLiveReceiveHighlight() : true); m.put("chartCrosshairInfoVisible", s.getChartCrosshairInfoVisible() != null ? s.getChartCrosshairInfoVisible() : true); + m.put("chartCrosshairMarkersVisible", s.getChartCrosshairMarkersVisible() != null ? s.getChartCrosshairMarkersVisible() : true); m.put("chartHoverToolbarVisible", s.getChartHoverToolbarVisible() != null ? s.getChartHoverToolbarVisible() : true); m.put("chartLegendOptions", parseJson(s.getChartLegendOptionsJson())); m.put("chartPaneSeparator", parseJson(s.getChartPaneSeparatorJson())); diff --git a/backend/src/main/resources/db/migration/V69__chart_crosshair_markers_visible.sql b/backend/src/main/resources/db/migration/V69__chart_crosshair_markers_visible.sql new file mode 100644 index 0000000..cf39025 --- /dev/null +++ b/backend/src/main/resources/db/migration/V69__chart_crosshair_markers_visible.sql @@ -0,0 +1,3 @@ +ALTER TABLE gc_app_settings + ADD COLUMN chart_crosshair_markers_visible BOOLEAN NOT NULL DEFAULT TRUE + COMMENT '세로 크로스헤어와 지표 선 교차점 점 표시'; diff --git a/frontend/src/chart/ChartWorkspaceContext.tsx b/frontend/src/chart/ChartWorkspaceContext.tsx index 9aa8486..816f6f1 100644 --- a/frontend/src/chart/ChartWorkspaceContext.tsx +++ b/frontend/src/chart/ChartWorkspaceContext.tsx @@ -52,6 +52,8 @@ const NULL_SETTINGS_PAGE_PROPS: SettingsPageChartBindings = { onChartLegendOptionsChange: () => {}, chartCrosshairInfoVisible: true, onChartCrosshairInfoVisible: () => {}, + chartCrosshairMarkersVisible: true, + onChartCrosshairMarkersVisible: () => {}, chartHoverToolbarVisible: true, onChartHoverToolbarVisible: () => {}, chartPaneSeparator: undefined, diff --git a/frontend/src/chart/useChartWorkspace.ts b/frontend/src/chart/useChartWorkspace.ts index c4128fa..bc7d0e4 100644 --- a/frontend/src/chart/useChartWorkspace.ts +++ b/frontend/src/chart/useChartWorkspace.ts @@ -192,6 +192,8 @@ export interface SettingsPageChartBindings { onChartLegendOptionsChange: (patch: Partial) => void; chartCrosshairInfoVisible: boolean; onChartCrosshairInfoVisible: (v: boolean) => void; + chartCrosshairMarkersVisible: boolean; + onChartCrosshairMarkersVisible: (v: boolean) => void; chartHoverToolbarVisible: boolean; onChartHoverToolbarVisible: (v: boolean) => void; chartPaneSeparator: import('../types/chartPaneSeparator').ChartPaneSeparatorOptions | undefined; @@ -610,6 +612,7 @@ export function useChartWorkspace({ const chartLiveReceiveHighlight = appDefaults.chartLiveReceiveHighlight ?? true; const chartLegendOptions = resolveChartLegendOptions(appDefaults.chartLegendOptions); const chartCrosshairInfoVisible = appDefaults.chartCrosshairInfoVisible ?? true; + const chartCrosshairMarkersVisible = appDefaults.chartCrosshairMarkersVisible ?? true; const chartHoverToolbarVisible = appDefaults.chartHoverToolbarVisible ?? true; const chartPaneSeparator = resolveChartPaneSeparatorOptions(appDefaults.chartPaneSeparator); const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true; @@ -1010,6 +1013,12 @@ export function useChartWorkspace({ }); }, [appSettingsLoaded, chartCandleAreaPriceLabels, chartSeriesPriceLabels]); + useEffect(() => { + if (!appSettingsLoaded) return; + managerRef.current?.setCrosshairMarkersVisible(chartCrosshairMarkersVisible); + slotRefs.current.forEach(slot => slot?.setCrosshairMarkersVisible(chartCrosshairMarkersVisible)); + }, [appSettingsLoaded, chartCrosshairMarkersVisible]); + useEffect(() => { if (!appSettingsLoaded) return; const wrapper = document.querySelector('.chart-wrapper') as HTMLElement | null; @@ -1903,6 +1912,12 @@ export function useChartWorkspace({ }, chartCrosshairInfoVisible, onChartCrosshairInfoVisible: v => saveAppDef({ chartCrosshairInfoVisible: v }), + chartCrosshairMarkersVisible, + onChartCrosshairMarkersVisible: v => { + saveAppDef({ chartCrosshairMarkersVisible: v }); + managerRef.current?.setCrosshairMarkersVisible(v); + slotRefs.current.forEach(slot => slot?.setCrosshairMarkersVisible(v)); + }, chartHoverToolbarVisible, onChartHoverToolbarVisible: v => saveAppDef({ chartHoverToolbarVisible: v }), chartPaneSeparator, @@ -1929,6 +1944,7 @@ export function useChartWorkspace({ handleAddIndicator, handleRemoveByType, handleIndicatorListOrderChange, chartCandleAreaPriceLabels, chartSeriesPriceLabels, chartVolumeVisible, chartLiveReceiveHighlight, chartLegendOptions, chartCrosshairInfoVisible, + chartCrosshairMarkersVisible, chartHoverToolbarVisible, chartPaneSeparator, saveAppDef, clearLiveMarkers, refreshPaperAccount, handleTimezoneChange, handleChartTimeFormatChange, handleTradeAlertTimeFormatChange, displayTimezone, chartTimeFormat, tradeAlertTimeFormat, diff --git a/frontend/src/components/ChartSlot.tsx b/frontend/src/components/ChartSlot.tsx index ea0c1f9..0c6a69f 100644 --- a/frontend/src/components/ChartSlot.tsx +++ b/frontend/src/components/ChartSlot.tsx @@ -137,6 +137,7 @@ export interface ChartSlotHandle { setSeriesPriceLabelsEnabled: (enabled: boolean) => void; setCandleAreaPriceLabelsEnabled: (enabled: boolean) => void; setIndicatorAreaPriceLabelsEnabled: (enabled: boolean) => void; + setCrosshairMarkersVisible: (enabled: boolean) => void; /** 거래량 pane on/off */ setVolumeVisible: (visible: boolean) => void; /** 9·20일 신고가·신저가 표시 */ @@ -382,6 +383,9 @@ const ChartSlot = forwardRef(function ChartSlot setIndicatorAreaPriceLabelsEnabled: (enabled: boolean) => { managerRef.current?.setIndicatorAreaPriceLabelsEnabled(enabled); }, + setCrosshairMarkersVisible: (enabled: boolean) => { + managerRef.current?.setCrosshairMarkersVisible(enabled); + }, setVolumeVisible: (visible: boolean) => { managerRef.current?.setVolumeVisible(visible); }, diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index b363b85..779e1b2 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -129,6 +129,8 @@ interface SettingsPageProps { onChartLegendOptionsChange?: (patch: Partial) => void; chartCrosshairInfoVisible?: boolean; onChartCrosshairInfoVisible?: (v: boolean) => void; + chartCrosshairMarkersVisible?: boolean; + onChartCrosshairMarkersVisible?: (v: boolean) => void; chartHoverToolbarVisible?: boolean; onChartHoverToolbarVisible?: (v: boolean) => void; chartPaneSeparator?: ChartPaneSeparatorOptions; @@ -1010,6 +1012,8 @@ interface ChartPanelProps { onChartLegendOptionsChange?: (patch: Partial) => void; chartCrosshairInfoVisible?: boolean; onChartCrosshairInfoVisible?: (v: boolean) => void; + chartCrosshairMarkersVisible?: boolean; + onChartCrosshairMarkersVisible?: (v: boolean) => void; chartHoverToolbarVisible?: boolean; onChartHoverToolbarVisible?: (v: boolean) => void; chartPaneSeparator?: ChartPaneSeparatorOptions; @@ -1036,6 +1040,8 @@ const ChartPanel: React.FC = ({ onChartLegendOptionsChange, chartCrosshairInfoVisible = true, onChartCrosshairInfoVisible, + chartCrosshairMarkersVisible = true, + onChartCrosshairMarkersVisible, chartHoverToolbarVisible = true, onChartHoverToolbarVisible, chartPaneSeparator, @@ -1248,6 +1254,19 @@ const ChartPanel: React.FC = ({ + + + )} @@ -1955,6 +1974,8 @@ const SettingsPage: React.FC = ({ onChartLegendOptionsChange, chartCrosshairInfoVisible = true, onChartCrosshairInfoVisible, + chartCrosshairMarkersVisible = true, + onChartCrosshairMarkersVisible, chartHoverToolbarVisible = true, onChartHoverToolbarVisible, chartPaneSeparator, @@ -2077,6 +2098,8 @@ const SettingsPage: React.FC = ({ onChartLegendOptionsChange={onChartLegendOptionsChange} chartCrosshairInfoVisible={chartCrosshairInfoVisible} onChartCrosshairInfoVisible={onChartCrosshairInfoVisible} + chartCrosshairMarkersVisible={chartCrosshairMarkersVisible} + onChartCrosshairMarkersVisible={onChartCrosshairMarkersVisible} chartHoverToolbarVisible={chartHoverToolbarVisible} onChartHoverToolbarVisible={onChartHoverToolbarVisible} chartPaneSeparator={chartPaneSeparator} diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 138933b..ba709ca 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -6,6 +6,7 @@ import { formatUpbitKrwPrice } from '../utils/safeFormat'; import { useChartTimeFormat } from '../utils/chartTimeFormat'; import { setIndicatorChartContext } from '../utils/indicatorRegistry'; import { DISPLAY_COUNT } from '../hooks/useUpbitData'; +import { useAppSettings, resolveAppDefaults } from '../hooks/useAppSettings'; /** Upbit 실시간 — 초기 히스토리가 이 개수 미만이면 setData 하지 않음 (종목 전환 직후 sparse 봉 → 캔들 폭 비정상) */ const MIN_BARS_FOR_FULL_RELOAD = Math.min(50, Math.floor(DISPLAY_COUNT / 4)); @@ -165,6 +166,8 @@ interface TradingChartProps { showCandleOverlayControls?: boolean; /** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 (기본 true) */ crosshairInfoVisible?: boolean; + /** 세로 크로스헤어와 선 그래프 교차점 점 표시 (미지정 시 전역 차트 설정) */ + crosshairMarkersVisible?: boolean; /** 외부 캔들 데이터 fetch 중 (실시간 차트 초기 로딩) */ dataLoading?: boolean; /** chartPaintReady 변경 알림 (워크스페이스 전체 로딩 오버레이용) */ @@ -237,6 +240,7 @@ const TradingChart: React.FC = ({ showChartRightToolbar, showCandleOverlayControls = false, crosshairInfoVisible = true, + crosshairMarkersVisible, dataLoading = false, onChartPaintReadyChange, defaultCandleOnly = false, @@ -533,6 +537,10 @@ const TradingChart: React.FC = ({ mgr.setVolumeVisible(volumeVisible, wrapperH > 0 ? wrapperH : undefined); }, [volumeVisible]); + const { settings } = useAppSettings(); + const appDefaults = resolveAppDefaults(settings); + const resolvedCrosshairMarkers = crosshairMarkersVisible ?? appDefaults.chartCrosshairMarkersVisible ?? true; + const resolvedCandlePriceLabels = candleAreaPriceLabelsEnabled ?? seriesPriceLabelsEnabled ?? true; const resolvedIndicatorPriceLabels = indicatorAreaPriceLabelsEnabled ?? seriesPriceLabelsEnabled ?? true; @@ -544,6 +552,10 @@ const TradingChart: React.FC = ({ managerRef.current?.setIndicatorAreaPriceLabelsEnabled(resolvedIndicatorPriceLabels); }, [resolvedIndicatorPriceLabels]); + useEffect(() => { + managerRef.current?.setCrosshairMarkersVisible(resolvedCrosshairMarkers); + }, [resolvedCrosshairMarkers]); + useEffect(() => { if (!paneSeparatorOptions) return; managerRef.current?.setPaneSeparatorOptions(paneSeparatorOptions); @@ -1025,6 +1037,7 @@ const TradingChart: React.FC = ({ } mgr.setCandleAreaPriceLabelsEnabled(resolvedCandlePriceLabels); mgr.setIndicatorAreaPriceLabelsEnabled(resolvedIndicatorPriceLabels); + mgr.setCrosshairMarkersVisible(resolvedCrosshairMarkers); if (paneSeparatorOptions) mgr.setPaneSeparatorOptions(paneSeparatorOptions); if (auxIndicatorOnlyRef.current) mgr.applyAuxIndicatorOnlyLayout(true); onManagerReady(mgr); diff --git a/frontend/src/components/strategyEvaluation/ChartPriceAxisLabelSettings.tsx b/frontend/src/components/strategyEvaluation/ChartPriceAxisLabelSettings.tsx index 38c1b46..80d9642 100644 --- a/frontend/src/components/strategyEvaluation/ChartPriceAxisLabelSettings.tsx +++ b/frontend/src/components/strategyEvaluation/ChartPriceAxisLabelSettings.tsx @@ -8,6 +8,8 @@ interface Props { indicatorAreaEnabled: boolean; onCandleAreaChange: (enabled: boolean) => void; onIndicatorAreaChange: (enabled: boolean) => void; + crosshairMarkersEnabled?: boolean; + onCrosshairMarkersChange?: (enabled: boolean) => void; className?: string; } @@ -16,8 +18,11 @@ const ChartPriceAxisLabelSettings: React.FC = ({ indicatorAreaEnabled, onCandleAreaChange, onIndicatorAreaChange, + crosshairMarkersEnabled = true, + onCrosshairMarkersChange, className = '', }) => ( + <>

지표 가격축 라벨·설명

@@ -68,6 +73,30 @@ const ChartPriceAxisLabelSettings: React.FC = ({
+ {onCrosshairMarkersChange && ( +
+

크로스헤어 교차점

+
+
+
+ 교차점 점 표시 + + 켜면 세로 크로스헤어가 캔들·보조지표 선과 만나는 위치에 색 점이 표시됩니다. + +
+ +
+
+
+ )} + ); export default ChartPriceAxisLabelSettings; diff --git a/frontend/src/components/strategyEvaluation/StrategyEvaluationChartSettingsTab.tsx b/frontend/src/components/strategyEvaluation/StrategyEvaluationChartSettingsTab.tsx index b060f7c..ffe726d 100644 --- a/frontend/src/components/strategyEvaluation/StrategyEvaluationChartSettingsTab.tsx +++ b/frontend/src/components/strategyEvaluation/StrategyEvaluationChartSettingsTab.tsx @@ -99,6 +99,7 @@ const StrategyEvaluationChartSettingsTab: React.FC = ({ const appDefaults = useMemo(() => resolveAppDefaults(settings), [settings]); const candlePriceLabels = appDefaults.chartCandleAreaPriceLabels !== false; const indicatorPriceLabels = appDefaults.chartSeriesPriceLabels !== false; + const crosshairMarkers = appDefaults.chartCrosshairMarkersVisible !== false; const reloadFromDb = useCallback(() => { const next: Record = {}; @@ -221,6 +222,8 @@ const StrategyEvaluationChartSettingsTab: React.FC = ({ indicatorAreaEnabled={indicatorPriceLabels} onCandleAreaChange={v => saveAppSettings({ chartCandleAreaPriceLabels: v })} onIndicatorAreaChange={v => saveAppSettings({ chartSeriesPriceLabels: v })} + crosshairMarkersEnabled={crosshairMarkers} + onCrosshairMarkersChange={v => saveAppSettings({ chartCrosshairMarkersVisible: v })} /> diff --git a/frontend/src/hooks/useAppSettings.ts b/frontend/src/hooks/useAppSettings.ts index e429bf7..741e1e3 100644 --- a/frontend/src/hooks/useAppSettings.ts +++ b/frontend/src/hooks/useAppSettings.ts @@ -177,6 +177,7 @@ export function resolveAppDefaults(s: AppSettingsDto) { chartVolumeVisible: s.chartVolumeVisible ?? true, chartLiveReceiveHighlight: s.chartLiveReceiveHighlight ?? true, chartCrosshairInfoVisible: s.chartCrosshairInfoVisible ?? true, + chartCrosshairMarkersVisible: s.chartCrosshairMarkersVisible ?? true, chartHoverToolbarVisible: s.chartHoverToolbarVisible ?? true, chartLegendOptions: resolveChartLegendOptions( s.chartLegendOptions as Partial | null | undefined, diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 4c62018..a0b847b 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -347,6 +347,8 @@ export class ChartManager { private _candleAreaPriceLabelsEnabled = true; /** 하단 보조지표 pane(2+) — 우측 가격축 라벨·설명 */ private _indicatorAreaPriceLabelsEnabled = true; + /** 세로 크로스헤어와 선 그래프 교차점 점 표시 */ + private _crosshairMarkersVisible = true; /** 차트 하단 거래량 pane 표시 */ private _volumeVisible = true; @@ -596,15 +598,23 @@ export class ChartManager { borderUpColor: t.upColor, borderDownColor: t.downColor, wickUpColor: t.upColor, wickDownColor: t.downColor, priceFormat: MAIN_PRICE_FORMAT, + ...this._crosshairMarkerSeriesOptions(), }); case 'bar': - return this.chart.addSeries(BarSeries, { upColor: t.upColor, downColor: t.downColor, priceFormat: MAIN_PRICE_FORMAT }); + return this.chart.addSeries(BarSeries, { + upColor: t.upColor, downColor: t.downColor, priceFormat: MAIN_PRICE_FORMAT, + ...this._crosshairMarkerSeriesOptions(), + }); case 'line': - return this.chart.addSeries(LineSeries, { color: '#2962FF', lineWidth: 2, crosshairMarkerVisible: true, priceFormat: MAIN_PRICE_FORMAT }); + return this.chart.addSeries(LineSeries, { + color: '#2962FF', lineWidth: 2, priceFormat: MAIN_PRICE_FORMAT, + ...this._crosshairMarkerSeriesOptions(), + }); case 'area': return this.chart.addSeries(AreaSeries, { lineColor: '#2962FF', topColor: '#2962FF44', bottomColor: '#2962FF00', lineWidth: 2, priceFormat: MAIN_PRICE_FORMAT, + ...this._crosshairMarkerSeriesOptions(), }); case 'baseline': return this.chart.addSeries(BaselineSeries, { @@ -612,6 +622,7 @@ export class ChartManager { topLineColor: '#26a69a', topFillColor1: '#26a69a44', topFillColor2: '#26a69a00', bottomLineColor: '#ef5350', bottomFillColor1: '#ef535000', bottomFillColor2: '#ef535044', priceFormat: MAIN_PRICE_FORMAT, + ...this._crosshairMarkerSeriesOptions(), }); default: return this.chart.addSeries(CandlestickSeries, { @@ -619,6 +630,7 @@ export class ChartManager { borderUpColor: t.upColor, borderDownColor: t.downColor, wickUpColor: t.upColor, wickDownColor: t.downColor, priceFormat: MAIN_PRICE_FORMAT, + ...this._crosshairMarkerSeriesOptions(), }); } } @@ -979,8 +991,7 @@ export class ChartManager { lineStyle: plotSeriesLineStyle(plotDef.lineStyle), priceLineVisible: false, lastValueVisible: showPriceLabel, - crosshairMarkerVisible: true, - crosshairMarkerRadius: plotId === 'plot0' ? 4 : 3, + ...this._crosshairMarkerSeriesOptions(plotId === 'plot0' ? 4 : 3), title: showSeriesTitle ? this.seriesTitleForPlot(enriched, plotDef) : '', visible: isPlotVisible, priceScaleId: obvScaleId, @@ -1046,7 +1057,7 @@ export class ChartManager { lineWidth: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1) as 1 | 2 | 3 | 4, lineStyle: plotSeriesLineStyle(plotDef.lineStyle), priceLineVisible: false, - crosshairMarkerVisible: true, + ...this._crosshairMarkerSeriesOptions(), lastValueVisible: showPriceLabel, title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '', visible: isPlotVisible, @@ -1100,6 +1111,7 @@ export class ChartManager { lastValueVisible: showPriceLabel, title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '', visible: isPlotVisible, + ...this._crosshairMarkerSeriesOptions(), ...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}), }, pane); series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value }))); @@ -1277,7 +1289,7 @@ export class ChartManager { lineWidth: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1) as 1 | 2 | 3 | 4, lineStyle: plotSeriesLineStyle(plotDef.lineStyle), priceLineVisible: false, - crosshairMarkerVisible: true, + ...this._crosshairMarkerSeriesOptions(), lastValueVisible: showPriceLabel, title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '', visible: isPlotVisible, @@ -2089,6 +2101,7 @@ export class ChartManager { lastValueVisible: showPriceLabel && lineVisible, title: showPriceLabel && lineVisible ? info.title : '', visible: lineVisible, + ...this._crosshairMarkerSeriesOptions(), }, pane); series.setData(seriesData); seriesList.push(series); @@ -3544,6 +3557,57 @@ export class ChartManager { return this._indicatorAreaPriceLabelsEnabled; } + setCrosshairMarkersVisible(enabled: boolean): void { + if (this._crosshairMarkersVisible === enabled) return; + this._crosshairMarkersVisible = enabled; + this._refreshCrosshairMarkerStyles(); + } + + getCrosshairMarkersVisible(): boolean { + return this._crosshairMarkersVisible; + } + + private _crosshairMarkerSeriesOptions(radius?: number): { + crosshairMarkerVisible: boolean; + crosshairMarkerRadius?: number; + } { + return { + crosshairMarkerVisible: this._crosshairMarkersVisible, + ...(radius != null ? { crosshairMarkerRadius: radius } : {}), + }; + } + + private _refreshCrosshairMarkerStyles(): void { + const visible = this._crosshairMarkersVisible; + if (this.mainSeries) { + try { + this.mainSeries.applyOptions({ crosshairMarkerVisible: visible } as Parameters['applyOptions']>[0]); + } catch { /* ok */ } + } + for (const s of this.compareSeriesMap.values()) { + try { + s.applyOptions({ crosshairMarkerVisible: visible } as Parameters['applyOptions']>[0]); + } catch { /* ok */ } + } + for (const entry of this.indicators.values()) { + entry.seriesList.forEach((series, i) => { + if (entry.seriesMeta[i]?.isHistogram) return; + const pid = entry.seriesMeta[i]?.plotId; + const radius = entry.type === 'OBV' && pid === 'plot0' + ? 4 + : entry.type === 'OBV' && pid === 'plot1' + ? 3 + : undefined; + try { + series.applyOptions({ + crosshairMarkerVisible: visible, + ...(radius != null ? { crosshairMarkerRadius: radius } : {}), + } as Parameters['applyOptions']>[0]); + } catch { /* ok */ } + }); + } + } + private _refreshPriceLabelStyles(): void { for (const entry of this.indicators.values()) { this.applyIndicatorStyle(entry.config); @@ -3649,7 +3713,7 @@ export class ChartManager { : Math.max(1, plot.lineWidth ?? regPlot?.lineWidth ?? 1)) as 1 | 2 | 3 | 4) : Math.max(1, plot.lineWidth ?? regPlot?.lineWidth ?? 1); opts['lineStyle'] = plotSeriesLineStyle(plot.lineStyle); - opts['crosshairMarkerVisible'] = true; + opts['crosshairMarkerVisible'] = this._crosshairMarkersVisible; let plotAllows = true; if (entry.type === 'IchimokuCloud') { plotAllows = entry.seriesMeta[i]?.plotId !== 'plot2'; @@ -4980,7 +5044,10 @@ export class ChartManager { // ─── Compare overlay ────────────────────────────────────────────────────── addCompareSeries(symbol: string, bars: OHLCVBar[], color: string): void { if (this.compareSeriesMap.has(symbol)) return; - const s = this.chart.addSeries(LineSeries, { color, lineWidth: 2, priceLineVisible: false }); + const s = this.chart.addSeries(LineSeries, { + color, lineWidth: 2, priceLineVisible: false, + ...this._crosshairMarkerSeriesOptions(), + }); const base = bars[0]?.close ?? 1; s.setData(bars.map(b => ({ time: b.time as Time, value: (b.close / base - 1) * 100 }))); this.compareSeriesMap.set(symbol, s); diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index ca7768d..483de4a 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -526,6 +526,8 @@ export interface AppSettingsDto { chartLiveReceiveHighlight?: boolean; /** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 (기본 true) */ chartCrosshairInfoVisible?: boolean; + /** 세로 크로스헤어와 지표 선 교차점 점 표시 (기본 true) */ + chartCrosshairMarkersVisible?: boolean; /** 차트 마우스오버 줌·이동 플로팅 툴바 표시 (기본 true) */ chartHoverToolbarVisible?: boolean; /** 차트 상단 범례(tv-legend) 항목별 표시 옵션 */