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
|
|
@ -15,6 +15,7 @@ import { getSoundEffectService } from './services/SoundEffectService'
|
||||||
import { getAutoLaunchService } from './services/AutoLaunchService'
|
import { getAutoLaunchService } from './services/AutoLaunchService'
|
||||||
import { getAudioCaptureService } from './services/AudioCaptureService'
|
import { getAudioCaptureService } from './services/AudioCaptureService'
|
||||||
import { initLicenseService } from './services/LicenseService'
|
import { initLicenseService } from './services/LicenseService'
|
||||||
|
import { openLocal } from './db'
|
||||||
import {
|
import {
|
||||||
createMainWindow,
|
createMainWindow,
|
||||||
getMainWindow,
|
getMainWindow,
|
||||||
|
|
@ -45,8 +46,10 @@ export async function bootstrap(): Promise<void> {
|
||||||
const steps: BootstrapStep[] = [
|
const steps: BootstrapStep[] = [
|
||||||
{ name: 'logger', critical: false, fn: initLogger },
|
{ name: 'logger', critical: false, fn: initLogger },
|
||||||
{ name: 'config', critical: false, fn: initConfig },
|
{ name: 'config', critical: false, fn: initConfig },
|
||||||
// Phase 1 (SaaS 빅뱅): DB는 더 이상 bootstrap 타이밍에 열지 않는다.
|
// 빅뱅 Phase 1.5: 앱 시작 시 익명 로컬 DB를 먼저 연다.
|
||||||
// CloudSyncService._onAuthenticated(session)가 사용자별 DB를 연다.
|
// 무료 로컬 모드가 사용자 entry point — 회원가입 없이 바로 킬러 피처 사용 가능.
|
||||||
|
// CloudSyncService._onAuthenticated()에서 로그인 시점에 user DB로 스위치한다.
|
||||||
|
{ name: 'database', critical: true, fn: initLocalDatabase },
|
||||||
{ name: 'license', critical: false, fn: initLicense },
|
{ name: 'license', critical: false, fn: initLicense },
|
||||||
{ name: 'create-windows', critical: true, fn: createWindows },
|
{ name: 'create-windows', critical: true, fn: createWindows },
|
||||||
{ name: 'tray', critical: false, fn: initTray },
|
{ name: 'tray', critical: false, fn: initTray },
|
||||||
|
|
@ -91,6 +94,11 @@ async function initConfig(): Promise<void> {
|
||||||
initConfigService()
|
initConfigService()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initLocalDatabase(): Promise<void> {
|
||||||
|
const { created, dbPath } = openLocal()
|
||||||
|
logger.info(`[bootstrap] local database opened: ${dbPath} (created=${created})`)
|
||||||
|
}
|
||||||
|
|
||||||
async function initLicense(): Promise<void> {
|
async function initLicense(): Promise<void> {
|
||||||
initLicenseService()
|
initLicenseService()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,16 @@ const logger = getLogger('database')
|
||||||
// 모듈 상태
|
// 모듈 상태
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로컬(익명) 모드 user id. OAuth 로그인 없이 동작하는 무료 로컬 모드의
|
||||||
|
* 데이터 디렉토리 식별자. `userData/users/_local/d3ro.db`에 저장된다.
|
||||||
|
*
|
||||||
|
* 비즈니스 모델 관점:
|
||||||
|
* - `_local`: 무료 익명 사용자 entry point (킬러 피처 — 회원가입 0, 로컬 STT/LLM)
|
||||||
|
* - 실제 uuid: OAuth 로그인 후 클라우드 sync + premium 기능 (유료 SaaS 수익화)
|
||||||
|
*/
|
||||||
|
export const LOCAL_USER_ID = '_local'
|
||||||
|
|
||||||
let currentUserId: string | null = null
|
let currentUserId: string | null = null
|
||||||
let sqlite: Database.Database | null = null
|
let sqlite: Database.Database | null = null
|
||||||
let db: BetterSQLite3Database<typeof schema> | null = null
|
let db: BetterSQLite3Database<typeof schema> | null = null
|
||||||
|
|
@ -38,6 +48,11 @@ function getUserDbPath(userId: string): string {
|
||||||
return path.join(app.getPath('userData'), 'users', userId, 'd3ro.db')
|
return path.join(app.getPath('userData'), 'users', userId, 'd3ro.db')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 현재 모드가 익명 로컬 모드인지. */
|
||||||
|
export function isLocalMode(): boolean {
|
||||||
|
return currentUserId === LOCAL_USER_ID
|
||||||
|
}
|
||||||
|
|
||||||
/** 레거시(V1) 단일 DB 파일 경로. 한 번도 정식 서비스되지 않았으므로 발견 시 silent archive. */
|
/** 레거시(V1) 단일 DB 파일 경로. 한 번도 정식 서비스되지 않았으므로 발견 시 silent archive. */
|
||||||
function getLegacyDbPath(): string {
|
function getLegacyDbPath(): string {
|
||||||
return path.join(app.getPath('userData'), 'd3ro-voice.db')
|
return path.join(app.getPath('userData'), 'd3ro-voice.db')
|
||||||
|
|
@ -338,6 +353,16 @@ export function openForUser(userId: string): OpenForUserResult {
|
||||||
return { created: !existed, dbPath }
|
return { created: !existed, dbPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 익명 로컬 모드 DB 열기. 앱 시작 시 기본으로 호출되는 entry point.
|
||||||
|
* 로그인 없이 바로 동작해야 하는 킬러 피처의 데이터 저장소.
|
||||||
|
*
|
||||||
|
* `userData/users/_local/d3ro.db`에 저장. 로그아웃 복귀 시 같은 경로로 재오픈.
|
||||||
|
*/
|
||||||
|
export function openLocal(): OpenForUserResult {
|
||||||
|
return openForUser(LOCAL_USER_ID)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 현재 열려있는 DB 닫기. 아무 것도 안 열려 있으면 no-op.
|
* 현재 열려있는 DB 닫기. 아무 것도 안 열려 있으면 no-op.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import {
|
||||||
import { eq, gt } from 'drizzle-orm'
|
import { eq, gt } from 'drizzle-orm'
|
||||||
import { getLogger } from './LoggerService'
|
import { getLogger } from './LoggerService'
|
||||||
import { configGet, isSupabaseBuildTimeConfigured } from './ConfigService'
|
import { configGet, isSupabaseBuildTimeConfigured } from './ConfigService'
|
||||||
import { getDatabase, openForUser, closeCurrent } from '../db'
|
import { getDatabase, openForUser, openLocal, closeCurrent } from '../db'
|
||||||
import { history, dictionary, meetingSessions, meetingMemos, meetingDocuments } from '../db/schema'
|
import { history, dictionary, meetingSessions, meetingMemos, meetingDocuments } from '../db/schema'
|
||||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||||
|
|
||||||
|
|
@ -219,12 +219,22 @@ class CloudSyncService extends EventEmitter {
|
||||||
this._session = null
|
this._session = null
|
||||||
this._clearStoredRefreshToken()
|
this._clearStoredRefreshToken()
|
||||||
|
|
||||||
// 5) renderer에 auth-changed(null) emit — 게이트가 LoginScreen으로 복귀
|
// 5) 사용자 DB 닫고 로컬 모드 DB로 복귀
|
||||||
this.emit('auth-changed', { user: null })
|
// — 로그아웃 후에도 앱은 익명 로컬 모드로 계속 동작 (entry point 철학)
|
||||||
|
|
||||||
// 6) DB close — 여기 이후엔 getDatabase() throw
|
|
||||||
closeCurrent()
|
closeCurrent()
|
||||||
logger.info('Signed out and database closed')
|
try {
|
||||||
|
const { dbPath } = openLocal()
|
||||||
|
logger.info(`[signOut] Reverted to local DB: ${dbPath}`)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`[signOut] Local DB reopen failed: ${err instanceof Error ? err.message : String(err)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) renderer에 auth-changed(null) emit — Settings는 다시 로그인 화면으로,
|
||||||
|
// 메인 UI는 로컬 모드로 계속 동작
|
||||||
|
this.emit('auth-changed', { user: null })
|
||||||
|
logger.info('Signed out — continuing in local mode')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 상태 조회 ──────────────────────────────────────────
|
// ── 상태 조회 ──────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,20 @@
|
||||||
// src/renderer/App.tsx — 루트 컴포넌트
|
// src/renderer/App.tsx — 루트 컴포넌트
|
||||||
// 테마 시스템: auto(시스템) / dark / light. 기본은 auto → 다크.
|
// 테마 시스템: auto(시스템) / dark / light. 기본은 auto → 다크.
|
||||||
// i18n: I18nProvider가 전체 트리를 감쌈. ConfigService에서 언어 로드.
|
// i18n: I18nProvider가 전체 트리를 감쌈. ConfigService에서 언어 로드.
|
||||||
|
//
|
||||||
|
// 인증 정책: 무료 로컬 모드가 사용자 entry point.
|
||||||
|
// 앱은 로그인 없이 바로 메인 UI에 진입해서 로컬 STT/LLM을 사용할 수 있다.
|
||||||
|
// OAuth 로그인은 Settings → Cloud Sync 섹션에서 선택적으로 수행 (유료 SaaS 경로).
|
||||||
|
|
||||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
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 { getTheme } from '@d3ro/ui/theme'
|
||||||
import { I18nProvider, type I18nStorage, type Locale } from '@d3ro/i18n'
|
import { I18nProvider, type I18nStorage, type Locale } from '@d3ro/i18n'
|
||||||
import { AppLayout } from './components/AppLayout'
|
import { AppLayout } from './components/AppLayout'
|
||||||
import { UpgradePromptModal } from './components/UpgradePromptModal'
|
import { UpgradePromptModal } from './components/UpgradePromptModal'
|
||||||
import { LoginScreen } from './components/LoginScreen'
|
|
||||||
import { startSystemAudioCapture, stopSystemAudioCapture } from './utils/systemAudioCapture'
|
import { startSystemAudioCapture, stopSystemAudioCapture } from './utils/systemAudioCapture'
|
||||||
import type { ThemeMode, ConfigChangedEvent } from '@d3ro/core/types'
|
import type { ThemeMode, ConfigChangedEvent } from '@d3ro/core/types'
|
||||||
|
|
||||||
// CloudSync 게이트 상태
|
|
||||||
type AuthGateState =
|
|
||||||
| { status: 'loading' }
|
|
||||||
| { status: 'authenticated' }
|
|
||||||
| { status: 'login-required' }
|
|
||||||
| { status: 'legacy' } // 빌드 타임 SaaS 모드 아님 → 기존 동작 (선택적 클라우드)
|
|
||||||
|
|
||||||
// Electron ConfigService에 바인딩된 i18n 영속화 어댑터
|
// Electron ConfigService에 바인딩된 i18n 영속화 어댑터
|
||||||
const electronI18nStorage: I18nStorage = {
|
const electronI18nStorage: I18nStorage = {
|
||||||
load: async () => {
|
load: async () => {
|
||||||
|
|
@ -34,7 +30,6 @@ export function App(): React.ReactElement {
|
||||||
const [themeMode, setThemeMode] = useState<ThemeMode>('auto')
|
const [themeMode, setThemeMode] = useState<ThemeMode>('auto')
|
||||||
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
|
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)')
|
||||||
const systemAudioCleanupRef = useRef<(() => void) | null>(null)
|
const systemAudioCleanupRef = useRef<(() => void) | null>(null)
|
||||||
const [authGate, setAuthGate] = useState<AuthGateState>({ status: 'loading' })
|
|
||||||
|
|
||||||
// 설정에서 테마 로드 + 변경 감지
|
// 설정에서 테마 로드 + 변경 감지
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -52,38 +47,6 @@ export function App(): React.ReactElement {
|
||||||
return unsub
|
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(() => {
|
useEffect(() => {
|
||||||
const unsubStart = window.electronAPI.caption.onStartSystemAudio(() => {
|
const unsubStart = window.electronAPI.caption.onStartSystemAudio(() => {
|
||||||
|
|
@ -111,39 +74,12 @@ export function App(): React.ReactElement {
|
||||||
|
|
||||||
const theme = useMemo(() => getTheme(themeMode, prefersDark), [themeMode, prefersDark])
|
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 (
|
return (
|
||||||
<I18nProvider storage={electronI18nStorage}>
|
<I18nProvider storage={electronI18nStorage}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
{gateContent}
|
<AppLayout />
|
||||||
|
<UpgradePromptModal />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -153,18 +153,16 @@ V2 6차 고도화 (2026-04-11):
|
||||||
- `configGet('supabaseUrl'/'supabaseAnonKey')`은 빌드 타임 값 절대 우선
|
- `configGet('supabaseUrl'/'supabaseAnonKey')`은 빌드 타임 값 절대 우선
|
||||||
- `configSet`는 빌드 타임 모드일 때 supabaseUrl/AnonKey 변경 거부
|
- `configSet`는 빌드 타임 모드일 때 supabaseUrl/AnonKey 변경 거부
|
||||||
|
|
||||||
### SaaS [2] OAuth 첫 실행 강제 게이트
|
### SaaS [2] OAuth 첫 실행 강제 게이트 → **철회 (Phase 1.5에서 로컬 모드 entry point로 변경)**
|
||||||
- `CloudSyncState`에 `saasMode: boolean` 필드 추가
|
- `CloudSyncState`에 `saasMode: boolean` 필드 추가 (유지)
|
||||||
- `LoginScreen.tsx` 신규: Google/GitHub 버튼만, MetalCard + PhosphorText, URL/Key 입력 노출 0
|
- `CloudSyncSection`: saasMode에서 OAuth 버튼만, legacy에서는 URL/Key 입력 UI (유지)
|
||||||
- `App.tsx` `AuthGate` 상태머신: `loading | login-required | authenticated | legacy`
|
- ~~`LoginScreen.tsx` 강제 게이트~~ → 제거 (App.tsx에서 사용 안 함, 파일 자체는 유지)
|
||||||
- `saasMode && !authenticated` → LoginScreen 강제
|
- ~~`App.tsx` AuthGate 상태머신~~ → 제거 (항상 메인 UI 렌더)
|
||||||
- `saasMode && authenticated` → 기존 AppLayout
|
|
||||||
- `!saasMode` → legacy (기존 동작 유지)
|
**철회 이유** (2026-04-11 빅뱅 Phase 1.5):
|
||||||
- `onAuthChanged` 구독으로 로그인 후 자동 전환
|
사용자 피드백 "무료 로컬은 사용자 엔트리 포인트가 될 거구, 수익화는 클라우드 SaaS 서비스임".
|
||||||
- `CloudSyncSection` (Settings 안):
|
로컬 STT/LLM이 이 앱의 킬러 피처 → 회원가입 없이 즉시 체험 가능해야 함 (onboarding funnel 입구).
|
||||||
- `saasMode`일 때 URL/Key 입력 필드 완전 숨김 (OAuth 버튼만)
|
OAuth provider도 아직 Supabase에 설정 안 된 상태. 강제 게이트는 철학 + 인프라 양쪽 모두 위배.
|
||||||
- `!saasMode` (legacy)일 때만 기존 입력 화면 유지
|
|
||||||
- 검증: 이전엔 항상 떴던 `CloudSync disabled — Supabase URL/key not configured` 로그 사라짐
|
|
||||||
|
|
||||||
### SaaS [3] 사용자별 SQLite 격리 — ✅ **완료 (2026-04-11, 빅뱅 Phase 1)**
|
### SaaS [3] 사용자별 SQLite 격리 — ✅ **완료 (2026-04-11, 빅뱅 Phase 1)**
|
||||||
|
|
||||||
|
|
@ -210,6 +208,40 @@ V2 6차 고도화 (2026-04-11):
|
||||||
- `~/Library/Application Support/d3ro-voice/users/${uuid}/d3ro.db` 파일 생성 확인
|
- `~/Library/Application Support/d3ro-voice/users/${uuid}/d3ro.db` 파일 생성 확인
|
||||||
- 기존 `d3ro-voice.db`가 있는 환경에서 archive rename 동작 확인
|
- 기존 `d3ro-voice.db`가 있는 환경에서 archive rename 동작 확인
|
||||||
|
|
||||||
|
### SaaS [3.5] 로컬 모드 = entry point (Phase 1.5, 2026-04-11)
|
||||||
|
|
||||||
|
> 비즈니스 모델: 무료 로컬 모드 = onboarding funnel 입구 (킬러 피처),
|
||||||
|
> 유료 클라우드 SaaS = 수익화 (OAuth 로그인 + pro 구독 + premium 모델 + sync).
|
||||||
|
> SaaS [2] OAuth 강제 게이트 철회.
|
||||||
|
|
||||||
|
- **`db/index.ts`**
|
||||||
|
- `LOCAL_USER_ID = '_local'` 상수 export
|
||||||
|
- `openLocal()` 헬퍼 — `openForUser('_local')` 래퍼. 경로: `users/_local/d3ro.db`
|
||||||
|
- `isLocalMode()` 헬퍼 — `currentUserId === LOCAL_USER_ID`
|
||||||
|
- **`bootstrap.ts`**
|
||||||
|
- `database` step 복원 (critical). `openLocal()` 호출로 앱 시작 시 로컬 DB 자동 오픈
|
||||||
|
- 순서: logger → config → **database (local)** → license → windows → tray → ipc → ... → cloud-sync
|
||||||
|
- **`CloudSyncService._onAuthenticated(session)`**
|
||||||
|
- `openForUser(userId)` — 내부에서 기존 로컬 DB close 후 사용자 DB 재오픈 (자동)
|
||||||
|
- auth-changed(user) emit 후 Realtime 시작
|
||||||
|
- **`CloudSyncService._onSignOut()`**
|
||||||
|
- VoiceMode cancel → MeetingMode stopRecording → Caption stop → Realtime stop → auth.signOut → token clear
|
||||||
|
- `closeCurrent()` → `openLocal()` — **로컬 모드 DB로 복귀** (앱은 계속 로컬 모드로 동작)
|
||||||
|
- auth-changed(null) emit
|
||||||
|
- **`App.tsx`**
|
||||||
|
- AuthGate 상태머신 완전 제거. `LoginScreen` import 제거.
|
||||||
|
- 항상 `<AppLayout /> <UpgradePromptModal />` 렌더링 (로그인/로그아웃 무관)
|
||||||
|
- **`LoginScreen.tsx`**
|
||||||
|
- 파일 유지 (dead code — 추후 Welcome 화면 재활용 가능)
|
||||||
|
- **검증**
|
||||||
|
- desktop `tsc --noEmit` ✅
|
||||||
|
- desktop `npm run build` ✅ (main 992kB, renderer 2,126kB)
|
||||||
|
- dev 런타임 ✅:
|
||||||
|
- `[bootstrap] local database opened: .../users/_local/d3ro.db (created=true)`
|
||||||
|
- `[bootstrap] database initialized` → 나머지 step 정상
|
||||||
|
- `Main window shown` — LoginScreen 없이 바로 메인 UI
|
||||||
|
- 파일시스템: `users/_local/d3ro.db` + WAL/SHM 정상 생성
|
||||||
|
|
||||||
## 빅뱅 다음 세션 목표 (2026-04-11 wrap-up)
|
## 빅뱅 다음 세션 목표 (2026-04-11 wrap-up)
|
||||||
|
|
||||||
데스크톱을 진짜 SaaS 클라이언트로 완성. 자세한 계획 + 단계별 prompt는
|
데스크톱을 진짜 SaaS 클라이언트로 완성. 자세한 계획 + 단계별 prompt는
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue