diff --git a/src/main/index.ts b/src/main/index.ts index fbb4155..e38197f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -23,7 +23,7 @@ import { createInboxWindow, getInboxWindow } from './windows/inboxWindow.js'; import { createQuickCaptureWindow, showQuickCapture, getQuickCaptureWindow } from './windows/quickCaptureWindow.js'; -import { createTray, refreshTray, refreshTrayOllama, refreshTrayFailedCount } from './tray.js'; +import { createTray, refreshTray } from './tray.js'; import { MediaGc } from './services/MediaGc.js'; import { BackupService } from './services/BackupService.js'; import { ExportService } from './services/ExportService.js'; @@ -120,7 +120,7 @@ app.whenReady().then(async () => { onUpdate: (status) => { logger.info('ai.health', { ...status } as Record); pushOllamaStatus(getInboxWindow, status); - refreshTrayOllama(status.ok); + refreshTray({ ollamaOk: status.ok }); }, onTelemetry: (ev) => { if (ev.kind === 'ollama_unreachable') { @@ -138,8 +138,7 @@ app.whenReady().then(async () => { onUpdate: (note) => { pushNoteUpdated(getInboxWindow, note); // F4-C: AI 처리 완료 = 새 캡처가 inbox 에 합류한 시점, tray 도 즉시 갱신. - refreshTray(repo.countToday()); - refreshTrayFailedCount(repo.countFailed()); + refreshTray({ todayCount: repo.countToday(), failedCount: repo.countFailed() }); }, logger, telemetry @@ -223,10 +222,10 @@ app.whenReady().then(async () => { }); }); - createTray( - () => createInboxWindow(), - () => showQuickCapture(), - async () => { + createTray({ + showInbox: () => createInboxWindow(), + showCapture: () => showQuickCapture(), + runBackup: async () => { try { const r = await backup.runDaily(); new Notification({ @@ -245,7 +244,7 @@ app.whenReady().then(async () => { }).show(); } }, - async () => { + runExport: async () => { const win = getInboxWindow(); const dialogOpts: Electron.OpenDialogOptions = { title: '내보낼 폴더 선택', @@ -279,7 +278,7 @@ app.whenReady().then(async () => { }).show(); } }, - async () => { + runImport: async () => { const win = getInboxWindow(); const dirOpts: Electron.OpenDialogOptions = { title: '복원할 백업 폴더 선택', @@ -341,7 +340,7 @@ app.whenReady().then(async () => { }).show(); } }, - async () => { + runSync: async () => { // runSync — 트레이 "지금 동기화" try { const r = await syncSvc.sync(); @@ -364,7 +363,7 @@ app.whenReady().then(async () => { new Notification({ title: 'Inkling', body: '동기화를 완료하지 못했습니다.', silent: true }).show(); } }, - /* runExportTelemetry */ async () => { + runExportTelemetry: async () => { const win = getInboxWindow(); const dialogOpts: Electron.OpenDialogOptions = { title: '사용 로그를 내보낼 폴더 선택', @@ -393,21 +392,20 @@ app.whenReady().then(async () => { }).show(); } }, - /* runOllamaRecheck */ () => { void health.runOnce({ manual: true }); }, - /* runRetryAllFailed */ () => { void capture.retryAllFailed(); }, - /* runOpenOllamaSettings */ () => { + runOllamaRecheck: () => { void health.runOnce({ manual: true }); }, + runRetryAllFailed: () => { void capture.retryAllFailed(); }, + runOpenOllamaSettings: () => { const win = getInboxWindow(); if (win) win.webContents.send('inbox:openOllamaSettings'); } - ); + }); // F4-C 환경 앵커 — tray tooltip + 메뉴 첫 항목을 오늘 KST 캡처 수로 갱신. // 초기 1회 + 60s interval. AiWorker.onUpdate 도 별도 갱신 트리거. // cleanup 은 위 통합 before-quit 핸들러에서 처리. - refreshTray(repo.countToday()); - refreshTrayFailedCount(repo.countFailed()); + refreshTray({ todayCount: repo.countToday(), failedCount: repo.countFailed() }); trayInterval = setInterval(() => { - refreshTray(repo.countToday()); + refreshTray({ todayCount: repo.countToday() }); }, 60_000); app.on('activate', () => { diff --git a/src/main/tray.ts b/src/main/tray.ts index b509994..38d8425 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -32,47 +32,66 @@ function showAboutDialog(): void { }); } -let tray: TrayType | null = null; -let _showInbox: () => void = () => {}; -let _showCapture: () => void = () => {}; -let _runBackup: () => void = () => {}; -let _runExport: () => void = () => {}; -let _runImport: () => void = () => {}; -let _runSync: () => void = () => {}; -let _runExportTelemetry: () => void = () => {}; -let _runOllamaRecheck: () => void = () => {}; -let _ollamaOk = true; -let _todayCount = 0; -let _runRetryAllFailed: () => void = () => {}; -let _failedCount = 0; -let _runOpenOllamaSettings: () => void = () => {}; +/** + * v0.2.6 C2 — 트레이 메뉴 콜백 묶음. createTray 가 1-arg 로 받음. + */ +export interface TrayCallbacks { + showInbox: () => void; + showCapture: () => void; + runBackup: () => void; + runExport: () => void; + runImport: () => void; + runSync: () => void; + runExportTelemetry: () => void; + runOllamaRecheck: () => void; + runRetryAllFailed: () => void; + runOpenOllamaSettings: () => void; +} -function buildMenu() { +/** + * v0.2.6 C3 — 메뉴 라벨/활성화에 영향 주는 reactive state. refreshTray() 로 partial 갱신. + */ +export interface TrayState { + ollamaOk: boolean; + todayCount: number; + failedCount: number; +} + +let tray: TrayType | null = null; +let _callbacks: TrayCallbacks | null = null; +let _state: TrayState = { ollamaOk: true, todayCount: 0, failedCount: 0 }; + +function buildMenu(): electron.Menu { const items: MenuItemConstructorOptions[] = []; + const cb = _callbacks; + if (!cb) { + // createTray 호출 전이면 빈 메뉴 (defensive) + return Menu.buildFromTemplate([{ label: '로딩 중...', enabled: false }]); + } // F4-C: count > 0 시 비활성 라벨로 정체성 신호 노출. count = 0 시 메뉴를 자연스럽게 시작. - if (_todayCount > 0) { - items.push({ label: `오늘 ${_todayCount}번 잡아둠`, enabled: false }); + if (_state.todayCount > 0) { + items.push({ label: `오늘 ${_state.todayCount}번 잡아둠`, enabled: false }); items.push({ type: 'separator' }); } - items.push({ label: '보관한 메모 보기', click: _showInbox }); - items.push({ label: '한 줄 적기', click: _showCapture }); + items.push({ label: '보관한 메모 보기', click: cb.showInbox }); + items.push({ label: '한 줄 적기', click: cb.showCapture }); items.push({ type: 'separator' }); - items.push({ label: '지금 백업', click: _runBackup }); - items.push({ label: '내보내기...', click: _runExport }); - items.push({ label: '백업에서 복원...', click: _runImport }); - items.push({ label: '지금 동기화', click: _runSync }); - items.push({ label: '사용 로그 내보내기...', click: _runExportTelemetry }); + items.push({ label: '지금 백업', click: cb.runBackup }); + items.push({ label: '내보내기...', click: cb.runExport }); + items.push({ label: '백업에서 복원...', click: cb.runImport }); + items.push({ label: '지금 동기화', click: cb.runSync }); + items.push({ label: '사용 로그 내보내기...', click: cb.runExportTelemetry }); items.push({ label: 'Ollama 재확인', - enabled: !_ollamaOk, - click: _runOllamaRecheck + enabled: !_state.ollamaOk, + click: cb.runOllamaRecheck }); items.push({ - label: `지금 AI 처리 (실패 ${_failedCount}건)`, - enabled: _failedCount > 0, - click: _runRetryAllFailed + label: `지금 AI 처리 (실패 ${_state.failedCount}건)`, + enabled: _state.failedCount > 0, + click: cb.runRetryAllFailed }); - items.push({ label: 'Ollama 설정...', click: () => _runOpenOllamaSettings() }); + items.push({ label: 'Ollama 설정...', click: cb.runOpenOllamaSettings }); if (app.isPackaged) { // v0.2.6 #45 — args 명시 전달로 openAtLogin 비교 정확도. setLoginItemSettings 가 // args 와 함께 LoginItem 등록하므로 read 시도 같은 args 로 비교해야 매치됨. @@ -97,62 +116,32 @@ function buildMenu() { return Menu.buildFromTemplate(items); } -export function createTray( - showInbox: () => void, - showCapture: () => void, - runBackup: () => void, - runExport: () => void, - runImport: () => void, - runSync: () => void, - runExportTelemetry: () => void, - runOllamaRecheck: () => void, - runRetryAllFailed: () => void, - runOpenOllamaSettings: () => void -): TrayType { - _showInbox = showInbox; - _showCapture = showCapture; - _runBackup = runBackup; - _runExport = runExport; - _runImport = runImport; - _runSync = runSync; - _runExportTelemetry = runExportTelemetry; - _runOllamaRecheck = runOllamaRecheck; - _runRetryAllFailed = runRetryAllFailed; - _runOpenOllamaSettings = runOpenOllamaSettings; +/** + * v0.2.6 C2 — 1-arg createTray. 기존 10 positional 폐기. + */ +export function createTray(callbacks: TrayCallbacks): TrayType { + _callbacks = callbacks; const icon = nativeImage.createEmpty(); tray = new Tray(icon); - tray.setToolTip(`Inkling — 오늘 ${_todayCount}`); + tray.setToolTip(`Inkling — 오늘 ${_state.todayCount}`); tray.setContextMenu(buildMenu()); - tray.on('click', showInbox); + tray.on('click', callbacks.showInbox); return tray; } /** - * F4-C 환경 앵커 — tooltip + 메뉴 첫 항목을 오늘 캡처 수로 갱신. - * `src/main/index.ts` 가 60s interval / AiWorker onUpdate 시점에 호출. + * v0.2.6 C3 — 통합 state 갱신. partial 으로 받아 _state merge + 메뉴 재빌드. + * + * Replaces: refreshTrayOllama(ok), refreshTrayFailedCount(count), 기존 refreshTray(todayCount). + * + * 호출 예: + * refreshTray({ todayCount: 5 }); + * refreshTray({ ollamaOk: false }); + * refreshTray({ failedCount: 2 }); */ -export function refreshTray(todayCount: number): void { - _todayCount = todayCount; - if (tray === null) return; - tray.setToolTip(`Inkling — 오늘 ${todayCount}`); - tray.setContextMenu(buildMenu()); -} - -/** - * v0.2.3 #1 — Ollama 상태가 변할 때 main 의 health.onUpdate 가 호출. - * 메뉴의 "Ollama 재확인" 활성/비활성 상태 갱신. - */ -export function refreshTrayOllama(ok: boolean): void { - _ollamaOk = ok; - if (tray === null) return; - tray.setContextMenu(buildMenu()); -} - -/** - * v0.2.3 #2 — AiWorker.onUpdate 시 실패 카운트 변하면 메뉴 라벨 + enabled 갱신. - */ -export function refreshTrayFailedCount(count: number): void { - _failedCount = count; +export function refreshTray(state: Partial): void { + _state = { ..._state, ...state }; if (tray === null) return; + tray.setToolTip(`Inkling — 오늘 ${_state.todayCount}`); tray.setContextMenu(buildMenu()); }