From 92375edc31d90d79d7cb0666045e02fabcfe82c3 Mon Sep 17 00:00:00 2001 From: altair823 Date: Sat, 9 May 2026 15:51:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(v029):=20=ED=97=A4=EB=8D=94=204=ED=83=AD?= =?UTF-8?q?=20(Inbox/=EC=99=84=EB=A3=8C/=EB=B3=B4=EA=B4=80/=ED=9C=B4?= =?UTF-8?q?=EC=A7=80=ED=86=B5)=20+=20count=20badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App.tsx: 기존 2탭 (Inbox/휴지통) → 4탭. setView/counts 사용. - onNavigate 도 setView 로 위임 (mirror state 동기 갱신). - App.test: 4탭 렌더 + 클릭 → setView('completed') + aria-pressed (3 cases). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/renderer/inbox/App.tsx | 46 +++++++++++++++++++------------------- tests/unit/App.test.tsx | 44 +++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/renderer/inbox/App.tsx b/src/renderer/inbox/App.tsx index 8421d73..8aa6e98 100644 --- a/src/renderer/inbox/App.tsx +++ b/src/renderer/inbox/App.tsx @@ -23,6 +23,10 @@ export function App(): React.ReactElement { } = useInbox(); const showSettings = useInbox((s) => s.showSettings); const setShowSettings = useInbox((s) => s.setShowSettings); + // v0.2.9 Cut B Task 5 — 4탭 (Inbox/완료/보관/휴지통). + const view = useInbox((s) => s.view); + const counts = useInbox((s) => s.counts); + const setView = useInbox((s) => s.setView); const [recoveryDismissed, setRecoveryDismissed] = useState(isRecoveryDismissedToday()); useEffect(() => { @@ -35,15 +39,8 @@ export function App(): React.ReactElement { useInbox.setState({ ollamaStatus: status }); }); const unsubNav = inboxApi.onNavigate((view) => { - if (view === 'settings') { - useInbox.getState().setShowSettings(true); - } else if (view === 'inbox') { - useInbox.getState().setShowSettings(false); - if (useInbox.getState().showTrash) void useInbox.getState().toggleShowTrash(); - } else if (view === 'trash') { - useInbox.getState().setShowSettings(false); - if (!useInbox.getState().showTrash) void useInbox.getState().toggleShowTrash(); - } + // v0.2.9 Cut B Task 4 — setView 가 mirror state (showTrash/showSettings) 동기 갱신. + useInbox.getState().setView(view); }); const onFocus = () => { void refreshMeta(); }; window.addEventListener('focus', onFocus); @@ -72,20 +69,23 @@ export function App(): React.ReactElement {

Inkling

- - + {( + [ + { key: 'inbox', label: 'Inbox', count: counts.active }, + { key: 'completed', label: '완료', count: counts.completed }, + { key: 'archived', label: '보관', count: counts.archived }, + { key: 'trash', label: '휴지통', count: counts.trashed } + ] as const + ).map((t) => ( + + ))}
diff --git a/tests/unit/App.test.tsx b/tests/unit/App.test.tsx index 8e077f5..f289ebe 100644 --- a/tests/unit/App.test.tsx +++ b/tests/unit/App.test.tsx @@ -6,6 +6,8 @@ import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/re vi.mock('../../src/renderer/inbox/api.js', () => ({ inboxApi: { listNotes: vi.fn(async () => []), + listByStatus: vi.fn(async () => []), + countsByStatus: vi.fn(async () => ({ active: 0, completed: 0, archived: 0, trashed: 0 })), getContinuity: vi.fn(async () => ({ weekStart: '', weekCount: 0, weekTarget: 7, consecutiveCompleteWeeks: 0, showRecoveryToast: false, lastNoteAt: null @@ -58,7 +60,12 @@ import { inboxApi } from '../../src/renderer/inbox/api.js'; describe('App — settings view', () => { beforeEach(() => { cleanup(); - useInbox.setState({ showSettings: false, notes: [], trashNotes: [], trashCount: 0 }); + useInbox.setState({ + view: 'inbox', + counts: { active: 0, completed: 0, archived: 0, trashed: 0 }, + showSettings: false, showTrash: false, + notes: [], trashNotes: [], trashCount: 0 + }); }); it('renders SettingsPage when showSettings=true', async () => { @@ -89,3 +96,38 @@ describe('App — settings view', () => { await waitFor(() => expect(useInbox.getState().showSettings).toBe(true)); }); }); + +describe('App header — 4 tabs', () => { + beforeEach(() => { + cleanup(); + useInbox.setState({ + view: 'inbox', + counts: { active: 5, completed: 3, archived: 2, trashed: 1 }, + notes: [], trashNotes: [], trashCount: 0, + showTrash: false, showSettings: false + }); + }); + + it('renders 4 tabs with counts', () => { + render(); + expect(screen.getByRole('button', { name: /Inbox\(5\)/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /완료\(3\)/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /보관\(2\)/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /휴지통\(1\)/ })).toBeInTheDocument(); + }); + + it('clicking 완료 tab sets view=completed', () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /완료/ })); + expect(useInbox.getState().view).toBe('completed'); + }); + + it('aria-pressed reflects current view', () => { + useInbox.setState({ view: 'archived' }); + render(); + const archivedBtn = screen.getByRole('button', { name: /보관/ }); + expect(archivedBtn.getAttribute('aria-pressed')).toBe('true'); + const inboxBtn = screen.getByRole('button', { name: /Inbox/ }); + expect(inboxBtn.getAttribute('aria-pressed')).toBe('false'); + }); +});