# 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 인터페이스 ```typescript class CloudSyncService extends EventEmitter { // 초기화 (저장된 세션 복원) async init(): Promise // 인증 상태 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 // 로그아웃 async signOut(): Promise // 동기화: 마지막 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