diff --git a/src/renderer/inbox/components/OnboardingWizard.tsx b/src/renderer/inbox/components/OnboardingWizard.tsx new file mode 100644 index 0000000..47e2cc3 --- /dev/null +++ b/src/renderer/inbox/components/OnboardingWizard.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { inboxApi } from '../api.js'; + +/** + * v0.2.9 Cut B Task 11 — 첫 launch onboarding 위저드. + * + * 3 옵션 (AI 사용 / 원문만 / 나중에) 중 하나를 선택. AI 옵션 (true/false) 은 + * setAiEnabled 로 settings 에 저장, 모든 옵션은 setOnboardingCompleted(true) 로 + * 두 번째 launch 부터 미노출. "나중에" 는 ai_enabled 기본값 (true) 유지 — 사용자 + * 가 SettingsPage 에서 추후 선택 가능. + */ +export function OnboardingWizard({ onClose }: { onClose: () => void }): React.ReactElement { + async function choose(aiEnabled: boolean | null): Promise { + if (aiEnabled !== null) await inboxApi.setAiEnabled(aiEnabled); + await inboxApi.setOnboardingCompleted(true); + onClose(); + } + + return ( +
+
+

Inkling 사용 시작

+

+ Inkling 은 로컬 LLM (Ollama) 으로 메모를 자동 정리합니다. + Ollama 가 설치되어 있고 한국어 지원 모델 (gemma3, gemma2 등) 이 pull 되어 있어야 최적의 경험이 가능합니다. +

+

+ 설치 가이드:  + ollama.com/download +

+
+ + + +
+
+
+ ); +} diff --git a/tests/unit/OnboardingWizard.test.tsx b/tests/unit/OnboardingWizard.test.tsx new file mode 100644 index 0000000..e80c3e0 --- /dev/null +++ b/tests/unit/OnboardingWizard.test.tsx @@ -0,0 +1,58 @@ +// @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'; + +const { mockSetAi, mockSetCompleted } = vi.hoisted(() => ({ + mockSetAi: vi.fn(async () => ({ ok: true as const })), + mockSetCompleted: vi.fn(async () => ({ ok: true as const })) +})); +vi.mock('../../src/renderer/inbox/api.js', () => ({ + inboxApi: { setAiEnabled: mockSetAi, setOnboardingCompleted: mockSetCompleted } +})); + +import { OnboardingWizard } from '../../src/renderer/inbox/components/OnboardingWizard'; + +describe('OnboardingWizard', () => { + beforeEach(() => { + vi.clearAllMocks(); + cleanup(); + }); + + it('renders 3 buttons + 설치 가이드 link', () => { + render(); + expect(screen.getByRole('button', { name: /AI 자동 처리 사용/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /원문만 저장/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /나중에 설정/ })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /ollama\.com/ })).toBeInTheDocument(); + }); + + it('"AI 사용" → setAiEnabled(true) + setOnboardingCompleted(true) + onClose', async () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button', { name: /AI 자동 처리 사용/ })); + await waitFor(() => { + expect(mockSetAi).toHaveBeenCalledWith(true); + expect(mockSetCompleted).toHaveBeenCalledWith(true); + expect(onClose).toHaveBeenCalled(); + }); + }); + + it('"원문만" → setAiEnabled(false) + setOnboardingCompleted(true)', async () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /원문만 저장/ })); + await waitFor(() => { + expect(mockSetAi).toHaveBeenCalledWith(false); + expect(mockSetCompleted).toHaveBeenCalledWith(true); + }); + }); + + it('"나중에" → setOnboardingCompleted(true) only (no setAiEnabled)', async () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /나중에 설정/ })); + await waitFor(() => { + expect(mockSetCompleted).toHaveBeenCalledWith(true); + expect(mockSetAi).not.toHaveBeenCalled(); + }); + }); +});