diff --git a/src/main/ipc/settingsApi.ts b/src/main/ipc/settingsApi.ts index a5b3823..e4f6b0d 100644 --- a/src/main/ipc/settingsApi.ts +++ b/src/main/ipc/settingsApi.ts @@ -1,6 +1,7 @@ import electron from 'electron'; import type { BrowserWindow } from 'electron'; -const { ipcMain, app, dialog, Notification } = electron; +import { platform, release, EOL } from 'node:os'; +const { ipcMain, app, dialog, Notification, shell, clipboard } = electron; import { logger } from '../logger.js'; import type { BackupService } from '../services/BackupService.js'; import type { ExportService } from '../services/ExportService.js'; @@ -40,6 +41,32 @@ export function registerSettingsApi(deps?: SettingsIpcDeps): void { return { openAtLogin: r.openAtLogin }; }); + // v0.2.7 정보 섹션 (Task 11) — 트레이 showAboutDialog 의 detail 형식 그대로 (clipboard 일관성). + // 트레이 showAboutDialog 자체 제거는 Task 25 (Phase 6 cleanup) — 본 task 는 추가만. + ipcMain.handle('settings:get-app-info', () => ({ + version: app.getVersion(), + electron: process.versions.electron ?? '?', + node: process.versions.node ?? '?', + os: `${platform()} ${release()}`, + profileDir: app.getPath('userData') + })); + + ipcMain.handle('settings:open-profile-dir', async () => { + await shell.openPath(app.getPath('userData')); + }); + + ipcMain.handle('settings:copy-app-info', () => { + const v = app.getVersion(); + const detail = [ + `버전: ${v}`, + `Electron: ${process.versions.electron ?? '?'}`, + `Node: ${process.versions.node ?? '?'}`, + `OS: ${platform()} ${release()}`, + `데이터 위치: ${app.getPath('userData')}` + ].join(EOL); + clipboard.writeText(`Inkling ${v}${EOL}${detail}`); + }); + if (!deps) return; const { backup, exportSvc, importSvc, syncSvc, telemetry, getInboxWindow } = deps; diff --git a/src/preload/index.ts b/src/preload/index.ts index a6a926d..65e06ca 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -61,6 +61,10 @@ const api: InklingApi = { runImport: () => ipcRenderer.invoke('settings:run-import'), runSync: () => ipcRenderer.invoke('settings:run-sync'), runExportTelemetry: () => ipcRenderer.invoke('settings:run-export-telemetry'), + // v0.2.7 정보 섹션 (Task 11) — 트레이 showAboutDialog 의 IPC 대응 + getAppInfo: () => ipcRenderer.invoke('settings:get-app-info'), + openProfileDir: () => ipcRenderer.invoke('settings:open-profile-dir'), + copyAppInfo: () => ipcRenderer.invoke('settings:copy-app-info'), } }; diff --git a/src/renderer/inbox/components/SettingsPage.tsx b/src/renderer/inbox/components/SettingsPage.tsx index 62f98a5..a4b807a 100644 --- a/src/renderer/inbox/components/SettingsPage.tsx +++ b/src/renderer/inbox/components/SettingsPage.tsx @@ -3,6 +3,7 @@ import { useInbox } from '../store.js'; import { AiProviderSection } from './settings/AiProviderSection.js'; import { AutostartSection } from './settings/AutostartSection.js'; import { BackupSection } from './settings/BackupSection.js'; +import { InfoSection } from './settings/InfoSection.js'; export function SettingsPage(): React.ReactElement { const setShowSettings = useInbox((s) => s.setShowSettings); @@ -37,7 +38,7 @@ export function SettingsPage(): React.ReactElement {

정보

