feat(v030): sync IPC + preload (configure / test / list-conflicts / resolve / status)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
altair823
2026-05-10 03:49:10 +09:00
parent 62e68dcfe7
commit 9e48624495
4 changed files with 361 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ import type { BackupService } from '../services/BackupService.js';
import type { ExportService } from '../services/ExportService.js';
import type { ImportService } from '../services/ImportService.js';
import type { SyncService } from '../services/SyncService.js';
import { GitClient } from '../services/GitClient.js';
import type { TelemetryService } from '../services/TelemetryService.js';
import type { SettingsService } from '../services/SettingsService.js';
import { collectAutostartState } from '../services/AutostartDiagnostic.js';
@@ -281,4 +282,78 @@ export function registerSettingsApi(deps?: SettingsIpcDeps): void {
}
return { ok: true } as const;
});
// v0.3.0 Cut E — sync IPC.
// settings:configure-sync — URL 저장 + git init + remote add (없으면).
// null URL → 저장만 (init 안 함). 빈 문자열도 null 처리.
ipcMain.handle('settings:configure-sync', async (_e, url: string | null) => {
const trimmed = typeof url === 'string' ? url.trim() : '';
const finalUrl = trimmed.length === 0 ? null : trimmed;
try {
await deps.settings.setSyncRepoUrl(finalUrl);
} catch (e) {
return { ok: false as const, reason: `persist failed: ${(e as Error).message}` };
}
if (finalUrl === null) return { ok: true as const };
// git init + remote add origin
const syncDir = deps.syncSvc.getSyncDir();
const git = new GitClient(syncDir);
if (!(await git.isRepo())) {
const init = await git.run(['init']);
if (init.exitCode !== 0) {
return { ok: false as const, reason: `git init failed: ${init.stderr}` };
}
}
if (!(await git.hasRemote())) {
const add = await git.run(['remote', 'add', 'origin', finalUrl]);
if (add.exitCode !== 0) {
return { ok: false as const, reason: `remote add failed: ${add.stderr}` };
}
} else {
// remote exists — update URL
const set = await git.run(['remote', 'set-url', 'origin', finalUrl]);
if (set.exitCode !== 0) {
return { ok: false as const, reason: `remote set-url failed: ${set.stderr}` };
}
}
return { ok: true as const };
});
// settings:test-sync-connection — git ls-remote 결과
ipcMain.handle('settings:test-sync-connection', async () => {
const syncDir = deps.syncSvc.getSyncDir();
const git = new GitClient(syncDir);
if (!(await git.isRepo())) return { ok: false as const, reason: 'not_initialized' };
const r = await git.run(['ls-remote', 'origin']);
if (r.exitCode !== 0) return { ok: false as const, reason: r.stderr || 'connection failed' };
return { ok: true as const };
});
// sync:list-conflicts — SyncService 캐시 결과
ipcMain.handle('sync:list-conflicts', () => deps.syncSvc.listConflicts());
// sync:resolve-conflict — local/remote 2 choice
ipcMain.handle('sync:resolve-conflict', async (_e, noteId: string, choice: 'local' | 'remote') => {
if (choice !== 'local' && choice !== 'remote') {
return { ok: false as const, reason: 'invalid choice' };
}
return deps.syncSvc.resolveConflict(noteId, choice);
});
// sync:get-status — lastAt + lastResult + nextAt 계산
ipcMain.handle('sync:get-status', async () => {
const last = deps.syncSvc.getLastStatus();
let nextAt: string | null = null;
if (await deps.settings.isAutoSyncEnabled()) {
const intervalMin = await deps.settings.getSyncIntervalMin();
const baseMs = last.lastAt ? new Date(last.lastAt).getTime() : Date.now();
nextAt = new Date(baseMs + intervalMin * 60 * 1000).toISOString();
}
return { lastAt: last.lastAt, lastResult: last.lastResult, nextAt };
});
}

View File

@@ -88,6 +88,13 @@ const api: InklingApi = {
// v0.2.11 Cut D — search + 회고 aggregate.
search: (query, opts) => ipcRenderer.invoke('inbox:search', query, opts ?? {}),
reviewAggregate: (period) => ipcRenderer.invoke('inbox:review-aggregate', period),
// v0.3.0 Cut E — 양방향 sync.
configureSync: (url: string | null) => ipcRenderer.invoke('settings:configure-sync', url),
testSyncConnection: () => ipcRenderer.invoke('settings:test-sync-connection'),
listConflicts: () => ipcRenderer.invoke('sync:list-conflicts'),
resolveConflict: (noteId: string, choice: 'local' | 'remote') =>
ipcRenderer.invoke('sync:resolve-conflict', noteId, choice),
getSyncStatus: () => ipcRenderer.invoke('sync:get-status'),
}
};

View File

@@ -40,6 +40,29 @@ export interface ReviewAggregate {
dueProgress: { total: number; passed: number; pending: number };
}
// v0.3.0 Cut E — 양방향 sync 결과 + conflict.
export interface SyncConflict {
noteId: string;
localText: string;
remoteText: string;
}
export interface SyncStatus {
ok: boolean;
reason?: string;
changed?: boolean;
localSha?: string | null;
pushed?: boolean;
importedCount?: number;
conflicts?: SyncConflict[];
}
export interface SyncStatusSnapshot {
lastAt: string | null;
lastResult: SyncStatus | null;
nextAt: string | null;
}
export interface Note {
id: string;
rawText: string;
@@ -182,6 +205,12 @@ export interface InboxApi {
// v0.2.11 Cut D — FTS5 search + 회고 aggregate.
search(query: string, opts?: { limit?: number; status?: NoteStatus }): Promise<Note[]>;
reviewAggregate(period: ReviewPeriod): Promise<ReviewAggregate>;
// v0.3.0 Cut E — 양방향 sync.
configureSync(url: string | null): Promise<{ ok: true } | { ok: false; reason: string }>;
testSyncConnection(): Promise<{ ok: true } | { ok: false; reason: string }>;
listConflicts(): Promise<SyncConflict[]>;
resolveConflict(noteId: string, choice: 'local' | 'remote'): Promise<{ ok: true } | { ok: false; reason: string }>;
getSyncStatus(): Promise<SyncStatusSnapshot>;
}
export interface InklingApi {