From 77effb45260eea5c7085c4df525286216d66bbe2 Mon Sep 17 00:00:00 2001 From: altair823 Date: Thu, 7 May 2026 02:16:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(v027):=20TrayCallbacks/TrayState=20?= =?UTF-8?q?=EC=8A=AC=EB=A6=BC=20+=20buildMenu=204=20=ED=95=AD=EB=AA=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/tray.ts | 74 ++++++++++------------------------------- tests/unit/tray.test.ts | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 tests/unit/tray.test.ts diff --git a/src/main/tray.ts b/src/main/tray.ts index 38d8425..8bef426 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -33,33 +33,32 @@ function showAboutDialog(): void { } /** - * v0.2.6 C2 — 트레이 메뉴 콜백 묶음. createTray 가 1-arg 로 받음. + * v0.2.7 Phase 3 (Task 14) — 트레이 메뉴 슬림. 13 → 4 항목. + * + * 백업/내보내기/복원/동기화/사용 로그/Ollama 재확인/AI 재처리/Ollama 설정/자동실행/정보 → + * 모두 설정 페이지로 이전. 트레이는 4 항목만 노출: + * 1. 한 줄 적기 (showCapture) + * 2. 보관한 메모 보기 (showInbox) + * 3. 설정... (showSettings — 설정 페이지로 navigate) + * 4. 종료 */ export interface TrayCallbacks { showInbox: () => void; showCapture: () => void; - runBackup: () => void; - runExport: () => void; - runImport: () => void; - runSync: () => void; - runExportTelemetry: () => void; - runOllamaRecheck: () => void; - runRetryAllFailed: () => void; - runOpenOllamaSettings: () => void; + showSettings: () => void; } /** - * v0.2.6 C3 — 메뉴 라벨/활성화에 영향 주는 reactive state. refreshTray() 로 partial 갱신. + * v0.2.7 Phase 3 (Task 14) — TrayState 슬림. todayCount 만 잔류 (오늘 N번 잡아둠 라벨). + * ollamaOk / failedCount 메뉴 항목이 사라져 더 이상 필요 없음. */ 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 }; +let _state: TrayState = { todayCount: 0 }; function buildMenu(): electron.Menu { const items: MenuItemConstructorOptions[] = []; @@ -73,51 +72,18 @@ function buildMenu(): electron.Menu { items.push({ label: `오늘 ${_state.todayCount}번 잡아둠`, enabled: false }); items.push({ type: 'separator' }); } - items.push({ label: '보관한 메모 보기', click: cb.showInbox }); items.push({ label: '한 줄 적기', click: cb.showCapture }); + items.push({ label: '보관한 메모 보기', click: cb.showInbox }); + items.push({ type: 'separator' }); + items.push({ label: '설정...', click: cb.showSettings }); items.push({ type: 'separator' }); - 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: !_state.ollamaOk, - click: cb.runOllamaRecheck - }); - items.push({ - label: `지금 AI 처리 (실패 ${_state.failedCount}건)`, - enabled: _state.failedCount > 0, - click: cb.runRetryAllFailed - }); - items.push({ label: 'Ollama 설정...', click: cb.runOpenOllamaSettings }); - if (app.isPackaged) { - // v0.2.6 #45 — args 명시 전달로 openAtLogin 비교 정확도. setLoginItemSettings 가 - // args 와 함께 LoginItem 등록하므로 read 시도 같은 args 로 비교해야 매치됨. - const { openAtLogin } = app.getLoginItemSettings({ args: ['--hidden'] }); - items.push({ - label: '윈도우 시작 시 자동 실행', - type: 'checkbox', - checked: openAtLogin, - click: (item) => { - app.setLoginItemSettings({ - openAtLogin: item.checked, - args: ['--hidden'] - }); - } - }); - items.push({ type: 'separator' }); - } else { - items.push({ type: 'separator' }); - } - items.push({ label: 'Inkling 정보...', click: showAboutDialog }); items.push({ label: '종료', click: () => { app.isQuitting = true; app.quit(); } }); return Menu.buildFromTemplate(items); } /** * v0.2.6 C2 — 1-arg createTray. 기존 10 positional 폐기. + * v0.2.7 Phase 3 — TrayCallbacks 3-필드로 슬림. */ export function createTray(callbacks: TrayCallbacks): TrayType { _callbacks = callbacks; @@ -131,13 +97,7 @@ export function createTray(callbacks: TrayCallbacks): TrayType { /** * v0.2.6 C3 — 통합 state 갱신. partial 으로 받아 _state merge + 메뉴 재빌드. - * - * Replaces: refreshTrayOllama(ok), refreshTrayFailedCount(count), 기존 refreshTray(todayCount). - * - * 호출 예: - * refreshTray({ todayCount: 5 }); - * refreshTray({ ollamaOk: false }); - * refreshTray({ failedCount: 2 }); + * v0.2.7 Phase 3 — TrayState 가 todayCount 만 갖도록 슬림. */ export function refreshTray(state: Partial): void { _state = { ..._state, ...state }; diff --git a/tests/unit/tray.test.ts b/tests/unit/tray.test.ts new file mode 100644 index 0000000..a98efce --- /dev/null +++ b/tests/unit/tray.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('electron', () => ({ + default: { + app: { + on: vi.fn(), + getPath: vi.fn(), + getVersion: vi.fn(() => '0.2.7'), + isPackaged: false, + getLoginItemSettings: vi.fn(() => ({ openAtLogin: false })) + }, + Tray: vi.fn(function (this: unknown) { + Object.assign(this as object, { + setToolTip: vi.fn(), + setContextMenu: vi.fn(), + on: vi.fn() + }); + }), + Menu: { buildFromTemplate: vi.fn((items: unknown) => ({ items })) }, + nativeImage: { createEmpty: vi.fn() }, + dialog: {}, + shell: {}, + clipboard: {} + } +})); + +import { createTray, type TrayCallbacks } from '../../src/main/tray'; + +describe('tray menu — slim 4 items', () => { + beforeEach(() => { vi.clearAllMocks(); }); + + function makeCallbacks(): TrayCallbacks { + return { + showInbox: vi.fn(), + showCapture: vi.fn(), + showSettings: vi.fn() + }; + } + + it('builds menu with 4 click items + 2 separators', async () => { + createTray(makeCallbacks()); + const electron = (await import('electron')).default; + const calls = (electron.Menu.buildFromTemplate as any).mock.calls; + const items = calls[calls.length - 1][0]; + const labels = items.filter((i: any) => i.type !== 'separator').map((i: any) => i.label); + expect(labels).toEqual(['한 줄 적기', '보관한 메모 보기', '설정...', '종료']); + }); + + it('does not include removed items', async () => { + createTray(makeCallbacks()); + const electron = (await import('electron')).default; + const calls = (electron.Menu.buildFromTemplate as any).mock.calls; + const items = calls[calls.length - 1][0]; + const labels = items.filter((i: any) => i.type !== 'separator').map((i: any) => i.label); + expect(labels).not.toContain('지금 백업'); + expect(labels).not.toContain('내보내기...'); + expect(labels).not.toContain('Ollama 재확인'); + expect(labels).not.toContain('Ollama 설정...'); + }); + + it('"설정..." click invokes showSettings callback', async () => { + const cb = makeCallbacks(); + createTray(cb); + const electron = (await import('electron')).default; + const calls = (electron.Menu.buildFromTemplate as any).mock.calls; + const items = calls[calls.length - 1][0]; + const settingsItem = items.find((i: any) => i.label === '설정...'); + settingsItem.click(); + expect(cb.showSettings).toHaveBeenCalled(); + }); +});