From e728a11e0973691dcf5b3038780644f69348c9e3 Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 26 Apr 2026 10:56:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(import):=20wire=20ImportService=20?= =?UTF-8?q?=E2=80=94=20tray=20'=EB=B0=B1=EC=97=85=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B3=B5=EC=9B=90...'=20+=20preview=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tray gets 5th callback. Directory chooser → preview (count of new/skip/forked + media) → confirm message box → run. Slice §1.1 copy policy preserved (no '실패'/'끊김'). Notification on success/failure. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/index.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++++ src/main/tray.ts | 11 +++++--- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 80f35b9..5e04a1a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -26,6 +26,7 @@ import { createTray } from './tray.js'; import { MediaGc } from './services/MediaGc.js'; import { BackupService } from './services/BackupService.js'; import { ExportService } from './services/ExportService.js'; +import { ImportService } from './services/ImportService.js'; const HIDDEN_ARG = '--hidden'; const startedHidden = process.argv.includes(HIDDEN_ARG); @@ -104,6 +105,7 @@ app.whenReady().then(async () => { void gc.run().then((r) => logger.info('media.gc', { ...r } as Record)); const exportSvc = new ExportService(repo, store); + const importSvc = new ImportService(repo, store); const backup = new BackupService(db, join(paths.profileDir, 'backups')); void backup.runDaily() @@ -179,6 +181,68 @@ app.whenReady().then(async () => { silent: true }).show(); } + }, + async () => { + const win = getInboxWindow(); + const dirOpts: Electron.OpenDialogOptions = { + title: '복원할 백업 폴더 선택', + message: 'F5 export 형식의 폴더를 선택하세요. notes/ 하위의 마크다운 파일이 적재됩니다.', + buttonLabel: '여기서 복원', + properties: ['openDirectory'] + }; + const dirResult = win + ? await dialog.showOpenDialog(win, dirOpts) + : await dialog.showOpenDialog(dirOpts); + if (dirResult.canceled || dirResult.filePaths.length === 0) return; + const sourceDir = dirResult.filePaths[0]!; + let plan; + try { + plan = await importSvc.preview(sourceDir); + } catch (e) { + logger.warn('import.preview.failed', { reason: String(e) }); + new Notification({ + title: 'Inkling', + body: '백업 폴더를 읽지 못했습니다.', + silent: true + }).show(); + return; + } + const detail = `총 ${plan.total}개 노트\n · 신규 ${plan.newCount}개\n · 동일 (스킵) ${plan.unchangedCount}개\n · 충돌→새 id (${plan.forkedCount}개, raw_text 보존)\n\n이미지 ${plan.mediaCount}개 복사 예정.`; + const confirmOpts: Electron.MessageBoxOptions = { + type: 'question', + buttons: ['복원', '취소'], + defaultId: 0, + cancelId: 1, + title: 'Inkling 복원', + message: '복원 미리보기', + detail + }; + const confirm = win + ? await dialog.showMessageBox(win, confirmOpts) + : await dialog.showMessageBox(confirmOpts); + if (confirm.response !== 0) return; + try { + const r = await importSvc.run(sourceDir); + logger.info('import.done', { + total: r.total, + new: r.newCount, + unchanged: r.unchangedCount, + forked: r.forkedCount, + media: r.mediaCount + }); + new Notification({ + title: 'Inkling', + body: `복원 완료 — 신규 ${r.newCount}개, 스킵 ${r.unchangedCount}개, 충돌 ${r.forkedCount}개`, + silent: true + }).show(); + } catch (e) { + logger.warn('import.run.failed', { reason: String(e) }); + new Notification({ + title: 'Inkling', + body: '복원을 완료하지 못했습니다.', + silent: true + }).show(); + } } ); diff --git a/src/main/tray.ts b/src/main/tray.ts index 6ac6739..cddd8dd 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -8,14 +8,16 @@ function buildMenu( showInbox: () => void, showCapture: () => void, runBackup: () => void, - runExport: () => void + runExport: () => void, + runImport: () => void ) { const items: MenuItemConstructorOptions[] = [ { label: '구출한 메모 보기', click: showInbox }, { label: '기억 구출하기', click: showCapture }, { type: 'separator' }, { label: '지금 백업', click: runBackup }, - { label: '내보내기...', click: runExport } + { label: '내보내기...', click: runExport }, + { label: '백업에서 복원...', click: runImport } ]; if (app.isPackaged) { const { openAtLogin } = app.getLoginItemSettings(); @@ -42,12 +44,13 @@ export function createTray( showInbox: () => void, showCapture: () => void, runBackup: () => void, - runExport: () => void + runExport: () => void, + runImport: () => void ): TrayType { const icon = nativeImage.createEmpty(); tray = new Tray(icon); tray.setToolTip('Inkling'); - tray.setContextMenu(buildMenu(showInbox, showCapture, runBackup, runExport)); + tray.setContextMenu(buildMenu(showInbox, showCapture, runBackup, runExport, runImport)); tray.on('click', showInbox); return tray; }