Feat(design-system): Card 컴포넌트 구현#40
Conversation
Walkthrough디자인 시스템에 카드 컴포넌트 군(BaseCard, RemindCard, MyBookmarkCard, Card)과 해당 스토리북 스토리를 추가하고, 컴포넌트 인덱스에 Card를 공개 export로 등록했습니다. 또한 Vite 타입 선언 파일을 추가하고, 공용 Tailwind 베이스 스타일과 폰트 스택을 업데이트했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant App as 소비자(App)
participant Card as Card (wrapper)
participant RC as RemindCard
participant BC as MyBookmarkCard
participant Base as BaseCard
App->>Card: render({ type, title, content, category, imageUrl, timeRemaining, date })
alt type === "remind"
Card->>RC: render({ title, content, category, imageUrl, timeRemaining })
RC->>Base: wrap content
Base-->>RC: container
RC-->>App: Remind UI
else type === "bookmark"
Card->>BC: render({ title, content, category, imageUrl, date })
BC->>Base: wrap content
Base-->>BC: container
BC-->>App: Bookmark UI
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/tailwind-config/shared-styles.css (1)
161-170: 기본 팔레트 초기화로 인한 text-white 등 기본 색상 클래스 무력화
@theme내--color-*: initial;로 Tailwind v4 기본 컬러 토큰이 초기화됩니다. 이 경우, 컴포넌트에서 사용하는text-white등 기본 색상 유틸이 동작하지 않습니다(RemindCard 상단 바에서 사용됨). 최소한 white/black 토큰은 재정의해야 합니다.아래처럼 기본 토큰을 복원하거나, 컴포넌트에서 커스텀 토큰(
text-white-bg등)만 사용하도록 일관화하세요. 전자는 변경 영향이 적습니다.@theme { - --color-*: initial; + --color-*: initial; + /* Restore commonly used base tokens to keep Tailwind defaults usable */ + --color-white: #ffffff; + --color-black: #000000;
🧹 Nitpick comments (14)
packages/tailwind-config/shared-styles.css (2)
1-1: CDN 폰트 로딩은 확장 프로그램(CSP) 및 오프라인 환경에서 이슈 가능공유 스타일에 CDN 폰트(@import url(...pretendard.min.css))를 직접 포함하면:
- 브라우저 확장(Chrome/Firefox) 환경의 기본 CSP에서 차단될 수 있음
- 오프라인/느린 네트워크에서 FOUT/FOIT 발생 가능, 퍼포먼스 회귀
권장:
- 디자인 시스템 패키지에 Pretendard(또는 Variable 버전)를 정적 자산으로 포함하고 self-host
- font-face에 font-display: swap 지정
- 앱 레벨에서 선택적으로 로드하도록 분리(공유 스타일에 강제 포함 대신, 각 앱의 엔트리 CSS에서 import)
232-241: reset 속성의 적용 범위 재검토
html에padding: 0; border: 0; vertical-align: baseline;은 의미가 거의 없거나 관례적이지 않습니다. reset이 필요하다면 normalize/preflight 수준(이미 Tailwind가 제공)으로 위임하고, 여기서는 프로젝트 특화 베이스만 유지하는 것을 권장합니다.packages/design-system/src/components/card/BaseCard.tsx (1)
5-11: 확장성: className/나머지 속성 전달로 래퍼 유연성 확보현재 고정 width와 outline이 하드코딩돼 있어 변형/반응형 대응이 어렵습니다.
className병합과 나머지 속성 전달을 추가해 컴포넌트 소비자가 스타일을 덮어쓸 수 있게 해주세요.-const BaseCard = ({ children }: BaseCardProps) => { +const BaseCard = ({ children, className, ...rest }: BaseCardProps) => { return ( - <div className="outline-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] bg-white outline"> + <div + className={`outline-gray200 w-[24.8rem] overflow-hidden rounded-[1.2rem] bg-white outline ${className ?? ''}`} + {...rest} + > {children} </div> ); };추가로, 팀에서 고정 폭을 기본으로 유지하되 variant나 size prop으로 제어하거나, 최소폭/최대폭 기반으로 기본을 느슨하게 설정하는 방식을 고려해 보세요.
packages/design-system/src/components/card/RemindCard.tsx (3)
5-11: timeRemaining 설계 피드백: 표현-로직 분리 및 타입 유연화 제안현재는 “완성된 문자열”을 받습니다. 디자인 시스템 관점에서 다음 중 하나를 고려하면 재사용성이 올라갑니다.
- 만료 시각(expiresAt: Date | number)만 받고 포맷은 내부(또는 포맷 함수 props)에서 처리
- 혹은
timeRemaining?: ReactNode로 두고 완전한 UI 조각을 주입(아이콘/강조 포함 가능)예시(간결형):
+import type { ReactNode } from 'react'; interface RemindCardProps { title: string; content?: string; category: string; imageUrl?: string; - timeRemaining?: string; + timeRemaining?: ReactNode; + /** 또는: expiresAt?: Date | number; formatRemaining?: (ms: number) => string */ }API 결정은 팀 합의가 필요하지만, 최소한
string | ReactNode유니온으로 시작해도 유연성이 큽니다.
22-28: 상단 바 색상 토큰 사용 확인(기본 팔레트 초기화 영향)상단 바에서
text-white를 사용합니다. shared-styles에서 기본 팔레트 초기화(--color-*: initial)가 유지된다면text-white가 동작하지 않을 수 있습니다. 앞서 제안한 대로--color-white토큰을 복원하거나, 이 위치를text-white-bg등 커스텀 토큰으로 교체하세요.토큰 복원 시 변경 불필요하지만, 커스텀 토큰 사용 시:
-<div className="bg-gray900 flex items-center gap-[0.4rem] py-[1.2rem] pl-[1.6rem] text-sm text-white"> +<div className="bg-gray900 flex items-center gap-[0.4rem] py-[1.2rem] pl-[1.6rem] text-sm text-white-bg">
44-55: 타이틀 heading 레벨 및 배지 레이아웃 유연성
- 카드 그리드에서 반복 사용 시 h3 남발로 문서 아웃라인이 왜곡될 수 있습니다. 필요하다면
asprop 또는role="heading" aria-level={n}패턴을 지원하세요.- 카테고리 배지에 고정 폭(
w-[6.2rem])이 있어 텍스트 길이에 따라 잘림/줄바꿈 이슈가 생길 수 있습니다.inline-flex w-auto min-w-[6.2rem]로 유연하게 하거나truncate/line-clamp-1을 적용해 주세요.-<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]"> +<span className="bg-category-red-bg caption2-sb text-category-red-text inline-flex h-[2.2rem] min-w-[6.2rem] rounded-[0.4rem] px-[0.8rem] py-[0.2rem]"> {category} </span>packages/design-system/src/components/index.ts (1)
4-4: 대소문자/경로 규칙 통일 권장
'./switch/switch'경로의 파일명이 실제로 소문자switch.tsx인지 확인해 주세요. 크로스 플랫폼(대소문자 구분 파일시스템) 이슈를 방지하려면 컴포넌트 파일명은 PascalCase로 통일하는 것을 권장합니다.packages/design-system/src/components/card/Card.tsx (2)
23-45: 불필요한 Fragment 제거 및 분기 가독성 개선두 분기 중 하나만 렌더링되므로 Fragment 없이 조기 반환(early return)으로 단순화할 수 있습니다.
- return ( - <> - {type === 'remind' && ( - <RemindCard - title={title} - content={content} - category={category} - imageUrl={imageUrl} - timeRemaining={timeRemaining} - /> - )} - - {type === 'bookmark' && ( - <MyBookmarkCard - title={title} - content={content} - category={category} - imageUrl={imageUrl} - date={date} - /> - )} - </> - ); + if (type === 'remind') { + return ( + <RemindCard + title={title} + content={content} + category={category} + imageUrl={imageUrl} + timeRemaining={timeRemaining} + /> + ); + } + return ( + <MyBookmarkCard + title={title} + content={content} + category={category} + imageUrl={imageUrl} + date={date} + /> + );
10-11: timeRemaining 설계 피드백: 표현 문자열이 아닌 근본 데이터 전달 고려현 구조는 서식이 적용된 완성 문자열을 받도록 되어 있어 i18n, 포맷 통일, 단위 변경(예: 분→초), 상대/절대 표기 전환 시 매 사용처에서 재구현해야 합니다. 아래 대안을 제안드립니다.
- 옵션 A(간단/권장): remainingMs: number와 formatter?: (ms) => string를 받도록 설계. 기본 formatter 제공.
- 옵션 B(기능 확장): deadline: Date | string(ISO) 을 받아 내부에서 formatDistanceToNowStrict 등으로 계산.
- 옵션 C(현행 유지+보완): timeRemaining: string 유지하되, utils로 공용 formatter를 제공해 모든 호출부가 동일 로직을 사용.
필요 시 유틸과 타입까지 같이 정리해 드리겠습니다.
packages/design-system/src/components/card/MyBookmarkCard.tsx (2)
42-45: content 미지정 시 빈 단락 렌더링 방지없는 값을 위한 고정 높이 공간을 유지할 목적이 아니라면 조건부 렌더링이 더 깔끔합니다.
- <p className="body3-r text-font-gray-2 mb-[1.2rem] line-clamp-2 h-[4.2rem]"> - {content} - </p> + {content && ( + <p className="body3-r text-font-gray-2 mb-[1.2rem] line-clamp-2"> + {content} + </p> + )}
52-53: 날짜 시멘틱 마크업() 적용 권장date가 날짜라면 시멘틱 엘리먼트를 쓰는 편이 a11y/SEO에 유리합니다. ISO 형식으로 변환 가능한 경우 dateTime에 적용해 주세요.
- <p className="caption2-m text-font-ltgray-4 mt-[1.2rem]">{date}</p> + {date && ( + <time + className="caption2-m text-font-ltgray-4 mt-[1.2rem]" + dateTime={date.replaceAll('.', '-')} // 예: 2025.08.26 → 2025-08-26 + > + {date} + </time> + )}packages/design-system/src/components/card/Card.stories.tsx (3)
12-15: 문구 오탈자 및 마침표 중복 수정끝 문장에 마침표가 두 번 찍혀 있습니다.
- '**`remind`**, **`bookmark`** 중 type을 선택하여 사용할 수 있으며 이에 따라 다른 인터페이스를 제공합니다..', + '**`remind`**, **`bookmark`** 중 type을 선택하여 사용할 수 있으며 이에 따라 다른 인터페이스를 제공합니다.',
55-66: 스토리 이름 컨벤션 정리(대소문자) 및 일관성BookMark → Bookmark로 통일하는 편이 검색성/일관성에 유리합니다.
-export const BookMark: Story = { - name: 'type: bookmark', +export const Bookmark: Story = { + name: 'type: bookmark',
1-125: Chromatic에서 고정 width/반응형 시나리오를 시각 회귀로 커버PR 설명의 고정 width 이슈를 고려하면, 다양한 viewport 스냅샷을 찍어두면 이후 사이즈 변경 시 회귀 탐지가 수월합니다. 스토리 단위로 아래 파라미터 추가를 검토해 주세요.
const meta: Meta<typeof Card> = { title: 'Components/Card', component: Card, tags: ['autodocs'], parameters: { - layout: 'centered', + layout: 'centered', + chromatic: { viewports: [360, 480, 768, 1024] }, },
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
packages/design-system/src/assets/chippi_no_image.svgis excluded by!**/*.svg
📒 Files selected for processing (8)
packages/design-system/src/components/card/BaseCard.tsx(1 hunks)packages/design-system/src/components/card/Card.stories.tsx(1 hunks)packages/design-system/src/components/card/Card.tsx(1 hunks)packages/design-system/src/components/card/MyBookmarkCard.tsx(1 hunks)packages/design-system/src/components/card/RemindCard.tsx(1 hunks)packages/design-system/src/components/index.ts(1 hunks)packages/design-system/src/vite-env.d.ts(1 hunks)packages/tailwind-config/shared-styles.css(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.678Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
📚 Learning: 2025-08-19T17:18:57.678Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.678Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
Applied to files:
packages/tailwind-config/shared-styles.css
📚 Learning: 2025-07-04T10:12:01.690Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#10
File: apps/landing/src/index.css:1-1
Timestamp: 2025-07-04T10:12:01.690Z
Learning: In TailwindCSS v4, the import "tailwindcss"; syntax is the new recommended approach that replaces the three separate tailwind directives (tailwind base;, tailwind components;, tailwind utilities;). This single import pulls in all of Tailwind's preflight, components, and utilities in one step and is used with the tailwindcss/vite plugin.
Applied to files:
packages/tailwind-config/shared-styles.css
🔇 Additional comments (8)
packages/design-system/src/vite-env.d.ts (2)
1-1: Vite 타입 선언 추가 LGTM디자인 시스템 패키지 내 TSX에서 Vite 타입 사용 시 정상 동작을 보장합니다.
1-1: tsconfig 포함 범위 확인 권장이 선언 파일이 실제로 컴파일에 반영되도록 packages/design-system/의 tsconfig.json(또는 프로젝트 루트 tsconfig)이 해당 경로를 include하고 있는지 확인해 주세요. Storybook/Vite/TS 빌드 모두에서 일관되게 로드되는지 검증을 부탁드립니다.
packages/design-system/src/components/card/RemindCard.tsx (1)
1-1: 경로 별칭(@) 사용 — 번들/배포 환경 호환성 확인 필요
@/icons별칭은 Vite/TS 설정(plugins, tsconfig paths)에 의존합니다. 디자인 시스템 패키지 자체를 외부 소비자가 사용하거나 Storybook/Chromatic, Jest 등 다양한 툴체인에서 문제 없이 동작하는지 확인해 주세요. 필요 시 상대 경로나 공개 경로 별칭을 패키지 레벨에서 보장해야 합니다.packages/design-system/src/components/index.ts (1)
2-2: Card 공개 export 추가 LGTM소비자 관점에서 배럴(export) 사용성이 좋아졌습니다.
packages/design-system/src/components/card/Card.tsx (1)
4-12: CardProps 분기형 유니온으로 리팩터링 및 하위 컴포넌트 타입 검증 필요CardProps를 Discriminated Union으로 전환하고, 하위 컴포넌트(RemindCard, MyBookmarkCard)에서도 올바른 서브타입을 참조하는지 반드시 확인해주세요.
• packages/design-system/src/components/card/Card.tsx
– 기존interface CardProps { … }→ 다음과 같은type RemindCardProps | BookmarkCardProps유니온으로 교체
• packages/design-system/src/components/card/RemindCard.tsx
– props 타입을RemindCardProps로 변경
–timeRemaining이 필수(string),date가never인지 검증
• packages/design-system/src/components/card/MyBookmarkCard.tsx
– props 타입을BookmarkCardProps로 변경
–category가 optional,date가string(선택),timeRemaining이never인지 검증-interface CardProps { - type: 'remind' | 'bookmark'; - title: string; - content?: string; - category: string; - imageUrl?: string; - timeRemaining?: string; - date?: string; -} +type RemindCardProps = { + type: 'remind'; + title: string; + content?: string; + category: string; + imageUrl?: string; + timeRemaining: string; // 리마인드에선 필수 + date?: never; +}; + +type BookmarkCardProps = { + type: 'bookmark'; + title: string; + content?: string; + category?: string; // 북마크에선 선택(스토리 NoCategoryBookmark 케이스 대응) + imageUrl?: string; + date?: string; + timeRemaining?: never; +}; + +export type CardProps = RemindCardProps | BookmarkCardProps;위 변경 후, 기존 RemindCard/MyBookmarkCard 사용처에서 컴파일 에러 없이 정상 동작하는지 확인 부탁드립니다.
packages/design-system/src/components/card/MyBookmarkCard.tsx (1)
36-40: 버튼에 스크린리더용 레이블 추가 및 아이콘 숨김 처리
- 버튼 요소에
aria-label="카테고리 상세 보기"를 추가해 스크린리더에서 버튼의 목적을 명확히 전달합니다.Icon컴포넌트는React.SVGProps<SVGSVGElement>를 확장해 임의 props를 지원하며, 기본적으로ariaHidden=true로 SVG에aria-hidden을 적용합니다. (필요 시focusable="false"도 전달 가능합니다.)- <button type="button" className="cursor-pointer self-start"> - <Icon name="ic_details_category" /> + <button + type="button" + aria-label="카테고리 상세 보기" + className="cursor-pointer self-start" + > + <Icon name="ic_details_category" /> </button>packages/design-system/src/components/card/Card.stories.tsx (2)
1-1:@storybook/react-vite에서 Meta와 StoryObj를 가져오는 것이 맞습니다.Storybook 공식 문서(“Writing stories in TypeScript”)에 따르면, 스토리를 작성할 때는 사용하는 프레임워크 이름(여기서는
react-vite)에 맞춰import type { Meta, StoryObj } from '@storybook/react-vite';로 가져와야 합니다. 따라서 현재 코드(
import { Meta, StoryObj } from '@storybook/react-vite';)는 올바른 사용이며,@storybook/react로 변경할 필요가 없습니다. (storybook.js.org)Likely an incorrect or invalid review comment.
1-125: 공유 스타일이 Storybook 전역에 이미 적용되어 있습니다
packages/design-system/.storybook/preview.ts에서../styles.css를 임포트하고 있으며, 해당styles.css파일 내부에@pinback/tailwind-config/shared-styles.css가 포함되어 있어 공유 스타일이 전역으로 적용됩니다. 추가 조치는 필요하지 않습니다.
| interface BaseCardProps { | ||
| children: React.ReactNode; | ||
| } |
There was a problem hiding this comment.
타입 참조에 React 네임스페이스 사용 — 타입 임포트 누락
React.ReactNode를 사용하지만 react로부터 타입 임포트가 없습니다. TS 설정에 따라 “Cannot find namespace 'React'” 오류가 발생합니다. 타입 임포트를 추가하고, 가능하면 표준 DOM 속성 전달을 위해 HTMLAttributes 상속을 권장합니다.
+import type { HTMLAttributes, ReactNode } from 'react';
-interface BaseCardProps {
- children: React.ReactNode;
-}
+interface BaseCardProps extends HTMLAttributes<HTMLDivElement> {
+ children: ReactNode;
+}📝 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.
| interface BaseCardProps { | |
| children: React.ReactNode; | |
| } | |
| import type { HTMLAttributes, ReactNode } from 'react'; | |
| interface BaseCardProps extends HTMLAttributes<HTMLDivElement> { | |
| children: ReactNode; | |
| } |
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/BaseCard.tsx around lines 1-3, the
code uses the React namespace (React.ReactNode) but does not import React types,
causing "Cannot find namespace 'React'"; update the file to import the needed
types from 'react' (for example import type { ReactNode, HTMLAttributes } from
'react') and change the props to extend HTMLAttributes<HTMLDivElement> (or the
appropriate element) so DOM props pass through, using ReactNode for children
(optionally making children optional if desired).
| 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', | ||
| }, | ||
| }; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
NoCategoryBookmark 스토리와 Card 타입 정의 불일치
이 스토리는 category를 전달하지 않습니다. 현재 Card.tsx의 CardProps에서 category가 필수라면 타입 에러가 납니다. 선택지는 다음 중 하나입니다.
- CardProps를 분기형 유니온으로 바꾸고 bookmark 타입에서만 category를 optional로 허용(권장).
- 혹은 스토리에 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 정의와 category 필드의 optional 여부를 확인하기 위해 아래 스크립트를 실행해주세요.
🏁 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 category가 누락되어 타입 에러 발생
packages/design-system/src/components/card/Card.tsx의 CardProps 인터페이스를 확인한 결과, category: string으로 필수 필드로 정의되어 있습니다. 따라서 NoCategoryBookmark 스토리에는 category를 전달해야 합니다.
해결 방안(택1):
CardProps를 분기형 유니온으로 리팩터링하여type: 'remind'일 때는category: string필수type: 'bookmark'일 때는category?: stringoptional
- 또는 스토리에
category: ''(또는 적절한 카테고리 값) 를 명시적으로 추가
위치:
- packages/design-system/src/components/card/Card.tsx
- 인터페이스 정의 (8번째 줄):
category: string;
- 인터페이스 정의 (8번째 줄):
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/Card.stories.tsx around lines 114
to 124, the NoCategoryBookmark story omits the required category prop defined as
category: string in CardProps, causing a type error; fix by either (A) adding an
explicit category value to the story args (e.g., category: '' or an appropriate
category string) or (B) refactor CardProps in
packages/design-system/src/components/card/Card.tsx into a discriminated union
so that type: 'bookmark' makes category optional and type: 'remind' keeps
category required — implement one of these two fixes and ensure types and story
compilation succeed.
| interface MyBookmarkCardProps { | ||
| title: string; | ||
| content?: string; | ||
| category: string; | ||
| imageUrl?: string; | ||
| date?: string; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
category 필드 선택값 여부를 타입과 구현에 일치
이 컴포넌트는 category가 없을 때 배지를 표시하지 않도록 분기하고 있습니다. 스토리(NoCategoryBookmark)와 일치시키려면 타입도 optional이어야 합니다.
-interface MyBookmarkCardProps {
+interface MyBookmarkCardProps {
title: string;
content?: string;
- category: string;
+ category?: string;
imageUrl?: string;
date?: string;
}Card.tsx의 CardProps도 함께 조정(북마크 타입에서만 optional)되어야 일관성이 유지됩니다.
📝 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.
| interface MyBookmarkCardProps { | |
| title: string; | |
| content?: string; | |
| category: string; | |
| imageUrl?: string; | |
| date?: string; | |
| } | |
| interface MyBookmarkCardProps { | |
| title: string; | |
| content?: string; | |
| category?: string; | |
| imageUrl?: string; | |
| date?: string; | |
| } |
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/MyBookmarkCard.tsx around lines
5-11, the prop "category" is currently required but the component and the
NoCategoryBookmark story expect it to be optional; change MyBookmarkCardProps to
make category optional (category?: string) and update any internal checks
accordingly. Also update packages/design-system/src/components/card/Card.tsx
CardProps so that only the bookmark-specific variant allows an optional category
(e.g., adjust the generic/union type or create a BookmarkCardProps extension
where category is optional) and fix any consuming code or stories to match the
new prop typing.
| <img src={imageUrl} className="h-full w-full object-cover" /> | ||
| ) : ( | ||
| <img | ||
| src={chippiNoImage} | ||
| alt="이미지 없을 경우 logo" | ||
| className="h-[12rem]" | ||
| /> | ||
| )} |
There was a problem hiding this comment.
🛠️ 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"
+ />
+ )}- 카드 썸네일이 장식적이면 alt=""로 숨기고, 실패 시에만 대체 텍스트를 제공하는 패턴도 가능합니다.
📝 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.
| <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" | |
| /> | |
| )} |
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/MyBookmarkCard.tsx around lines 24
to 31, the real image tag lacks alt text and resiliency; update the real-image
<img> to include an appropriate alt (or alt="" if purely decorative), add
loading="lazy" for deferred loading, and attach an onError handler that swaps
the src to the chippiNoImage fallback and sets a fallback alt (e.g., "이미지 없음")
to improve accessibility and recover from network failures; ensure the fallback
<img> still has loading and alt attributes consistent with this behavior.
jjangminii
left a comment
There was a problem hiding this comment.
카드 ui 너무 이쁘게 잘된거같아요 특히 프리탠다드가 적용돼서 그런가 더 이쁜거 같네요 ㅎㅎ
카드 타입별로 사용하는거 좋은 방식일 것 같은데 코멘트 한번만 확인해보시고
타입 분리에 대해 어떻게 생각하시는지 궁금합니다-!!
| interface MyBookmarkCardProps { | ||
| title: string; | ||
| content?: string; | ||
| category: string; | ||
| imageUrl?: string; | ||
| date?: string; | ||
| } |
There was a problem hiding this comment.
| interface MyBookmarkCardProps { | |
| title: string; | |
| content?: string; | |
| category: string; | |
| imageUrl?: string; | |
| date?: string; | |
| } | |
| }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; // 금지 | |
| }; | |
| type CardProps = RemindProps | BookmarkProps; |
타입 분리해서 사용하는거 좋을거같아요-!! 다만 이런식으로 타입별 필수,불가 필드를 분리해서 사용하는 방식은 어떤가요?
There was a problem hiding this comment.
헉. 제가 두 개 합친 Card로 두면서 interface가 참 애매하다고 생각했는데, type 키워드로 확장하는 방법이 있었군요!
반영했습니다~~ 👍
jllee000
left a comment
There was a problem hiding this comment.
고생하셨습니다!
단순 질문이 있는데!
리마인드 카드랑 북마크 카드의 큰 차이가 카운트다운 헤더 랑 날짜표시있냐 없냐 차이로 보이는데, 어차피 Card에서 타입에 따라 분기할거면, 헤더만 분기처리해서 (rounded만 조심하게 떼버리기) 하면 되는 방법으로 안하신 이유가 있었을까요??
| return ( | ||
| <> | ||
| {type === 'remind' && ( | ||
| <RemindCard | ||
| title={title} | ||
| content={content} | ||
| category={category} | ||
| imageUrl={imageUrl} | ||
| timeRemaining={timeRemaining} | ||
| /> | ||
| )} | ||
|
|
||
| {type === 'bookmark' && ( | ||
| <MyBookmarkCard | ||
| title={title} |
There was a problem hiding this comment.
카드라는 컴포넌트 하나로 묶여있긴 하지만
사실상 그냥 type에 따라 bookmarkCard / remindCard로 결국 따로 불러오는 것인데
그냥 아예 외부창에서 굳이 를 하는것과 하는 것의 차이가 있을까요?
There was a problem hiding this comment.
처음에 디자인이 바뀌기 전에는 remind와 bookmark card의 요소 순서가 많이 달랐어요! 그래서 너무 많은 분기를 하는 것보다 아예 분기를 선택했는데, 중간에 바뀌어 버렸습니다...!! 그래서 말씀해주신 것처럼 하나의 컴포넌트에서 분기를 해도 괜찮을 것 같네요!
이후에 리팩토링 고려해보겠습니다 감사해요 👍
| <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> |
There was a problem hiding this comment.
크게 상관없지만 이후에 카테고리 칩 갈아 끼우는 구간 TODO 표시해두면 좋을 것 같아요!
| <span className="body2-m text-main400 mr-[0.2rem]"> | ||
| {timeRemaining || '-'} |
There was a problem hiding this comment.
디자인 상으로는 없었는데 남은 시간이 다 지나서 remind에서 일반 카드 형태로 변경이 될 때 짧은 시간 동안 timeRemaining 값이 없어서 표시가 안되는 부분을 고려했습니다!
사용자에게 -를 보여주는 것이 어떨까 생각해서 임의로 설정해서, 이 부분은 디자이너 분들과 상의해서 한 번 더 수정할게요 👍
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/design-system/src/components/card/RemindCard.tsx (1)
30-38: 이미지 접근성/탄력성/성능 보강: alt, lazy, decoding, onError 처리 추가이전 코멘트에서 안내된 항목 중 alt/lazy/decoding/onError는 아직 반영되지 않았습니다. 네트워크 실패 시 대체 이미지로 안전하게 전환하고, 대체 텍스트를 제공해 주세요.
- {imageUrl ? ( - <img src={imageUrl} className="h-full w-full object-cover" /> - ) : ( - <img - src={chippiNoImage} - alt="이미지 없을 경우 logo" - className="h-[12rem]" - /> - )} + {imageUrl ? ( + <img + src={imageUrl} + alt={imageAlt ?? title} + loading="lazy" + decoding="async" + className="h-full w-full object-cover" + onError={(e) => { + const img = e.currentTarget as HTMLImageElement; + img.src = chippiNoImage; + img.alt = ''; + }} + /> + ) : ( + <img + src={chippiNoImage} + alt="" + loading="lazy" + decoding="async" + className="h-[12rem]" + /> + )}추가 팁
- 가능한 경우 width/height 속성 고정값을 함께 제공해 CLS를 줄이세요(예: width={320} height={192}).
🧹 Nitpick comments (8)
packages/design-system/src/components/card/RemindCard.tsx (8)
5-11: Props 설계: timeRemaining 문자열 의존 완화 + 이미지 대체 텍스트 제어 추가 제안
- timeRemaining을 외부에서 완성된 문자열로만 받으면 제품별/언어별 포맷 일관 관리가 어렵습니다. number(초) 또는 Date(만료시각)와 formatter를 함께 받는 확장 포인트를 고려해 주세요.
- 동적 이미지의 대체 텍스트를 컴포넌트 외부에서 제어할 수 있도록 imageAlt를 추가하면 접근성과 유연성이 좋아집니다.
-interface RemindCardProps { +export interface RemindCardProps { title: string; content?: string; category: string; imageUrl?: string; timeRemaining?: string; + imageAlt?: string; + // 제안: 만료 시각 or 남은 초 + 포맷터 (택1) + // expiresAt?: Date | string; + // secondsRemaining?: number; + // formatTimeRemaining?: (sec: number) => string; }
22-28: 남은 시간 영역 시맨틱/접근성 개선: 요소와 aria 적용시맨틱 태그를 쓰면 보조기기에서 더 잘 이해합니다. 또한 아이콘은 장식이라면 스크린리더 제외가 적절합니다.
- <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 || '-'} - </span> + <div + className="bg-gray900 flex items-center gap-[0.4rem] py-[1.2rem] pl-[1.6rem] text-sm text-white" + aria-label={`남은 시간 ${timeRemaining ?? '정보 없음'}`} + > + <Icon name="ic_clock_active" aria-hidden /> + <time + className="body2-m text-main400 mr-[0.2rem]" + title={timeRemaining} + > + {timeRemaining ?? '-'} + </time> <span className="body2-m text-white-bg">이후에 사라져요</span> </div>
44-44: 길어지는 제목 대응: title 속성으로 호버 시 전체 텍스트 노출시각적으로는 line-clamp-2 유지하되, 툴팁/접근성을 위해 title을 추가하는 것을 권장합니다.
- <h3 className="head6 line-clamp-2">{title}</h3> + <h3 className="head6 line-clamp-2" title={title}> + {title} + </h3>
45-51: 아이콘 버튼 접근성/키보드 포커스 가시성이미 aria-label을 추가하신 점은 좋습니다. 아이콘은 중복 낭독을 피하기 위해 숨기고, 키보드 사용자를 위해 focus-visible 스타일을 권장합니다.
- <button - type="button" - aria-label="카테고리 상세" - className="cursor-pointer self-start" - > - <Icon name="ic_details_category" /> + <button + type="button" + aria-label="카테고리 상세" + className="cursor-pointer self-start focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main400 focus-visible:rounded-[0.4rem]" + > + <Icon name="ic_details_category" aria-hidden /> </button>
53-55: 빈 본문 처리: content 미제공 시제거 권장
content가 없을 때 빈 단락과 고정 높이는 불필요한 공간을 만들 수 있습니다. 조건부 렌더링을 권장합니다. 제품 요구가 “카드 높이 통일”이라면, 시각적 스페이서는 남기되 role="presentation" 또는 aria-hidden을 고려하세요.
- <p className="body3-r text-font-gray-2 mb-[1.2rem] line-clamp-2 h-[4.2rem]"> - {content} - </p> + {content ? ( + <p + className="body3-r text-font-gray-2 mb-[1.2rem] line-clamp-2" + title={content} + > + {content} + </p> + ) : null}
58-60: 카테고리 칩 고정폭(w-[6.2rem])로 인한 잘림 위험가변 길이 텍스트 대응을 위해 고정폭을 제거하고 truncate를 내포시키는 편이 안전합니다.
- <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> + <span className="bg-category-red-bg caption2-sb text-category-red-text inline-flex h-[2.2rem] max-w-full items-center rounded-[0.4rem] px-[0.8rem] py-[0.2rem]"> + <span className="truncate">{category}</span> + </span>
27-27: 고정 문자열 i18n 처리 권장“이후에 사라져요”는 제품 전역 문구와 톤을 맞추기 위해 i18n 리소스로 이동하는 것을 권장합니다.
66-66: 타입 재사용성: Props 타입 exportRemindCard를 외부에서 직접 쓰지 않더라도 스토리/테스트/조합 컴포넌트에서 타입 재사용이 유용합니다. 위에서 export로 변경한 제안을 참고해 주세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/design-system/src/components/card/MyBookmarkCard.tsx(1 hunks)packages/design-system/src/components/card/RemindCard.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/design-system/src/components/card/MyBookmarkCard.tsx
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.678Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
🔇 Additional comments (1)
packages/design-system/src/components/card/RemindCard.tsx (1)
20-22: 구조 LGTM: BaseCard 래핑과 섹션 분리 명확헤더(남은 시간) / 이미지 / 본문 영역을 명확히 구분해 가독성과 재사용성이 좋습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
packages/design-system/src/components/card/RemindCard.tsx (3)
45-51: 아이콘 중복 낭독 방지아이콘만 들어있는 버튼에 aria-label은 추가됐지만, 아이콘 자체는 스크린리더에 숨기는 것이 안전합니다. Icon 컴포넌트가 role="img" 등을 내보낸다면 이중 낭독이 될 수 있습니다.
- <Icon name="ic_details_category" /> + <Icon name="ic_details_category" aria-hidden="true" />
30-30: 임의 색상 문법 수정 건 반영 확인bg-[#F8F8FA]로 Tailwind arbitrary value 문법이 올바르게 적용되었습니다. 지난 지적사항 잘 반영됐습니다.
30-38: 이미지 접근성/복원력/퍼포먼스 보강 필요주 이미지에 alt가 없고, fallback 이미지는 장식용인데도 의미 있는 alt가 지정되어 있습니다. 또한 네트워크 오류 복구(onError), lazy/async 디코딩이 없습니다. 이전 코멘트와 동일한 논점입니다.
아래와 같이 보완을 제안합니다.
- <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="flex h-[12rem] w-full items-center justify-center overflow-hidden bg-[#F8F8FA]"> + {imageUrl ? ( + <img + src={imageUrl} + alt={title} + loading="lazy" + decoding="async" + className="h-full w-full object-cover" + onError={(e) => { + const img = e.currentTarget as HTMLImageElement; + img.src = chippiNoImage; + img.alt = ''; + }} + /> + ) : ( + <img + src={chippiNoImage} + alt="" + loading="lazy" + decoding="async" + className="h-[12rem]" + /> + )} + </div>
🧹 Nitpick comments (4)
packages/design-system/src/components/card/RemindCard.tsx (3)
53-55: 빈 본문 처리: 조건부 렌더링 또는 스켈레톤content가 없을 때 빈 p 요소가 접근성 트리에는 의미 없는 노드로 남습니다. 고정 높이를 유지할 필요가 없다면 조건부 렌더링을 권장합니다. 고정 높이가 필요하다면 스켈레톤/placeholder로 대체하세요.
- <p className="body3-r text-font-gray-2 mb-[1.2rem] line-clamp-2 h-[4.2rem]"> - {content} - </p> + {content ? ( + <p className="body3-r text-font-gray-2 mb-[1.2rem] line-clamp-2 h-[4.2rem]"> + {content} + </p> + ) : null}
57-60: 카테고리 값 없을 때 빈 배지 노출 방지category가 optional이라면 빈 span이 찍힐 수 있습니다. 조건부로 렌더링해 주세요. 추후 전용 컴포넌트로 치환될 때도 동일한 조건을 유지하는 것이 좋습니다.
- <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> + {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> + )}
23-28: timeRemaining 설계: 포맷 문자열 vs. 원천 데이터현재는 외부에서 포맷된 문자열을 주입하고 내부에서는 단순 표기만 합니다. 다국어(i18n), 일관된 포맷, ‘만료됨’ 상태 전환 등의 요구가 늘면 유지보수가 어려워집니다. 아래 중 하나로의 전환을 고려해 주세요.
- expiresAt(UTC ISO/Date) 또는 timeRemainingMs(number)를 받고, 컴포넌트 내부(또는 공용 util)에서 포맷.
- 상태 지시자 추가: status: 'active' | 'expired'로 라벨(“이후에 사라져요”)과 표시 로직 분리.
또한 텍스트는 디자인 시스템에서 하드코딩하기보다 i18n 키나 슬롯 프롭으로 받는 편이 확장에 유리합니다.packages/design-system/src/components/card/Card.tsx (1)
11-21: timeRemaining의 required 여부 재검토Remind 변형에서 timeRemaining이 필수(string)인데, RemindCard 내부는 '-' 대체 표기를 두고 있어 “일시적으로 값이 없음” 상태를 상정한 듯합니다. 실제로 optional이어야 한다면 Card의 RemindProps도
timeRemaining?: string으로 일치시켜 주세요. 반대로 항상 필수라면 RemindCard에서 대체 표기는 제거하고 만료/전환 상태를 상위에서 명확히 제어하는 편이 일관됩니다. 설계 의도 확인 부탁드립니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
packages/design-system/src/components/card/Card.tsx(1 hunks)packages/design-system/src/components/card/MyBookmarkCard.tsx(1 hunks)packages/design-system/src/components/card/RemindCard.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/design-system/src/components/card/MyBookmarkCard.tsx
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#5
File: apps/extension/src/index.css:1-1
Timestamp: 2025-08-19T17:18:57.678Z
Learning: constantly-dev는 디자인 시스템 설정 PR 머지 후 `import 'pinback/tailwind-config/shared-styles.css';`를 `app.css`나 `index.css`에서 사용하여 공유 스타일을 관리할 계획입니다.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
🔇 Additional comments (2)
packages/design-system/src/components/card/RemindCard.tsx (1)
1-1: ‘@’ 경로 별칭 설정 정상 확인됨
- packages/design-system/tsconfig.json 에서
"paths": { "@/*": ["./src/*"] }설정이 존재합니다.- packages/design-system/vite.config.ts 의
resolve.alias에도
alias: { '@': path.resolve(__dirname, 'src') }가 정의되어 있어
Vite 빌드 단계에서@/icons→packages/design-system/src/icons로 올바르게 해석됩니다.따라서
packages/design-system/src/components/card/RemindCard.tsx내
import { Icon } from '@/icons';사용에 대한 추가 조치는 필요하지 않습니다.packages/design-system/src/components/card/Card.tsx (1)
11-23: 분기별 Prop 타입 설계 훌륭합니다Discriminated union(never 가드 포함)으로 변형 간 프롭 충돌을 방지한 점 좋아요. 외부 사용성 측면에서도 명확합니다.
| return ( | ||
| <> | ||
| {type === 'remind' && <RemindCard {...props} />} | ||
|
|
||
| {type === 'bookmark' && <MyBookmarkCard {...props} />} | ||
| </> |
There was a problem hiding this comment.
스프레드 전달 시 불필요/미정의 프롭 전파 위험(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}
+ />
+ );Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/design-system/src/components/card/Card.tsx around lines 28 to 33,
the component spreads the entire props object into both <RemindCard /> and
<MyBookmarkCard /> which forwards unrelated/undefined props (type, date,
timeRemaining) to children; change to explicitly pick and pass only the props
each variant needs (e.g., destructure props at the top or use selective object
picks) so RemindCard receives only its own props (exclude bookmark-only props
like type) and MyBookmarkCard receives only its own props (exclude remind-only
props like date/timeRemaining); update the Card props typing if necessary so
each branch calls the child with the correct prop shape instead of {...props}.
📌 Related Issues
✅ 체크 리스트
📄 Tasks
⭐ PR Point (To Reviewer)
Card 컴포넌트는 2개의 type로 분기 처리돼요.
이 두 개 중에 선택해서 사용하면 돼요. 그리고 2개의 카드마다 다른 인터페이스를 제공해요.
따라서 사용할 때 필요한 인터페이스를 참고해서 사용해주세요. 그리고 내부에서 꽤 많은 분기 처리가 있는데 이 case에 대해서는 크로마틱을 참고해주세요!
현재는 일단
width를 고정시켰는데, 이후 스프린트에서 반응형이 추가되거나 사이드바 이동에 따라 카드 사이즈가 달라지게 되면 다시 고려해보면 좋을 것 같아요.Note
remind 카드의 경우 timeRemaining가 필요한데 지금은 단순 string을 받도록 설정했어요.
따라서 밖에서 시간, 분을 계산하고 전체 텍스트를 보내주도록 설계했는데 이 설계에 대해서 어떻게 생각하시는지 궁금해요!
더 좋은 방법이 있다면 편하게 의견 내주세요 👍
Summary by CodeRabbit