---
.../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();
+ });
+});