src/main: 트레이/감시자/저장소/IPC/알림 배치 (Electron 메인)

tray.ts: 트레이 아이콘, 팝오버 BrowserWindow 토글, 컨텍스트 메뉴.
watcher.ts: chokidar 감시, dedup TTL 2s, auto/manual 모드, 글로벌 일시정지.
store.ts: userData/store.json 영속화 (watchedDirs/settings/undoLog).
ipc.ts: 18개 IPC 채널 핸들러.
notifier.ts: interval 기반 알림 배치 처리 (알림 폭주 방지).
settings-window.ts: 설정창 BrowserWindow 라이프사이클.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 15:41:08 +09:00
parent 4e92bb2690
commit 3dff470044
8 changed files with 665 additions and 0 deletions

126
src/main/ipc.ts Normal file
View File

@@ -0,0 +1,126 @@
import { ipcMain, dialog, app, BrowserWindow } from 'electron';
import { randomUUID } from 'crypto';
import { scan } from '../core/scanner';
import { normalizeEntry } from '../core/normalizer';
import { nanoid } from './nanoid';
import * as store from './store';
import * as watcher from './watcher';
import type { WatchedDir, UndoEntry } from '../core/types';
export function registerIpcHandlers(): void {
// ── 디렉토리 선택 ──
ipcMain.handle('dialog:selectDirectory', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showOpenDialog(win!, { properties: ['openDirectory'] });
return result.canceled ? null : result.filePaths[0];
});
// ── 디렉토리 CRUD ──
ipcMain.handle('dirs:list', async () => store.getDirs());
ipcMain.handle('dirs:add', async (_e, dirPath: string) => {
const settings = await store.getSettings();
const dir: WatchedDir = {
id: randomUUID(),
path: dirPath,
recursive: false,
mode: settings.defaultMode,
enabled: true,
customRanges: [],
};
await store.addDir(dir);
await watcher.startDir(dir);
return dir;
});
ipcMain.handle('dirs:update', async (_e, id: string, patch: Partial<WatchedDir>) => {
await store.updateDir(id, patch);
// 모드/recursive 변경 시 재시작
if ('mode' in patch || 'recursive' in patch || 'enabled' in patch || 'customRanges' in patch) {
await watcher.stopDir(id);
const dirs = await store.getDirs();
const dir = dirs.find((d) => d.id === id);
if (dir?.enabled) await watcher.startDir(dir);
}
});
ipcMain.handle('dirs:remove', async (_e, id: string) => {
await watcher.stopDir(id);
await store.removeDir(id);
});
// ── Dry-run 스캔 ──
ipcMain.handle('dirs:scan', async (_e, id: string) => {
const dirs = await store.getDirs();
const dir = dirs.find((d) => d.id === id);
if (!dir) return [];
return scan(dir.path, dir.recursive, { customRanges: dir.customRanges });
});
// ── 수동 모드 대기 큐 적용 ──
ipcMain.handle('dirs:applyQueue', async (_e, id: string) => {
const dirs = await store.getDirs();
const dir = dirs.find((d) => d.id === id);
if (!dir) return [];
return watcher.applyManualQueue(id, dir);
});
ipcMain.handle('dirs:pendingQueue', async (_e, id: string) => {
return watcher.getPendingQueue(id);
});
// ── 감시 일시정지/재개/상태 ──
ipcMain.handle('watcher:status', () => ({ paused: watcher.isGloballyPaused() }));
ipcMain.handle('watcher:pauseAll', async () => {
await watcher.pauseAll();
});
ipcMain.handle('watcher:resumeAll', async () => {
const dirs = await store.getDirs();
await watcher.resumeAll(dirs);
});
// ── Undo ──
ipcMain.handle('undo:list', async () => store.getUndoLog());
ipcMain.handle('undo:revertEntry', async (_e, entry: UndoEntry) => {
try {
const result = await normalizeEntry(entry.newPath, 'file');
// 실제 되돌리기: newPath → oldPath
const fs = await import('fs/promises');
await fs.rename(entry.newPath, entry.oldPath);
await store.markUndoReverted([entry.id]);
return { success: true, result };
} catch (err) {
return { success: false, error: String(err) };
}
});
ipcMain.handle('undo:revertLastBatch', async () => {
const log = await store.getUndoLog();
if (!log.length) return [];
const lastTs = log[log.length - 1].ts;
// 마지막 5초 이내의 항목을 한 배치로 묶음
const batch = log.filter((e) => !e.reverted && lastTs - e.ts < 5000);
const fs = await import('fs/promises');
const results = [];
for (const entry of [...batch].reverse()) {
try {
await fs.rename(entry.newPath, entry.oldPath);
results.push({ ...entry, success: true });
} catch (err) {
results.push({ ...entry, success: false, error: String(err) });
}
}
await store.markUndoReverted(batch.map((e) => e.id));
return results;
});
// ── 설정 ──
ipcMain.handle('settings:get', async () => store.getSettings());
ipcMain.handle('settings:update', async (_e, patch) => store.updateSettings(patch));
// ── 앱 정보 ──
ipcMain.handle('app:version', () => app.getVersion());
}