설정 db 저장
This commit is contained in:
+12
-2
@@ -28,8 +28,10 @@ import { syncDocumentTheme } from './utils/documentTheme';
|
||||
import {
|
||||
loadLocalTheme,
|
||||
migrateDbThemeToLocalIfNeeded,
|
||||
resolveThemeForSession,
|
||||
saveLocalTheme,
|
||||
} from './utils/localThemeStorage';
|
||||
import { hasRegisteredUser } from './utils/backendApi';
|
||||
import { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions';
|
||||
import { firstAllowedTopMenu, normalizeRole, type SettingsCategoryId } from './utils/permissions';
|
||||
import { clearAdminUnlock } from './utils/adminUnlock';
|
||||
@@ -694,9 +696,12 @@ function App() {
|
||||
setTheme(t => {
|
||||
const next: Theme = t === 'dark' ? 'blue' : t === 'blue' ? 'light' : 'dark';
|
||||
saveLocalTheme(next);
|
||||
if (hasRegisteredUser()) {
|
||||
saveAppDef({ defaultTheme: next });
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
}, [saveAppDef]);
|
||||
|
||||
useEffect(() => {
|
||||
syncDocumentTheme(theme);
|
||||
@@ -704,7 +709,12 @@ function App() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!appSettingsLoaded) return;
|
||||
setTheme(migrateDbThemeToLocalIfNeeded(getAppSettingsCache().defaultTheme));
|
||||
const dbTheme = getAppSettingsCache().defaultTheme;
|
||||
if (hasRegisteredUser()) {
|
||||
setTheme(resolveThemeForSession(dbTheme, true));
|
||||
} else {
|
||||
setTheme(migrateDbThemeToLocalIfNeeded(dbTheme));
|
||||
}
|
||||
}, [appSettingsLoaded]);
|
||||
|
||||
const openNotificationsPage = useCallback(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useLayoutEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useState, useEffect, useLayoutEffect, useCallback, useRef, useMemo, type SetStateAction } from 'react';
|
||||
import DrawingToolbar from '../components/DrawingToolbar';
|
||||
import BottomBar from '../components/BottomBar';
|
||||
import TradingChart from '../components/TradingChart';
|
||||
@@ -90,7 +90,8 @@ import {
|
||||
import {
|
||||
sortIndicatorsByTypeOrder,
|
||||
} from '../utils/indicatorListOrder';
|
||||
import { getAppSettingsCache } from '../hooks/useAppSettings';
|
||||
import { getAppSettingsCache, isAppSettingsCacheLoaded, subscribeAppSettings } from '../hooks/useAppSettings';
|
||||
import { loadMagnetMode, saveMagnetMode, type MagnetMode } from '../utils/chartMagnetModeStorage';
|
||||
import type { AppSettingsDto } from '../utils/backendApi';
|
||||
import MarketPanel from '../components/MarketPanel';
|
||||
import { type MenuPage } from '../components/TopMenuBar';
|
||||
@@ -245,7 +246,22 @@ export function useChartWorkspace({
|
||||
const [drawingsLocked, setDrawingsLocked] = useState(false);
|
||||
const [drawingsVisible, setDrawingsVisible] = useState(true);
|
||||
const [indicatorsVisible, setIndicatorsVisible] = useState(true);
|
||||
const [magnetMode, setMagnetMode] = useState<'off'|'weak'|'strong'>('off');
|
||||
const [magnetMode, setMagnetModeState] = useState<MagnetMode>('off');
|
||||
const setMagnetMode = useCallback((value: SetStateAction<MagnetMode>) => {
|
||||
setMagnetModeState(prev => {
|
||||
const next = typeof value === 'function' ? value(prev) : value;
|
||||
if (isAppSettingsCacheLoaded()) saveMagnetMode(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const syncMagnet = () => {
|
||||
if (isAppSettingsCacheLoaded()) setMagnetModeState(loadMagnetMode());
|
||||
};
|
||||
syncMagnet();
|
||||
return subscribeAppSettings(syncMagnet);
|
||||
}, []);
|
||||
const [snapToIndicator, setSnapToIndicator] = useState(false);
|
||||
const [keepLockedOnDelete,setKeepLockedOnDelete]= useState(false);
|
||||
const [legend, setLegend] = useState<LegendData | null>(null);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - defaultSymbol : 기본 종목 (DEFAULT_STATE.symbol)
|
||||
* - defaultTimeframe : 기본 타임프레임 (DEFAULT_STATE.timeframe)
|
||||
* - defaultChartType : 기본 차트 타입
|
||||
* - (테마는 DB 미사용 — localStorage, see localThemeStorage)
|
||||
* - defaultTheme : 기본 테마 (로그인 시 DB 공유, 게스트는 localStorage)
|
||||
* - defaultLogScale : 기본 로그 스케일
|
||||
* - defaultLayoutId : 기본 레이아웃 ID
|
||||
* - mainChartStyle : 캔들 색상 (DEFAULT_MAIN_CHART_STYLE)
|
||||
@@ -20,7 +20,7 @@
|
||||
* // 기본 심볼 사용
|
||||
* const symbol = settings.defaultSymbol ?? 'KRW-BTC';
|
||||
*
|
||||
* // 테마 변경 시 saveLocalTheme() (App.tsx)
|
||||
* // 테마 변경 시 saveAppDef({ defaultTheme }) + saveLocalTheme() (App.tsx)
|
||||
* ```
|
||||
*/
|
||||
|
||||
@@ -65,7 +65,7 @@ import {
|
||||
} from '../types/chartPaneSeparator';
|
||||
import { migrateLocalStorageToUiPreferences } from '../utils/uiPreferencesMigrate';
|
||||
import { reconcilePendingUiPreferencesOnLoad } from '../utils/uiPreferencesDb';
|
||||
import { loadLocalTheme } from '../utils/localThemeStorage';
|
||||
import { loadLocalTheme, resolveThemeForSession } from '../utils/localThemeStorage';
|
||||
|
||||
// 전역 캐시 — 여러 컴포넌트에서 공유하여 중복 요청 방지
|
||||
let _cache: AppSettingsDto | null = null;
|
||||
@@ -127,6 +127,21 @@ function ensureLoaded(): Promise<AppSettingsDto> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasRegisteredUser() && _cache) {
|
||||
const local = loadLocalTheme();
|
||||
const dbTheme = _cache.defaultTheme;
|
||||
const dbIsDefault = !dbTheme || dbTheme === 'dark';
|
||||
if (dbIsDefault && local !== 'dark') {
|
||||
try {
|
||||
const saved = await saveAppSettings({ defaultTheme: local });
|
||||
if (generation === _loadGeneration && saved) {
|
||||
_cache = { ..._cache, ...saved, defaultTheme: local };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[useAppSettings] 테마 localStorage→DB 이전 실패:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasRegisteredUser()) {
|
||||
try {
|
||||
await loadStrategies();
|
||||
@@ -161,11 +176,12 @@ export function reloadAppSettingsCache(): Promise<AppSettingsDto> {
|
||||
|
||||
/** DB 또는 fallback 기본값 접근 헬퍼 */
|
||||
export function resolveAppDefaults(s: AppSettingsDto) {
|
||||
const accountTheme = resolveThemeForSession(s.defaultTheme, hasRegisteredUser());
|
||||
return {
|
||||
symbol: (s.defaultSymbol ?? 'KRW-BTC') as string,
|
||||
timeframe: (s.defaultTimeframe ?? '1D') as Timeframe,
|
||||
chartType: (s.defaultChartType ?? 'candlestick') as ChartType,
|
||||
theme: loadLocalTheme(),
|
||||
theme: accountTheme,
|
||||
logScale: s.defaultLogScale ?? false,
|
||||
layoutId: s.defaultLayoutId ?? '1',
|
||||
mainChartStyle:(s.mainChartStyle as unknown as MainChartStyle | null) ?? DEFAULT_MAIN_CHART_STYLE,
|
||||
@@ -184,7 +200,7 @@ export function resolveAppDefaults(s: AppSettingsDto) {
|
||||
),
|
||||
chartPaneSeparator: resolveChartPaneSeparatorOptions(
|
||||
s.chartPaneSeparator as Partial<ChartPaneSeparatorOptions> | null | undefined,
|
||||
loadLocalTheme(),
|
||||
accountTheme,
|
||||
),
|
||||
tradeAlertPopup: s.tradeAlertPopup ?? true,
|
||||
tradeAlertSoundEnabled: s.tradeAlertSoundEnabled ?? true,
|
||||
@@ -288,9 +304,7 @@ export function useAppSettings(sessionKey = 0) {
|
||||
* 낙관적 업데이트 — DB 저장 실패해도 UI 는 즉시 반영.
|
||||
*/
|
||||
const save = useCallback((patch: AppSettingsDto) => {
|
||||
const { defaultTheme: _omitTheme, ...patchWithoutTheme } = patch;
|
||||
const apiPatch = 'defaultTheme' in patch ? patchWithoutTheme : patch;
|
||||
const merged = { ...(_cache ?? {}), ...apiPatch };
|
||||
const merged = { ...(_cache ?? {}), ...patch };
|
||||
_cache = merged;
|
||||
notifyAppSettingsListeners();
|
||||
setSettings(merged);
|
||||
@@ -303,7 +317,7 @@ export function useAppSettings(sessionKey = 0) {
|
||||
if (patch.tradeAlertTimeFormat != null) {
|
||||
setTradeAlertTimeFormat(normalizeChartTimeFormat(patch.tradeAlertTimeFormat));
|
||||
}
|
||||
saveAppSettings(apiPatch).then(updated => {
|
||||
saveAppSettings(patch).then(updated => {
|
||||
if (updated) {
|
||||
_cache = { ...(_cache ?? {}), ...updated };
|
||||
notifyAppSettingsListeners();
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface UiPreferences {
|
||||
chart?: {
|
||||
alertPrices?: number[];
|
||||
legacyIndicators?: IndicatorConfig[];
|
||||
/** 차트 자석 모드 — 설정 화면·툴바 공유 */
|
||||
magnetMode?: 'off' | 'weak' | 'strong';
|
||||
};
|
||||
panels?: Record<string, number | boolean | string>;
|
||||
virtual?: {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { getUiPreferences, patchUiPreferences } from './uiPreferencesDb';
|
||||
|
||||
export type MagnetMode = 'off' | 'weak' | 'strong';
|
||||
|
||||
function normalizeMagnetMode(v: unknown): MagnetMode {
|
||||
return v === 'weak' || v === 'strong' ? v : 'off';
|
||||
}
|
||||
|
||||
/** DB ui_preferences.chart.magnetMode */
|
||||
export function loadMagnetMode(): MagnetMode {
|
||||
return normalizeMagnetMode(getUiPreferences().chart?.magnetMode);
|
||||
}
|
||||
|
||||
export function saveMagnetMode(mode: MagnetMode): void {
|
||||
patchUiPreferences({ chart: { magnetMode: mode } });
|
||||
}
|
||||
@@ -8,7 +8,7 @@ function isTheme(v: unknown): v is Theme {
|
||||
return typeof v === 'string' && (VALID as readonly string[]).includes(v);
|
||||
}
|
||||
|
||||
/** 이 브라우저(기기)에 저장된 UI 테마. 계정·DB와 무관 */
|
||||
/** 이 브라우저(기기)에 저장된 UI 테마 — 게스트·오프라인 폴백 */
|
||||
export function loadLocalTheme(): Theme {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
@@ -27,7 +27,13 @@ export function saveLocalTheme(theme: Theme): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** localStorage에 없을 때만 DB defaultTheme을 이 기기에 1회 복사 */
|
||||
/** 로그인 사용자: DB defaultTheme 우선. 게스트: localStorage */
|
||||
export function resolveThemeForSession(dbTheme: string | undefined, useAccountScope: boolean): Theme {
|
||||
if (useAccountScope && isTheme(dbTheme)) return dbTheme;
|
||||
return loadLocalTheme();
|
||||
}
|
||||
|
||||
/** localStorage에 없을 때만 DB defaultTheme을 이 기기에 1회 복사 (게스트) */
|
||||
export function migrateDbThemeToLocalIfNeeded(dbTheme: string | undefined): Theme {
|
||||
try {
|
||||
const existing = localStorage.getItem(STORAGE_KEY);
|
||||
@@ -39,3 +45,5 @@ export function migrateDbThemeToLocalIfNeeded(dbTheme: string | undefined): Them
|
||||
saveLocalTheme(theme);
|
||||
return theme;
|
||||
}
|
||||
|
||||
export { isTheme };
|
||||
|
||||
Reference in New Issue
Block a user