diff --git a/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java b/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java index 6610106..9a04167 100644 --- a/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java +++ b/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java @@ -97,7 +97,12 @@ public class GcAppSettings { @Builder.Default private Boolean btShowPrice = true; - /** 보조지표 우측 가격축 라벨·설명·금액 하이라이트 (기본 true) */ + /** 캔들 영역(오버레이) 우측 가격축 라벨·설명·금액 하이라이트 (기본 true) */ + @Column(name = "chart_candle_area_price_labels", nullable = false) + @Builder.Default + private Boolean chartCandleAreaPriceLabels = true; + + /** 하단 보조지표 pane 우측 가격축 라벨·설명·금액 하이라이트 (기본 true) */ @Column(name = "chart_series_price_labels", nullable = false) @Builder.Default private Boolean chartSeriesPriceLabels = true; diff --git a/backend/src/main/java/com/goldenchart/service/AppSettingsService.java b/backend/src/main/java/com/goldenchart/service/AppSettingsService.java index d022c91..463ee90 100644 --- a/backend/src/main/java/com/goldenchart/service/AppSettingsService.java +++ b/backend/src/main/java/com/goldenchart/service/AppSettingsService.java @@ -90,6 +90,8 @@ public class AppSettingsService { Boolean.parseBoolean(d.get("btAutoPopup").toString())); if (d.containsKey("btShowPrice")) s.setBtShowPrice( Boolean.parseBoolean(d.get("btShowPrice").toString())); + if (d.containsKey("chartCandleAreaPriceLabels")) s.setChartCandleAreaPriceLabels( + Boolean.parseBoolean(d.get("chartCandleAreaPriceLabels").toString())); if (d.containsKey("chartSeriesPriceLabels")) s.setChartSeriesPriceLabels( Boolean.parseBoolean(d.get("chartSeriesPriceLabels").toString())); if (d.containsKey("chartVolumeVisible")) s.setChartVolumeVisible( @@ -198,6 +200,7 @@ public class AppSettingsService { m.put("syncOptions", parseJson(s.getSyncOptionsJson())); m.put("btAutoPopup", s.getBtAutoPopup() != null ? s.getBtAutoPopup() : true); m.put("btShowPrice", s.getBtShowPrice() != null ? s.getBtShowPrice() : true); + m.put("chartCandleAreaPriceLabels", s.getChartCandleAreaPriceLabels() != null ? s.getChartCandleAreaPriceLabels() : true); m.put("chartSeriesPriceLabels", s.getChartSeriesPriceLabels() != null ? s.getChartSeriesPriceLabels() : true); m.put("chartVolumeVisible", s.getChartVolumeVisible() != null ? s.getChartVolumeVisible() : true); m.put("chartLiveReceiveHighlight", s.getChartLiveReceiveHighlight() != null ? s.getChartLiveReceiveHighlight() : true); diff --git a/backend/src/main/resources/db/migration/V49__chart_candle_area_price_labels.sql b/backend/src/main/resources/db/migration/V49__chart_candle_area_price_labels.sql new file mode 100644 index 0000000..b105b67 --- /dev/null +++ b/backend/src/main/resources/db/migration/V49__chart_candle_area_price_labels.sql @@ -0,0 +1,12 @@ +-- ============================================================ +-- V49: 캔들 차트 영역(오버레이 지표) 가격축 라벨·설명 — 보조지표 pane 과 분리 +-- chart_series_price_labels = 하단 보조지표 pane (기존) +-- chart_candle_area_price_labels = 캔들·거래량 pane 위 오버레이 (pane 0~1) +-- ============================================================ + +ALTER TABLE gc_app_settings + ADD COLUMN chart_candle_area_price_labels TINYINT(1) NOT NULL DEFAULT 1 + COMMENT '캔들 영역 오버레이 지표 가격축 라벨·설명 (1=ON, 0=OFF)'; + +UPDATE gc_app_settings +SET chart_candle_area_price_labels = chart_series_price_labels; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ae0de4c..8818ae1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -634,6 +634,7 @@ function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [appSettingsLoaded, dbLoaded]); + const chartCandleAreaPriceLabels = appDefaults.chartCandleAreaPriceLabels ?? true; const chartSeriesPriceLabels = appDefaults.chartSeriesPriceLabels ?? true; const chartVolumeVisible = appDefaults.chartVolumeVisible ?? true; const chartLiveReceiveHighlight = appDefaults.chartLiveReceiveHighlight ?? true; @@ -1007,9 +1008,13 @@ function App() { useEffect(() => { if (!appSettingsLoaded) return; - managerRef.current?.setSeriesPriceLabelsEnabled(chartSeriesPriceLabels); - slotRefs.current.forEach(slot => slot?.setSeriesPriceLabelsEnabled(chartSeriesPriceLabels)); - }, [appSettingsLoaded, chartSeriesPriceLabels]); + managerRef.current?.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels); + managerRef.current?.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels); + slotRefs.current.forEach(slot => { + slot?.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels); + slot?.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels); + }); + }, [appSettingsLoaded, chartCandleAreaPriceLabels, chartSeriesPriceLabels]); useEffect(() => { if (!appSettingsLoaded) return; @@ -1789,6 +1794,7 @@ function App() { @@ -1858,11 +1864,17 @@ function App() { onAddIndicatorToChart={handleAddIndicator} onRemoveIndicatorFromChart={handleRemoveByType} onIndicatorListOrderChange={handleIndicatorListOrderChange} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} + onChartCandleAreaPriceLabels={v => { + saveAppDef({ chartCandleAreaPriceLabels: v }); + managerRef.current?.setCandleAreaPriceLabelsEnabled(v); + slotRefs.current.forEach(slot => slot?.setCandleAreaPriceLabelsEnabled(v)); + }} chartSeriesPriceLabels={chartSeriesPriceLabels} onChartSeriesPriceLabels={v => { saveAppDef({ chartSeriesPriceLabels: v }); - managerRef.current?.setSeriesPriceLabelsEnabled(v); - slotRefs.current.forEach(slot => slot?.setSeriesPriceLabelsEnabled(v)); + managerRef.current?.setIndicatorAreaPriceLabelsEnabled(v); + slotRefs.current.forEach(slot => slot?.setIndicatorAreaPriceLabelsEnabled(v)); }} chartVolumeVisible={chartVolumeVisible} onChartVolumeVisible={v => { @@ -2201,6 +2213,7 @@ function App() { onSymbolChange={activeSlot === 0 ? handleActiveSlotSymbolChange : undefined} magnetMode={magnetMode} onTradeOrderRequest={(req, idx) => applyTradeFill(req, idx)} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartVolumeVisible={chartVolumeVisible} chartPaneSeparator={chartPaneSeparator} @@ -2242,6 +2255,7 @@ function App() { onSymbolChange={activeSlot === i + 1 ? handleActiveSlotSymbolChange : undefined} magnetMode={magnetMode} onTradeOrderRequest={(req, idx) => applyTradeFill(req, idx)} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartVolumeVisible={chartVolumeVisible} chartPaneSeparator={chartPaneSeparator} @@ -2301,7 +2315,8 @@ function App() { } else { pendingRealtimeBarRef.current = null; } - mgr.setSeriesPriceLabelsEnabled(chartSeriesPriceLabels); + mgr.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels); + mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels); const wrapper = document.querySelector('.chart-wrapper') as HTMLElement | null; mgr.setVolumeVisible(chartVolumeVisible, wrapper?.clientHeight); mgr.setPaneSeparatorOptions(chartPaneSeparator); @@ -2347,6 +2362,8 @@ function App() { onToggleIndicatorHidden={handleToggleIndicatorHidden} onOpenIndicatorSettings={id => setSettingsModalId(id)} volumeVisible={chartVolumeVisible} + candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels} + indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels} paneSeparatorOptions={chartPaneSeparator} /> {isSingleLoadingMore && ( diff --git a/frontend/src/components/ChartSlot.tsx b/frontend/src/components/ChartSlot.tsx index 24c72b6..b7f55da 100644 --- a/frontend/src/components/ChartSlot.tsx +++ b/frontend/src/components/ChartSlot.tsx @@ -117,8 +117,10 @@ export interface ChartSlotHandle { setIndicators: (inds: IndicatorConfig[]) => void; /** 크로스헤어 자석모드 설정 */ setCrosshairMagnet: (enabled: boolean) => void; - /** 가격축 라벨·설명 on/off */ + /** 가격축 라벨·설명 on/off (캔들·보조지표 동일 값) */ setSeriesPriceLabelsEnabled: (enabled: boolean) => void; + setCandleAreaPriceLabelsEnabled: (enabled: boolean) => void; + setIndicatorAreaPriceLabelsEnabled: (enabled: boolean) => void; /** 거래량 pane on/off */ setVolumeVisible: (visible: boolean) => void; setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => void; @@ -172,7 +174,9 @@ export interface ChartSlotProps { magnetMode?: 'off' | 'weak' | 'strong'; /** 차트 우클릭 → 매수·매도 패널 자동 입력 */ onTradeOrderRequest?: (req: { market: string; price: number; side: 'buy' | 'sell' }, slotIndex: number) => void; - /** 보조지표 우측 가격축 라벨·설명 표시 */ + /** 캔들 영역 오버레이 지표 가격축 라벨·설명 */ + chartCandleAreaPriceLabels?: boolean; + /** 하단 보조지표 pane 가격축 라벨·설명 */ chartSeriesPriceLabels?: boolean; /** 차트 하단 거래량 바 표시 */ chartVolumeVisible?: boolean; @@ -206,6 +210,7 @@ const ChartSlot = forwardRef(function ChartSlot onSymbolChange, magnetMode = 'off', onTradeOrderRequest, + chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartVolumeVisible = true, chartRealtimeSource = 'BACKEND_STOMP', @@ -331,6 +336,12 @@ const ChartSlot = forwardRef(function ChartSlot setSeriesPriceLabelsEnabled: (enabled: boolean) => { managerRef.current?.setSeriesPriceLabelsEnabled(enabled); }, + setCandleAreaPriceLabelsEnabled: (enabled: boolean) => { + managerRef.current?.setCandleAreaPriceLabelsEnabled(enabled); + }, + setIndicatorAreaPriceLabelsEnabled: (enabled: boolean) => { + managerRef.current?.setIndicatorAreaPriceLabelsEnabled(enabled); + }, setVolumeVisible: (visible: boolean) => { managerRef.current?.setVolumeVisible(visible); }, @@ -349,7 +360,11 @@ const ChartSlot = forwardRef(function ChartSlot }, [magnetMode]); useEffect(() => { - managerRef.current?.setSeriesPriceLabelsEnabled(chartSeriesPriceLabels); + managerRef.current?.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels); + }, [chartCandleAreaPriceLabels]); + + useEffect(() => { + managerRef.current?.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels); }, [chartSeriesPriceLabels]); useEffect(() => { @@ -855,6 +870,8 @@ const ChartSlot = forwardRef(function ChartSlot drawingsVisible={!compactMode} showHoverToolbar={!compactMode} volumeVisible={chartVolumeVisible} + candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels} + indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels} paneSeparatorOptions={chartPaneSeparator} crosshairInfoVisible={chartCrosshairInfoVisible} onCrosshair={data => { @@ -873,7 +890,8 @@ const ChartSlot = forwardRef(function ChartSlot } else { pendingRealtimeBarRef.current = null; } - mgr.setSeriesPriceLabelsEnabled(chartSeriesPriceLabels); + mgr.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels); + mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels); mgr.setVolumeVisible(chartVolumeVisible); if (chartPaneSeparator) mgr.setPaneSeparatorOptions(chartPaneSeparator); // Time sync: 가시 시간 범위 변화 구독 diff --git a/frontend/src/components/IndicatorSettingsSections.tsx b/frontend/src/components/IndicatorSettingsSections.tsx index 8c1b4aa..1f4d198 100644 --- a/frontend/src/components/IndicatorSettingsSections.tsx +++ b/frontend/src/components/IndicatorSettingsSections.tsx @@ -331,7 +331,7 @@ export const SettingsOutputFooter: React.FC<{
가격축 라벨·설명
-
diff --git a/frontend/src/components/trendSearch/TrendSearchChartPanel.tsx b/frontend/src/components/trendSearch/TrendSearchChartPanel.tsx index 9793142..c1258f1 100644 --- a/frontend/src/components/trendSearch/TrendSearchChartPanel.tsx +++ b/frontend/src/components/trendSearch/TrendSearchChartPanel.tsx @@ -19,6 +19,7 @@ interface Props { timeframe: string; theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; + chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions; } @@ -39,6 +40,7 @@ const TrendSearchChartPanel: React.FC = ({ timeframe, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', + chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartPaneSeparator, }) => { @@ -184,7 +186,8 @@ const TrendSearchChartPanel: React.FC = ({ magnifierEnabled={false} volumeVisible showHoverToolbar={false} - seriesPriceLabelsEnabled={chartSeriesPriceLabels} + candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels} + indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels} paneSeparatorOptions={chartPaneSeparator} />
diff --git a/frontend/src/components/trendSearch/TrendSearchResultCard.tsx b/frontend/src/components/trendSearch/TrendSearchResultCard.tsx index 8d5e749..bb0f42a 100644 --- a/frontend/src/components/trendSearch/TrendSearchResultCard.tsx +++ b/frontend/src/components/trendSearch/TrendSearchResultCard.tsx @@ -21,6 +21,7 @@ interface Props { onSelect?: () => void; theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; + chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions; ticker?: TickerData; @@ -58,6 +59,7 @@ const TrendSearchResultCard: React.FC = ({ onSelect, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', + chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartPaneSeparator, ticker, @@ -140,6 +142,7 @@ const TrendSearchResultCard: React.FC = ({ timeframe={timeframe} theme={theme} chartRealtimeSource={chartRealtimeSource} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartPaneSeparator={chartPaneSeparator} /> diff --git a/frontend/src/components/trendSearch/TrendSearchResultsCardGrid.tsx b/frontend/src/components/trendSearch/TrendSearchResultsCardGrid.tsx index 49907ba..23ce424 100644 --- a/frontend/src/components/trendSearch/TrendSearchResultsCardGrid.tsx +++ b/frontend/src/components/trendSearch/TrendSearchResultsCardGrid.tsx @@ -16,6 +16,7 @@ interface Props { flashMarkets?: Set; theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; + chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions; tickers?: Map; @@ -37,6 +38,7 @@ const TrendSearchResultsCardGrid: React.FC = ({ flashMarkets, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', + chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartPaneSeparator, tickers, @@ -85,6 +87,7 @@ const TrendSearchResultsCardGrid: React.FC = ({ onSelect={onSelect ? () => onSelect(row) : undefined} theme={theme} chartRealtimeSource={chartRealtimeSource} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartPaneSeparator={chartPaneSeparator} ticker={tickers?.get(row.market)} diff --git a/frontend/src/components/virtual/VirtualStrategyChartPopup.tsx b/frontend/src/components/virtual/VirtualStrategyChartPopup.tsx index 0ce7b89..ffefdc3 100644 --- a/frontend/src/components/virtual/VirtualStrategyChartPopup.tsx +++ b/frontend/src/components/virtual/VirtualStrategyChartPopup.tsx @@ -28,7 +28,9 @@ const VirtualStrategyChartPopup: React.FC = ({ onClose, }) => { const { settings: appSettings } = useAppSettings(); - const chartSeriesPriceLabels = resolveAppDefaults(appSettings).chartSeriesPriceLabels; + const chartDefaults = resolveAppDefaults(appSettings); + const chartCandleAreaPriceLabels = chartDefaults.chartCandleAreaPriceLabels; + const chartSeriesPriceLabels = chartDefaults.chartSeriesPriceLabels; const ko = getKoreanName(market); const sym = market.replace(/^KRW-/, ''); @@ -56,6 +58,7 @@ const VirtualStrategyChartPopup: React.FC = ({ theme={theme} running={running} chartRealtimeSource={chartRealtimeSource} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} /> diff --git a/frontend/src/components/virtual/VirtualTargetCard.tsx b/frontend/src/components/virtual/VirtualTargetCard.tsx index e30281d..f8bb325 100644 --- a/frontend/src/components/virtual/VirtualTargetCard.tsx +++ b/frontend/src/components/virtual/VirtualTargetCard.tsx @@ -42,6 +42,7 @@ interface Props { onSelect?: () => void; theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; + chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions; chartLiveReceiveHighlight?: boolean; @@ -72,6 +73,7 @@ const VirtualTargetCard: React.FC = ({ onSelect, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', + chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartPaneSeparator, chartLiveReceiveHighlight = true, @@ -194,6 +196,7 @@ const VirtualTargetCard: React.FC = ({ theme={theme} running={running} chartRealtimeSource={chartRealtimeSource} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartPaneSeparator={chartPaneSeparator} chartTimeframe={chartTimeframe} diff --git a/frontend/src/components/virtual/VirtualTargetCardChart.tsx b/frontend/src/components/virtual/VirtualTargetCardChart.tsx index 7ba27ff..fd4d29a 100644 --- a/frontend/src/components/virtual/VirtualTargetCardChart.tsx +++ b/frontend/src/components/virtual/VirtualTargetCardChart.tsx @@ -30,6 +30,7 @@ interface Props { theme?: Theme; running?: boolean; chartRealtimeSource?: ChartRealtimeSource; + chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions; /** 전체보기 분할 pane — 캔버스 높이 100% */ @@ -46,6 +47,7 @@ const VirtualTargetCardChart: React.FC = ({ theme = 'dark', running = false, chartRealtimeSource = 'BACKEND_STOMP', + chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartPaneSeparator, fillHeight = false, @@ -261,7 +263,8 @@ const VirtualTargetCardChart: React.FC = ({ showPaneLegend={false} showCandlePaneControls={false} defaultCandleOnly - seriesPriceLabelsEnabled={chartSeriesPriceLabels} + candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels} + indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels} paneSeparatorOptions={chartPaneSeparator} /> diff --git a/frontend/src/components/virtual/VirtualTargetFocusView.tsx b/frontend/src/components/virtual/VirtualTargetFocusView.tsx index 40508e0..2c93d40 100644 --- a/frontend/src/components/virtual/VirtualTargetFocusView.tsx +++ b/frontend/src/components/virtual/VirtualTargetFocusView.tsx @@ -32,6 +32,7 @@ interface Props { onExit: () => void; theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; + chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions; chartLiveReceiveHighlight?: boolean; @@ -55,6 +56,7 @@ const VirtualTargetFocusView: React.FC = ({ onExit, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', + chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartPaneSeparator, chartLiveReceiveHighlight = true, @@ -114,6 +116,7 @@ const VirtualTargetFocusView: React.FC = ({ theme={theme} running={running} chartRealtimeSource={chartRealtimeSource} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartPaneSeparator={chartPaneSeparator} fillHeight diff --git a/frontend/src/components/virtual/VirtualTargetGrid.tsx b/frontend/src/components/virtual/VirtualTargetGrid.tsx index 30692f9..1475fc4 100644 --- a/frontend/src/components/virtual/VirtualTargetGrid.tsx +++ b/frontend/src/components/virtual/VirtualTargetGrid.tsx @@ -31,6 +31,7 @@ interface Props { onSelectMarket?: (market: string) => void; theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; + chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions; chartLiveReceiveHighlight?: boolean; @@ -48,6 +49,7 @@ const VirtualTargetGrid: React.FC = ({ onSelectMarket, theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', + chartCandleAreaPriceLabels = true, chartSeriesPriceLabels = true, chartPaneSeparator, chartLiveReceiveHighlight = true, @@ -133,6 +135,7 @@ const VirtualTargetGrid: React.FC = ({ onExit={() => setFocusMarket(null)} theme={theme} chartRealtimeSource={chartRealtimeSource} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartPaneSeparator={chartPaneSeparator} chartLiveReceiveHighlight={chartLiveReceiveHighlight} @@ -172,7 +175,8 @@ const VirtualTargetGrid: React.FC = ({ onSelect={onSelectMarket ? () => onSelectMarket(t.market) : undefined} theme={theme} chartRealtimeSource={chartRealtimeSource} - chartSeriesPriceLabels={chartSeriesPriceLabels} + chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} + chartSeriesPriceLabels={chartSeriesPriceLabels} chartPaneSeparator={chartPaneSeparator} chartLiveReceiveHighlight={chartLiveReceiveHighlight} ticker={tickers?.get(t.market)} diff --git a/frontend/src/hooks/useAppSettings.ts b/frontend/src/hooks/useAppSettings.ts index 2e9db40..c054c89 100644 --- a/frontend/src/hooks/useAppSettings.ts +++ b/frontend/src/hooks/useAppSettings.ts @@ -142,6 +142,7 @@ export function resolveAppDefaults(s: AppSettingsDto) { syncOptions: (s.syncOptions as unknown as SyncOptions | null) ?? DEFAULT_SYNC, btAutoPopup: s.btAutoPopup ?? true, btShowPrice: s.btShowPrice ?? true, + chartCandleAreaPriceLabels: s.chartCandleAreaPriceLabels ?? s.chartSeriesPriceLabels ?? true, chartSeriesPriceLabels: s.chartSeriesPriceLabels ?? true, chartVolumeVisible: s.chartVolumeVisible ?? true, chartLiveReceiveHighlight: s.chartLiveReceiveHighlight ?? true, diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index 5d2310f..b31c3e6 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -245,8 +245,10 @@ export class ChartManager { /** 현재 크로스헤어가 위에 있는 시리즈의 entryId ('__main__' | indicatorId | null) */ private _crosshairEntryId: string | null = null; - /** 우측 가격축 라벨·plot 설명(전환선 등)·금액 색상 하이라이트 */ - private _seriesPriceLabelsEnabled = true; + /** 캔들 pane(0~1) 오버레이 지표 — 우측 가격축 라벨·설명 */ + private _candleAreaPriceLabelsEnabled = true; + /** 하단 보조지표 pane(2+) — 우측 가격축 라벨·설명 */ + private _indicatorAreaPriceLabelsEnabled = true; /** 차트 하단 거래량 pane 표시 */ private _volumeVisible = true; @@ -514,12 +516,31 @@ export class ChartManager { fmt, ); this.chart.applyOptions({ - localization: { timeFormatter, priceFormatter: formatChartAxisPrice }, + localization: { timeFormatter }, timeScale: { tickMarkFormatter: this._tickMarkFormatter() }, }); + this._applyAllSeriesPriceFormats(); this._notifyPaneLayout(); } + /** pane별 priceFormat — 전역 priceFormatter 는 보조지표 라벨 포맷을 덮어씀 */ + private _applyAllSeriesPriceFormats(): void { + if (this.mainSeries) { + try { + this.mainSeries.applyOptions({ priceFormat: MAIN_PRICE_FORMAT } as Parameters['applyOptions']>[0]); + } catch { /* ok */ } + } + for (const entry of this.indicators.values()) { + const fmt = priceFormatForIndicatorPane(entry.paneIndex ?? 0); + if (!fmt) continue; + for (const series of entry.seriesList) { + try { + series.applyOptions({ priceFormat: fmt } as Parameters['applyOptions']>[0]); + } catch { /* ok */ } + } + } + } + setDisplayTimezone(tz: string, timeframe?: Timeframe, chartTimeFormat?: string): void { this.displayTimezone = tz || DEFAULT_DISPLAY_TIMEZONE; if (timeframe) this.displayTimeframe = timeframe; @@ -688,7 +709,7 @@ export class ChartManager { seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color }); } else { const isPlotVisible = !indicatorHidden && config.plotVisibility?.[plotDef.id] !== false; - const showPriceLabel = this.shouldShowSeriesPriceLabel(config); + const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane); const showSeriesTitle = pane < 2 && showPriceLabel; series = this.chart.addSeries(LineSeries, { color: plotDef.color, @@ -777,6 +798,7 @@ export class ChartManager { this._removeOrphanSubPanes(); if (this._activeIndicatorPaneIndices().size > 0) { this._resyncIndicatorPaneIndices(); + this._applyAllSeriesPriceFormats(); } else { this._trimTrailingEmptySubPanes(); } @@ -1253,7 +1275,7 @@ export class ChartManager { ); } - const showPriceLabel = this.shouldShowSeriesPriceLabel(_config, info.lastVal); + const showPriceLabel = this.shouldShowSeriesPriceLabel(_config, info.lastVal, pane); const series = this.chart.addSeries(LineSeries, { color: info.color, lineWidth: 1, @@ -2275,16 +2297,35 @@ export class ChartManager { series.applyOptions(opts as any); } - /** 차트 설정: 보조지표 우측 가격축 라벨·설명 on/off */ + /** 차트 설정: 캔들·보조지표 영역 가격축 라벨·설명 (동일 값이면 기존 API 호환) */ setSeriesPriceLabelsEnabled(enabled: boolean): void { - this._seriesPriceLabelsEnabled = enabled; + this.setCandleAreaPriceLabelsEnabled(enabled); + this.setIndicatorAreaPriceLabelsEnabled(enabled); + } + + setCandleAreaPriceLabelsEnabled(enabled: boolean): void { + this._candleAreaPriceLabelsEnabled = enabled; + this._refreshPriceLabelStyles(); + } + + setIndicatorAreaPriceLabelsEnabled(enabled: boolean): void { + this._indicatorAreaPriceLabelsEnabled = enabled; + this._refreshPriceLabelStyles(); + } + + getCandleAreaPriceLabelsEnabled(): boolean { + return this._candleAreaPriceLabelsEnabled; + } + + getIndicatorAreaPriceLabelsEnabled(): boolean { + return this._indicatorAreaPriceLabelsEnabled; + } + + private _refreshPriceLabelStyles(): void { for (const entry of this.indicators.values()) { this.applyIndicatorStyle(entry.config); } - } - - getSeriesPriceLabelsEnabled(): boolean { - return this._seriesPriceLabelsEnabled; + this._applyAllSeriesPriceFormats(); } /** 차트 설정: 거래량 pane 표시 on/off */ @@ -2308,8 +2349,12 @@ export class ChartManager { private shouldShowSeriesPriceLabel( config: IndicatorConfig, plotAllowsLastValue = true, + paneIndex = 0, ): boolean { - return this._seriesPriceLabelsEnabled + const areaEnabled = paneIndex < 2 + ? this._candleAreaPriceLabelsEnabled + : this._indicatorAreaPriceLabelsEnabled; + return areaEnabled && plotAllowsLastValue && config.lastValueVisible !== false; } @@ -2318,8 +2363,9 @@ export class ChartManager { config: IndicatorConfig, plot: PlotDef | undefined, plotAllowsLastValue = true, + paneIndex = 0, ): string { - if (!this.shouldShowSeriesPriceLabel(config, plotAllowsLastValue)) return ''; + if (!this.shouldShowSeriesPriceLabel(config, plotAllowsLastValue, paneIndex)) return ''; if (config.type === 'IchimokuCloud' && plot?.id) { return getIchimokuPlotTitle(plot.id); } @@ -2362,10 +2408,10 @@ export class ChartManager { if (entry.type === 'IchimokuCloud') { plotAllows = entry.seriesMeta[i]?.plotId !== 'plot2'; } - const showPriceLabel = this.shouldShowSeriesPriceLabel(config, plotAllows); + const showPriceLabel = this.shouldShowSeriesPriceLabel(config, plotAllows, paneIdx); opts['lastValueVisible'] = showPriceLabel; opts['title'] = paneIdx < 2 && showPriceLabel - ? this.seriesTitleForPlot(config, plot, plotAllows) + ? this.seriesTitleForPlot(config, plot, plotAllows, paneIdx) : ''; } else { opts['color'] = plot.color ?? '#aaa'; diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index 6e9f74d..9c57b28 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -386,7 +386,9 @@ export interface AppSettingsDto { btAutoPopup?: boolean; /** 백테스팅 매수/매도 마커에 금액 표시 여부 (기본 true) */ btShowPrice?: boolean; - /** 보조지표 우측 가격축 라벨·설명·금액 하이라이트 (기본 true) */ + /** 캔들 영역 오버레이 지표 가격축 라벨·설명 (기본 true) */ + chartCandleAreaPriceLabels?: boolean; + /** 하단 보조지표 pane 가격축 라벨·설명 (기본 true) */ chartSeriesPriceLabels?: boolean; /** 차트 하단 거래량 바 표시 (기본 true) */ chartVolumeVisible?: boolean; diff --git a/packages/shared/src/api/backendApi.ts b/packages/shared/src/api/backendApi.ts index 7070ce9..c3d249c 100644 --- a/packages/shared/src/api/backendApi.ts +++ b/packages/shared/src/api/backendApi.ts @@ -471,6 +471,7 @@ export interface AppSettingsDto { /** 백테스팅 매수/매도 마커에 금액 표시 여부 (기본 true) */ btShowPrice?: boolean; /** 보조지표 우측 가격축 라벨·설명·금액 하이라이트 (기본 true) */ + chartCandleAreaPriceLabels?: boolean; chartSeriesPriceLabels?: boolean; /** 차트 하단 거래량 바 표시 (기본 true) */ chartVolumeVisible?: boolean;