테마 설정 local storage 설정하도록 수정
This commit is contained in:
+18
-8
@@ -89,7 +89,7 @@ import {
|
|||||||
getOrderedSettingsIndicatorTypes,
|
getOrderedSettingsIndicatorTypes,
|
||||||
sortIndicatorsByTypeOrder,
|
sortIndicatorsByTypeOrder,
|
||||||
} from './utils/indicatorListOrder';
|
} from './utils/indicatorListOrder';
|
||||||
import { useAppSettings } from './hooks/useAppSettings';
|
import { getAppSettingsCache, useAppSettings } from './hooks/useAppSettings';
|
||||||
import { clampVirtualTargetMax } from './utils/virtualTargetLimits';
|
import { clampVirtualTargetMax } from './utils/virtualTargetLimits';
|
||||||
import { resolveTrendSearchAppSettings } from './utils/trendSearchAppSettings';
|
import { resolveTrendSearchAppSettings } from './utils/trendSearchAppSettings';
|
||||||
import MarketPanel from './components/MarketPanel';
|
import MarketPanel from './components/MarketPanel';
|
||||||
@@ -154,6 +154,11 @@ import SplashScreen from './components/SplashScreen';
|
|||||||
import { getAuthSession, setAuthSession, clearAuthSession, type AuthSession } from './utils/auth';
|
import { getAuthSession, setAuthSession, clearAuthSession, type AuthSession } from './utils/auth';
|
||||||
import { isAppEntered, setAppEntered, clearAppEntered } from './utils/appEntry';
|
import { isAppEntered, setAppEntered, clearAppEntered } from './utils/appEntry';
|
||||||
import { syncDocumentTheme } from './utils/documentTheme';
|
import { syncDocumentTheme } from './utils/documentTheme';
|
||||||
|
import {
|
||||||
|
loadLocalTheme,
|
||||||
|
migrateDbThemeToLocalIfNeeded,
|
||||||
|
saveLocalTheme,
|
||||||
|
} from './utils/localThemeStorage';
|
||||||
import { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions';
|
import { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions';
|
||||||
import { firstAllowedTopMenu, normalizeRole, type SettingsCategoryId } from './utils/permissions';
|
import { firstAllowedTopMenu, normalizeRole, type SettingsCategoryId } from './utils/permissions';
|
||||||
import { clearAdminUnlock } from './utils/adminUnlock';
|
import { clearAdminUnlock } from './utils/adminUnlock';
|
||||||
@@ -227,7 +232,7 @@ function App() {
|
|||||||
const [symbol, setSymbol] = useState(initial.symbol);
|
const [symbol, setSymbol] = useState(initial.symbol);
|
||||||
const [timeframe, setTimeframe] = useState<Timeframe>(initial.timeframe);
|
const [timeframe, setTimeframe] = useState<Timeframe>(initial.timeframe);
|
||||||
const [chartType, setChartType] = useState<ChartType>(initial.chartType);
|
const [chartType, setChartType] = useState<ChartType>(initial.chartType);
|
||||||
const [theme, setTheme] = useState<Theme>(initial.theme);
|
const [theme, setTheme] = useState<Theme>(loadLocalTheme);
|
||||||
const [mode, setMode] = useState<ChartMode>('chart');
|
const [mode, setMode] = useState<ChartMode>('chart');
|
||||||
const [logScale, setLogScale] = useState(false);
|
const [logScale, setLogScale] = useState(false);
|
||||||
const [indicators, setIndicators] = useState<IndicatorConfig[]>(initial.indicators ?? []);
|
const [indicators, setIndicators] = useState<IndicatorConfig[]>(initial.indicators ?? []);
|
||||||
@@ -1261,11 +1266,11 @@ function App() {
|
|||||||
syncOptions,
|
syncOptions,
|
||||||
slots: [{
|
slots: [{
|
||||||
slotIndex: 0,
|
slotIndex: 0,
|
||||||
symbol, timeframe, chartType, theme, mode, logScale,
|
symbol, timeframe, chartType, mode, logScale,
|
||||||
indicators, drawings, drawingsLocked, drawingsVisible,
|
indicators, drawings, drawingsLocked, drawingsVisible,
|
||||||
mainChartStyle,
|
mainChartStyle,
|
||||||
}],
|
}],
|
||||||
}), [layoutDef.id, syncOptions, symbol, timeframe, chartType, theme,
|
}), [layoutDef.id, syncOptions, symbol, timeframe, chartType,
|
||||||
mode, logScale, indicators, drawings, drawingsLocked, drawingsVisible, mainChartStyle]);
|
mode, logScale, indicators, drawings, drawingsLocked, drawingsVisible, mainChartStyle]);
|
||||||
|
|
||||||
const { isLoaded: _wsLoaded } = useWorkspacePersist({
|
const { isLoaded: _wsLoaded } = useWorkspacePersist({
|
||||||
@@ -1285,7 +1290,6 @@ function App() {
|
|||||||
if (slot0.symbol) setSymbol(slot0.symbol as string);
|
if (slot0.symbol) setSymbol(slot0.symbol as string);
|
||||||
if (slot0.timeframe) setTimeframe(slot0.timeframe as Timeframe);
|
if (slot0.timeframe) setTimeframe(slot0.timeframe as Timeframe);
|
||||||
if (slot0.chartType) setChartType(slot0.chartType as ChartType);
|
if (slot0.chartType) setChartType(slot0.chartType as ChartType);
|
||||||
if (slot0.theme) setTheme(slot0.theme as Theme);
|
|
||||||
if (slot0.mode) setMode(slot0.mode as ChartMode);
|
if (slot0.mode) setMode(slot0.mode as ChartMode);
|
||||||
if (slot0.logScale != null) setLogScale(Boolean(slot0.logScale));
|
if (slot0.logScale != null) setLogScale(Boolean(slot0.logScale));
|
||||||
if (Array.isArray(slot0.indicators)) {
|
if (Array.isArray(slot0.indicators)) {
|
||||||
@@ -1677,14 +1681,20 @@ function App() {
|
|||||||
slotRefs.current.forEach(slot => slot?.setCrosshairMagnet(enabled));
|
slotRefs.current.forEach(slot => slot?.setCrosshairMagnet(enabled));
|
||||||
}, [magnetMode]);
|
}, [magnetMode]);
|
||||||
|
|
||||||
// 테마 순환: dark → blue → light → dark, DB 에 저장
|
// 테마 순환: dark → blue → light → dark (기기별 localStorage)
|
||||||
const handleThemeToggle = useCallback(() => {
|
const handleThemeToggle = useCallback(() => {
|
||||||
setTheme(t => {
|
setTheme(t => {
|
||||||
const next: Theme = t === 'dark' ? 'blue' : t === 'blue' ? 'light' : 'dark';
|
const next: Theme = t === 'dark' ? 'blue' : t === 'blue' ? 'light' : 'dark';
|
||||||
saveAppDef({ defaultTheme: next });
|
saveLocalTheme(next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, [saveAppDef]);
|
}, []);
|
||||||
|
|
||||||
|
// 최초 이 기기: DB defaultTheme → localStorage 1회만 (이후 기기별 독립)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!appSettingsLoaded) return;
|
||||||
|
setTheme(migrateDbThemeToLocalIfNeeded(getAppSettingsCache().defaultTheme));
|
||||||
|
}, [appSettingsLoaded]);
|
||||||
|
|
||||||
const handleTimeframe = useCallback((tf: Timeframe) => {
|
const handleTimeframe = useCallback((tf: Timeframe) => {
|
||||||
if (layoutDef.count > 1) {
|
if (layoutDef.count > 1) {
|
||||||
|
|||||||
@@ -424,7 +424,6 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
symbol,
|
symbol,
|
||||||
timeframe,
|
timeframe,
|
||||||
chartType: 'candlestick',
|
chartType: 'candlestick',
|
||||||
theme,
|
|
||||||
mode: 'normal',
|
mode: 'normal',
|
||||||
logScale: false,
|
logScale: false,
|
||||||
drawingsLocked: false,
|
drawingsLocked: false,
|
||||||
@@ -437,7 +436,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); };
|
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); };
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [slotIndex, symbol, timeframe, theme, indicators, mainChartStyle]);
|
}, [slotIndex, symbol, timeframe, indicators, mainChartStyle]);
|
||||||
|
|
||||||
// ── 동기화 콜백 refs (항상 최신 값을 참조하도록) ─────────────────────────
|
// ── 동기화 콜백 refs (항상 최신 값을 참조하도록) ─────────────────────────
|
||||||
const onVisibleRangeChangeRef = useRef(onVisibleRangeChange);
|
const onVisibleRangeChangeRef = useRef(onVisibleRangeChange);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* - defaultSymbol : 기본 종목 (DEFAULT_STATE.symbol)
|
* - defaultSymbol : 기본 종목 (DEFAULT_STATE.symbol)
|
||||||
* - defaultTimeframe : 기본 타임프레임 (DEFAULT_STATE.timeframe)
|
* - defaultTimeframe : 기본 타임프레임 (DEFAULT_STATE.timeframe)
|
||||||
* - defaultChartType : 기본 차트 타입
|
* - defaultChartType : 기본 차트 타입
|
||||||
* - defaultTheme : 기본 테마
|
* - (테마는 DB 미사용 — localStorage, see localThemeStorage)
|
||||||
* - defaultLogScale : 기본 로그 스케일
|
* - defaultLogScale : 기본 로그 스케일
|
||||||
* - defaultLayoutId : 기본 레이아웃 ID
|
* - defaultLayoutId : 기본 레이아웃 ID
|
||||||
* - mainChartStyle : 캔들 색상 (DEFAULT_MAIN_CHART_STYLE)
|
* - mainChartStyle : 캔들 색상 (DEFAULT_MAIN_CHART_STYLE)
|
||||||
@@ -20,8 +20,7 @@
|
|||||||
* // 기본 심볼 사용
|
* // 기본 심볼 사용
|
||||||
* const symbol = settings.defaultSymbol ?? 'KRW-BTC';
|
* const symbol = settings.defaultSymbol ?? 'KRW-BTC';
|
||||||
*
|
*
|
||||||
* // 테마 변경 시 DB 에 저장
|
* // 테마 변경 시 saveLocalTheme() (App.tsx)
|
||||||
* save({ defaultTheme: newTheme });
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -61,6 +60,7 @@ import {
|
|||||||
type ChartPaneSeparatorOptions,
|
type ChartPaneSeparatorOptions,
|
||||||
} from '../types/chartPaneSeparator';
|
} from '../types/chartPaneSeparator';
|
||||||
import { migrateLocalStorageToUiPreferences } from '../utils/uiPreferencesMigrate';
|
import { migrateLocalStorageToUiPreferences } from '../utils/uiPreferencesMigrate';
|
||||||
|
import { loadLocalTheme } from '../utils/localThemeStorage';
|
||||||
|
|
||||||
// 전역 캐시 — 여러 컴포넌트에서 공유하여 중복 요청 방지
|
// 전역 캐시 — 여러 컴포넌트에서 공유하여 중복 요청 방지
|
||||||
let _cache: AppSettingsDto | null = null;
|
let _cache: AppSettingsDto | null = null;
|
||||||
@@ -146,7 +146,7 @@ export function resolveAppDefaults(s: AppSettingsDto) {
|
|||||||
symbol: (s.defaultSymbol ?? 'KRW-BTC') as string,
|
symbol: (s.defaultSymbol ?? 'KRW-BTC') as string,
|
||||||
timeframe: (s.defaultTimeframe ?? '1D') as Timeframe,
|
timeframe: (s.defaultTimeframe ?? '1D') as Timeframe,
|
||||||
chartType: (s.defaultChartType ?? 'candlestick') as ChartType,
|
chartType: (s.defaultChartType ?? 'candlestick') as ChartType,
|
||||||
theme: (s.defaultTheme ?? 'dark') as Theme,
|
theme: loadLocalTheme(),
|
||||||
logScale: s.defaultLogScale ?? false,
|
logScale: s.defaultLogScale ?? false,
|
||||||
layoutId: s.defaultLayoutId ?? '1',
|
layoutId: s.defaultLayoutId ?? '1',
|
||||||
mainChartStyle:(s.mainChartStyle as unknown as MainChartStyle | null) ?? DEFAULT_MAIN_CHART_STYLE,
|
mainChartStyle:(s.mainChartStyle as unknown as MainChartStyle | null) ?? DEFAULT_MAIN_CHART_STYLE,
|
||||||
@@ -164,7 +164,7 @@ export function resolveAppDefaults(s: AppSettingsDto) {
|
|||||||
),
|
),
|
||||||
chartPaneSeparator: resolveChartPaneSeparatorOptions(
|
chartPaneSeparator: resolveChartPaneSeparatorOptions(
|
||||||
s.chartPaneSeparator as Partial<ChartPaneSeparatorOptions> | null | undefined,
|
s.chartPaneSeparator as Partial<ChartPaneSeparatorOptions> | null | undefined,
|
||||||
(s.defaultTheme ?? 'dark') as Theme,
|
loadLocalTheme(),
|
||||||
),
|
),
|
||||||
tradeAlertPopup: s.tradeAlertPopup ?? true,
|
tradeAlertPopup: s.tradeAlertPopup ?? true,
|
||||||
tradeAlertSoundEnabled: s.tradeAlertSoundEnabled ?? true,
|
tradeAlertSoundEnabled: s.tradeAlertSoundEnabled ?? true,
|
||||||
@@ -263,7 +263,9 @@ export function useAppSettings(sessionKey = 0) {
|
|||||||
* 낙관적 업데이트 — DB 저장 실패해도 UI 는 즉시 반영.
|
* 낙관적 업데이트 — DB 저장 실패해도 UI 는 즉시 반영.
|
||||||
*/
|
*/
|
||||||
const save = useCallback((patch: AppSettingsDto) => {
|
const save = useCallback((patch: AppSettingsDto) => {
|
||||||
const merged = { ...(_cache ?? {}), ...patch };
|
const { defaultTheme: _omitTheme, ...patchWithoutTheme } = patch;
|
||||||
|
const apiPatch = 'defaultTheme' in patch ? patchWithoutTheme : patch;
|
||||||
|
const merged = { ...(_cache ?? {}), ...apiPatch };
|
||||||
_cache = merged;
|
_cache = merged;
|
||||||
notifyAppSettingsListeners();
|
notifyAppSettingsListeners();
|
||||||
setSettings(merged);
|
setSettings(merged);
|
||||||
@@ -276,7 +278,7 @@ export function useAppSettings(sessionKey = 0) {
|
|||||||
if (patch.tradeAlertTimeFormat != null) {
|
if (patch.tradeAlertTimeFormat != null) {
|
||||||
setTradeAlertTimeFormat(normalizeChartTimeFormat(patch.tradeAlertTimeFormat));
|
setTradeAlertTimeFormat(normalizeChartTimeFormat(patch.tradeAlertTimeFormat));
|
||||||
}
|
}
|
||||||
saveAppSettings(patch).then(updated => {
|
saveAppSettings(apiPatch).then(updated => {
|
||||||
if (updated) {
|
if (updated) {
|
||||||
_cache = { ...(_cache ?? {}), ...updated };
|
_cache = { ...(_cache ?? {}), ...updated };
|
||||||
notifyAppSettingsListeners();
|
notifyAppSettingsListeners();
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Theme } from '../types';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'gc_local_theme';
|
||||||
|
|
||||||
|
const VALID: readonly Theme[] = ['dark', 'light', 'blue'];
|
||||||
|
|
||||||
|
function isTheme(v: unknown): v is Theme {
|
||||||
|
return typeof v === 'string' && (VALID as readonly string[]).includes(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 이 브라우저(기기)에 저장된 UI 테마. 계정·DB와 무관 */
|
||||||
|
export function loadLocalTheme(): Theme {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (isTheme(raw)) return raw;
|
||||||
|
} catch {
|
||||||
|
/* private mode 등 */
|
||||||
|
}
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveLocalTheme(theme: Theme): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, theme);
|
||||||
|
} catch {
|
||||||
|
/* quota / private mode */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** localStorage에 없을 때만 DB defaultTheme을 이 기기에 1회 복사 */
|
||||||
|
export function migrateDbThemeToLocalIfNeeded(dbTheme: string | undefined): Theme {
|
||||||
|
try {
|
||||||
|
const existing = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (isTheme(existing)) return existing;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
const theme: Theme = isTheme(dbTheme) ? dbTheme : 'dark';
|
||||||
|
saveLocalTheme(theme);
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user