-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(design-system): Card 컴포넌트 구현 #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ad9ec7f
86bf4e3
6fdaecc
8afc705
31196a6
cbdd377
97abd10
ff4db9c
e7d6f4f
452c320
4a059f0
d0126da
9745742
dc15864
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| interface BaseCardProps { | ||
| children: React.ReactNode; | ||
| } | ||
|
|
||
| const BaseCard = ({ children }: BaseCardProps) => { | ||
| return ( | ||
| <div className="outline-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] bg-white outline"> | ||
| {children} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default BaseCard; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import Card from './Card'; | ||
|
|
||
| const meta: Meta<typeof Card> = { | ||
| title: 'Components/Card', | ||
| component: Card, | ||
| tags: ['autodocs'], | ||
| parameters: { | ||
| layout: 'centered', | ||
| docs: { | ||
| description: { | ||
| component: | ||
| '**Card** 컴포넌트는 리마인드 카드와 북마크 카드 두 가지 유형을 지원합니다.<br/>' + | ||
| '**`remind`**, **`bookmark`** 중 type을 선택하여 사용할 수 있으며 이에 따라 다른 인터페이스를 제공합니다..', | ||
| }, | ||
| }, | ||
| }, | ||
| argTypes: { | ||
| type: { | ||
| control: 'inline-radio', | ||
| options: ['remind', 'bookmark'], | ||
| description: '카드의 종류를 선택합니다.', | ||
| }, | ||
| title: { control: 'text' }, | ||
| content: { control: 'text' }, | ||
| category: { control: 'text' }, | ||
| imageUrl: { control: 'text' }, | ||
| timeRemaining: { | ||
| control: 'text', | ||
| if: { arg: 'type', eq: 'remind' }, | ||
| }, | ||
| date: { | ||
| control: 'text', | ||
| if: { arg: 'type', eq: 'bookmark' }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof Card>; | ||
|
|
||
| export const Remind: Story = { | ||
| name: 'type: remind', | ||
| args: { | ||
| type: 'remind', | ||
| title: '리마인드 카드 타이틀', | ||
| content: '리마인드 컨텐츠 내용입니다.', | ||
| category: '개발', | ||
| timeRemaining: '3시간 25분', | ||
| imageUrl: | ||
| 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=2070&auto=format&fit=crop', | ||
| }, | ||
| }; | ||
|
|
||
| export const BookMark: Story = { | ||
| name: 'type: bookmark', | ||
| args: { | ||
| type: 'bookmark', | ||
| title: '북마크 카드 타이틀', | ||
| content: '북마크 컨텐츠 내용입니다.', | ||
| category: '디자인', | ||
| date: '2025.08.26', | ||
| imageUrl: | ||
| 'https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?q=80&w=2070&auto=format&fit=crop', | ||
| }, | ||
| }; | ||
|
|
||
| export const LongTextSideBySide: Story = { | ||
| name: 'case: Long Text', | ||
| render: () => ( | ||
| <div style={{ display: 'flex', gap: '20px', alignItems: 'flex-start' }}> | ||
| <Card | ||
| type="remind" | ||
| title="두 줄로 표시되는 긴 리마인드 카드 타이틀입니다. 제한 길이를 넘어가면 ... 처리가 됩니다." | ||
| content="여기에 두 줄 이상으로 늘어나는 리마인드 컨텐츠 내용이 들어갑니다. 길이를 확인해보세요." | ||
| category="기획" | ||
| timeRemaining="3시간 25분" | ||
| imageUrl="https://images.unsplash.com/photo-1587620962725-abab7fe55159?q=80&w=1931&auto=format&fit=crop" | ||
| /> | ||
| <Card | ||
| type="bookmark" | ||
| title="두 줄로 표시되는 긴 북마크 카드 타이틀입니다. 제한 길이를 넘어가면 ... 처리가 됩니다." | ||
| content="여기에 두 줄 이상으로 늘어나는 북마크 컨텐츠 내용이 들어갑니다. 길이를 확인해보세요." | ||
| category="마케팅" | ||
| date="2025.09.15" | ||
| imageUrl="https://images.unsplash.com/photo-1522204523234-8729aa6e3d5f?q=80&w=2070&auto=format&fit=crop" | ||
| /> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| export const NoImageSideBySide: Story = { | ||
| name: 'case: No imageUrl', | ||
| render: () => ( | ||
| <div style={{ display: 'flex', gap: '20px', alignItems: 'flex-start' }}> | ||
| <Card | ||
| type="remind" | ||
| title="두 줄로 표시되는 긴 리마인드 카드 타이틀입니다. 제한 길이를 넘어가면 ... 처리가 됩니다." | ||
| content="여기에 두 줄 이상으로 늘어나는 리마인드 컨텐츠 내용이 들어갑니다. 길이를 확인해보세요." | ||
| category="기획" | ||
| timeRemaining="3시간 25분" | ||
| /> | ||
| <Card | ||
| type="bookmark" | ||
| title="두 줄로 표시되는 긴 북마크 카드 타이틀입니다. 제한 길이를 넘어가면 ... 처리가 됩니다." | ||
| content="여기에 두 줄 이상으로 늘어나는 북마크 컨텐츠 내용이 들어갑니다. 길이를 확인해보세요." | ||
| category="마케팅" | ||
| date="2025.09.15" | ||
| /> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| export const NoCategoryBookmark: Story = { | ||
| name: 'case: No category (bookmark)', | ||
| args: { | ||
| type: 'bookmark', | ||
| title: '북마크 카드 타이틀', | ||
| content: '북마크 컨텐츠 내용입니다.', | ||
| date: '2025.08.26', | ||
| imageUrl: | ||
| 'https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?q=80&w=2070&auto=format&fit=crop', | ||
| }, | ||
| }; | ||
|
Comment on lines
+114
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainNoCategoryBookmark 스토리와 Card 타입 정의 불일치 이 스토리는 category를 전달하지 않습니다. 현재 Card.tsx의 CardProps에서 category가 필수라면 타입 에러가 납니다. 선택지는 다음 중 하나입니다.
앞선 코멘트의 유니온 타입 변경을 적용하면 본 스토리는 자연스럽게 통과합니다. CardProps 정의를 확인하기 위해 아래 스크립트를 실행해주세요. 🏁 Script executed: #!/bin/bash
# CardProps가 interface로 선언된 위치 찾기
rg -nP -- "interface CardProps\b" -C3 packages/design-system/src/components/card/Card.tsx
# type으로 선언된 경우도 검색
rg -nP -- "type CardProps\b" -C3 packages/design-system/src/components/card/Card.tsxLength of output: 342 CardProps 정의와 🏁 Script executed: #!/bin/bash
# CardProps 인터페이스 정의 전체 확인
rg -nP -C10 "interface CardProps" packages/design-system/src/components/card/Card.tsx
# category 필드 선언부 확인 (optional 여부 포함)
rg -n -C2 "category" packages/design-system/src/components/card/Card.tsxLength of output: 989 NoCategoryBookmark 스토리에 필수 prop
해결 방안(택1):
위치:
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import MyBookmarkCard from './MyBookmarkCard'; | ||
| import RemindCard from './RemindCard'; | ||
|
|
||
| type BaseProps = { | ||
| title: string; | ||
| content?: string; | ||
| category?: string; | ||
| imageUrl?: string; | ||
| }; | ||
|
|
||
| type RemindProps = BaseProps & { | ||
| type: 'remind'; | ||
| timeRemaining: string; | ||
| date?: never; | ||
| }; | ||
|
|
||
| type BookmarkProps = BaseProps & { | ||
| type: 'bookmark'; | ||
| date: string; | ||
| timeRemaining?: never; | ||
| }; | ||
|
|
||
| export type CardProps = RemindProps | BookmarkProps; | ||
|
|
||
| const Card = (props: CardProps) => { | ||
| const { type } = props; | ||
|
|
||
| return ( | ||
| <> | ||
| {type === 'remind' && <RemindCard {...props} />} | ||
|
|
||
| {type === 'bookmark' && <MyBookmarkCard {...props} />} | ||
| </> | ||
|
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스프레드 전달 시 불필요/미정의 프롭 전파 위험(type/date/timeRemaining) 현재 <RemindCard {...props}> / <MyBookmarkCard {...props}>는 각 자식 컴포넌트가 기대하지 않는 프롭(type, 서로의 전용 프롭)을 함께 전달합니다. TS에서는 “Property 'type' does not exist …”류의 오류가 발생할 수 있고, 빌드 설정에 따라 사일런트 실패/누수도 생길 수 있습니다. 각 변형에 필요한 프롭만 명시적으로 전달해 주세요. - return (
- <>
- {type === 'remind' && <RemindCard {...props} />}
-
- {type === 'bookmark' && <MyBookmarkCard {...props} />}
- </>
- );
+ if (props.type === 'remind') {
+ const { title, content, category, imageUrl, timeRemaining } = props;
+ return (
+ <RemindCard
+ title={title}
+ content={content}
+ category={category}
+ imageUrl={imageUrl}
+ timeRemaining={timeRemaining}
+ />
+ );
+ }
+
+ const { title, content, category, imageUrl, date } = props;
+ return (
+ <MyBookmarkCard
+ title={title}
+ content={content}
+ category={category}
+ imageUrl={imageUrl}
+ date={date}
+ />
+ );
🤖 Prompt for AI Agents |
||
| ); | ||
| }; | ||
|
|
||
| export default Card; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,63 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Icon } from '@/icons'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import chippiNoImage from '../../assets/chippi_no_image.svg'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import BaseCard from './BaseCard'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface MyBookmarkCardProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| category?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imageUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| date: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MyBookmarkCard = ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| category, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imageUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| date, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: MyBookmarkCardProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <BaseCard> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex h-[12rem] w-full items-center justify-center overflow-hidden bg-[#F8F8FA]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {imageUrl ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img src={imageUrl} className="h-full w-full object-cover" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={chippiNoImage} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt="이미지 없을 경우 logo" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="h-[12rem]" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 이미지 a11y/회복력 보강: alt, lazy, onError 폴백 추가 실이미지에 alt가 없어 스크린리더 접근성 저하가 있고, 네트워크 오류 시 깨진 이미지가 노출됩니다. 아래처럼 개선을 권장합니다. - {imageUrl ? (
- <img src={imageUrl} className="h-full w-full object-cover" />
- ) : (
- <img
- src={chippiNoImage}
- alt="이미지 없을 경우 logo"
- className="h-[12rem]"
- />
- )}
+ {imageUrl ? (
+ <img
+ src={imageUrl}
+ alt=""
+ className="h-full w-full object-cover"
+ loading="lazy"
+ decoding="async"
+ onError={(e) => {
+ e.currentTarget.src = chippiNoImage;
+ e.currentTarget.alt = '이미지를 불러오지 못했습니다.';
+ e.currentTarget.className = 'h-[12rem] object-contain';
+ }}
+ />
+ ) : (
+ <img
+ src={chippiNoImage}
+ alt="이미지를 불러오지 못했습니다."
+ className="h-[12rem] object-contain"
+ loading="lazy"
+ decoding="async"
+ />
+ )}
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="px-[1.6rem] py-[2.4rem]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mb-[0.8rem] flex h-[5.6rem] justify-between gap-[0.8rem]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h3 className="head6 line-clamp-2">{title}</h3> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="카테고리 상세" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="cursor-pointer self-start" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Icon name="ic_details_category" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="body3-r text-font-gray-2 mb-[1.2rem] line-clamp-2 h-[4.2rem]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {content} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* TODO: 카테고리 컴포넌트로 교체 */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {category && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="bg-category-red-bg caption2-sb text-category-red-text h-[2.2rem] w-[6.2rem] rounded-[0.4rem] px-[0.8rem] py-[0.2rem]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {category} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="caption2-m text-font-ltgray-4 mt-[1.2rem]">{date}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </BaseCard> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default MyBookmarkCard; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { Icon } from '@/icons'; | ||
| import chippiNoImage from '../../assets/chippi_no_image.svg'; | ||
| import BaseCard from './BaseCard'; | ||
|
|
||
| interface RemindCardProps { | ||
| title: string; | ||
| content?: string; | ||
| category?: string; | ||
| imageUrl?: string; | ||
| timeRemaining: string; | ||
| } | ||
|
|
||
| const RemindCard = ({ | ||
| title, | ||
| content, | ||
| category, | ||
| imageUrl, | ||
| timeRemaining, | ||
| }: RemindCardProps) => { | ||
| return ( | ||
| <BaseCard> | ||
| <div className="bg-gray900 flex items-center gap-[0.4rem] py-[1.2rem] pl-[1.6rem] text-sm text-white"> | ||
| <Icon name="ic_clock_active" /> | ||
| <span className="body2-m text-main400 mr-[0.2rem]"> | ||
| {timeRemaining || '-'} | ||
|
Comment on lines
+24
to
+25
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요 - 의 의미는 무엇일까요?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 디자인 상으로는 없었는데 남은 시간이 다 지나서 |
||
| </span> | ||
| <span className="body2-m text-white-bg">이후에 사라져요</span> | ||
| </div> | ||
|
|
||
| <div className="flex h-[12rem] w-full items-center justify-center overflow-hidden bg-[#F8F8FA]"> | ||
| {imageUrl ? ( | ||
| <img src={imageUrl} className="h-full w-full object-cover" /> | ||
| ) : ( | ||
| <img | ||
| src={chippiNoImage} | ||
| alt="이미지 없을 경우 logo" | ||
| className="h-[12rem]" | ||
| /> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="px-[1.6rem] py-[2.4rem]"> | ||
| <div className="mb-[0.8rem] flex h-[5.6rem] justify-between gap-[0.8rem]"> | ||
| <h3 className="head6 line-clamp-2">{title}</h3> | ||
| <button | ||
| type="button" | ||
| aria-label="카테고리 상세" | ||
| className="cursor-pointer self-start" | ||
| > | ||
| <Icon name="ic_details_category" /> | ||
| </button> | ||
| </div> | ||
| <p className="body3-r text-font-gray-2 mb-[1.2rem] line-clamp-2 h-[4.2rem]"> | ||
| {content} | ||
| </p> | ||
|
|
||
| {/* TODO: 카테고리 컴포넌트로 교체 */} | ||
| <span className="bg-category-red-bg caption2-sb text-category-red-text h-[2.2rem] w-[6.2rem] rounded-[0.4rem] px-[0.8rem] py-[0.2rem]"> | ||
| {category} | ||
| </span> | ||
|
Comment on lines
+58
to
+60
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 크게 상관없지만 이후에 카테고리 칩 갈아 끼우는 구간 TODO 표시해두면 좋을 것 같아요!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| </div> | ||
| </BaseCard> | ||
| ); | ||
| }; | ||
|
|
||
| export default RemindCard; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| export { default as Button } from './button/Button'; | ||
| export { Switch } from './switch/Switch'; | ||
| export { default as Badge } from './badge/Badge'; | ||
| export { default as Input } from './input/Input'; | ||
| export { default as Button } from './button/Button'; | ||
| export { default as Card } from './card/Card'; | ||
| export { default as Chip } from './chip/Chip'; | ||
| export { default as Input } from './input/Input'; | ||
| export { default as Level } from './level/Level'; | ||
| export { Textarea } from './textarea/Textarea'; | ||
| export { Progress } from './progress/Progress'; | ||
| export { Switch } from './switch/Switch'; | ||
| export { Textarea } from './textarea/Textarea'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /// <reference types="vite/client" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
타입 참조에 React 네임스페이스 사용 — 타입 임포트 누락
React.ReactNode를 사용하지만react로부터 타입 임포트가 없습니다. TS 설정에 따라 “Cannot find namespace 'React'” 오류가 발생합니다. 타입 임포트를 추가하고, 가능하면 표준 DOM 속성 전달을 위해 HTMLAttributes 상속을 권장합니다.📝 Committable suggestion
🤖 Prompt for AI Agents