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
121
apps/desktop/src/main/ipc/cloud-sync-handlers.ts
Normal file
121
apps/desktop/src/main/ipc/cloud-sync-handlers.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// src/main/ipc/cloud-sync-handlers.ts
|
||||
// Phase V2-4: CloudSync IPC 핸들러
|
||||
|
||||
import { ipcMain, BrowserWindow } from 'electron'
|
||||
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||
import type { IPCResult } from '@d3ro/core/errors'
|
||||
import { getCloudSyncService } from '../services/CloudSyncService'
|
||||
import { configSet } from '../services/ConfigService'
|
||||
import { getLogger } from '../services/LoggerService'
|
||||
|
||||
const logger = getLogger('cloud-sync-handlers')
|
||||
|
||||
interface SignInParams {
|
||||
provider: 'google' | 'github'
|
||||
}
|
||||
|
||||
interface ConfigureParams {
|
||||
url: string
|
||||
anonKey: string
|
||||
}
|
||||
|
||||
function ok<T>(data: T): IPCResult<T> {
|
||||
return { success: true, data }
|
||||
}
|
||||
|
||||
function fail(error: unknown): IPCResult<never> {
|
||||
if (error instanceof D3ROError) {
|
||||
return { success: false, error: { code: error.code, message: error.message } }
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
success: false,
|
||||
error: { code: ErrorCode.LLMProcessingFailed, message }
|
||||
}
|
||||
}
|
||||
|
||||
export function registerCloudSyncHandlers(): void {
|
||||
const sync = getCloudSyncService()
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.GET_STATE, () => {
|
||||
try {
|
||||
return ok(sync.getState())
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.CONFIGURE, async (_e, params: ConfigureParams) => {
|
||||
try {
|
||||
configSet('supabaseUrl' as never, params.url as never)
|
||||
configSet('supabaseAnonKey' as never, params.anonKey as never)
|
||||
// CloudSyncService 재초기화
|
||||
await sync.init()
|
||||
return ok(sync.getState())
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.SIGN_IN, async (_e, params: SignInParams) => {
|
||||
try {
|
||||
await sync.startSignIn(params.provider)
|
||||
return ok({ started: true })
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.HANDLE_CALLBACK, async (_e, params: { code: string }) => {
|
||||
try {
|
||||
await sync.handleAuthCallback(params.code)
|
||||
return ok(sync.getState())
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.SIGN_OUT, async () => {
|
||||
try {
|
||||
await sync.signOut()
|
||||
return ok(sync.getState())
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.CLOUD_SYNC.PUSH_ALL, async () => {
|
||||
try {
|
||||
const result = await sync.pushAll()
|
||||
configSet('cloudSyncLastAt' as never, Date.now() as never)
|
||||
return ok(result)
|
||||
} catch (e) {
|
||||
return fail(e)
|
||||
}
|
||||
})
|
||||
|
||||
// 이벤트 → renderer 브로드캐스트
|
||||
function broadcast(channel: string, payload: unknown): void {
|
||||
BrowserWindow.getAllWindows().forEach((win) => {
|
||||
if (!win.isDestroyed()) {
|
||||
win.webContents.send(channel, payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sync.on('auth-changed', (payload) => {
|
||||
broadcast(IPC_CHANNELS.CLOUD_SYNC.AUTH_CHANGED, payload)
|
||||
})
|
||||
sync.on('sync-progress', (payload) => {
|
||||
broadcast(IPC_CHANNELS.CLOUD_SYNC.SYNC_PROGRESS, payload)
|
||||
})
|
||||
sync.on('sync-complete', (payload) => {
|
||||
broadcast(IPC_CHANNELS.CLOUD_SYNC.SYNC_COMPLETE, payload)
|
||||
})
|
||||
sync.on('sync-error', (payload) => {
|
||||
broadcast(IPC_CHANNELS.CLOUD_SYNC.SYNC_ERROR, payload)
|
||||
})
|
||||
|
||||
logger.info('Cloud Sync IPC handlers registered')
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue