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 배포 + 설정 입력 후 검증
This commit is contained in:
parent
d0c33ca259
commit
5c0f4a2b98
13 changed files with 1069 additions and 1 deletions
97
docs/v2/phase-V2-4.md
Normal file
97
docs/v2/phase-V2-4.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# 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<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
|
||||
Loading…
Add table
Add a link
Reference in a new issue