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 배포 + 설정 입력 후 검증
3.4 KiB
3.4 KiB
Phase V2-4: 데스크톱 ↔ Supabase 동기화 — 설계
apps/desktop이 V2-2 Supabase 인프라에 연결되어 사용자 데이터를 클라우드와 동기화한다. Local-first 원칙: SQLite가 source of truth, Supabase는 미러.
1. 목표
- OAuth 로그인: 외부 브라우저로 Supabase Auth 진행 → Electron deep link 콜백
- 세션 영속화: refresh token을
electron-store에 (가급적safeStorage로 암호화) 저장 - 수동 동기화: 사용자가 Settings에서 "Sync Now" 클릭 → SQLite 데이터를 Postgres로 push
- 자동 동기화: 향후 기능 (V2-4b, 백그라운드 schedule)
- 충돌 해결: 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 }
}
4. Deep link 처리
- 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