feat(desktop): two-way cloud sync with mobile and web

Rewrites the desktop mirror as services/sync/SyncEngine: a persistent
outbox, per-account server-clock keyset cursors with paging, pulls that never
overwrite unsent local edits, deletions both ways through sync_tombstones and
per-row failure isolation. It now covers history titles and favorites,
dictionary, every meeting's memos and documents, memo tags, user commands and
dictation/meeting templates, and registers the desktop as a device that the
phone can disconnect.

Fixes shipped defects: the first pull after sign-in fetched nothing, only
the first meeting's children were pushed, team meetings leaked into the
personal database and lost team_id on re-push, and Realtime never connected
because Electron's Node 20 has no global WebSocket (ws is now the transport).
Anonymous local-mode records are imported into the first account that signs
in. The settings sync section is translated and shows pending/rejected
changes; synced screens reload on app:dataChanged.
This commit is contained in:
Yun Chan 2026-09-27 14:04:49 +09:00
parent b5c9ff9f31
commit 0a4f5aee64
49 changed files with 3940 additions and 1033 deletions

View file

@ -145,6 +145,7 @@ export const IPC_CHANNELS = {
GET_BY_ID: 'history:getById',
DELETE: 'history:delete',
DELETE_ALL: 'history:deleteAll',
SET_FAVORITE: 'history:setFavorite',
SEARCH: 'history:search',
EXPORT: 'history:export',
// Main → Renderer events

View file

@ -479,6 +479,12 @@ export interface AppConfig {
supabaseAnonKey: string
/** V2-4: 마지막 동기화 epoch ms */
cloudSyncLastAt: number | null
/**
* 이 설치본의 기기 식별자(uuid). Supabase devices.installation_id —
* 모바일 "연결된 기기" 목록에 이 데스크톱이 나타나고 원격 연결 해제 대상이 된다.
* 원격 해제된 id는 재사용할 수 없어(서버 규칙) 재로그인 때 새로 발급한다.
*/
deviceInstallationId: string | null
/** 빅뱅 Phase 2: 첫 실행 온보딩 완료 여부 (로컬 모드 entry point) */
onboardingCompleted: boolean
/** Phase 6: Custom instructions (CustomInstructionService) */
@ -622,6 +628,8 @@ export interface HistoryEntry {
appVersion: string
/** Phase 12.2: 회의록 요약 마크다운 */
summaryText: string | null
/** 모바일·웹과 공유하는 즐겨찾기 (Supabase history.is_favorite) */
isFavorite: boolean
}
export interface HistoryQueryParams {
@ -647,6 +655,11 @@ export interface HistoryDeleteParams {
id: string
}
export interface HistorySetFavoriteParams {
id: string
isFavorite: boolean
}
export interface HistorySearchParams {
query: string
page: number