From 9feb712c60ebcb5b930f68c4c3d5fbcbbe0815fd Mon Sep 17 00:00:00 2001 From: altair823 Date: Sun, 10 May 2026 00:39:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(v0211):=20ReviewView=20=E2=80=94=20?= =?UTF-8?q?=EC=9D=BC/=EC=A3=BC/=EC=9B=94=20=ED=9A=8C=EA=B3=A0=20+=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20dropdown=20=EC=A7=84=EC=9E=85=EC=A0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/inbox/App.tsx | 20 ++++++ src/renderer/inbox/components/ReviewView.tsx | 56 +++++++++++++++++ tests/unit/ReviewView.test.tsx | 64 ++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 src/renderer/inbox/components/ReviewView.tsx create mode 100644 tests/unit/ReviewView.test.tsx diff --git a/src/renderer/inbox/App.tsx b/src/renderer/inbox/App.tsx index 9fb8f19..691fef6 100644 --- a/src/renderer/inbox/App.tsx +++ b/src/renderer/inbox/App.tsx @@ -15,6 +15,8 @@ import { RecallBanner } from './components/RecallBanner.js'; import { SettingsPage } from './components/SettingsPage.js'; import { OnboardingWizard } from './components/OnboardingWizard.js'; import { SearchBox } from './components/SearchBox.js'; +import { ReviewView } from './components/ReviewView.js'; +import type { InboxView } from './store.js'; export function App(): React.ReactElement { const { @@ -69,6 +71,10 @@ export function App(): React.ReactElement { if (showOnboarding === null) return <>; if (showOnboarding) return setShowOnboarding(false)} />; + if (view === 'review-daily') return ; + if (view === 'review-weekly') return ; + if (view === 'review-monthly') return ; + if (showSettings) return ; const showRecovery = continuity.showRecoveryToast && !recoveryDismissed; @@ -108,6 +114,20 @@ export function App(): React.ReactElement { ))} +
diff --git a/src/renderer/inbox/components/ReviewView.tsx b/src/renderer/inbox/components/ReviewView.tsx new file mode 100644 index 0000000..78be1e0 --- /dev/null +++ b/src/renderer/inbox/components/ReviewView.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useInbox } from '../store.js'; +import { NoteCard } from './NoteCard.js'; + +interface Props { + period: 'daily' | 'weekly' | 'monthly'; +} + +const periodLabel: Record = { + daily: '์ผ๊ฐ„', + weekly: '์ฃผ๊ฐ„', + monthly: '์›”๊ฐ„' +}; + +export function ReviewView({ period }: Props): React.ReactElement { + const reviewData = useInbox((s) => s.reviewData); + if (!reviewData) { + return
๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘โ€ฆ
; + } + const max = reviewData.tagCounts[0]?.count ?? 1; + return ( +
+

{periodLabel[period]} ํšŒ๊ณ 

+
+ ์ด {reviewData.totalCount}๊ฑด +
+
+

ํƒœ๊ทธ ๋ถ„ํฌ

+ {reviewData.tagCounts.length === 0 && ( +
ํƒœ๊ทธ ์—†์Œ
+ )} + {reviewData.tagCounts.slice(0, 10).map((t) => ( +
+ {t.tag} +
+
+
+ {t.count} +
+ ))} +
+
+

๋งˆ๊ฐ ์ง„ํ–‰

+
+ ์™„๋ฃŒ {reviewData.dueProgress.passed} / {reviewData.dueProgress.total} ยท ๋Œ€๊ธฐ {reviewData.dueProgress.pending} +
+
+
+

์ตœ๊ทผ ๋…ธํŠธ ({reviewData.recentNotes.length})

+ {reviewData.recentNotes.map((n) => ( + {}} /> + ))} +
+
+ ); +} diff --git a/tests/unit/ReviewView.test.tsx b/tests/unit/ReviewView.test.tsx new file mode 100644 index 0000000..e94268a --- /dev/null +++ b/tests/unit/ReviewView.test.tsx @@ -0,0 +1,64 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import '@testing-library/jest-dom/vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import React from 'react'; + +const baseState = { + reviewData: { + totalCount: 12, + recentNotes: [], + tagCounts: [{ tag: 'ํšŒ์˜', count: 5 }, { tag: '๊ฒฐ์žฌ', count: 3 }], + dueProgress: { total: 10, passed: 4, pending: 6 } + } +}; + +vi.mock('../../src/renderer/inbox/api.js', () => ({ + inboxApi: { + openMedia: vi.fn(), + deleteNote: vi.fn(), + restoreNote: vi.fn(), + permanentDeleteNote: vi.fn(), + updateAiFields: vi.fn(), + setDueDate: vi.fn(), + setIntent: vi.fn(), + dismissIntent: vi.fn(), + setStatus: vi.fn(async () => ({ ok: true as const })), + classifyStatus: vi.fn(async () => ({ recommended: 'archived' as const, rationale: 'stub' })), + updateRawText: vi.fn(async () => ({ ok: true as const })), + listRevisions: vi.fn(async () => []), + getRevision: vi.fn() + } +})); + +vi.mock('../../src/renderer/inbox/store.js', () => ({ + useInbox: Object.assign( + (selector?: (s: typeof baseState) => unknown) => (selector ? selector(baseState) : baseState), + { getState: () => baseState } + ) +})); + +import { ReviewView } from '../../src/renderer/inbox/components/ReviewView'; + +describe('ReviewView', () => { + beforeEach(() => { cleanup(); }); + + it('daily โ€” ๋ผ๋ฒจ + totalCount + tagBar + dueProgress ๋ Œ๋”', () => { + render(); + expect(screen.getByText(/์ผ๊ฐ„/)).toBeInTheDocument(); + expect(screen.getByText(/์ด.*12๊ฑด/)).toBeInTheDocument(); + expect(screen.getByText('ํšŒ์˜')).toBeInTheDocument(); + expect(screen.getByText('๊ฒฐ์žฌ')).toBeInTheDocument(); + expect(screen.getByText(/4.*\/.*10/)).toBeInTheDocument(); + }); + + it('weekly โ€” ๋ผ๋ฒจ weekly', () => { + render(); + expect(screen.getByText(/์ฃผ๊ฐ„/)).toBeInTheDocument(); + }); + + it('monthly โ€” ๋ผ๋ฒจ monthly', () => { + render(); + expect(screen.getByText(/์›”๊ฐ„/)).toBeInTheDocument(); + }); +});