Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { Tabs, TabItem } from './ui/Tabs';
export { TextArea } from './ui/TextArea';
export { Tooltip } from './ui/Tooltip';
export { Title1, Title2, Title3, Caption1, Body1, Body2, Body3 } from './ui/Typography';
export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/Table';

export { Icons, iconNames } from './ui/assets';
export type { IconName } from './ui/assets';
Expand Down
73 changes: 73 additions & 0 deletions src/shared/ui/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './index';

const PrimitiveMeta = {
title: 'Components/Table',
component: Table,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: '기본적인 Table 레이아웃 구성 요소 세트입니다. ',
},
},
},
} satisfies Meta<typeof Table>;
export default PrimitiveMeta;

const data = [
{
scoreCategory: '동아리 활동 보고서',
reason: '2회차 활동보고서',
amount: 4.615,
createdAt: '2025-03-31 15:42:26',
},
{
scoreCategory: '동아리 활동 보고서',
reason: '2회차 활동보고서',
amount: 0.001,
createdAt: '2025-03-31 16:02:31',
},
{
scoreCategory: '전동대회',
reason: '3월 전동대회 참여',
amount: 1.0,
createdAt: '2025-04-01 11:40:36',
},
{
scoreCategory: '전동대회',
reason: '4월 전동대회 참여',
amount: 1.0,
createdAt: '2025-04-28 19:44:04',
},
{
scoreCategory: '동아리 활동 보고서',
reason: '3회차 활동보고서',
amount: 4.615,
createdAt: '2025-04-30 14:56:26',
},
];

