Skip to content

Commit 95142e5

Browse files
committed
feat: Table Component 추가
1 parent 9a1ece7 commit 95142e5

File tree

6 files changed

+173
-3
lines changed

6 files changed

+173
-3
lines changed

packages/notion-to-jsx/src/components/Renderer/components/Block/BlockRenderer.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CodeBlock } from '../Code';
1010
import { Heading1, Heading2, Heading3, Paragraph } from '../Typography';
1111
import { ColumnList } from '../Column';
1212
import { Quote } from '../Quote';
13+
import Table from '../Table';
1314

1415
export interface Props {
1516
block: any;
@@ -23,7 +24,6 @@ const BlockRenderer: React.FC<Props> = ({ block, onFocus, index }) => {
2324
const blockProps = {
2425
tabIndex: 0,
2526
onFocus,
26-
'data-block-id': block.id,
2727
};
2828

2929
switch (block.type) {
@@ -93,7 +93,10 @@ const BlockRenderer: React.FC<Props> = ({ block, onFocus, index }) => {
9393
return null;
9494

9595
case 'quote':
96-
return <Quote richText={block.quote.rich_text} {...blockProps} />;
96+
return <Quote richTexts={block.quote.rich_text} {...blockProps} />;
97+
98+
case 'table':
99+
return <Table block={block} tabIndex={blockProps.tabIndex} />;
97100

98101
default:
99102
console.log(`지원되지 않는 블록 타입: ${block.type}`, block);

packages/notion-to-jsx/src/components/Renderer/components/List/ListBlocksRenderer.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const RecursiveListItem: React.FC<{
1515
}> = ({ block, index }) => {
1616
const blockProps = {
1717
tabIndex: 0,
18-
'data-block-id': block.id,
1918
};
2019

2120
const blockType = block.type;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React from 'react';
2+
import { tableContainer, table, headerCell, hasRowHeader } from './styles.css';
3+
import TableRow from './TableRow';
4+
import { NotionBlock } from '../../../../types';
5+
6+
interface TableProps {
7+
block: NotionBlock;
8+
tabIndex?: number;
9+
}
10+
11+
const Table: React.FC<TableProps> = ({ block, tabIndex = 0 }) => {
12+
if (!block.table || !block.children) {
13+
return null;
14+
}
15+
16+
const { table_width, has_column_header, has_row_header } = block.table;
17+
const rows =
18+
block.children?.filter(
19+
(child: NotionBlock) => child.type === 'table_row'
20+
) || [];
21+
22+
return (
23+
<div className={tableContainer}>
24+
<table className={table} tabIndex={tabIndex}>
25+
{rows.length > 0 && (
26+
<>
27+
{has_column_header && rows[0] && (
28+
<thead>
29+
<TableRow rowBlock={rows[0]} cellClassName={headerCell} />
30+
</thead>
31+
)}
32+
<tbody>
33+
{/* 유효한 row만 매핑하도록 필터링 추가 */}
34+
{rows
35+
.filter(
36+
(row): row is NotionBlock =>
37+
row !== undefined && row.type === 'table_row'
38+
)
39+
.map((row: NotionBlock, rowIndex: number) => {
40+
// 열 헤더가 있고 첫 번째 행이면 이미 thead에서 렌더링되었으므로 건너뜁니다
41+
if (has_column_header && rowIndex === 0) {
42+
return null;
43+
}
44+
45+
const actualRowIndex = has_column_header
46+
? rowIndex - 1
47+
: rowIndex;
48+
// 타입 체크를 통해 row가 실제 Block 타입임을 확인합니다
49+
return (
50+
<TableRow
51+
key={row.id}
52+
rowBlock={row}
53+
rowHeaderIndex={has_row_header ? 0 : -1}
54+
/>
55+
);
56+
})}
57+
</tbody>
58+
</>
59+
)}
60+
</table>
61+
</div>
62+
);
63+
};
64+
65+
export default Table;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import { tableCell, firstCell, lastCell, hasRowHeader } from './styles.css';
3+
import { MemoizedRichText } from '../MemoizedComponents';
4+
import { NotionBlock } from '../../../../types';
5+
import { RichTextItem } from '../RichText/RichTexts';
6+
7+
interface TableRowProps {
8+
rowBlock: NotionBlock;
9+
cellClassName?: string;
10+
rowHeaderIndex?: number;
11+
}
12+
13+
const TableRow: React.FC<TableRowProps> = ({
14+
rowBlock,
15+
cellClassName = '',
16+
rowHeaderIndex = -1,
17+
}) => {
18+
if (!rowBlock.table_row?.cells) {
19+
return null;
20+
}
21+
22+
const { cells } = rowBlock.table_row;
23+
24+
return (
25+
<tr>
26+
{cells.map((cell: RichTextItem[], index: number) => {
27+
const isFirstCell = index === 0;
28+
const isLastCell = index === cells.length - 1;
29+
const isRowHeader = index === rowHeaderIndex;
30+
31+
let cellClasses = [tableCell, cellClassName];
32+
33+
if (isFirstCell) cellClasses.push(firstCell);
34+
if (isLastCell) cellClasses.push(lastCell);
35+
if (isRowHeader) cellClasses.push(hasRowHeader);
36+
37+
return (
38+
<td
39+
key={`${rowBlock.id}-cell-${index}`}
40+
className={cellClasses.filter(Boolean).join(' ')}
41+
>
42+
<MemoizedRichText richTexts={cell} />
43+
</td>
44+
);
45+
})}
46+
</tr>
47+
);
48+
};
49+
50+
export default TableRow;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Table from './Table';
2+
import TableRow from './TableRow';
3+
4+
export { TableRow };
5+
export default Table;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { style } from '@vanilla-extract/css';
2+
import { vars } from '../../../../styles/theme.css';
3+
4+
export const tableContainer = style({
5+
width: '100%',
6+
marginTop: vars.spacing.xs,
7+
marginBottom: vars.spacing.xs,
8+
borderRadius: vars.borderRadius.sm,
9+
overflow: 'hidden',
10+
});
11+
12+
export const table = style({
13+
width: '100%',
14+
borderCollapse: 'collapse',
15+
borderSpacing: 0,
16+
fontSize: vars.typography.fontSize.small,
17+
color: 'inherit',
18+
});
19+
20+
export const headerCell = style({
21+
backgroundColor: '#f7f6f3',
22+
fontWeight: vars.typography.fontWeight.semibold,
23+
});
24+
25+
export const tableCell = style({
26+
position: 'relative',
27+
padding: `${vars.spacing.xs} ${vars.spacing.sm}`,
28+
minHeight: '2rem',
29+
border: '1px solid rgba(55, 53, 47, 0.09)',
30+
borderLeft: 'none',
31+
borderRight: 'none',
32+
verticalAlign: 'top',
33+
textAlign: 'left',
34+
userSelect: 'text',
35+
});
36+
37+
export const firstCell = style({
38+
borderLeft: '1px solid rgba(55, 53, 47, 0.09)',
39+
});
40+
41+
export const lastCell = style({
42+
borderRight: '1px solid rgba(55, 53, 47, 0.09)',
43+
});
44+
45+
export const hasRowHeader = style({
46+
backgroundColor: '#f7f6f3',
47+
fontWeight: vars.typography.fontWeight.semibold,
48+
});

0 commit comments

Comments
 (0)