날짜포멧 수정
This commit is contained in:
@@ -72,6 +72,11 @@ public class GcAppSettings {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String chartTimeFormat = "MM-dd HH:mm";
|
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 구조) */
|
/** 캔들 색상 JSON (frontend MainChartStyle 구조) */
|
||||||
@Column(name = "main_chart_style_json", columnDefinition = "JSON")
|
@Column(name = "main_chart_style_json", columnDefinition = "JSON")
|
||||||
@JdbcTypeCode(SqlTypes.JSON)
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ public class AppSettingsService {
|
|||||||
if (d.containsKey("defaultLayoutId")) s.setDefaultLayoutId((String) d.get("defaultLayoutId"));
|
if (d.containsKey("defaultLayoutId")) s.setDefaultLayoutId((String) d.get("defaultLayoutId"));
|
||||||
if (d.containsKey("displayTimezone")) s.setDisplayTimezone((String) d.get("displayTimezone"));
|
if (d.containsKey("displayTimezone")) s.setDisplayTimezone((String) d.get("displayTimezone"));
|
||||||
if (d.containsKey("chartTimeFormat")) s.setChartTimeFormat(normalizeChartTimeFormat((String) d.get("chartTimeFormat")));
|
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("mainChartStyle")) s.setMainChartStyleJson(toJson(d.get("mainChartStyle")));
|
||||||
if (d.containsKey("syncOptions")) s.setSyncOptionsJson(toJson(d.get("syncOptions")));
|
if (d.containsKey("syncOptions")) s.setSyncOptionsJson(toJson(d.get("syncOptions")));
|
||||||
if (d.containsKey("btAutoPopup")) s.setBtAutoPopup(
|
if (d.containsKey("btAutoPopup")) s.setBtAutoPopup(
|
||||||
@@ -190,6 +191,7 @@ public class AppSettingsService {
|
|||||||
m.put("defaultLayoutId", s.getDefaultLayoutId() != null ? s.getDefaultLayoutId() : "1");
|
m.put("defaultLayoutId", s.getDefaultLayoutId() != null ? s.getDefaultLayoutId() : "1");
|
||||||
m.put("displayTimezone", s.getDisplayTimezone() != null ? s.getDisplayTimezone() : "Asia/Seoul");
|
m.put("displayTimezone", s.getDisplayTimezone() != null ? s.getDisplayTimezone() : "Asia/Seoul");
|
||||||
m.put("chartTimeFormat", s.getChartTimeFormat() != null ? s.getChartTimeFormat() : "MM-dd HH:mm");
|
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("mainChartStyle", parseJson(s.getMainChartStyleJson()));
|
||||||
m.put("syncOptions", parseJson(s.getSyncOptionsJson()));
|
m.put("syncOptions", parseJson(s.getSyncOptionsJson()));
|
||||||
m.put("btAutoPopup", s.getBtAutoPopup() != null ? s.getBtAutoPopup() : true);
|
m.put("btAutoPopup", s.getBtAutoPopup() != null ? s.getBtAutoPopup() : true);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -9438,7 +9438,8 @@ html.theme-blue {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
max-width: 360px;
|
max-width: 420px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.stg-chart-time-format-custom {
|
.stg-chart-time-format-custom {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
+24
-5
@@ -21,6 +21,10 @@ import {
|
|||||||
setChartTimeFormat,
|
setChartTimeFormat,
|
||||||
useChartTimeFormat,
|
useChartTimeFormat,
|
||||||
} from './utils/chartTimeFormat';
|
} from './utils/chartTimeFormat';
|
||||||
|
import {
|
||||||
|
setTradeAlertTimeFormat,
|
||||||
|
useTradeAlertTimeFormat,
|
||||||
|
} from './utils/tradeAlertTimeFormat';
|
||||||
import { normalizeTimezone, setDisplayTimezone, useDisplayTimezone } from './utils/timezone';
|
import { normalizeTimezone, setDisplayTimezone, useDisplayTimezone } from './utils/timezone';
|
||||||
import { calcStats, type PriceStats } from './utils/calculations';
|
import { calcStats, type PriceStats } from './utils/calculations';
|
||||||
import { getKoreanName } from './utils/marketNameCache';
|
import { getKoreanName } from './utils/marketNameCache';
|
||||||
@@ -192,6 +196,7 @@ function App() {
|
|||||||
const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey);
|
const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey);
|
||||||
const displayTimezone = useDisplayTimezone();
|
const displayTimezone = useDisplayTimezone();
|
||||||
const chartTimeFormat = useChartTimeFormat();
|
const chartTimeFormat = useChartTimeFormat();
|
||||||
|
const tradeAlertTimeFormat = useTradeAlertTimeFormat();
|
||||||
|
|
||||||
// DB 로드 전에는 localStorage 기본값, 로드 후 DB 값으로 오버라이드됨
|
// DB 로드 전에는 localStorage 기본값, 로드 후 DB 값으로 오버라이드됨
|
||||||
const [symbol, setSymbol] = useState(initial.symbol);
|
const [symbol, setSymbol] = useState(initial.symbol);
|
||||||
@@ -266,13 +271,24 @@ function App() {
|
|||||||
managerRef.current?.setDisplayTimezone(next, tf, chartTimeFormat);
|
managerRef.current?.setDisplayTimezone(next, tf, chartTimeFormat);
|
||||||
}, [saveAppDef, layoutDef.count, activeSlotTf, timeframe, 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 handleChartTimeFormatChange = useCallback((format: string) => {
|
||||||
const next = normalizeChartTimeFormat(format);
|
const next = normalizeChartTimeFormat(format);
|
||||||
setChartTimeFormat(next);
|
setChartTimeFormat(next);
|
||||||
saveAppDef({ chartTimeFormat: next });
|
saveAppDef({ chartTimeFormat: next });
|
||||||
const tf = layoutDef.count > 1 ? activeSlotTf : timeframe;
|
applyChartTimeFormatToManagers(next);
|
||||||
managerRef.current?.setDisplayTimezone(displayTimezone, tf, next);
|
}, [saveAppDef, applyChartTimeFormatToManagers]);
|
||||||
}, [saveAppDef, layoutDef.count, activeSlotTf, timeframe, displayTimezone]);
|
|
||||||
|
const handleTradeAlertTimeFormatChange = useCallback((format: string) => {
|
||||||
|
const next = normalizeChartTimeFormat(format);
|
||||||
|
setTradeAlertTimeFormat(next);
|
||||||
|
saveAppDef({ tradeAlertTimeFormat: next });
|
||||||
|
}, [saveAppDef]);
|
||||||
|
|
||||||
const chartSymbol = layoutDef.count > 1 ? activeSlotSymbol : symbol;
|
const chartSymbol = layoutDef.count > 1 ? activeSlotSymbol : symbol;
|
||||||
const { orderbook, wsStatus: obWsStatus, spread } = useUpbitOrderbook(chartSymbol);
|
const { orderbook, wsStatus: obWsStatus, spread } = useUpbitOrderbook(chartSymbol);
|
||||||
@@ -975,8 +991,9 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!appSettingsLoaded) return;
|
if (!appSettingsLoaded) return;
|
||||||
const tf = layoutDef.count > 1 ? activeSlotTf : timeframe;
|
const tf = layoutDef.count > 1 ? activeSlotTf : timeframe;
|
||||||
managerRef.current?.setDisplayTimezone(displayTimezone, tf, chartTimeFormat);
|
managerRef.current?.setChartTimeFormat(chartTimeFormat);
|
||||||
}, [appSettingsLoaded, displayTimezone, chartTimeFormat, timeframe, activeSlotTf, layoutDef.count]);
|
slotRefs.current.forEach(slot => slot?.setChartTimeFormat?.(chartTimeFormat, tf));
|
||||||
|
}, [appSettingsLoaded, chartTimeFormat, timeframe, activeSlotTf, layoutDef.count]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!appSettingsLoaded) return;
|
if (!appSettingsLoaded) return;
|
||||||
@@ -1910,6 +1927,8 @@ function App() {
|
|||||||
onDisplayTimezoneChange={handleTimezoneChange}
|
onDisplayTimezoneChange={handleTimezoneChange}
|
||||||
chartTimeFormat={chartTimeFormat}
|
chartTimeFormat={chartTimeFormat}
|
||||||
onChartTimeFormatChange={handleChartTimeFormatChange}
|
onChartTimeFormatChange={handleChartTimeFormatChange}
|
||||||
|
tradeAlertTimeFormat={tradeAlertTimeFormat}
|
||||||
|
onTradeAlertTimeFormatChange={handleTradeAlertTimeFormatChange}
|
||||||
verificationIssueNotify={appDefaults.verificationIssueNotify}
|
verificationIssueNotify={appDefaults.verificationIssueNotify}
|
||||||
onVerificationIssueNotify={v => saveAppDef({ verificationIssueNotify: v })}
|
onVerificationIssueNotify={v => saveAppDef({ verificationIssueNotify: v })}
|
||||||
onFcmTest={async () => {
|
onFcmTest={async () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import type { ChartManager } from '../utils/ChartManager';
|
import type { ChartManager } from '../utils/ChartManager';
|
||||||
|
import { subscribeChartTimeFormat } from '../utils/chartTimeFormat';
|
||||||
|
|
||||||
const AXIS_HEIGHT = 22;
|
const AXIS_HEIGHT = 22;
|
||||||
|
|
||||||
@@ -26,12 +27,14 @@ const CandlePaneTimeAxis: React.FC<CandlePaneTimeAxisProps> = ({ manager, contai
|
|||||||
|
|
||||||
const unsubLayout = manager.subscribePaneLayout(sync);
|
const unsubLayout = manager.subscribePaneLayout(sync);
|
||||||
const unsubRange = manager.subscribeViewport(sync);
|
const unsubRange = manager.subscribeViewport(sync);
|
||||||
|
const unsubFormat = subscribeChartTimeFormat(sync);
|
||||||
|
|
||||||
const timers = [0, 50, 150, 400].map(ms => window.setTimeout(sync, ms));
|
const timers = [0, 50, 150, 400].map(ms => window.setTimeout(sync, ms));
|
||||||
return () => {
|
return () => {
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
unsubLayout();
|
unsubLayout();
|
||||||
unsubRange();
|
unsubRange();
|
||||||
|
unsubFormat();
|
||||||
timers.forEach(clearTimeout);
|
timers.forEach(clearTimeout);
|
||||||
};
|
};
|
||||||
}, [manager, containerEl, sync]);
|
}, [manager, containerEl, sync]);
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ export interface ChartSlotHandle {
|
|||||||
/** 거래량 pane on/off */
|
/** 거래량 pane on/off */
|
||||||
setVolumeVisible: (visible: boolean) => void;
|
setVolumeVisible: (visible: boolean) => void;
|
||||||
setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => void;
|
setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => void;
|
||||||
|
/** 차트 시간축·크로스헤어 날짜 형식 */
|
||||||
|
setChartTimeFormat: (format: string, timeframe?: Timeframe) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Props ─────────────────────────────────────────────────────────────────
|
// ── Props ─────────────────────────────────────────────────────────────────
|
||||||
@@ -323,7 +325,11 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => {
|
setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => {
|
||||||
managerRef.current?.setPaneSeparatorOptions(opts);
|
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 크로스헤어 모드 즉시 반영
|
// 자석모드 변경 → ChartManager 크로스헤어 모드 즉시 반영
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const ChartTimeFormatPicker: React.FC<ChartTimeFormatPickerProps> = ({ value, on
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="stg-input stg-chart-time-format-custom"
|
className="stg-input stg-chart-time-format-custom"
|
||||||
placeholder="예: yyyy-MM-dd HH:mm"
|
placeholder="예: yyyy-MM-dd HH:mm:ss, dd/MM/yyyy HH:mm"
|
||||||
value={customText}
|
value={customText}
|
||||||
onChange={e => setCustomText(e.target.value)}
|
onChange={e => setCustomText(e.target.value)}
|
||||||
onBlur={commitCustom}
|
onBlur={commitCustom}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import IndicatorMainDefaultsPanel, {
|
|||||||
import type { IndicatorConfig } from '../types';
|
import type { IndicatorConfig } from '../types';
|
||||||
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
import type { PlotDef, HLineDef } from '../utils/indicatorRegistry';
|
||||||
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
|
import type { IchimokuCloudColors } from '../utils/ichimokuConfig';
|
||||||
|
import ChartTimeFormatPicker from './ChartTimeFormatPicker';
|
||||||
import {
|
import {
|
||||||
TRADE_ALERT_SOUND_OPTIONS,
|
TRADE_ALERT_SOUND_OPTIONS,
|
||||||
normalizeTradeAlertSoundId,
|
normalizeTradeAlertSoundId,
|
||||||
@@ -43,7 +44,6 @@ import {
|
|||||||
LINE_WIDTH_OPTIONS,
|
LINE_WIDTH_OPTIONS,
|
||||||
} from '../utils/plotColorUtils';
|
} from '../utils/plotColorUtils';
|
||||||
import TimezonePicker from './TimezonePicker';
|
import TimezonePicker from './TimezonePicker';
|
||||||
import ChartTimeFormatPicker from './ChartTimeFormatPicker';
|
|
||||||
import AdminPasswordGate from './AdminPasswordGate';
|
import AdminPasswordGate from './AdminPasswordGate';
|
||||||
import AdminSettingsPanel from './AdminSettingsPanel';
|
import AdminSettingsPanel from './AdminSettingsPanel';
|
||||||
import {
|
import {
|
||||||
@@ -155,6 +155,8 @@ interface SettingsPageProps {
|
|||||||
onDisplayTimezoneChange?: (tz: string) => void;
|
onDisplayTimezoneChange?: (tz: string) => void;
|
||||||
chartTimeFormat?: string;
|
chartTimeFormat?: string;
|
||||||
onChartTimeFormatChange?: (format: string) => void;
|
onChartTimeFormatChange?: (format: string) => void;
|
||||||
|
tradeAlertTimeFormat?: string;
|
||||||
|
onTradeAlertTimeFormatChange?: (format: string) => void;
|
||||||
menuPermissions?: Record<string, boolean>;
|
menuPermissions?: Record<string, boolean>;
|
||||||
verificationIssueNotify?: boolean;
|
verificationIssueNotify?: boolean;
|
||||||
onVerificationIssueNotify?: (v: boolean) => void;
|
onVerificationIssueNotify?: (v: boolean) => void;
|
||||||
@@ -550,18 +552,25 @@ const PaperPanel: React.FC<PaperPanelProps> = ({
|
|||||||
const GeneralPanel: React.FC<{
|
const GeneralPanel: React.FC<{
|
||||||
displayTimezone: string;
|
displayTimezone: string;
|
||||||
onDisplayTimezoneChange: (tz: string) => void;
|
onDisplayTimezoneChange: (tz: string) => void;
|
||||||
|
chartTimeFormat?: string;
|
||||||
|
onChartTimeFormatChange?: (format: string) => void;
|
||||||
|
tradeAlertTimeFormat?: string;
|
||||||
|
onTradeAlertTimeFormatChange?: (format: string) => void;
|
||||||
verificationIssueNotify?: boolean;
|
verificationIssueNotify?: boolean;
|
||||||
onVerificationIssueNotify?: (v: boolean) => void;
|
onVerificationIssueNotify?: (v: boolean) => void;
|
||||||
showVerificationNotify?: boolean;
|
showVerificationNotify?: boolean;
|
||||||
}> = ({
|
}> = ({
|
||||||
displayTimezone,
|
displayTimezone,
|
||||||
onDisplayTimezoneChange,
|
onDisplayTimezoneChange,
|
||||||
|
chartTimeFormat = 'MM-dd HH:mm',
|
||||||
|
onChartTimeFormatChange,
|
||||||
|
tradeAlertTimeFormat = 'MM-dd HH:mm',
|
||||||
|
onTradeAlertTimeFormatChange,
|
||||||
verificationIssueNotify = true,
|
verificationIssueNotify = true,
|
||||||
onVerificationIssueNotify,
|
onVerificationIssueNotify,
|
||||||
showVerificationNotify = true,
|
showVerificationNotify = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [lang, setLang] = useState('ko');
|
const [lang, setLang] = useState('ko');
|
||||||
const [dateFormat, setDateFmt] = useState('YYYY-MM-DD');
|
|
||||||
const [startPage, setStart] = useState('chart');
|
const [startPage, setStart] = useState('chart');
|
||||||
const [animations, setAnim] = useState(true);
|
const [animations, setAnim] = useState(true);
|
||||||
const [tooltips, setTool] = useState(true);
|
const [tooltips, setTool] = useState(true);
|
||||||
@@ -576,12 +585,23 @@ const GeneralPanel: React.FC<{
|
|||||||
<option value="ja">日本語</option>
|
<option value="ja">日本語</option>
|
||||||
</select>
|
</select>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow label="날짜 형식" desc="차트 및 UI에서 날짜를 표시하는 방식입니다.">
|
<SettingRow
|
||||||
<select className="stg-select" value={dateFormat} onChange={e => setDateFmt(e.target.value)}>
|
label="날짜·시간 형식"
|
||||||
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
|
desc="캔들·보조지표 차트 하단 시간축·크로스헤어에 적용됩니다. 프리셋 선택 또는 직접 입력(토큰: yyyy, yy, MM, dd, HH, mm, ss)이 가능합니다."
|
||||||
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
|
>
|
||||||
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
|
<ChartTimeFormatPicker
|
||||||
</select>
|
value={chartTimeFormat}
|
||||||
|
onChange={fmt => onChartTimeFormatChange?.(fmt)}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
label="매매 시그널 알림 시간 형식"
|
||||||
|
desc="매매 시그널 알림 팝업·상세에 표시되는 캔들·수신 시각 형식입니다. 프리셋 선택 또는 직접 입력이 가능합니다."
|
||||||
|
>
|
||||||
|
<ChartTimeFormatPicker
|
||||||
|
value={tradeAlertTimeFormat}
|
||||||
|
onChange={fmt => onTradeAlertTimeFormatChange?.(fmt)}
|
||||||
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow label="시간대" desc="차트·하단 시계·알림에 사용되는 표시 시간대입니다.">
|
<SettingRow label="시간대" desc="차트·하단 시계·알림에 사용되는 표시 시간대입니다.">
|
||||||
<TimezonePicker
|
<TimezonePicker
|
||||||
@@ -1598,6 +1618,8 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
onDisplayTimezoneChange,
|
onDisplayTimezoneChange,
|
||||||
chartTimeFormat = 'MM-dd HH:mm',
|
chartTimeFormat = 'MM-dd HH:mm',
|
||||||
onChartTimeFormatChange,
|
onChartTimeFormatChange,
|
||||||
|
tradeAlertTimeFormat = 'MM-dd HH:mm',
|
||||||
|
onTradeAlertTimeFormatChange,
|
||||||
menuPermissions,
|
menuPermissions,
|
||||||
verificationIssueNotify = true,
|
verificationIssueNotify = true,
|
||||||
onVerificationIssueNotify,
|
onVerificationIssueNotify,
|
||||||
@@ -1688,6 +1710,10 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
|||||||
<GeneralPanel
|
<GeneralPanel
|
||||||
displayTimezone={displayTimezone}
|
displayTimezone={displayTimezone}
|
||||||
onDisplayTimezoneChange={onDisplayTimezoneChange ?? (() => {})}
|
onDisplayTimezoneChange={onDisplayTimezoneChange ?? (() => {})}
|
||||||
|
chartTimeFormat={chartTimeFormat}
|
||||||
|
onChartTimeFormatChange={onChartTimeFormatChange}
|
||||||
|
tradeAlertTimeFormat={tradeAlertTimeFormat}
|
||||||
|
onTradeAlertTimeFormatChange={onTradeAlertTimeFormatChange}
|
||||||
verificationIssueNotify={verificationIssueNotify}
|
verificationIssueNotify={verificationIssueNotify}
|
||||||
onVerificationIssueNotify={onVerificationIssueNotify}
|
onVerificationIssueNotify={onVerificationIssueNotify}
|
||||||
showVerificationNotify={menuPermissions == null || menuPermissions['verification-board'] === true}
|
showVerificationNotify={menuPermissions == null || menuPermissions['verification-board'] === true}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
||||||
import { placePaperOrder, placeLiveOrder, loadLiveSummary } from '../utils/backendApi';
|
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 {
|
export interface TradeSignalInfo {
|
||||||
market: string;
|
market: string;
|
||||||
@@ -30,9 +31,6 @@ interface Props {
|
|||||||
const fmt = (n: number): string =>
|
const fmt = (n: number): string =>
|
||||||
n > 0 ? n.toLocaleString('ko-KR', { maximumFractionDigits: 0 }) : '0';
|
n > 0 ? n.toLocaleString('ko-KR', { maximumFractionDigits: 0 }) : '0';
|
||||||
|
|
||||||
function fmtTime(unix: number): string {
|
|
||||||
return formatUnixDateTime(unix);
|
|
||||||
}
|
|
||||||
|
|
||||||
function execLabel(type?: string, candleType?: string): string {
|
function execLabel(type?: string, candleType?: string): string {
|
||||||
const base = type === 'REALTIME_TICK' ? '실시간 틱' : '봉 마감';
|
const base = type === 'REALTIME_TICK' ? '실시간 틱' : '봉 마감';
|
||||||
@@ -61,6 +59,7 @@ export const TradeAlertModal: React.FC<Props> = ({
|
|||||||
paperAutoTradeBudgetPct = 95,
|
paperAutoTradeBudgetPct = 95,
|
||||||
onOrderDone,
|
onOrderDone,
|
||||||
}) => {
|
}) => {
|
||||||
|
useTradeAlertTimeFormat();
|
||||||
const isBuy = signal.signalType === 'BUY';
|
const isBuy = signal.signalType === 'BUY';
|
||||||
const accentColor = isBuy ? '#3f7ef5' : '#ef5350';
|
const accentColor = isBuy ? '#3f7ef5' : '#ef5350';
|
||||||
const accentGlow = isBuy ? 'rgba(63,126,245,0.22)' : 'rgba(239,83,80,0.22)';
|
const accentGlow = isBuy ? 'rgba(63,126,245,0.22)' : 'rgba(239,83,80,0.22)';
|
||||||
@@ -197,7 +196,7 @@ export const TradeAlertModal: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="tam-price-time">
|
<div className="tam-price-time">
|
||||||
발생 시각: {fmtTime(signal.candleTime)}
|
발생 시각: {formatSignalTime(signal.candleTime)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
getSignalTypeKo,
|
getSignalTypeKo,
|
||||||
type TradeSignalDisplaySource,
|
type TradeSignalDisplaySource,
|
||||||
} from '../utils/tradeSignalDisplay';
|
} from '../utils/tradeSignalDisplay';
|
||||||
|
import { useTradeAlertTimeFormat } from '../utils/tradeAlertTimeFormat';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: TradeSignalDisplaySource;
|
item: TradeSignalDisplaySource;
|
||||||
@@ -22,6 +23,7 @@ export const TradeSignalDetailBody: React.FC<Props> = ({
|
|||||||
variant = 'compact',
|
variant = 'compact',
|
||||||
showHeadline = true,
|
showHeadline = true,
|
||||||
}) => {
|
}) => {
|
||||||
|
useTradeAlertTimeFormat();
|
||||||
const isBuy = item.signalType === 'BUY';
|
const isBuy = item.signalType === 'BUY';
|
||||||
const rows = buildSignalDetailRows(item);
|
const rows = buildSignalDetailRows(item);
|
||||||
|
|
||||||
|
|||||||
@@ -1026,6 +1026,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
managerRef.current?.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat);
|
managerRef.current?.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat);
|
||||||
|
managerRef.current?.setChartTimeFormat(chartTimeFormat);
|
||||||
}, [displayTimezone, timeframe, chartTimeFormat]);
|
}, [displayTimezone, timeframe, chartTimeFormat]);
|
||||||
|
|
||||||
// 종목·타임프레임 변경 시 barsKey 캐시만 초기화 (reloadAll 은 bars effect 에서 1회만)
|
// 종목·타임프레임 변경 시 barsKey 캐시만 초기화 (reloadAll 은 bars effect 에서 1회만)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
getMarketDisplayLine,
|
getMarketDisplayLine,
|
||||||
getSignalTypeKo,
|
getSignalTypeKo,
|
||||||
} from '../../utils/tradeSignalDisplay';
|
} from '../../utils/tradeSignalDisplay';
|
||||||
|
import { useTradeAlertTimeFormat } from '../../utils/tradeAlertTimeFormat';
|
||||||
import {
|
import {
|
||||||
buildStrategyChartIndicators,
|
buildStrategyChartIndicators,
|
||||||
candleTypeToTimeframe,
|
candleTypeToTimeframe,
|
||||||
@@ -66,6 +67,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
onDetail,
|
onDetail,
|
||||||
onGoToChart,
|
onGoToChart,
|
||||||
}) => {
|
}) => {
|
||||||
|
useTradeAlertTimeFormat();
|
||||||
const isBuy = item.signalType === 'BUY';
|
const isBuy = item.signalType === 'BUY';
|
||||||
const allDetailRows = buildSignalDetailRows(item);
|
const allDetailRows = buildSignalDetailRows(item);
|
||||||
const metaRows = allDetailRows.filter(
|
const metaRows = allDetailRows.filter(
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ import {
|
|||||||
normalizeChartTimeFormat,
|
normalizeChartTimeFormat,
|
||||||
setChartTimeFormat,
|
setChartTimeFormat,
|
||||||
} from '../utils/chartTimeFormat';
|
} from '../utils/chartTimeFormat';
|
||||||
|
import {
|
||||||
|
DEFAULT_TRADE_ALERT_TIME_FORMAT,
|
||||||
|
setTradeAlertTimeFormat,
|
||||||
|
} from '../utils/tradeAlertTimeFormat';
|
||||||
import { DEFAULT_DISPLAY_TIMEZONE, normalizeTimezone, setDisplayTimezone } from '../utils/timezone';
|
import { DEFAULT_DISPLAY_TIMEZONE, normalizeTimezone, setDisplayTimezone } from '../utils/timezone';
|
||||||
import {
|
import {
|
||||||
clampVirtualTargetMax,
|
clampVirtualTargetMax,
|
||||||
@@ -176,6 +180,7 @@ export function resolveAppDefaults(s: AppSettingsDto) {
|
|||||||
fcmPushEnabled: s.fcmPushEnabled ?? false,
|
fcmPushEnabled: s.fcmPushEnabled ?? false,
|
||||||
displayTimezone: normalizeTimezone(s.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE),
|
displayTimezone: normalizeTimezone(s.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE),
|
||||||
chartTimeFormat: normalizeChartTimeFormat(s.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT),
|
chartTimeFormat: normalizeChartTimeFormat(s.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT),
|
||||||
|
tradeAlertTimeFormat: normalizeChartTimeFormat(s.tradeAlertTimeFormat ?? DEFAULT_TRADE_ALERT_TIME_FORMAT),
|
||||||
trendSearchSettings: resolveTrendSearchAppSettings(
|
trendSearchSettings: resolveTrendSearchAppSettings(
|
||||||
s.trendSearchSettings as Partial<TrendSearchAppSettings> | null | undefined,
|
s.trendSearchSettings as Partial<TrendSearchAppSettings> | null | undefined,
|
||||||
),
|
),
|
||||||
@@ -201,6 +206,7 @@ export function useAppSettings(sessionKey = 0) {
|
|||||||
setSettings(_cache);
|
setSettings(_cache);
|
||||||
setDisplayTimezone(normalizeTimezone(_cache.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
|
setDisplayTimezone(normalizeTimezone(_cache.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
|
||||||
setChartTimeFormat(normalizeChartTimeFormat(_cache.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
|
setChartTimeFormat(normalizeChartTimeFormat(_cache.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
|
||||||
|
setTradeAlertTimeFormat(normalizeChartTimeFormat(_cache.tradeAlertTimeFormat ?? DEFAULT_TRADE_ALERT_TIME_FORMAT));
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
} else {
|
} else {
|
||||||
setIsLoaded(false);
|
setIsLoaded(false);
|
||||||
@@ -210,6 +216,7 @@ export function useAppSettings(sessionKey = 0) {
|
|||||||
setSettings(_cache);
|
setSettings(_cache);
|
||||||
setDisplayTimezone(normalizeTimezone(_cache.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
|
setDisplayTimezone(normalizeTimezone(_cache.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
|
||||||
setChartTimeFormat(normalizeChartTimeFormat(_cache.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
|
setChartTimeFormat(normalizeChartTimeFormat(_cache.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
|
||||||
|
setTradeAlertTimeFormat(normalizeChartTimeFormat(_cache.tradeAlertTimeFormat ?? DEFAULT_TRADE_ALERT_TIME_FORMAT));
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
} else {
|
} else {
|
||||||
setIsLoaded(false);
|
setIsLoaded(false);
|
||||||
@@ -220,6 +227,7 @@ export function useAppSettings(sessionKey = 0) {
|
|||||||
setSettings(data);
|
setSettings(data);
|
||||||
setDisplayTimezone(normalizeTimezone(data.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
|
setDisplayTimezone(normalizeTimezone(data.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
|
||||||
setChartTimeFormat(normalizeChartTimeFormat(data.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
|
setChartTimeFormat(normalizeChartTimeFormat(data.chartTimeFormat ?? DEFAULT_CHART_TIME_FORMAT));
|
||||||
|
setTradeAlertTimeFormat(normalizeChartTimeFormat(data.tradeAlertTimeFormat ?? DEFAULT_TRADE_ALERT_TIME_FORMAT));
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
@@ -250,6 +258,9 @@ export function useAppSettings(sessionKey = 0) {
|
|||||||
if (patch.chartTimeFormat != null) {
|
if (patch.chartTimeFormat != null) {
|
||||||
setChartTimeFormat(normalizeChartTimeFormat(patch.chartTimeFormat));
|
setChartTimeFormat(normalizeChartTimeFormat(patch.chartTimeFormat));
|
||||||
}
|
}
|
||||||
|
if (patch.tradeAlertTimeFormat != null) {
|
||||||
|
setTradeAlertTimeFormat(normalizeChartTimeFormat(patch.tradeAlertTimeFormat));
|
||||||
|
}
|
||||||
saveAppSettings(patch).then(updated => {
|
saveAppSettings(patch).then(updated => {
|
||||||
if (updated) {
|
if (updated) {
|
||||||
_cache = { ...(_cache ?? {}), ...updated };
|
_cache = { ...(_cache ?? {}), ...updated };
|
||||||
|
|||||||
@@ -503,6 +503,12 @@ export class ChartManager {
|
|||||||
this._applyDisplayTimezoneOptions();
|
this._applyDisplayTimezoneOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 시간축·크로스헤어·캔들 pane 시간 라벨 포맷만 갱신 */
|
||||||
|
setChartTimeFormat(chartTimeFormat: string): void {
|
||||||
|
this.chartTimeFormat = normalizeChartTimeFormat(chartTimeFormat);
|
||||||
|
this._applyDisplayTimezoneOptions();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Theme ──────────────────────────────────────────────────────────────
|
// ─── Theme ──────────────────────────────────────────────────────────────
|
||||||
setTheme(theme: Theme): void {
|
setTheme(theme: Theme): void {
|
||||||
this.currentTheme = theme;
|
this.currentTheme = theme;
|
||||||
|
|||||||
@@ -446,6 +446,8 @@ export interface AppSettingsDto {
|
|||||||
displayTimezone?: string;
|
displayTimezone?: string;
|
||||||
/** 차트 하단 시간축 포맷 (예: yyyy-MM-dd HH:mm) */
|
/** 차트 하단 시간축 포맷 (예: yyyy-MM-dd HH:mm) */
|
||||||
chartTimeFormat?: string;
|
chartTimeFormat?: string;
|
||||||
|
/** 매매 시그널 알림 팝업 시간 포맷 */
|
||||||
|
tradeAlertTimeFormat?: string;
|
||||||
/** 추세검색 기본 설정 */
|
/** 추세검색 기본 설정 */
|
||||||
trendSearchSettings?: import('./trendSearchAppSettings').TrendSearchAppSettings | null;
|
trendSearchSettings?: import('./trendSearchAppSettings').TrendSearchAppSettings | null;
|
||||||
/** UI 설정 통합 (편집기·팔레트·가상투자 목록·패널 크기 등) */
|
/** UI 설정 통합 (편집기·팔레트·가상투자 목록·패널 크기 등) */
|
||||||
|
|||||||
@@ -13,15 +13,25 @@ export interface ChartTimeFormatPreset {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 통상적인 차트 시간축 프리셋 */
|
/** 통상적인 차트 시간축 프리셋 (일반설정·차트 설정 공통) */
|
||||||
export const CHART_TIME_FORMAT_PRESETS: ChartTimeFormatPreset[] = [
|
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:ss', label: 'yyyy-MM-dd HH:mm:ss' },
|
||||||
{ id: 'yyyy-MM-dd HH:mm', label: 'yyyy-MM-dd HH:mm' },
|
{ id: 'yyyy-MM-dd HH:mm', label: 'yyyy-MM-dd HH:mm' },
|
||||||
{ id: 'yyyy-MM-dd', label: 'yyyy-MM-dd' },
|
{ 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:ss', label: 'MM-dd HH:mm:ss' },
|
||||||
{ id: 'MM-dd HH:mm', label: 'MM-dd HH:mm' },
|
{ 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', 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 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:ss', label: 'HH:mm:ss' },
|
||||||
{ id: 'HH:mm', label: 'HH:mm' },
|
{ id: 'HH:mm', label: 'HH:mm' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* 매매 시그널 알림 표시용 포맷·라벨
|
* 매매 시그널 알림 표시용 포맷·라벨
|
||||||
*/
|
*/
|
||||||
|
import { formatUnixWithChartPattern, splitChartTimePattern } from './chartTimeFormat';
|
||||||
import { getKoreanName } from './marketNameCache';
|
import { getKoreanName } from './marketNameCache';
|
||||||
import { formatUnixDateTime, formatUnixClock, getDisplayTimezone } from './timezone';
|
import { getTradeAlertTimeFormat } from './tradeAlertTimeFormat';
|
||||||
|
import { getDisplayTimezone } from './timezone';
|
||||||
|
|
||||||
export interface TradeSignalDisplaySource {
|
export interface TradeSignalDisplaySource {
|
||||||
market: string;
|
market: string;
|
||||||
@@ -31,8 +33,12 @@ export function formatSignalPrice(price: number): string {
|
|||||||
export function formatSignalTime(unixSec: number, withDate = true): string {
|
export function formatSignalTime(unixSec: number, withDate = true): string {
|
||||||
if (!unixSec) return '—';
|
if (!unixSec) return '—';
|
||||||
const tz = getDisplayTimezone();
|
const tz = getDisplayTimezone();
|
||||||
if (!withDate) return formatUnixClock(unixSec, tz, true);
|
const fmt = getTradeAlertTimeFormat();
|
||||||
return formatUnixDateTime(unixSec, tz);
|
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 {
|
export function getSignalTypeKo(type: 'BUY' | 'SELL'): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user