- {/* InfoSection — Task 11 */} +
); diff --git a/src/renderer/inbox/components/settings/InfoSection.tsx b/src/renderer/inbox/components/settings/InfoSection.tsx new file mode 100644 index 0000000..b76f2cf --- /dev/null +++ b/src/renderer/inbox/components/settings/InfoSection.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from 'react'; +import { inboxApi } from '../../api.js'; + +interface AppInfo { + version: string; + electron: string; + node: string; + os: string; + profileDir: string; +} + +export function InfoSection(): React.ReactElement { + const [info, setInfo] = useState(null); + + useEffect(() => { + void (async () => setInfo(await inboxApi.getAppInfo()))(); + }, []); + + if (!info) return
로딩 중...
; + + return ( +
+
+
버전
+
{info.version}
+
Electron
+
{info.electron}
+
Node
+
{info.node}
+
OS
+
{info.os}
+
데이터 위치
+
{info.profileDir}
+
+
+ + +
+
+ ); +} diff --git a/src/shared/types.ts b/src/shared/types.ts index 38b3bec..5be2325 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -101,6 +101,16 @@ export interface InboxApi { runImport(): Promise<{ ok: true }>; runSync(): Promise<{ ok: true }>; runExportTelemetry(): Promise<{ ok: true }>; + // v0.2.7 정보 섹션 (Task 11) — 트레이 showAboutDialog 의 IPC 대응. 트레이 잔류 → Task 25 cleanup. + getAppInfo(): Promise<{ + version: string; + electron: string; + node: string; + os: string; + profileDir: string; + }>; + openProfileDir(): Promise; + copyAppInfo(): Promise; } export interface InklingApi { diff --git a/tests/unit/InfoSection.test.tsx b/tests/unit/InfoSection.test.tsx new file mode 100644 index 0000000..7dcdb81 --- /dev/null +++ b/tests/unit/InfoSection.test.tsx @@ -0,0 +1,49 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import '@testing-library/jest-dom/vitest'; +import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; + +vi.mock('../../src/renderer/inbox/api.js', () => ({ + inboxApi: { + getAppInfo: vi.fn(async () => ({ + version: '0.2.7', + electron: '41.3.0', + node: '22.x', + os: 'darwin 23.6.0', + profileDir: '/Users/u/Library/Application Support/Inkling' + })), + openProfileDir: vi.fn(async () => undefined), + copyAppInfo: vi.fn(async () => undefined) + } +})); + +import { InfoSection } from '../../src/renderer/inbox/components/settings/InfoSection'; + +describe('InfoSection', () => { + beforeEach(() => { vi.clearAllMocks(); cleanup(); }); + + it('renders version, electron, node, OS, profileDir', async () => { + render(); + expect(await screen.findByText(/0\.2\.7/)).toBeInTheDocument(); + expect(screen.getByText(/41\.3\.0/)).toBeInTheDocument(); + expect(screen.getByText(/22\.x/)).toBeInTheDocument(); + expect(screen.getByText(/darwin/)).toBeInTheDocument(); + expect(screen.getByText(/Library\/Application Support\/Inkling/)).toBeInTheDocument(); + }); + + it('"데이터 위치 열기" calls openProfileDir', async () => { + const { inboxApi } = await import('../../src/renderer/inbox/api.js'); + render(); + await screen.findByText(/0\.2\.7/); + fireEvent.click(screen.getByRole('button', { name: /데이터 위치 열기/ })); + await waitFor(() => expect(inboxApi.openProfileDir).toHaveBeenCalled()); + }); + + it('"정보 복사" calls copyAppInfo', async () => { + const { inboxApi } = await import('../../src/renderer/inbox/api.js'); + render(); + await screen.findByText(/0\.2\.7/); + fireEvent.click(screen.getByRole('button', { name: /정보 복사/ })); + await waitFor(() => expect(inboxApi.copyAppInfo).toHaveBeenCalled()); + }); +}); diff --git a/tests/unit/SettingsPage.test.tsx b/tests/unit/SettingsPage.test.tsx index 371904b..3e0fa48 100644 --- a/tests/unit/SettingsPage.test.tsx +++ b/tests/unit/SettingsPage.test.tsx @@ -17,7 +17,16 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({ runExport: vi.fn(async () => ({ ok: true })), runImport: vi.fn(async () => ({ ok: true })), runSync: vi.fn(async () => ({ ok: true })), - runExportTelemetry: vi.fn(async () => ({ ok: true })) + runExportTelemetry: vi.fn(async () => ({ ok: true })), + getAppInfo: vi.fn(async () => ({ + version: '0.2.7', + electron: '41.3.0', + node: '22.x', + os: 'darwin 23.6.0', + profileDir: '/tmp/Inkling' + })), + openProfileDir: vi.fn(async () => undefined), + copyAppInfo: vi.fn(async () => undefined) } }));