d3ro-voice/docs/v2/phase-V2-4.md
yunchan8804 5c0f4a2b98 feat(V2-4): 데스크톱 ↔ Supabase 동기화 (push only MVP)
CloudSyncService (싱글톤 + EventEmitter):
- Supabase 클라이언트 lazy init (configGet으로 url/key)
- safeStorage 기반 refresh token 영속화 (cloud-sync.token)
- startSignIn(provider): OAuth URL 발급 후 shell.openExternal로 외부 브라우저
- handleAuthCallback(code): code -> session 교환
- pushAll(): history/dictionary/meetings/meeting_memos/meeting_documents
  를 last_sync_at 이후 변경분만 upsert (onConflict: id)
- 이벤트: auth-changed / sync-progress / sync-complete / sync-error

Deep link (d3ro-voice://auth-callback):
- main/index.ts에 setAsDefaultProtocolClient
- macOS open-url 핸들러
- Windows second-instance argv 검사
- handleDeepLink로 URL 파싱 후 CloudSyncService로 전달

IPC:
- @d3ro/core/ipc-channels에 CLOUD_SYNC 채널 추가
- ipc/cloud-sync-handlers.ts (configure/signIn/signOut/pushAll/state)
- preload index.ts에 cloudSync 노출
- bootstrap의 cloud-sync 단계 추가

Renderer:
- CloudSyncSection 컴포넌트 신규
  - 미설정: Supabase URL/Key 입력 폼
  - 미로그인: Google/GitHub OAuth 버튼
  - 로그인: 사용자 이메일 + Sync Now + 진행 바 + 로그아웃
- d3roPalette/typoSx로 SSOT 준수, useI18n 사용

ConfigService 키 5종 추가:
- hfToken, diarizationEnabled (Phase 15.5 누락분 보충)
- supabaseUrl, supabaseAnonKey, cloudSyncLastAt (V2-4)
- packages/core types.ts AppConfig에도 동일하게 추가

검증:
- desktop typecheck OK
- desktop build OK
- 실제 Supabase 동기화는 사용자가 V2-2 배포 + 설정 입력 후 검증
2026-04-09 16:28:19 +09:00

3.4 KiB

Phase V2-4: 데스크톱 ↔ Supabase 동기화 — 설계

apps/desktop이 V2-2 Supabase 인프라에 연결되어 사용자 데이터를 클라우드와 동기화한다. Local-first 원칙: SQLite가 source of truth, Supabase는 미러.


1. 목표

  1. OAuth 로그인: 외부 브라우저로 Supabase Auth 진행 → Electron deep link 콜백
  2. 세션 영속화: refresh token을 electron-store에 (가급적 safeStorage로 암호화) 저장
  3. 수동 동기화: 사용자가 Settings에서 "Sync Now" 클릭 → SQLite 데이터를 Postgres로 push
  4. 자동 동기화: 향후 기능 (V2-4b, 백그라운드 schedule)
  5. 충돌 해결: MVP에서는 last-write-wins (created_at 기준)

2. 동기화 매핑

로컬 (SQLite) 원격 (Postgres) 변환
history.id (nanoid) history.id (uuid) nanoid는 그대로 text 컬럼? → V2-4 MVP는 uuid로 새로 발급. 매핑 테이블 sync_mapping 필요
history.created_at (epoch ms int) history.created_at (timestamptz) new Date(epochMs).toISOString()
meeting_sessions.id meetings.id 동일 처리
meeting_memos.session_id meeting_memos.meeting_id 매핑된 새 uuid
dictionary.id dictionary.id 동일

매핑 테이블 (sync_mapping):

local_id  TEXT
remote_id TEXT
table     TEXT
synced_at INTEGER
PRIMARY KEY (local_id, table)

V2-4 MVP는 매핑 없이 새 데이터만 push (last_sync_at 이후 변경된 행). 기존 데이터는 push 안 함. 단순화.

3. CloudSyncService 인터페이스

class CloudSyncService extends EventEmitter {
  // 초기화 (저장된 세션 복원)
  async init(): Promise<void>

  // 인증 상태
  isAuthenticated(): boolean
  getUser(): { id: string; email: string | null } | null

  // OAuth 로그인 시작 (외부 브라우저 열기 + URL 반환)
  async startSignIn(provider: 'google' | 'github'): Promise<{ authUrl: string }>

  // Deep link callback 처리 (?code=...)
  async handleAuthCallback(code: string): Promise<void>

  // 로그아웃
  async signOut(): Promise<void>

  // 동기화: 마지막 sync 이후 변경된 history/meetings/dictionary push
  async pushAll(): Promise<{ pushed: number; errors: string[] }>

  // 마지막 동기화 시각
  getLastSyncAt(): Date | null

  // 이벤트:
  //   'auth-changed' { user: User | null }
  //   'sync-progress' { current: number; total: number; table: string }
  //   'sync-complete' { pushed: number; errors: string[] }
  //   'sync-error' { error: string }
}
  • macOS: app.setAsDefaultProtocolClient('d3ro-voice')
  • Windows: 같은 함수 + 레지스트리
  • Linux: .desktop 파일

OAuth redirect_to: d3ro-voice://auth-callback

app.on('open-url', ...): macOS 핸들러 app.on('second-instance', ...): Windows에서 두 번째 인스턴스가 deep link로 호출됐을 때

5. UI

SettingsModal에 새 섹션:

  • 로그아웃 상태: "Cloud Sync 로그인" 버튼 → CloudSyncService.startSignIn()
  • 로그인 상태: 사용자 이메일 + "Sync Now" 버튼 + "마지막 동기화: ..." + "로그아웃"

6. 검증

  • typecheck + build
  • dev 실행 → Settings에서 로그인 버튼 나타남
  • 실제 동기화는 Supabase 배포 후 (사용자 액션)

7. V2-4 → V2-4b 분리

  • V2-4 (이번): push만, 수동 트리거
  • V2-4b (나중): pull + 양방향 + Realtime 구독 + 자동 schedule