feat(ui): Sidebar + NotebookList + NotebookCreateModal stub
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
src/renderer/inbox/components/NotebookCreateModal.tsx
Normal file
15
src/renderer/inbox/components/NotebookCreateModal.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 120 }}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div style={{ background: '#fff', padding: 20, margin: '20% auto', width: 320 }}>
|
||||
(NotebookCreateModal — Task 13 에서 구현)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
src/renderer/inbox/components/NotebookList.tsx
Normal file
49
src/renderer/inbox/components/NotebookList.tsx
Normal file
@@ -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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{notebooks.map((nb) => {
|
||||
const active = nb.id === selectedId;
|
||||
return (
|
||||
<button
|
||||
key={nb.id}
|
||||
onClick={() => onSelect(nb.id)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 12px', background: active ? '#eaf3ff' : 'transparent',
|
||||
border: 'none', cursor: 'pointer', textAlign: 'left',
|
||||
color: active ? '#0a4b80' : '#333', fontSize: 13
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: 8, height: 8, borderRadius: '50%',
|
||||
background: nb.color ?? '#bbb', flexShrink: 0
|
||||
}} />
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{nb.name}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: '#888' }}>{nb.noteCount}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
onClick={onCreate}
|
||||
style={{
|
||||
padding: '6px 12px', background: 'transparent', border: 'none',
|
||||
cursor: 'pointer', textAlign: 'left', color: '#888', fontSize: 12
|
||||
}}
|
||||
>
|
||||
+ 새 노트북
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
src/renderer/inbox/components/Sidebar.tsx
Normal file
31
src/renderer/inbox/components/Sidebar.tsx
Normal file
@@ -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 (
|
||||
<aside style={{
|
||||
width, borderRight: '1px solid #e0e0e0',
|
||||
background: '#fafafa', overflowY: 'auto',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<NotebookList
|
||||
notebooks={notebooks}
|
||||
selectedId={selectedId}
|
||||
onSelect={selectNotebook}
|
||||
onCreate={() => setCreateOpen(true)}
|
||||
/>
|
||||
{createOpen && <NotebookCreateModal onClose={() => setCreateOpen(false)} />}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
44
tests/unit/NotebookList.test.tsx
Normal file
44
tests/unit/NotebookList.test.tsx
Normal file
@@ -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(<NotebookList notebooks={notebooks} selectedId="nb-1" onSelect={() => {}} 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(<NotebookList notebooks={notebooks} selectedId="nb-1" onSelect={onSelect} onCreate={() => {}} />);
|
||||
fireEvent.click(screen.getByText('회사'));
|
||||
expect(onSelect).toHaveBeenCalledWith('nb-2');
|
||||
});
|
||||
|
||||
it('+ 새 노트북 클릭 시 onCreate 호출', () => {
|
||||
const onCreate = vi.fn();
|
||||
render(<NotebookList notebooks={notebooks} selectedId="nb-1" onSelect={() => {}} onCreate={onCreate} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /새 노트북/ }));
|
||||
expect(onCreate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selected notebook 의 background 가 다름', () => {
|
||||
render(<NotebookList notebooks={notebooks} selectedId="nb-1" onSelect={() => {}} 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');
|
||||
});
|
||||
});
|
||||
35
tests/unit/Sidebar.test.tsx
Normal file
35
tests/unit/Sidebar.test.tsx
Normal file
@@ -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(<Sidebar />);
|
||||
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(<Sidebar />);
|
||||
expect(screen.getByText('기본')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user