// src/main/services/ConfigService.ts // electron-store 기반 설정 관리. 설계서 02의 AppConfig 타입 사용. import { EventEmitter } from 'events' import type { AppConfig, ConfigChangedEvent } from '@d3ro/core/types' import { getLogger } from './LoggerService' const logger = getLogger('ConfigService') // ============================================================ // SaaS 빌드 타임 주입 (electron.vite.config.ts의 define으로 박힘) // ============================================================ // D3RO_SUPABASE_URL / D3RO_SUPABASE_ANON_KEY가 비어있지 않으면 // 데스크톱 사용자가 Settings에서 직접 입력할 필요 없이 SaaS 인스턴스에 자동 연결. // 빈 문자열이면 기존 동작(사용자 입력) fallback. const BUILD_TIME_SUPABASE_URL: string = process.env.D3RO_SUPABASE_URL ?? '' const BUILD_TIME_SUPABASE_ANON_KEY: string = process.env.D3RO_SUPABASE_ANON_KEY ?? '' export function isSupabaseBuildTimeConfigured(): boolean { return BUILD_TIME_SUPABASE_URL.length > 0 && BUILD_TIME_SUPABASE_ANON_KEY.length > 0 } export function getBuildTimeSupabaseUrl(): string { return BUILD_TIME_SUPABASE_URL } export function getBuildTimeSupabaseAnonKey(): string { return BUILD_TIME_SUPABASE_ANON_KEY } // electron-store v10은 ESM 전용이므로 동적 import 필요 interface ElectronStore> { get(key: K): T[K] set(key: K, value: T[K]): void store: T } const CONFIG_DEFAULTS: AppConfig = { theme: 'auto', language: 'ko', closeToTray: true, autoLaunch: false, soundEnabled: true, selectedDeviceId: null, sttModelId: 'base', sttLanguage: 'auto', ttsVoiceId: null, ttsSpeed: 1.0, ollamaServerUrl: 'http://localhost:11434', llmModelId: null, // Phase 3.2: 기본값은 'local' — 누구나 로그인 없이 로컬 Ollama로 쓸 수 있는 // 엔트리 전략. 사용자가 Settings에서 'premium'으로 전환 시 로그인 + 구독 필요. llmBackend: 'local' as const, defaultLLMAction: 'refine', dictationShortcut: { keyCode: 0xa5, // Right Alt ctrl: false, alt: false, shift: false, meta: false, displayLabel: 'Right Alt' }, handsFreeShortcut: { keyCode: 0xa5, ctrl: false, alt: false, shift: false, meta: false, displayLabel: 'Right Alt (double)' }, commandShortcut: { keyCode: 0xa5, ctrl: true, alt: false, shift: false, meta: false, displayLabel: 'Ctrl + Right Alt' }, captionShortcut: { keyCode: 0xa5, ctrl: true, alt: false, shift: true, meta: false, displayLabel: 'Ctrl + Shift + Right Alt' }, hotkeyEnabled: true, insertMethod: 'clipboard', autoInsert: true, maxHistoryEntries: 1000, dictationEnabled: true, agentModeEnabled: false, handsFreeEnabled: false, screenContextEnabled: false, hfToken: '', diarizationEnabled: false, supabaseUrl: '', supabaseAnonKey: '', cloudSyncLastAt: null, onboardingCompleted: false, } let store: ElectronStore | null = null const emitter = new EventEmitter() export async function initConfigService(): Promise { const { default: Store } = await import('electron-store') store = new Store({ name: 'd3ro-voice-config', defaults: CONFIG_DEFAULTS }) logger.info('ConfigService initialized') } export function getConfigService(): ElectronStore | null { return store } export function configGet(key: K): AppConfig[K] { // SaaS 빌드 타임 주입은 사용자 설정보다 우선. if (key === 'supabaseUrl' && BUILD_TIME_SUPABASE_URL.length > 0) { return BUILD_TIME_SUPABASE_URL as AppConfig[K] } if (key === 'supabaseAnonKey' && BUILD_TIME_SUPABASE_ANON_KEY.length > 0) { return BUILD_TIME_SUPABASE_ANON_KEY as AppConfig[K] } if (!store) { logger.warn(`ConfigService not initialized, returning default for "${key}"`) return CONFIG_DEFAULTS[key] } return store.get(key) } export function configSet(key: K, value: AppConfig[K]): void { // SaaS 빌드 타임 주입된 값은 변경 차단 (사용자가 잘못된 값으로 덮어쓰는 것 방지). if ( (key === 'supabaseUrl' || key === 'supabaseAnonKey') && isSupabaseBuildTimeConfigured() ) { logger.warn(`Refusing to override build-time SaaS config: "${key}"`) return } if (!store) { logger.warn(`ConfigService not initialized, cannot set "${key}"`) return } const previousValue = store.get(key) store.set(key, value) const event: ConfigChangedEvent = { key, value, previousValue } emitter.emit('config-changed', event) logger.debug(`Config changed: ${key}`) } export function configGetAll(): AppConfig { if (!store) return { ...CONFIG_DEFAULTS } return store.store } export function configReset(key?: keyof AppConfig): void { if (!store) return if (key) { store.set(key, CONFIG_DEFAULTS[key]) } else { store.store = { ...CONFIG_DEFAULTS } } } export function onConfigChanged(callback: (event: ConfigChangedEvent) => void): () => void { emitter.on('config-changed', callback) return () => emitter.off('config-changed', callback) }