AppConfig 정식화: - packages/core types.ts: onboardingCompleted: boolean 추가 - ConfigService CONFIG_DEFAULTS 동기화 - 타입 캐스팅 3곳 제거 (AppLayout/OnboardingModal/SettingsModal) OnboardingModal 6 step 확장: - 기존: 환영 → 마이크 → 핫키 → Ollama → 완료 - 신규: 환영 → 마이크 → 핫키 → Ollama → Cloud Sync(선택) → 완료 - saasMode === true 일 때만 Cloud Sync step 노출 - Step 내용: tagline + 3 benefit (sync/premium LLM/team share) + "설정에서 언제든지 로그인" 안내 - 온보딩 scope 안에서는 로그인 버튼 미노출 — 부담 제로 유지 (Settings의 CloudSyncSection이 유일한 OAuth 진입점) i18n: - ko/en에 onboarding.cloud.* 6개 키 추가 - 나머지 10개 locale은 추후 일괄 (Phase 2 scope 밖) 의도적 제외: - Supabase profiles.onboarded_at — OAuth provider 설정 전까진 의미 없음 - OAuth 후 환영 페이지 — Phase 4 티어 enforcement와 함께 - STT 모델 선택 — sidecar가 자동 다운로드
176 lines
4.9 KiB
TypeScript
176 lines
4.9 KiB
TypeScript
// 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<T extends Record<string, unknown>> {
|
|
get<K extends keyof T>(key: K): T[K]
|
|
set<K extends keyof T>(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,
|
|
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<AppConfig> | null = null
|
|
const emitter = new EventEmitter()
|
|
|
|
export async function initConfigService(): Promise<void> {
|
|
const { default: Store } = await import('electron-store')
|
|
store = new Store<AppConfig>({
|
|
name: 'd3ro-voice-config',
|
|
defaults: CONFIG_DEFAULTS
|
|
})
|
|
logger.info('ConfigService initialized')
|
|
}
|
|
|
|
export function getConfigService(): ElectronStore<AppConfig> | null {
|
|
return store
|
|
}
|
|
|
|
export function configGet<K extends keyof AppConfig>(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<K extends keyof AppConfig>(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)
|
|
}
|