export const Basic: StoryObj<typeof PrimitiveMeta> = {
render: () => (
<Table>
<TableHeader>
<TableRow>
<TableHead>날짜</TableHead>
<TableHead>카테고리</TableHead>
<TableHead>점수</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, i) => (
<TableRow key={i}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

배열 인덱스를 key로 사용하고 있습니다.

정적 데모 데이터의 경우 허용되지만, 안정적인 고유 식별자를 key로 사용하는 것이 더 좋은 관행입니다. 예를 들어, createdAtscoreCategory를 조합하여 사용할 수 있습니다.

-        {data.map((row, i) => (
-          <TableRow key={i}>
+        {data.map((row) => (
+          <TableRow key={`${row.createdAt}-${row.scoreCategory}`}>
🤖 Prompt for AI Agents
In src/shared/ui/Table/Table.stories.tsx around line 64, the story currently
uses the array index as the React key which can cause unstable identity; replace
the index with a stable unique key built from the row data (e.g., combine
createdAt and scoreCategory into a single string) and ensure it’s deterministic
(stringify or format timestamps) and falls back to a safe unique value if either
field is missing.

<TableCell>{row.createdAt}</TableCell>
<TableCell>{row.scoreCategory}</TableCell>
<TableCell>{row.amount.toFixed(3)}점</TableCell>
</TableRow>
))}
</TableBody>
</Table>
),
};
Comment on lines +52 to +73
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

스토리를 더 인터랙티브하게 만들 수 있습니다.

현재 Basic 스토리는 render 함수에 데이터가 하드코딩되어 있어 Storybook의 Controls 패널을 통해 수정할 수 없습니다. 데이터를 args로 받도록 하면 사용자가 다양한 데이터 조합을 테스트할 수 있습니다.

다음과 같이 개선할 수 있습니다:

+type StoryArgs = {
+  data: typeof data;
+};
+
-export const Basic: StoryObj<typeof PrimitiveMeta> = {
+export const Basic: StoryObj<StoryArgs> = {
+  args: {
+    data,
+  },
-  render: () => (
+  render: ({ data }) => (
     <Table>
       <TableHeader>
         <TableRow>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const Basic: StoryObj<typeof PrimitiveMeta> = {
render: () => (
<Table>
<TableHeader>
<TableRow>
<TableHead>날짜</TableHead>
<TableHead>카테고리</TableHead>
<TableHead>점수</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, i) => (
<TableRow key={i}>
<TableCell>{row.createdAt}</TableCell>
<TableCell>{row.scoreCategory}</TableCell>
<TableCell>{row.amount.toFixed(3)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
),
};
type StoryArgs = {
data: typeof data;
};
export const Basic: StoryObj<StoryArgs> = {
args: {
data,
},
render: ({ data }) => (
<Table>
<TableHeader>
<TableRow>
<TableHead>날짜</TableHead>
<TableHead>카테고리</TableHead>
<TableHead>점수</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, i) => (
<TableRow key={i}>
<TableCell>{row.createdAt}</TableCell>
<TableCell>{row.scoreCategory}</TableCell>
<TableCell>{row.amount.toFixed(3)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
),
};
🤖 Prompt for AI Agents
In src/shared/ui/Table/Table.stories.tsx around lines 52-73, the Basic story
uses hardcoded data inside render which prevents Storybook Controls from
modifying it; refactor the story to accept args (e.g., render: (args) => ...)
and use args.data (with a sensible default via Basic.args) for the rows so users
can edit the dataset from Controls; update the StoryObj generic if needed to
reflect the arg type and add Basic.args = { data: [...] } providing the original
sample so backwards compatibility is preserved.

26 changes: 26 additions & 0 deletions src/shared/ui/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';

import { cn } from '@/shared/lib/core';

export type Props = React.HTMLAttributes<HTMLTableElement> & {
/**
* Optional custom className for additional styling.
*/
className?: string;

/**
* Table content, usually composed of `<TableHeader>`, `<TableBody>`, etc.
*/
children?: React.ReactNode;
};
export function Table({ className, children, ...props }: Props) {
return (
<div
className={cn('no-scrollbar relative w-full overflow-auto rounded-md border border-gray-100')}
>
<table className={cn('w-full table-fixed border-collapse text-sm', className)} {...props}>
{children}
</table>
</div>
);
}
16 changes: 16 additions & 0 deletions src/shared/ui/Table/TableBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';

import { cn } from '@/shared/lib/core';

export type Props = React.HTMLAttributes<HTMLTableSectionElement> & {
className?: string;
children?: React.ReactNode;
};

export function TableBody({ className, children, ...props }: Props) {
return (
<tbody className={cn('bg-white', className)} {...props}>
{children}
</tbody>
);
}
20 changes: 20 additions & 0 deletions src/shared/ui/Table/TableCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';

import { cn } from '@/shared/lib/core';

import { Body2 } from '../Typography';

export type Props = React.TdHTMLAttributes<HTMLTableCellElement> & {
className?: string;
children?: React.ReactNode;
};

export function TableCell({ className, children, ...props }: Props) {
return (
<td className={cn('px-4 py-2 align-middle md:px-4', className)} {...props}>
<Body2 className="whitespace-pre-wrap text-gray-400" weight="normal">
{children}
</Body2>
</td>
);
}
Comment on lines +12 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

중복된 md:px-4 클래스를 제거하세요.

px-4는 모든 화면 크기에 이미 적용되므로, md:px-4는 중복됩니다. Tailwind의 mobile-first 접근 방식에서 기본 클래스는 모든 브레이크포인트에 적용되며, md: 접두사는 해당 브레이크포인트 이상에서 값을 재정의할 때만 의미가 있습니다.

참고: Body2에서 패딩을 제거하여 이전 리뷰의 중복 패딩 문제가 해결된 점은 좋습니다.

다음과 같이 수정하여 중복을 제거하세요:

-    <td className={cn('px-4 py-2 align-middle md:px-4', className)} {...props}>
+    <td className={cn('px-4 py-2 align-middle', className)} {...props}>

만약 모바일과 데스크톱에서 다른 패딩을 의도했다면, 다음과 같은 패턴을 사용하세요:

-    <td className={cn('px-4 py-2 align-middle md:px-4', className)} {...props}>
+    <td className={cn('px-2 py-2 align-middle md:px-4', className)} {...props}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function TableCell({ className, children, ...props }: Props) {
return (
<td className={cn('px-4 py-2 align-middle md:px-4', className)} {...props}>
<Body2 className="whitespace-pre-wrap text-gray-400" weight="normal">
{children}
</Body2>
</td>
);
}
export function TableCell({ className, children, ...props }: Props) {
return (
<td className={cn('px-4 py-2 align-middle', className)} {...props}>
<Body2 className="whitespace-pre-wrap text-gray-400" weight="normal">
{children}
</Body2>
</td>
);
}
🤖 Prompt for AI Agents
In src/shared/ui/Table/TableCell.tsx around lines 12 to 20, remove the redundant
Tailwind class `md:px-4` from the td class list because `px-4` already applies
to all breakpoints; update the class string to only include `px-4 py-2
align-middle` (plus the incoming className) so you don't duplicate padding; if
you actually intended different padding on md and up, replace `md:px-4` with the
desired `md:px-{value}` instead of duplicating the same value.

21 changes: 21 additions & 0 deletions src/shared/ui/Table/TableHead.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';

import { cn } from '@/shared/lib/core';

import { Body2 } from '../Typography';

export type Props = React.ThHTMLAttributes<HTMLTableCellElement> & {
className?: string;
children?: React.ReactNode;
};

export function TableHead({ className, children, ...props }: Props) {
return (
<th
className={cn('h-10 px-4 py-2 text-left align-middle whitespace-nowrap', className)}
{...props}
>
<Body2 className="text-gray-700">{children}</Body2>
</th>
);
}
16 changes: 16 additions & 0 deletions src/shared/ui/Table/TableHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';

import { cn } from '@/shared/lib/core';

export type Props = React.HTMLAttributes<HTMLTableSectionElement> & {
className?: string;
children?: React.ReactNode;
};

export function TableHeader({ className, children, ...props }: Props) {
return (
<thead className={cn('bg-gray-50', className)} {...props}>
{children}
</thead>
);
}
16 changes: 16 additions & 0 deletions src/shared/ui/Table/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';

import { cn } from '@/shared/lib/core';

export type Props = React.HTMLAttributes<HTMLTableRowElement> & {
className?: string;
children?: React.ReactNode;
};

export function TableRow({ className, children, ...props }: Props) {
return (
<tr className={cn('border-b border-gray-200 last:border-0', className)} {...props}>
{children}
</tr>
);
}
6 changes: 6 additions & 0 deletions src/shared/ui/Table/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { Table } from './Table';
export { TableHeader } from './TableHeader';
export { TableBody } from './TableBody';
export { TableRow } from './TableRow';
export { TableHead } from './TableHead';
export { TableCell } from './TableCell';