145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
/**
|
|
* UI 설정 — gc_app_settings.ui_preferences_json (DB 단일 소스)
|
|
*/
|
|
import {
|
|
getAppSettingsCache,
|
|
isAppSettingsCacheLoaded,
|
|
patchAppSettingsCache,
|
|
subscribeAppSettings,
|
|
} from '../hooks/useAppSettings';
|
|
import { saveAppSettings } from './backendApi';
|
|
import {
|
|
EMPTY_UI_PREFERENCES,
|
|
UI_PREFERENCES_VERSION,
|
|
type UiPreferences,
|
|
} from '../types/uiPreferences';
|
|
import { normalizeWidgetLayout } from '../types/widgetDashboard';
|
|
|
|
const SAVE_DEBOUNCE_MS = 400;
|
|
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let pendingPatch: Partial<UiPreferences> | null = null;
|
|
|
|
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
}
|
|
|
|
function deepMerge<T extends Record<string, unknown>>(base: T, patch: Partial<T>): T {
|
|
const out = { ...base } as T;
|
|
for (const key of Object.keys(patch) as (keyof T)[]) {
|
|
const pv = patch[key];
|
|
if (pv === undefined) continue;
|
|
const bv = base[key];
|
|
if (isPlainObject(bv) && isPlainObject(pv)) {
|
|
out[key] = deepMerge(bv as Record<string, unknown>, pv as Record<string, unknown>) as T[keyof T];
|
|
} else {
|
|
out[key] = pv as T[keyof T];
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function resolveUiPreferences(raw: unknown): UiPreferences {
|
|
if (!raw || typeof raw !== 'object') {
|
|
return { ...EMPTY_UI_PREFERENCES };
|
|
}
|
|
const o = raw as UiPreferences;
|
|
return {
|
|
...EMPTY_UI_PREFERENCES,
|
|
...o,
|
|
v: UI_PREFERENCES_VERSION,
|
|
strategyEditor: { ...EMPTY_UI_PREFERENCES.strategyEditor, ...o.strategyEditor },
|
|
indicator: { ...EMPTY_UI_PREFERENCES.indicator, ...o.indicator },
|
|
chart: { ...EMPTY_UI_PREFERENCES.chart, ...o.chart },
|
|
panels: { ...EMPTY_UI_PREFERENCES.panels, ...o.panels },
|
|
virtual: { ...EMPTY_UI_PREFERENCES.virtual, ...o.virtual },
|
|
tradeNotifications: { ...EMPTY_UI_PREFERENCES.tradeNotifications, ...o.tradeNotifications },
|
|
widgetDashboard: o.widgetDashboard
|
|
? normalizeWidgetLayout(o.widgetDashboard)
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
/** DB 캐시 기준 UI 설정 (앱 설정 로드 후 유효) */
|
|
export function getUiPreferences(): UiPreferences {
|
|
return resolveUiPreferences(getAppSettingsCache().uiPreferences);
|
|
}
|
|
|
|
/** UI 설정 부분 갱신 — 낙관적 캐시 + 디바운스 DB 저장 */
|
|
export function patchUiPreferences(patch: Partial<UiPreferences>, immediate = false): void {
|
|
if (!isAppSettingsCacheLoaded()) {
|
|
pendingPatch = pendingPatch
|
|
? (deepMerge(pendingPatch as Record<string, unknown>, patch as Record<string, unknown>) as Partial<UiPreferences>)
|
|
: patch;
|
|
return;
|
|
}
|
|
|
|
const current = getUiPreferences();
|
|
const next = deepMerge(current as Record<string, unknown>, patch as Record<string, unknown>) as UiPreferences;
|
|
next.v = UI_PREFERENCES_VERSION;
|
|
|
|
patchAppSettingsCache({ uiPreferences: next });
|
|
|
|
pendingPatch = pendingPatch
|
|
? (deepMerge(pendingPatch as Record<string, unknown>, patch as Record<string, unknown>) as Partial<UiPreferences>)
|
|
: patch;
|
|
|
|
const flush = () => {
|
|
saveTimer = null;
|
|
if (!isAppSettingsCacheLoaded()) return;
|
|
const toSave = pendingPatch
|
|
? resolveUiPreferences(deepMerge(getUiPreferences() as Record<string, unknown>, pendingPatch as Record<string, unknown>))
|
|
: next;
|
|
pendingPatch = null;
|
|
void saveAppSettings({ uiPreferences: toSave }).catch(err => {
|
|
console.warn('[uiPreferences] DB 저장 실패:', err);
|
|
});
|
|
};
|
|
|
|
if (immediate) {
|
|
if (saveTimer != null) clearTimeout(saveTimer);
|
|
pendingPatch = null;
|
|
void saveAppSettings({ uiPreferences: next }).catch(err => {
|
|
console.warn('[uiPreferences] DB 저장 실패:', err);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (saveTimer != null) clearTimeout(saveTimer);
|
|
saveTimer = setTimeout(flush, SAVE_DEBOUNCE_MS);
|
|
}
|
|
|
|
/** 디바운스 대기 중인 UI 설정 즉시 DB 저장 (페이지 이탈 시) */
|
|
export function flushPendingUiPreferences(): void {
|
|
if (saveTimer != null) {
|
|
clearTimeout(saveTimer);
|
|
saveTimer = null;
|
|
}
|
|
if (!isAppSettingsCacheLoaded() || !pendingPatch) return;
|
|
const toSave = resolveUiPreferences(
|
|
deepMerge(getUiPreferences() as Record<string, unknown>, pendingPatch as Record<string, unknown>),
|
|
);
|
|
pendingPatch = null;
|
|
void saveAppSettings({ uiPreferences: toSave }).catch(err => {
|
|
console.warn('[uiPreferences] DB flush 저장 실패:', err);
|
|
});
|
|
}
|
|
|
|
export function subscribeUiPreferences(listener: () => void): () => void {
|
|
return subscribeAppSettings(listener);
|
|
}
|
|
|
|
/** 앱 설정 DB 로드 직후 — 로드 전에 큐잉된 patch 병합 */
|
|
export function reconcilePendingUiPreferencesOnLoad(): void {
|
|
if (!isAppSettingsCacheLoaded() || !pendingPatch) return;
|
|
const merged = resolveUiPreferences(
|
|
deepMerge(getUiPreferences() as Record<string, unknown>, pendingPatch as Record<string, unknown>),
|
|
);
|
|
patchAppSettingsCache({ uiPreferences: merged });
|
|
pendingPatch = null;
|
|
}
|
|
|
|
/** 중첩 경로 헬퍼 */
|
|
export function getUiPref<K extends keyof UiPreferences>(key: K): UiPreferences[K] {
|
|
return getUiPreferences()[key];
|
|
}
|