From 7b3450d0d5fa0928421ddab756d65549e4a5a80b Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Fri, 15 May 2026 10:47:07 +0900 Subject: [PATCH] feat(ui): Sidebar + NotebookList + NotebookCreateModal stub Co-Authored-By: Claude Opus 4.7 (1M context) --- .../inbox/components/NotebookCreateModal.tsx | 15 ++++++ .../inbox/components/NotebookList.tsx | 49 +++++++++++++++++++ src/renderer/inbox/components/Sidebar.tsx | 31 ++++++++++++ tests/unit/NotebookList.test.tsx | 44 +++++++++++++++++ tests/unit/Sidebar.test.tsx | 35 +++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 src/renderer/inbox/components/NotebookCreateModal.tsx create mode 100644 src/renderer/inbox/components/NotebookList.tsx create mode 100644 src/renderer/inbox/components/Sidebar.tsx create mode 100644 tests/unit/NotebookList.test.tsx create mode 100644 tests/unit/Sidebar.test.tsx diff --git a/src/renderer/inbox/components/NotebookCreateModal.tsx b/src/renderer/inbox/components/NotebookCreateModal.tsx new file mode 100644 index 0000000..3c85ce7 --- /dev/null +++ b/src/renderer/inbox/components/NotebookCreateModal.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +// Stub for Task 12 — full implementation in Task 13. +export function NotebookCreateModal({ onClose }: { onClose: () => void }): React.ReactElement { + return ( +
+
+ (NotebookCreateModal — Task 13 에서 구현) +
+
+ ); +} diff --git a/src/renderer/inbox/components/NotebookList.tsx b/src/renderer/inbox/components/NotebookList.tsx new file mode 100644 index 0000000..19091a6 --- /dev/null +++ b/src/renderer/inbox/components/NotebookList.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import type { Notebook } from '@shared/types'; + +interface Props { + notebooks: Notebook[]; + selectedId: string | null; + onSelect: (id: string) => void; + onCreate: () => void; +} + +export function NotebookList({ notebooks, selectedId, onSelect, onCreate }: Props): React.ReactElement { + return ( +
+ {notebooks.map((nb) => { + const active = nb.id === selectedId; + return ( + + ); + })} + +
+ ); +} diff --git a/src/renderer/inbox/components/Sidebar.tsx b/src/renderer/inbox/components/Sidebar.tsx new file mode 100644 index 0000000..3919c5d --- /dev/null +++ b/src/renderer/inbox/components/Sidebar.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import { useInbox } from '../store.js'; +import { NotebookList } from './NotebookList.js'; +import { NotebookCreateModal } from './NotebookCreateModal.js'; + +export function Sidebar(): React.ReactElement | null { + const visible = useInbox((s) => s.sidebarVisible); + const width = useInbox((s) => s.sidebarWidth); + const notebooks = useInbox((s) => s.notebooks); + const selectedId = useInbox((s) => s.selectedNotebookId); + const selectNotebook = useInbox((s) => s.selectNotebook); + const [createOpen, setCreateOpen] = useState(false); + + if (!visible) return null; + + return ( + + ); +} diff --git a/tests/unit/NotebookList.test.tsx b/tests/unit/NotebookList.test.tsx new file mode 100644 index 0000000..279ae0c --- /dev/null +++ b/tests/unit/NotebookList.test.tsx @@ -0,0 +1,44 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import '@testing-library/jest-dom/vitest'; +import { render, screen, fireEvent, cleanup } from '@testing-library/react'; +import { NotebookList } from '../../src/renderer/inbox/components/NotebookList'; + +const notebooks = [ + { id: 'nb-1', name: '기본', color: null, createdAt: 't', updatedAt: 't', noteCount: 3 }, + { id: 'nb-2', name: '회사', color: '#0a4b80', createdAt: 't', updatedAt: 't', noteCount: 7 } +]; + +describe('NotebookList', () => { + beforeEach(cleanup); + + it('노트북 이름 + count 렌더링', () => { + render( {}} onCreate={() => {}} />); + expect(screen.getByText('기본')).toBeInTheDocument(); + expect(screen.getByText('3')).toBeInTheDocument(); + expect(screen.getByText('회사')).toBeInTheDocument(); + expect(screen.getByText('7')).toBeInTheDocument(); + }); + + it('클릭 시 onSelect 호출', () => { + const onSelect = vi.fn(); + render( {}} />); + fireEvent.click(screen.getByText('회사')); + expect(onSelect).toHaveBeenCalledWith('nb-2'); + }); + + it('+ 새 노트북 클릭 시 onCreate 호출', () => { + const onCreate = vi.fn(); + render( {}} onCreate={onCreate} />); + fireEvent.click(screen.getByRole('button', { name: /새 노트북/ })); + expect(onCreate).toHaveBeenCalled(); + }); + + it('selected notebook 의 background 가 다름', () => { + render( {}} onCreate={() => {}} />); + const btn1 = screen.getByText('기본').closest('button')!; + const btn2 = screen.getByText('회사').closest('button')!; + expect(btn1.style.background).not.toBe('transparent'); + expect(btn2.style.background).toBe('transparent'); + }); +}); diff --git a/tests/unit/Sidebar.test.tsx b/tests/unit/Sidebar.test.tsx new file mode 100644 index 0000000..c2d231c --- /dev/null +++ b/tests/unit/Sidebar.test.tsx @@ -0,0 +1,35 @@ +// @vitest-environment jsdom +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import '@testing-library/jest-dom/vitest'; +import { render, screen, cleanup } from '@testing-library/react'; + +vi.mock('../../src/renderer/inbox/api.js', () => ({ + inboxApi: {} as never, + notebookApi: {} as never +})); + +import { Sidebar } from '../../src/renderer/inbox/components/Sidebar'; +import { useInbox } from '../../src/renderer/inbox/store'; + +describe('Sidebar', () => { + beforeEach(() => { + cleanup(); + useInbox.setState({ sidebarVisible: false, sidebarWidth: 240, notebooks: [], selectedNotebookId: null } as never); + }); + + it('sidebarVisible=false 면 렌더링 안 함', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('sidebarVisible=true 면 NotebookList 렌더링', () => { + useInbox.setState({ + sidebarVisible: true, + sidebarWidth: 240, + notebooks: [{ id: 'nb-1', name: '기본', color: null, createdAt: 't', updatedAt: 't', noteCount: 0 }], + selectedNotebookId: 'nb-1' + } as never); + render(); + expect(screen.getByText('기본')).toBeInTheDocument(); + }); +});