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:
parent
b5c9ff9f31
commit
0a4f5aee64
49 changed files with 3940 additions and 1033 deletions
|
|
@ -36,6 +36,10 @@ export function DictionaryPage(): React.ReactElement {
|
|||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
// 다른 기기(모바일·웹)에서 바뀐 사전이 반영되면 다시 불러온다.
|
||||
return window.electronAPI.app.onDataChanged((data) => {
|
||||
if (data.type === 'cloud-sync' && data.entities?.includes('dictionary')) loadData()
|
||||
})
|
||||
}, [loadData])
|
||||
|
||||
const openAdd = () => {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@ export function HistoryPage(): React.ReactElement {
|
|||
[loadData],
|
||||
)
|
||||
|
||||
const handleToggleFavorite = useCallback(async (entry: HistoryEntry) => {
|
||||
const next = !entry.isFavorite
|
||||
const apply = (value: boolean): void =>
|
||||
setData((prev) =>
|
||||
prev
|
||||
? { ...prev, entries: prev.entries.map((e) => (e.id === entry.id ? { ...e, isFavorite: value } : e)) }
|
||||
: prev,
|
||||
)
|
||||
apply(next)
|
||||
const result = await window.electronAPI.history.setFavorite({ id: entry.id, isFavorite: next })
|
||||
if (!result.success) apply(entry.isFavorite)
|
||||
}, [])
|
||||
|
||||
const handleTagClick = useCallback((tag: string) => {
|
||||
setActiveTag((prev) => (prev === tag ? null : tag))
|
||||
setSearch('')
|
||||
|
|
@ -232,6 +245,7 @@ export function HistoryPage(): React.ReactElement {
|
|||
entry={entry}
|
||||
onCopy={handleCopy}
|
||||
onDelete={handleDelete}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
showTags
|
||||
onTagClick={handleTagClick}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,25 @@ export function MeetingModePage(): React.ReactElement {
|
|||
if (view === 'list') loadSessions()
|
||||
}, [view, loadSessions])
|
||||
|
||||
// 다른 기기(모바일·웹)의 회의 변경이 반영되면: 목록은 다시 불러오고,
|
||||
// 보고 있던 회의가 지워졌으면 목록으로 돌아간다(편집 중인 상세는 덮어쓰지 않는다).
|
||||
useEffect(
|
||||
() =>
|
||||
window.electronAPI.app.onDataChanged((data) => {
|
||||
if (data.type !== 'cloud-sync') return
|
||||
const entities = data.entities ?? []
|
||||
if (!entities.some((e) => e === 'meetings' || e === 'meeting_memos' || e === 'meeting_documents')) return
|
||||
if (view === 'list') {
|
||||
loadSessions()
|
||||
} else if (view === 'detail' && detail) {
|
||||
window.electronAPI.meetingMode.getSession({ sessionId: detail.id }).then((resp) => {
|
||||
if (!resp.success) setView('list')
|
||||
})
|
||||
}
|
||||
}),
|
||||
[view, detail, loadSessions],
|
||||
)
|
||||
|
||||
// ── IPC 이벤트 구독 ──
|
||||
useEffect(() => {
|
||||
const unsubState = window.electronAPI.meetingMode.onStateChanged((info) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue