feat(import): wire ImportService — tray '백업에서 복원...' + preview dialog

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) <noreply@anthropic.com>
This commit is contained in:
altair823
2026-04-26 10:56:29 +09:00
parent d76cca68df
commit e728a11e09
2 changed files with 71 additions and 4 deletions

View File

@@ -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<string, unknown>));
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();
}
}
);

View File

@@ -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;
}