/** * UI 설정 — gc_app_settings.ui_preferences_json (DB 단일 소스) */ import { getAppSettingsCache, subscribeAppSettings, } from '../hooks/useAppSettings'; import { saveAppSettings, type AppSettingsDto } from './backendApi'; import { EMPTY_UI_PREFERENCES, UI_PREFERENCES_VERSION, type UiPreferences, } from '../types/uiPreferences'; const SAVE_DEBOUNCE_MS = 400; let saveTimer: ReturnType | null = null; let pendingPatch: Partial | null = null; function isPlainObject(v: unknown): v is Record { return v != null && typeof v === 'object' && !Array.isArray(v); } function deepMerge>(base: T, patch: Partial): 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, pv as Record) 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 }, }; } /** DB 캐시 기준 UI 설정 (앱 설정 로드 후 유효) */ export function getUiPreferences(): UiPreferences { return resolveUiPreferences(getAppSettingsCache().uiPreferences); } /** UI 설정 부분 갱신 — 낙관적 캐시 + 디바운스 DB 저장 */ export function patchUiPreferences(patch: Partial, immediate = false): void { const current = getUiPreferences(); const next = deepMerge(current as Record, patch as Record) as UiPreferences; next.v = UI_PREFERENCES_VERSION; const cache = getAppSettingsCache(); const merged: AppSettingsDto = { ...cache, uiPreferences: next }; Object.assign(cache, merged); pendingPatch = pendingPatch ? (deepMerge(pendingPatch as Record, patch as Record) as Partial) : patch; const flush = () => { saveTimer = null; const toSave = pendingPatch ? resolveUiPreferences(deepMerge(getUiPreferences() as Record, pendingPatch as Record)) : 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); } export function subscribeUiPreferences(listener: () => void): () => void { return subscribeAppSettings(listener); } /** 중첩 경로 헬퍼 */ export function getUiPref(key: K): UiPreferences[K] { return getUiPreferences()[key]; }