goldenChat base source add
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* useAppSettings
|
||||
*
|
||||
* 앱 전역 차트 기본 설정을 DB 와 동기화하는 훅.
|
||||
*
|
||||
* 관리 항목 (하드코딩 대체):
|
||||
* - defaultSymbol : 기본 종목 (DEFAULT_STATE.symbol)
|
||||
* - defaultTimeframe : 기본 타임프레임 (DEFAULT_STATE.timeframe)
|
||||
* - defaultChartType : 기본 차트 타입
|
||||
* - defaultTheme : 기본 테마
|
||||
* - defaultLogScale : 기본 로그 스케일
|
||||
* - defaultLayoutId : 기본 레이아웃 ID
|
||||
* - mainChartStyle : 캔들 색상 (DEFAULT_MAIN_CHART_STYLE)
|
||||
* - syncOptions : 멀티차트 동기화 옵션 (DEFAULT_SYNC)
|
||||
*
|
||||
* 사용 예:
|
||||
* ```tsx
|
||||
* const { settings, isLoaded, save } = useAppSettings();
|
||||
*
|
||||
* // 기본 심볼 사용
|
||||
* const symbol = settings.defaultSymbol ?? 'KRW-BTC';
|
||||
*
|
||||
* // 테마 변경 시 DB 에 저장
|
||||
* save({ defaultTheme: newTheme });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
loadAppSettings,
|
||||
saveAppSettings,
|
||||
type AppSettingsDto,
|
||||
} from '../utils/backendApi';
|
||||
import type { Theme, ChartType, Timeframe, MainChartStyle } from '../types';
|
||||
import { resolveChartLegendOptions } from '../types/chartLegend';
|
||||
import type { ChartLegendVisibility } from '../types/chartLegend';
|
||||
import { DEFAULT_MAIN_CHART_STYLE } from '../utils/storage';
|
||||
import { DEFAULT_SYNC, type SyncOptions } from '../utils/layoutTypes';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE, normalizeTimezone, setDisplayTimezone } from '../utils/timezone';
|
||||
|
||||
// 전역 캐시 — 여러 컴포넌트에서 공유하여 중복 요청 방지
|
||||
let _cache: AppSettingsDto | null = null;
|
||||
let _loadPromise: Promise<AppSettingsDto> | null = null;
|
||||
|
||||
function ensureLoaded(): Promise<AppSettingsDto> {
|
||||
if (_cache !== null) return Promise.resolve(_cache);
|
||||
if (_loadPromise) return _loadPromise;
|
||||
_loadPromise = loadAppSettings().then(data => {
|
||||
_cache = data ?? {};
|
||||
_loadPromise = null;
|
||||
return _cache;
|
||||
});
|
||||
return _loadPromise;
|
||||
}
|
||||
|
||||
export function invalidateAppSettingsCache() {
|
||||
_cache = null;
|
||||
_loadPromise = null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** DB 또는 fallback 기본값 접근 헬퍼 */
|
||||
export function resolveAppDefaults(s: AppSettingsDto) {
|
||||
return {
|
||||
symbol: (s.defaultSymbol ?? 'KRW-BTC') as string,
|
||||
timeframe: (s.defaultTimeframe ?? '1D') as Timeframe,
|
||||
chartType: (s.defaultChartType ?? 'candlestick') as ChartType,
|
||||
theme: (s.defaultTheme ?? 'dark') as Theme,
|
||||
logScale: s.defaultLogScale ?? false,
|
||||
layoutId: s.defaultLayoutId ?? '1',
|
||||
mainChartStyle:(s.mainChartStyle as unknown as MainChartStyle | null) ?? DEFAULT_MAIN_CHART_STYLE,
|
||||
syncOptions: (s.syncOptions as unknown as SyncOptions | null) ?? DEFAULT_SYNC,
|
||||
btAutoPopup: s.btAutoPopup ?? true,
|
||||
btShowPrice: s.btShowPrice ?? true,
|
||||
chartSeriesPriceLabels: s.chartSeriesPriceLabels ?? true,
|
||||
chartVolumeVisible: s.chartVolumeVisible ?? true,
|
||||
chartLegendOptions: resolveChartLegendOptions(
|
||||
s.chartLegendOptions as Partial<ChartLegendVisibility> | null | undefined,
|
||||
),
|
||||
tradeAlertPopup: s.tradeAlertPopup ?? true,
|
||||
tradeAlertSoundEnabled: s.tradeAlertSoundEnabled ?? true,
|
||||
tradeAlertSound: s.tradeAlertSound ?? 'bell',
|
||||
tradeAlertPopupPosition: s.tradeAlertPopupPosition ?? 'right',
|
||||
tradeAlertPopupLayout: s.tradeAlertPopupLayout ?? 'stack',
|
||||
tradeAlertPopupGridCols: s.tradeAlertPopupGridCols ?? 2,
|
||||
liveStrategyCheck: s.liveStrategyCheck ?? false,
|
||||
liveStrategyId: s.liveStrategyId ?? null,
|
||||
liveExecutionType: s.liveExecutionType ?? 'CANDLE_CLOSE',
|
||||
livePositionMode: s.livePositionMode ?? 'LONG_ONLY',
|
||||
paperTradingEnabled: s.paperTradingEnabled ?? true,
|
||||
paperInitialCapital: s.paperInitialCapital ?? 10_000_000,
|
||||
paperFeeRatePct: s.paperFeeRatePct ?? 0.05,
|
||||
paperSlippagePct: s.paperSlippagePct ?? 0,
|
||||
paperMinOrderKrw: s.paperMinOrderKrw ?? 5000,
|
||||
paperAutoTradeEnabled: s.paperAutoTradeEnabled ?? false,
|
||||
paperAutoTradeBudgetPct: s.paperAutoTradeBudgetPct ?? 95,
|
||||
tradingMode: s.tradingMode ?? 'PAPER',
|
||||
liveAutoTradeEnabled: s.liveAutoTradeEnabled ?? false,
|
||||
hasUpbitKeys: s.hasUpbitKeys ?? false,
|
||||
upbitAccessKeyMasked: s.upbitAccessKeyMasked ?? null,
|
||||
chartRealtimeSource: s.chartRealtimeSource ?? 'BACKEND_STOMP',
|
||||
liveAutoTradeBudgetPct: s.liveAutoTradeBudgetPct ?? 95,
|
||||
fcmPushEnabled: s.fcmPushEnabled ?? false,
|
||||
displayTimezone: normalizeTimezone(s.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE),
|
||||
};
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** sessionKey 변경 시(로그인/로그아웃) 설정을 DB에서 다시 로드 */
|
||||
export function useAppSettings(sessionKey = 0) {
|
||||
const [settings, setSettings] = useState<AppSettingsDto>(_cache ?? {});
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
setIsLoaded(false);
|
||||
invalidateAppSettingsCache();
|
||||
ensureLoaded().then(data => {
|
||||
if (mountedRef.current) {
|
||||
setSettings(data);
|
||||
setDisplayTimezone(normalizeTimezone(data.displayTimezone ?? DEFAULT_DISPLAY_TIMEZONE));
|
||||
setIsLoaded(true);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('[useAppSettings] load failed', err);
|
||||
if (mountedRef.current) setIsLoaded(true);
|
||||
});
|
||||
return () => { mountedRef.current = false; };
|
||||
}, [sessionKey]);
|
||||
|
||||
/**
|
||||
* 특정 필드를 DB 에 저장하고 로컬 캐시를 업데이트.
|
||||
* 낙관적 업데이트 — DB 저장 실패해도 UI 는 즉시 반영.
|
||||
*/
|
||||
const save = useCallback((patch: AppSettingsDto) => {
|
||||
const merged = { ...(_cache ?? {}), ...patch };
|
||||
_cache = merged;
|
||||
setSettings(merged);
|
||||
if (patch.displayTimezone != null) {
|
||||
setDisplayTimezone(normalizeTimezone(patch.displayTimezone));
|
||||
}
|
||||
saveAppSettings(patch).then(updated => {
|
||||
if (updated && mountedRef.current) {
|
||||
_cache = { ...(_cache ?? {}), ...updated };
|
||||
setSettings(prev => ({ ...prev, ...updated }));
|
||||
}
|
||||
}).catch(err =>
|
||||
console.error('[useAppSettings] save failed', err)
|
||||
);
|
||||
}, []);
|
||||
|
||||
/** 로드된 설정의 해석된 기본값 */
|
||||
const defaults = resolveAppDefaults(settings);
|
||||
|
||||
return { settings, defaults, isLoaded, save };
|
||||
}
|
||||
Reference in New Issue
Block a user