// src/renderer/App.tsx — 루트 컴포넌트 // 테마 시스템: auto(시스템) / dark / light. 기본은 auto → 다크. // i18n: I18nProvider가 전체 트리를 감쌈. ConfigService에서 언어 로드. // // 인증 정책: 무료 로컬 모드가 사용자 entry point. // 앱은 로그인 없이 바로 메인 UI에 진입해서 로컬 STT/LLM을 사용할 수 있다. // OAuth 로그인은 Settings → Cloud Sync 섹션에서 선택적으로 수행 (유료 SaaS 경로). import { useState, useEffect, useMemo, useRef } from 'react' import { ThemeProvider, CssBaseline, useMediaQuery } from '@mui/material' import { getTheme } from '@d3ro/ui/theme' import { I18nProvider, useI18n, type I18nStorage, type Locale } from '@d3ro/i18n' import { AppLayout } from './components/AppLayout' import { UpgradePromptModal } from './components/UpgradePromptModal' import { startSystemAudioCapture, stopSystemAudioCapture } from './utils/systemAudioCapture' import type { ThemeMode, ConfigChangedEvent } from '@d3ro/core/types' // Electron ConfigService에 바인딩된 i18n 영속화 어댑터 const electronI18nStorage: I18nStorage = { load: async () => { const result = await window.electronAPI.config.get({ key: 'language' }) return result.success && result.data ? (result.data as string) : null }, save: (locale: Locale) => { window.electronAPI.config.set({ key: 'language', value: locale }) } } /** 다른 기기(모바일)에서 바꾼 언어가 동기화로 들어오면 화면 언어도 바꾼다. */ function SyncedLocale(): null { const { locale, setLocale } = useI18n() useEffect( () => window.electronAPI.config.onChanged((e: ConfigChangedEvent) => { if (e.key === 'language' && typeof e.value === 'string' && e.value !== locale) { setLocale(e.value as Locale) } }), [locale, setLocale], ) return null } export function App(): React.ReactElement { const [themeMode, setThemeMode] = useState('auto') const prefersDark = useMediaQuery('(prefers-color-scheme: dark)') const systemAudioCleanupRef = useRef<(() => void) | null>(null) // 설정에서 테마 로드 + 변경 감지 useEffect(() => { window.electronAPI.config.getTheme().then((result) => { if (result.success) { setThemeMode(result.data) } }) const unsub = window.electronAPI.config.onChanged((e: ConfigChangedEvent) => { if (e.key === 'theme') { setThemeMode(e.value as ThemeMode) } }) return unsub }, []) // 시스템 오디오 캡처: 메인 프로세스의 시작/중지 요청에 응답 useEffect(() => { const unsubStart = window.electronAPI.caption.onStartSystemAudio(() => { startSystemAudioCapture((pcm16Buffer) => { window.electronAPI.caption.sendSystemAudioData(pcm16Buffer) }).catch(() => { // 시스템 오디오 캡처 실패 시 무시 — 마이크 자막만 사용 }) }) const unsubStop = window.electronAPI.caption.onStopSystemAudio(() => { stopSystemAudioCapture() }) systemAudioCleanupRef.current = () => { stopSystemAudioCapture() } return () => { unsubStart() unsubStop() systemAudioCleanupRef.current?.() } }, []) const theme = useMemo(() => getTheme(themeMode, prefersDark), [themeMode, prefersDark]) return ( ) }