feat(desktop): SaaS [3] 사용자별 SQLite 격리 (빅뱅 Phase 1)

db/index.ts 전면 재설계:
- initDatabase/closeDatabase 폐기
- openForUser(userId), closeCurrent, getDatabase, getCurrentUserId 신 API
- 경로: userData/users/${userId}/d3ro.db
- applySchema() 추출 — 새 user DB마다 테이블 + ALTER 마이그레이션 재실행
- legacy d3ro-voice.db는 silent archive (archive-legacy-ISO.db로 rename만)

CloudSyncService 인증 lifecycle SSOT:
- _onAuthenticated(session, reason) 단일 진입점
  init() 세션 복원, handleAuthCallback() 신규 로그인 모두 수렴
  DB open 실패 시 세션 롤백 + sync-error emit
- _onSignOut() 안전 종료 시퀀스
  VoiceMode cancel → MeetingMode stopRecording → Caption stop →
  Realtime stop → auth.signOut → token clear →
  auth-changed(null) emit → closeCurrent
- VoiceMode/MeetingMode/Caption은 dynamic import로 순환 의존 회피

bootstrap.ts:
- database step 제거 (DB는 _onAuthenticated에서만 열림)
- cloud-sync step이 마지막 유지 (세션 복원 → DB open → auth-changed)
This commit is contained in:
윤찬 2026-04-11 09:58:00 +09:00
parent 5ad1d19c3a
commit 816f527592
4 changed files with 354 additions and 79 deletions

View file

@ -166,7 +166,49 @@ V2 6차 고도화 (2026-04-11):
- `!saasMode` (legacy)일 때만 기존 입력 화면 유지
- 검증: 이전엔 항상 떴던 `CloudSync disabled — Supabase URL/key not configured` 로그 사라짐
### SaaS [3] 사용자별 SQLite 격리 — **다음 세션(빅뱅) 예정**
### SaaS [3] 사용자별 SQLite 격리 — ✅ **완료 (2026-04-11, 빅뱅 Phase 1)**
- **`apps/desktop/src/main/db/index.ts` 전면 재설계**
- 구 `initDatabase()` / `closeDatabase()` 폐기 (이름 차원에서 제거)
- 새 API: `openForUser(userId)`, `closeCurrent()`, `getDatabase()`, `getCurrentUserId()`, `isDatabaseOpen()`, `getCurrentDbPath()`
- 경로: `userData/users/${userId}/d3ro.db` (userId path-separator/.. 가드)
- `applySchema(s)`로 테이블 + 인라인 ALTER 마이그레이션 추출 — 새 DB 생성 시마다 재실행
- 모듈 상태: `currentUserId | null`, `sqlite`, `db` — closeCurrent 후 모두 null
- 같은 userId 재호출 시 no-op, 다른 userId면 먼저 close 후 재오픈
- `openForUser` 실패 시 부분 상태 정리 + `D3ROError(DBOpenFailed)` throw
- **legacy DB 정책**
- V1 `userData/d3ro-voice.db`는 한 번도 정식 서비스 안 됨 → `archiveLegacyDbIfExists()`로 첫 `openForUser` 직전 silent rename (`d3ro-voice.legacy-${ISO timestamp}.db`)
- 파일 삭제 없음, dialog 없음, 데이터 이전 없음 ("클린하게" 요청)
- **`CloudSyncService` 인증 lifecycle 단일 진입점** (SSOT)
- `_onAuthenticated(session, { reason: 'restore' | 'signin' })`:
1. `this._session = session`
2. `openForUser(userId)` — 실패 시 세션 무효화 + `auth-changed(null)` + `sync-error` emit 후 return
3. `auth-changed(user)` emit (DB가 열린 뒤에)
4. `startRealtime()` 자동 시작
- `init()` 세션 복원, `handleAuthCallback(code)` 신규 로그인 모두 이 함수로 수렴
- `_onSignOut()`:
1. VoiceMode active → `cancelSession()` (녹음 진행 중이면 강제 취소)
2. MeetingMode active → `stopRecording()` (await)
3. Caption → `stop()` (await)
4. `stopRealtime()`
5. Supabase `auth.signOut()`
6. `_session = null` + `_clearStoredRefreshToken()`
7. `auth-changed(null)` emit — 렌더러 LoginScreen 복귀
8. `closeCurrent()` — 이 시점 이후 `getDatabase()`는 throw
- `signOut()` public 메서드는 `_onSignOut()` 위임
- `import` 동적 변경: VoiceMode/MeetingMode/Caption 모두 dynamic import로 순환 의존 회피
- **`bootstrap.ts` 순서 변경**
- `database` step 완전 제거 (critical 2번째였던 자리 삭제)
- `cloud-sync` step은 마지막 유지 — 세션 복원 성공 시 DB가 여기서 열림
- `initDatabase` import 삭제
- **검증**
- desktop `tsc --noEmit`
- desktop `npm run build` ✅ (main bundle 997kB, vite 경고는 기존 동적 import 관련 pre-existing)
- runtime (dev 띄워서 LoginScreen → OAuth → user DB 생성 확인) — 사용자 수동 검증 대기
- **남은 작업 (Phase 1 follow-up)**
- 사용자 A → signOut → 사용자 B 로그인 시 A 데이터 안 보이는지 실측
- `~/Library/Application Support/d3ro-voice/users/${uuid}/d3ro.db` 파일 생성 확인
- 기존 `d3ro-voice.db`가 있는 환경에서 archive rename 동작 확인
## 빅뱅 다음 세션 목표 (2026-04-11 wrap-up)