feat(desktop): SaaS [3.5] 로컬 모드 = entry point (빅뱅 Phase 1.5)
비즈니스 모델 수정: - 무료 로컬 모드 = 사용자 onboarding funnel 입구 (킬러 피처, 회원가입 0) - 유료 클라우드 SaaS = 수익화 (OAuth 로그인 + pro + premium 모델 + sync) - 이전 세션 SaaS [2] OAuth 강제 게이트 철회 db/index.ts: - LOCAL_USER_ID = '_local' 상수 export - openLocal() 헬퍼 — userData/users/_local/d3ro.db - isLocalMode() 헬퍼 bootstrap.ts: - database step 복원 (critical). openLocal() 호출로 앱 시작 시 로컬 DB 자동 오픈 — 로그인 없이 즉시 메인 UI 진입 가능 CloudSyncService._onSignOut(): - closeCurrent() 후 openLocal() 호출 — 로그아웃 시 로컬 모드 DB로 복귀 - 앱은 계속 로컬 모드로 동작 (entry point 철학) App.tsx: - AuthGate 상태머신 제거 (loading/login-required/authenticated/legacy) - LoginScreen import 제거, 항상 AppLayout 렌더 - LoginScreen.tsx 파일은 유지 (추후 Welcome 화면 재활용 가능) 검증: - desktop tsc --noEmit ✅ - desktop build ✅ - dev 런타임: [bootstrap] local database opened → Main window shown - users/_local/d3ro.db 파일 생성 확인
This commit is contained in:
parent
816f527592
commit
1a01cea33c
5 changed files with 102 additions and 91 deletions
|
|
@ -1,24 +1,20 @@
|
|||
// 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, Box, CircularProgress } from '@mui/material'
|
||||
import { ThemeProvider, CssBaseline, useMediaQuery } from '@mui/material'
|
||||
import { getTheme } from '@d3ro/ui/theme'
|
||||
import { I18nProvider, type I18nStorage, type Locale } from '@d3ro/i18n'
|
||||
import { AppLayout } from './components/AppLayout'
|
||||
import { UpgradePromptModal } from './components/UpgradePromptModal'
|
||||
import { LoginScreen } from './components/LoginScreen'
|
||||
import { startSystemAudioCapture, stopSystemAudioCapture } from './utils/systemAudioCapture'
|
||||
import type { ThemeMode, ConfigChangedEvent } from '@d3ro/core/types'
|
||||
|
||||
// CloudSync 게이트 상태
|
||||
type AuthGateState =
|
||||
| { status: 'loading' }
|
||||
| { status: 'authenticated' }
|
||||
| { status: 'login-required' }
|
||||
| { status: 'legacy' } // 빌드 타임 SaaS 모드 아님 → 기존 동작 (선택적 클라우드)
|
||||
|
||||
// Electron ConfigService에 바인딩된 i18n 영속화 어댑터
|
||||
const electronI18nStorage: I18nStorage = {
|
||||
load: async () => {
|
||||
|
|
@ -34,7 +30,6 @@ export function App(): React.ReactElement {
|
|||
const [themeMode, setThemeMode] = useState<ThemeMode>('auto')
|
||||
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
|
||||
const systemAudioCleanupRef = useRef<(() => void) | null>(null)
|
||||
const [authGate, setAuthGate] = useState<AuthGateState>({ status: 'loading' })
|
||||
|
||||
// 설정에서 테마 로드 + 변경 감지
|
||||
useEffect(() => {
|
||||
|
|
@ -52,38 +47,6 @@ export function App(): React.ReactElement {
|
|||
return unsub
|
||||
}, [])
|
||||
|
||||
// SaaS 인증 게이트: 빌드 타임에 Supabase가 박힌 경우(=SaaS 모드)에만 활성화.
|
||||
// 인증 안 된 상태면 LoginScreen 강제. 로그인 후 자동 진입.
|
||||
// 빌드 타임 env가 없으면(legacy 개발자 모드) 게이트를 비활성화 — 기존 Settings UI 유지.
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
void window.electronAPI.cloudSync.getState().then((r) => {
|
||||
if (cancelled || !r.success) return
|
||||
if (!r.data.saasMode) {
|
||||
setAuthGate({ status: 'legacy' })
|
||||
return
|
||||
}
|
||||
setAuthGate({
|
||||
status: r.data.authenticated ? 'authenticated' : 'login-required'
|
||||
})
|
||||
})
|
||||
|
||||
const unsub = window.electronAPI.cloudSync.onAuthChanged((payload) => {
|
||||
if (cancelled) return
|
||||
// saasMode 여부는 변하지 않으므로 인증 상태만 토글.
|
||||
setAuthGate((prev) => {
|
||||
if (prev.status === 'legacy') return prev
|
||||
return { status: payload.user ? 'authenticated' : 'login-required' }
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
unsub()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 시스템 오디오 캡처: 메인 프로세스의 시작/중지 요청에 응답
|
||||
useEffect(() => {
|
||||
const unsubStart = window.electronAPI.caption.onStartSystemAudio(() => {
|
||||
|
|
@ -111,39 +74,12 @@ export function App(): React.ReactElement {
|
|||
|
||||
const theme = useMemo(() => getTheme(themeMode, prefersDark), [themeMode, prefersDark])
|
||||
|
||||
const gateContent = (() => {
|
||||
if (authGate.status === 'loading') {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
if (authGate.status === 'login-required') {
|
||||
return <LoginScreen />
|
||||
}
|
||||
// 'authenticated' or 'legacy' → 메인 UI 렌더
|
||||
return (
|
||||
<>
|
||||
<AppLayout />
|
||||
<UpgradePromptModal />
|
||||
</>
|
||||
)
|
||||
})()
|
||||
|
||||
return (
|
||||
<I18nProvider storage={electronI18nStorage}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{gateContent}
|
||||
<AppLayout />
|
||||
<UpgradePromptModal />
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue