From 03441337f2b11c4fbbc2bc66496eb5eb3afd74e7 Mon Sep 17 00:00:00 2001 From: Macbook Date: Fri, 29 May 2026 02:25:03 +0900 Subject: [PATCH] =?UTF-8?q?=EB=82=A0=EC=A7=9C=ED=8F=AC=EB=A9=A7=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/goldenchart/entity/GcAppSettings.java | 5 +++ .../service/AppSettingsService.java | 2 + .../V47__trade_alert_time_format.sql | 2 + frontend/src/App.css | 3 +- frontend/src/App.tsx | 29 +++++++++--- .../src/components/CandlePaneTimeAxis.tsx | 3 ++ frontend/src/components/ChartSlot.tsx | 8 +++- .../src/components/ChartTimeFormatPicker.tsx | 2 +- frontend/src/components/SettingsPage.tsx | 44 +++++++++++++++---- frontend/src/components/TradeAlertModal.tsx | 9 ++-- .../src/components/TradeSignalDetailBody.tsx | 2 + frontend/src/components/TradingChart.tsx | 1 + .../TradeNotificationListRow.tsx | 2 + frontend/src/hooks/useAppSettings.ts | 11 +++++ frontend/src/utils/ChartManager.ts | 6 +++ frontend/src/utils/backendApi.ts | 2 + frontend/src/utils/chartTimeFormat.ts | 12 ++++- frontend/src/utils/tradeAlertTimeFormat.ts | 37 ++++++++++++++++ frontend/src/utils/tradeSignalDisplay.ts | 12 +++-- 19 files changed, 166 insertions(+), 26 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V47__trade_alert_time_format.sql create mode 100644 frontend/src/utils/tradeAlertTimeFormat.ts diff --git a/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java b/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java index c54289f..db998af 100644 --- a/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java +++ b/backend/src/main/java/com/goldenchart/entity/GcAppSettings.java @@ -72,6 +72,11 @@ public class GcAppSettings { @Builder.Default private String chartTimeFormat = "MM-dd HH:mm"; + /** 매매 시그널 알림 팝업 시간 표시 포맷 */ + @Column(name = "trade_alert_time_format", length = 64, nullable = false) + @Builder.Default + private String tradeAlertTimeFormat = "MM-dd HH:mm"; + /** 캔들 색상 JSON (frontend MainChartStyle 구조) */ @Column(name = "main_chart_style_json", columnDefinition = "JSON") @JdbcTypeCode(SqlTypes.JSON) diff --git a/backend/src/main/java/com/goldenchart/service/AppSettingsService.java b/backend/src/main/java/com/goldenchart/service/AppSettingsService.java index 8ccc01a..c876ec3 100644 --- a/backend/src/main/java/com/goldenchart/service/AppSettingsService.java +++ b/backend/src/main/java/com/goldenchart/service/AppSettingsService.java @@ -83,6 +83,7 @@ public class AppSettingsService { if (d.containsKey("defaultLayoutId")) s.setDefaultLayoutId((String) d.get("defaultLayoutId")); if (d.containsKey("displayTimezone")) s.setDisplayTimezone((String) d.get("displayTimezone")); if (d.containsKey("chartTimeFormat")) s.setChartTimeFormat(normalizeChartTimeFormat((String) d.get("chartTimeFormat"))); + if (d.containsKey("tradeAlertTimeFormat")) s.setTradeAlertTimeFormat(normalizeChartTimeFormat((String) d.get("tradeAlertTimeFormat"))); if (d.containsKey("mainChartStyle")) s.setMainChartStyleJson(toJson(d.get("mainChartStyle"))); if (d.containsKey("syncOptions")) s.setSyncOptionsJson(toJson(d.get("syncOptions"))); if (d.containsKey("btAutoPopup")) s.setBtAutoPopup( @@ -190,6 +191,7 @@ public class AppSettingsService { m.put("defaultLayoutId", s.getDefaultLayoutId() != null ? s.getDefaultLayoutId() : "1"); m.put("displayTimezone", s.getDisplayTimezone() != null ? s.getDisplayTimezone() : "Asia/Seoul"); m.put("chartTimeFormat", s.getChartTimeFormat() != null ? s.getChartTimeFormat() : "MM-dd HH:mm"); + m.put("tradeAlertTimeFormat", s.getTradeAlertTimeFormat() != null ? s.getTradeAlertTimeFormat() : "MM-dd HH:mm"); m.put("mainChartStyle", parseJson(s.getMainChartStyleJson())); m.put("syncOptions", parseJson(s.getSyncOptionsJson())); m.put("btAutoPopup", s.getBtAutoPopup() != null ? s.getBtAutoPopup() : true); diff --git a/backend/src/main/resources/db/migration/V47__trade_alert_time_format.sql b/backend/src/main/resources/db/migration/V47__trade_alert_time_format.sql new file mode 100644 index 0000000..6265984 --- /dev/null +++ b/backend/src/main/resources/db/migration/V47__trade_alert_time_format.sql @@ -0,0 +1,2 @@ +ALTER TABLE gc_app_settings + ADD COLUMN trade_alert_time_format VARCHAR(64) NOT NULL DEFAULT 'MM-dd HH:mm' AFTER chart_time_format; diff --git a/frontend/src/App.css b/frontend/src/App.css index 6891998..802d6a9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -9438,7 +9438,8 @@ html.theme-blue { display: flex; flex-direction: column; gap: 8px; - max-width: 360px; + max-width: 420px; + width: 100%; } .stg-chart-time-format-custom { width: 100%; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c746778..bda5bb0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,10 @@ import { setChartTimeFormat, useChartTimeFormat, } from './utils/chartTimeFormat'; +import { + setTradeAlertTimeFormat, + useTradeAlertTimeFormat, +} from './utils/tradeAlertTimeFormat'; import { normalizeTimezone, setDisplayTimezone, useDisplayTimezone } from './utils/timezone'; import { calcStats, type PriceStats } from './utils/calculations'; import { getKoreanName } from './utils/marketNameCache'; @@ -192,6 +196,7 @@ function App() { const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey); const displayTimezone = useDisplayTimezone(); const chartTimeFormat = useChartTimeFormat(); + const tradeAlertTimeFormat = useTradeAlertTimeFormat(); // DB 로드 전에는 localStorage 기본값, 로드 후 DB 값으로 오버라이드됨 const [symbol, setSymbol] = useState(initial.symbol); @@ -266,13 +271,24 @@ function App() { managerRef.current?.setDisplayTimezone(next, tf, chartTimeFormat); }, [saveAppDef, layoutDef.count, activeSlotTf, timeframe, chartTimeFormat]); + const applyChartTimeFormatToManagers = useCallback((next: string) => { + const tf = layoutDef.count > 1 ? activeSlotTf : timeframe; + managerRef.current?.setChartTimeFormat(next); + slotRefs.current.forEach(slot => slot?.setChartTimeFormat?.(next, tf)); + }, [layoutDef.count, activeSlotTf, timeframe]); + const handleChartTimeFormatChange = useCallback((format: string) => { const next = normalizeChartTimeFormat(format); setChartTimeFormat(next); saveAppDef({ chartTimeFormat: next }); - const tf = layoutDef.count > 1 ? activeSlotTf : timeframe; - managerRef.current?.setDisplayTimezone(displayTimezone, tf, next); - }, [saveAppDef, layoutDef.count, activeSlotTf, timeframe, displayTimezone]); + applyChartTimeFormatToManagers(next); + }, [saveAppDef, applyChartTimeFormatToManagers]); + + const handleTradeAlertTimeFormatChange = useCallback((format: string) => { + const next = normalizeChartTimeFormat(format); + setTradeAlertTimeFormat(next); + saveAppDef({ tradeAlertTimeFormat: next }); + }, [saveAppDef]); const chartSymbol = layoutDef.count > 1 ? activeSlotSymbol : symbol; const { orderbook, wsStatus: obWsStatus, spread } = useUpbitOrderbook(chartSymbol); @@ -975,8 +991,9 @@ function App() { useEffect(() => { if (!appSettingsLoaded) return; const tf = layoutDef.count > 1 ? activeSlotTf : timeframe; - managerRef.current?.setDisplayTimezone(displayTimezone, tf, chartTimeFormat); - }, [appSettingsLoaded, displayTimezone, chartTimeFormat, timeframe, activeSlotTf, layoutDef.count]); + managerRef.current?.setChartTimeFormat(chartTimeFormat); + slotRefs.current.forEach(slot => slot?.setChartTimeFormat?.(chartTimeFormat, tf)); + }, [appSettingsLoaded, chartTimeFormat, timeframe, activeSlotTf, layoutDef.count]); useEffect(() => { if (!appSettingsLoaded) return; @@ -1910,6 +1927,8 @@ function App() { onDisplayTimezoneChange={handleTimezoneChange} chartTimeFormat={chartTimeFormat} onChartTimeFormatChange={handleChartTimeFormatChange} + tradeAlertTimeFormat={tradeAlertTimeFormat} + onTradeAlertTimeFormatChange={handleTradeAlertTimeFormatChange} verificationIssueNotify={appDefaults.verificationIssueNotify} onVerificationIssueNotify={v => saveAppDef({ verificationIssueNotify: v })} onFcmTest={async () => { diff --git a/frontend/src/components/CandlePaneTimeAxis.tsx b/frontend/src/components/CandlePaneTimeAxis.tsx index e00d975..80ccec6 100644 --- a/frontend/src/components/CandlePaneTimeAxis.tsx +++ b/frontend/src/components/CandlePaneTimeAxis.tsx @@ -3,6 +3,7 @@ */ import React, { useState, useEffect, useCallback } from 'react'; import type { ChartManager } from '../utils/ChartManager'; +import { subscribeChartTimeFormat } from '../utils/chartTimeFormat'; const AXIS_HEIGHT = 22; @@ -26,12 +27,14 @@ const CandlePaneTimeAxis: React.FC = ({ manager, contai const unsubLayout = manager.subscribePaneLayout(sync); const unsubRange = manager.subscribeViewport(sync); + const unsubFormat = subscribeChartTimeFormat(sync); const timers = [0, 50, 150, 400].map(ms => window.setTimeout(sync, ms)); return () => { ro.disconnect(); unsubLayout(); unsubRange(); + unsubFormat(); timers.forEach(clearTimeout); }; }, [manager, containerEl, sync]); diff --git a/frontend/src/components/ChartSlot.tsx b/frontend/src/components/ChartSlot.tsx index a0025f0..4c857c1 100644 --- a/frontend/src/components/ChartSlot.tsx +++ b/frontend/src/components/ChartSlot.tsx @@ -116,6 +116,8 @@ export interface ChartSlotHandle { /** 거래량 pane on/off */ setVolumeVisible: (visible: boolean) => void; setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => void; + /** 차트 시간축·크로스헤어 날짜 형식 */ + setChartTimeFormat: (format: string, timeframe?: Timeframe) => void; } // ── Props ───────────────────────────────────────────────────────────────── @@ -323,7 +325,11 @@ const ChartSlot = forwardRef(function ChartSlot setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => { managerRef.current?.setPaneSeparatorOptions(opts); }, - }), []); + setChartTimeFormat: (format: string, tf?: Timeframe) => { + managerRef.current?.setChartTimeFormat(format); + if (tf) managerRef.current?.setDisplayTimezone(displayTimezone ?? 'Asia/Seoul', tf, format); + }, + }), [displayTimezone]); // 자석모드 변경 → ChartManager 크로스헤어 모드 즉시 반영 useEffect(() => { diff --git a/frontend/src/components/ChartTimeFormatPicker.tsx b/frontend/src/components/ChartTimeFormatPicker.tsx index 9d7e697..eb002f2 100644 --- a/frontend/src/components/ChartTimeFormatPicker.tsx +++ b/frontend/src/components/ChartTimeFormatPicker.tsx @@ -71,7 +71,7 @@ const ChartTimeFormatPicker: React.FC = ({ value, on setCustomText(e.target.value)} onBlur={commitCustom} diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 776ef8f..3bcb23a 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -16,6 +16,7 @@ import IndicatorMainDefaultsPanel, { import type { IndicatorConfig } from '../types'; import type { PlotDef, HLineDef } from '../utils/indicatorRegistry'; import type { IchimokuCloudColors } from '../utils/ichimokuConfig'; +import ChartTimeFormatPicker from './ChartTimeFormatPicker'; import { TRADE_ALERT_SOUND_OPTIONS, normalizeTradeAlertSoundId, @@ -43,7 +44,6 @@ import { LINE_WIDTH_OPTIONS, } from '../utils/plotColorUtils'; import TimezonePicker from './TimezonePicker'; -import ChartTimeFormatPicker from './ChartTimeFormatPicker'; import AdminPasswordGate from './AdminPasswordGate'; import AdminSettingsPanel from './AdminSettingsPanel'; import { @@ -155,6 +155,8 @@ interface SettingsPageProps { onDisplayTimezoneChange?: (tz: string) => void; chartTimeFormat?: string; onChartTimeFormatChange?: (format: string) => void; + tradeAlertTimeFormat?: string; + onTradeAlertTimeFormatChange?: (format: string) => void; menuPermissions?: Record; verificationIssueNotify?: boolean; onVerificationIssueNotify?: (v: boolean) => void; @@ -550,18 +552,25 @@ const PaperPanel: React.FC = ({ const GeneralPanel: React.FC<{ displayTimezone: string; onDisplayTimezoneChange: (tz: string) => void; + chartTimeFormat?: string; + onChartTimeFormatChange?: (format: string) => void; + tradeAlertTimeFormat?: string; + onTradeAlertTimeFormatChange?: (format: string) => void; verificationIssueNotify?: boolean; onVerificationIssueNotify?: (v: boolean) => void; showVerificationNotify?: boolean; }> = ({ displayTimezone, onDisplayTimezoneChange, + chartTimeFormat = 'MM-dd HH:mm', + onChartTimeFormatChange, + tradeAlertTimeFormat = 'MM-dd HH:mm', + onTradeAlertTimeFormatChange, verificationIssueNotify = true, onVerificationIssueNotify, showVerificationNotify = true, }) => { - const [lang, setLang] = useState('ko'); - const [dateFormat, setDateFmt] = useState('YYYY-MM-DD'); + const [lang, setLang] = useState('ko'); const [startPage, setStart] = useState('chart'); const [animations, setAnim] = useState(true); const [tooltips, setTool] = useState(true); @@ -576,12 +585,23 @@ const GeneralPanel: React.FC<{ - - + + onChartTimeFormatChange?.(fmt)} + /> + + + onTradeAlertTimeFormatChange?.(fmt)} + /> = ({ onDisplayTimezoneChange, chartTimeFormat = 'MM-dd HH:mm', onChartTimeFormatChange, + tradeAlertTimeFormat = 'MM-dd HH:mm', + onTradeAlertTimeFormatChange, menuPermissions, verificationIssueNotify = true, onVerificationIssueNotify, @@ -1688,6 +1710,10 @@ const SettingsPage: React.FC = ({ {})} + chartTimeFormat={chartTimeFormat} + onChartTimeFormatChange={onChartTimeFormatChange} + tradeAlertTimeFormat={tradeAlertTimeFormat} + onTradeAlertTimeFormatChange={onTradeAlertTimeFormatChange} verificationIssueNotify={verificationIssueNotify} onVerificationIssueNotify={onVerificationIssueNotify} showVerificationNotify={menuPermissions == null || menuPermissions['verification-board'] === true} diff --git a/frontend/src/components/TradeAlertModal.tsx b/frontend/src/components/TradeAlertModal.tsx index da4298f..6dd38fb 100644 --- a/frontend/src/components/TradeAlertModal.tsx +++ b/frontend/src/components/TradeAlertModal.tsx @@ -2,7 +2,8 @@ import React, { useState } from 'react'; import { useDraggablePanel } from '../hooks/useDraggablePanel'; import { placePaperOrder, placeLiveOrder, loadLiveSummary } from '../utils/backendApi'; -import { formatUnixDateTime } from '../utils/timezone'; +import { formatSignalTime } from '../utils/tradeSignalDisplay'; +import { useTradeAlertTimeFormat } from '../utils/tradeAlertTimeFormat'; export interface TradeSignalInfo { market: string; @@ -30,9 +31,6 @@ interface Props { const fmt = (n: number): string => n > 0 ? n.toLocaleString('ko-KR', { maximumFractionDigits: 0 }) : '0'; -function fmtTime(unix: number): string { - return formatUnixDateTime(unix); -} function execLabel(type?: string, candleType?: string): string { const base = type === 'REALTIME_TICK' ? '실시간 틱' : '봉 마감'; @@ -61,6 +59,7 @@ export const TradeAlertModal: React.FC = ({ paperAutoTradeBudgetPct = 95, onOrderDone, }) => { + useTradeAlertTimeFormat(); const isBuy = signal.signalType === 'BUY'; const accentColor = isBuy ? '#3f7ef5' : '#ef5350'; const accentGlow = isBuy ? 'rgba(63,126,245,0.22)' : 'rgba(239,83,80,0.22)'; @@ -197,7 +196,7 @@ export const TradeAlertModal: React.FC = ({
- 발생 시각: {fmtTime(signal.candleTime)} + 발생 시각: {formatSignalTime(signal.candleTime)}
diff --git a/frontend/src/components/TradeSignalDetailBody.tsx b/frontend/src/components/TradeSignalDetailBody.tsx index d298c03..5c9bb2a 100644 --- a/frontend/src/components/TradeSignalDetailBody.tsx +++ b/frontend/src/components/TradeSignalDetailBody.tsx @@ -9,6 +9,7 @@ import { getSignalTypeKo, type TradeSignalDisplaySource, } from '../utils/tradeSignalDisplay'; +import { useTradeAlertTimeFormat } from '../utils/tradeAlertTimeFormat'; interface Props { item: TradeSignalDisplaySource; @@ -22,6 +23,7 @@ export const TradeSignalDetailBody: React.FC = ({ variant = 'compact', showHeadline = true, }) => { + useTradeAlertTimeFormat(); const isBuy = item.signalType === 'BUY'; const rows = buildSignalDetailRows(item); diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index 6d352fe..a1db2a7 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -1026,6 +1026,7 @@ const TradingChart: React.FC = ({ useEffect(() => { managerRef.current?.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat); + managerRef.current?.setChartTimeFormat(chartTimeFormat); }, [displayTimezone, timeframe, chartTimeFormat]); // 종목·타임프레임 변경 시 barsKey 캐시만 초기화 (reloadAll 은 bars effect 에서 1회만) diff --git a/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx b/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx index 8e21923..0f90135 100644 --- a/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx +++ b/frontend/src/components/tradeNotification/TradeNotificationListRow.tsx @@ -16,6 +16,7 @@ import { getMarketDisplayLine, getSignalTypeKo, } from '../../utils/tradeSignalDisplay'; +import { useTradeAlertTimeFormat } from '../../utils/tradeAlertTimeFormat'; import { buildStrategyChartIndicators, candleTypeToTimeframe, @@ -66,6 +67,7 @@ const TradeNotificationListRow: React.FC = ({ onDetail, onGoToChart, }) => { + useTradeAlertTimeFormat(); const isBuy = item.signalType === 'BUY'; const allDetailRows = buildSignalDetailRows(item); const metaRows = allDetailRows.filter( diff --git a/frontend/src/hooks/useAppSettings.ts b/frontend/src/hooks/useAppSettings.ts index a8760d4..de22ba9 100644 --- a/frontend/src/hooks/useAppSettings.ts +++ b/frontend/src/hooks/useAppSettings.ts @@ -41,6 +41,10 @@ import { normalizeChartTimeFormat, setChartTimeFormat, } from '../utils/chartTimeFormat'; +import { + DEFAULT_TRADE_ALERT_TIME_FORMAT, + setTradeAlertTimeFormat, +} from '../utils/tradeAlertTimeFormat'; import { DEFAULT_DISPLAY_TIMEZONE, normalizeTimezone, setDisplayTimezone } from '../utils/timezone'; import { clampVirtualTargetMax, @@ -176,6 +180,7 @@ export function resolveAppDefaults(s: AppSettingsDto) { fcmPushEnabled: s.fcmPushEnabled ?? false, displayTimezone: normalizeTimezone(s.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE), chartTimeFormat: normalizeChartTimeFormat(s.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT), + tradeAlertTimeFormat: normalizeChartTimeFormat(s.tradeAlertTimeFormat ?? DEFAULT_TRADE_ALERT_TIME_FORMAT), trendSearchSettings: resolveTrendSearchAppSettings( s.trendSearchSettings as Partial | null | undefined, ), @@ -201,6 +206,7 @@ export function useAppSettings(sessionKey = 0) { setSettings(_cache); setDisplayTimezone(normalizeTimezone(_cache.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE)); setChartTimeFormat(normalizeChartTimeFormat(_cache.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT)); + setTradeAlertTimeFormat(normalizeChartTimeFormat(_cache.tradeAlertTimeFormat ?? DEFAULT_TRADE_ALERT_TIME_FORMAT)); setIsLoaded(true); } else { setIsLoaded(false); @@ -210,6 +216,7 @@ export function useAppSettings(sessionKey = 0) { setSettings(_cache); setDisplayTimezone(normalizeTimezone(_cache.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE)); setChartTimeFormat(normalizeChartTimeFormat(_cache.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT)); + setTradeAlertTimeFormat(normalizeChartTimeFormat(_cache.tradeAlertTimeFormat ?? DEFAULT_TRADE_ALERT_TIME_FORMAT)); setIsLoaded(true); } else { setIsLoaded(false); @@ -220,6 +227,7 @@ export function useAppSettings(sessionKey = 0) { setSettings(data); setDisplayTimezone(normalizeTimezone(data.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE)); setChartTimeFormat(normalizeChartTimeFormat(data.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT)); + setTradeAlertTimeFormat(normalizeChartTimeFormat(data.tradeAlertTimeFormat ?? DEFAULT_TRADE_ALERT_TIME_FORMAT)); setIsLoaded(true); } }).catch(err => { @@ -250,6 +258,9 @@ export function useAppSettings(sessionKey = 0) { if (patch.chartTimeFormat != null) { setChartTimeFormat(normalizeChartTimeFormat(patch.chartTimeFormat)); } + if (patch.tradeAlertTimeFormat != null) { + setTradeAlertTimeFormat(normalizeChartTimeFormat(patch.tradeAlertTimeFormat)); + } saveAppSettings(patch).then(updated => { if (updated) { _cache = { ...(_cache ?? {}), ...updated }; diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index f53f46a..dbef92b 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -503,6 +503,12 @@ export class ChartManager { this._applyDisplayTimezoneOptions(); } + /** 시간축·크로스헤어·캔들 pane 시간 라벨 포맷만 갱신 */ + setChartTimeFormat(chartTimeFormat: string): void { + this.chartTimeFormat = normalizeChartTimeFormat(chartTimeFormat); + this._applyDisplayTimezoneOptions(); + } + // ─── Theme ────────────────────────────────────────────────────────────── setTheme(theme: Theme): void { this.currentTheme = theme; diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index 2a5e188..9af5dd7 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -446,6 +446,8 @@ export interface AppSettingsDto { displayTimezone?: string; /** 차트 하단 시간축 포맷 (예: yyyy-MM-dd HH:mm) */ chartTimeFormat?: string; + /** 매매 시그널 알림 팝업 시간 포맷 */ + tradeAlertTimeFormat?: string; /** 추세검색 기본 설정 */ trendSearchSettings?: import('./trendSearchAppSettings').TrendSearchAppSettings | null; /** UI 설정 통합 (편집기·팔레트·가상투자 목록·패널 크기 등) */ diff --git a/frontend/src/utils/chartTimeFormat.ts b/frontend/src/utils/chartTimeFormat.ts index 805f89f..b7b95f1 100644 --- a/frontend/src/utils/chartTimeFormat.ts +++ b/frontend/src/utils/chartTimeFormat.ts @@ -13,15 +13,25 @@ export interface ChartTimeFormatPreset { label: string; } -/** 통상적인 차트 시간축 프리셋 */ +/** 통상적인 차트 시간축 프리셋 (일반설정·차트 설정 공통) */ export const CHART_TIME_FORMAT_PRESETS: ChartTimeFormatPreset[] = [ { id: 'yyyy-MM-dd HH:mm:ss', label: 'yyyy-MM-dd HH:mm:ss' }, { id: 'yyyy-MM-dd HH:mm', label: 'yyyy-MM-dd HH:mm' }, { id: 'yyyy-MM-dd', label: 'yyyy-MM-dd' }, + { id: 'yyyy/MM/dd HH:mm:ss', label: 'yyyy/MM/dd HH:mm:ss' }, + { id: 'yyyy/MM/dd HH:mm', label: 'yyyy/MM/dd HH:mm' }, { id: 'MM-dd HH:mm:ss', label: 'MM-dd HH:mm:ss' }, { id: 'MM-dd HH:mm', label: 'MM-dd HH:mm' }, + { id: 'MM/dd/yyyy HH:mm:ss', label: 'MM/dd/yyyy HH:mm:ss' }, + { id: 'MM/dd/yyyy HH:mm', label: 'MM/dd/yyyy HH:mm' }, + { id: 'dd/MM/yyyy HH:mm:ss', label: 'dd/MM/yyyy HH:mm:ss' }, + { id: 'dd/MM/yyyy HH:mm', label: 'dd/MM/yyyy HH:mm' }, + { id: 'dd/MM HH:mm:ss', label: 'dd/MM HH:mm:ss' }, { id: 'dd/MM HH:mm', label: 'dd/MM HH:mm' }, + { id: 'dd-MM HH:mm:ss', label: 'dd-MM HH:mm:ss' }, { id: 'dd-MM HH:mm', label: 'dd-MM HH:mm' }, + { id: 'dd.MM.yyyy HH:mm:ss', label: 'dd.MM.yyyy HH:mm:ss' }, + { id: 'dd.MM.yyyy HH:mm', label: 'dd.MM.yyyy HH:mm' }, { id: 'HH:mm:ss', label: 'HH:mm:ss' }, { id: 'HH:mm', label: 'HH:mm' }, ]; diff --git a/frontend/src/utils/tradeAlertTimeFormat.ts b/frontend/src/utils/tradeAlertTimeFormat.ts new file mode 100644 index 0000000..7b421ad --- /dev/null +++ b/frontend/src/utils/tradeAlertTimeFormat.ts @@ -0,0 +1,37 @@ +/** + * 매매 시그널 알림 팝업·상세의 시간 표시 포맷 (일반설정, DB 동기화) + */ +import { useSyncExternalStore } from 'react'; +import { + DEFAULT_CHART_TIME_FORMAT, + normalizeChartTimeFormat, +} from './chartTimeFormat'; + +export const DEFAULT_TRADE_ALERT_TIME_FORMAT = DEFAULT_CHART_TIME_FORMAT; + +let _format = DEFAULT_TRADE_ALERT_TIME_FORMAT; +const _listeners = new Set<() => void>(); + +export function getTradeAlertTimeFormat(): string { + return _format; +} + +export function setTradeAlertTimeFormat(format: string): void { + const next = normalizeChartTimeFormat(format); + if (_format === next) return; + _format = next; + _listeners.forEach(l => l()); +} + +export function subscribeTradeAlertTimeFormat(cb: () => void): () => void { + _listeners.add(cb); + return () => _listeners.delete(cb); +} + +export function useTradeAlertTimeFormat(): string { + return useSyncExternalStore( + subscribeTradeAlertTimeFormat, + getTradeAlertTimeFormat, + () => DEFAULT_TRADE_ALERT_TIME_FORMAT, + ); +} diff --git a/frontend/src/utils/tradeSignalDisplay.ts b/frontend/src/utils/tradeSignalDisplay.ts index 7c8fb3d..c655564 100644 --- a/frontend/src/utils/tradeSignalDisplay.ts +++ b/frontend/src/utils/tradeSignalDisplay.ts @@ -1,8 +1,10 @@ /** * 매매 시그널 알림 표시용 포맷·라벨 */ +import { formatUnixWithChartPattern, splitChartTimePattern } from './chartTimeFormat'; import { getKoreanName } from './marketNameCache'; -import { formatUnixDateTime, formatUnixClock, getDisplayTimezone } from './timezone'; +import { getTradeAlertTimeFormat } from './tradeAlertTimeFormat'; +import { getDisplayTimezone } from './timezone'; export interface TradeSignalDisplaySource { market: string; @@ -31,8 +33,12 @@ export function formatSignalPrice(price: number): string { export function formatSignalTime(unixSec: number, withDate = true): string { if (!unixSec) return '—'; const tz = getDisplayTimezone(); - if (!withDate) return formatUnixClock(unixSec, tz, true); - return formatUnixDateTime(unixSec, tz); + const fmt = getTradeAlertTimeFormat(); + if (!withDate) { + const { time } = splitChartTimePattern(fmt); + return formatUnixWithChartPattern(unixSec, time || 'HH:mm', tz); + } + return formatUnixWithChartPattern(unixSec, fmt, tz); } export function getSignalTypeKo(type: 'BUY' | 'SELL'): string {