// src/main/config-effects.ts // 설정 키별 부수효과 레지스트리 — 어느 경로로 바뀌든(범용 config:set, 전용 IPC, 원격 동기화) // 같은 효과가 한 번씩 적용되게 한다. // // 예전엔 효과가 입력 경로에 붙어 있었다: 테마 팝업 다시 칠하기는 SET_THEME 와 원격 동기화 경로에만, // OS 로그인 항목 갱신은 SET_AUTO_LAUNCH 에만 있었다. 설정 화면은 범용 config:set 을 써서 두 효과가 // 모두 빠졌다(자동 실행이 재부팅 뒤 동작하지 않고, 미리 로드된 팝업이 이전 테마로 남았다). // 이제 효과는 설정 변경 이벤트 하나를 구독하는 이 레지스트리에 등록한다(OCP — 새 키는 등록만 한다). // Electron 에 의존하지 않는다: 구독 함수와 효과는 주입받는다(테스트는 가짜를 넘긴다). export interface ConfigChange { key: string value: unknown previousValue?: unknown } export type ConfigEffect = (value: unknown, previousValue: unknown) => void export interface ConfigEffectsDeps { /** 설정 변경 구독 (ConfigService.onConfigChanged) — 해지 함수를 돌려준다 */ subscribe: (listener: (change: ConfigChange) => void) => () => void /** 효과가 던진 오류 — 다른 효과와 설정 저장은 계속된다 */ onError?: (key: string, error: unknown) => void } function isPrimitive(value: unknown): boolean { return value === null || (typeof value !== 'object' && typeof value !== 'function') } /** * 값이 실제로 바뀌었는가. 원시값은 같으면 건너뛴다 — 효과 안에서 같은 값을 다시 저장해도 * (AutoLaunchService.setEnabled 는 적용 후 configSet 을 부른다) 재귀하지 않는다. * 객체·배열은 비교하지 않고 적용한다(효과는 멱등이어야 한다). */ export function isEffectiveChange(change: ConfigChange): boolean { if (isPrimitive(change.value) && isPrimitive(change.previousValue)) { return !Object.is(change.value, change.previousValue) } return true } export class ConfigEffects { private readonly _effects = new Map() private _unsubscribe: (() => void) | null = null constructor(private readonly _deps: ConfigEffectsDeps) {} /** 키에 효과를 등록한다. 돌려받은 함수로 등록을 푼다. */ register(key: string, effect: ConfigEffect): () => void { const list = this._effects.get(key) ?? [] list.push(effect) this._effects.set(key, list) return () => { const current = this._effects.get(key) if (!current) return const next = current.filter((e) => e !== effect) if (next.length > 0) this._effects.set(key, next) else this._effects.delete(key) } } /** 설정 변경 구독을 시작한다(멱등). */ start(): void { if (this._unsubscribe) return this._unsubscribe = this._deps.subscribe((change) => this.dispatch(change)) } dispose(): void { this._unsubscribe?.() this._unsubscribe = null this._effects.clear() } /** 변경 하나를 등록된 효과에 전달한다(테스트·직접 호출용). */ dispatch(change: ConfigChange): void { const effects = this._effects.get(change.key) if (!effects || effects.length === 0) return if (!isEffectiveChange(change)) return for (const effect of [...effects]) { try { effect(change.value, change.previousValue) } catch (err) { this._deps.onError?.(change.key, err) } } } }