Skip to content

Comments

Feat(design-system): Card 컴포넌트 구현#40

Merged
constantly-dev merged 14 commits intodevelopfrom
feat/#33/card-component
Aug 26, 2025
Merged

Feat(design-system): Card 컴포넌트 구현#40
constantly-dev merged 14 commits intodevelopfrom
feat/#33/card-component

Conversation

@constantly-dev
Copy link
Member

@constantly-dev constantly-dev commented Aug 26, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

✅ 체크 리스트

  • PR 제목의 형식을 잘 작성했나요? e.g. Feat(client): PR 템플릿 작성
  • 빌드가 성공했나요? (pnpm build)

📄 Tasks

  • Card 컴포넌트 구현
  • Card story 작성

⭐ PR Point (To Reviewer)

Card 컴포넌트는 2개의 type로 분기 처리돼요.

type: 'remind' | 'bookmark';

이 두 개 중에 선택해서 사용하면 돼요. 그리고 2개의 카드마다 다른 인터페이스를 제공해요.

interface CardProps {
  type: 'remind' | 'bookmark';
  title: string;
  content?: string;
  category: string;
  imageUrl?: string;
  timeRemaining?: string;
  date?: string;
}

따라서 사용할 때 필요한 인터페이스를 참고해서 사용해주세요. 그리고 내부에서 꽤 많은 분기 처리가 있는데 이 case에 대해서는 크로마틱을 참고해주세요!

현재는 일단 width를 고정시켰는데, 이후 스프린트에서 반응형이 추가되거나 사이드바 이동에 따라 카드 사이즈가 달라지게 되면 다시 고려해보면 좋을 것 같아요.

Note

remind 카드의 경우 timeRemaining가 필요한데 지금은 단순 string을 받도록 설정했어요.
따라서 밖에서 시간, 분을 계산하고 전체 텍스트를 보내주도록 설계했는데 이 설계에 대해서 어떻게 생각하시는지 궁금해요!
더 좋은 방법이 있다면 편하게 의견 내주세요 👍

Summary by CodeRabbit

  • New Features
    • 디자인 시스템에 카드 컴포넌트 추가 및 공개: Card(타입별 분기 지원), RemindCard, MyBookmarkCard, BaseCard. 이미지/대체이미지, 카테고리 배지, 남은시간/날짜 표시와 텍스트 트렁케이션 레이아웃 지원.
  • Documentation
    • Storybook에 카드 스토리 추가: Remind, BookMark, LongTextSideBySide, NoImageSideBySide, NoCategoryBookmark 및 ArgTypes 컨트롤.
  • Style
    • 공용 폰트 Pretendard 도입 및 기본 리셋/타이포그래피 보정.
  • Chores
    • Vite 환경 타입 참조 추가 및 컴포넌트 공개(export) 업데이트.

@coderabbitai
Copy link

coderabbitai bot commented Aug 26, 2025

Walkthrough

디자인 시스템에 카드 컴포넌트 군(BaseCard, RemindCard, MyBookmarkCard, Card)과 해당 스토리북 스토리를 추가하고, 컴포넌트 인덱스에 Card를 공개 export로 등록했습니다. 또한 Vite 타입 선언 파일을 추가하고, 공용 Tailwind 베이스 스타일과 폰트 스택을 업데이트했습니다.

Changes

Cohort / File(s) Change Summary
Card Components
packages/design-system/src/components/card/BaseCard.tsx, packages/design-system/src/components/card/Card.tsx, packages/design-system/src/components/card/RemindCard.tsx, packages/design-system/src/components/card/MyBookmarkCard.tsx
신규 카드 계층 도입: BaseCard 래퍼 추가, RemindCard 및 MyBookmarkCard 구현, 타입 분기형 Card 컴포넌트 추가 및 default export.
Stories
packages/design-system/src/components/card/Card.stories.tsx
Card 스토리북 스토리 추가: meta 정의, argTypes 구성, remind/bookmark 및 변형 사례(긴 텍스트, 이미지 없음 등) 스토리 제공.
Barrel Export
packages/design-system/src/components/index.ts
export { default as Card } from './card/Card'; 추가 및 파일 내 export 순서 조정.
Build/Types
packages/design-system/src/vite-env.d.ts
Vite 환경 타입 참조(/// ) 추가.
Shared Styles
packages/tailwind-config/shared-styles.css
Pretendard CDN 폰트 임포트와 @layer base에서 폰트 스택 및 일부 리셋(패딩/보더/vertical-align) 업데이트.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Card 컴포넌트 구현: Remind, MyBookmark (#33)
Progress 컴포넌트 구현·variant·접근성·스토리 (#25) Progress 컴포넌트·variant 구현 및 관련 스토리/접근성 속성이 PR에 포함되어 있지 않습니다.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Pretendard 폰트 임포트 및 base 리셋/폰트 스택 변경 (packages/tailwind-config/shared-styles.css) 전역 스타일/폰트 설정 변경으로, 카드 컴포넌트 구현(#33) 또는 Progress 구현(#25) 요구사항에 직접적으로 포함되어 있지 않습니다.
Vite 타입 참조 추가 (packages/design-system/src/vite-env.d.ts) 빌드/타입 선언 추가로 기능 요구사항과 직접적 연관이 없습니다.

Possibly related PRs

Suggested labels

frontend

Suggested reviewers

  • jllee000
  • jjangminii

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#33/card-component

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@constantly-dev constantly-dev self-assigned this Aug 26, 2025
@constantly-dev constantly-dev added the feat 기능 개발하라 개발 달려라 달려 label Aug 26, 2025
@github-actions
Copy link

github-actions bot commented Aug 26, 2025

✅ Storybook chromatic 배포 확인:
🐿️ storybook

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 속성의 적용 범위 재검토

htmlpadding: 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 남발로 문서 아웃라인이 왜곡될 수 있습니다. 필요하다면 as prop 또는 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 8b443b0 and 4a059f0.

⛔ Files ignored due to path filters (1)
  • packages/design-system/src/assets/chippi_no_image.svg is 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), datenever인지 검증
• packages/design-system/src/components/card/MyBookmarkCard.tsx
– props 타입을 BookmarkCardProps로 변경
category가 optional, datestring(선택), timeRemainingnever인지 검증

-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가 포함되어 있어 공유 스타일이 전역으로 적용됩니다. 추가 조치는 필요하지 않습니다.

Comment on lines +1 to +3
interface BaseCardProps {
children: React.ReactNode;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

타입 참조에 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.

Suggested change
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).

Comment on lines +114 to +124
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',
},
};
Copy link

Choose a reason for hiding this comment

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

💡 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.tsx

Length 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.tsx

Length of output: 989


NoCategoryBookmark 스토리에 필수 prop category가 누락되어 타입 에러 발생

packages/design-system/src/components/card/Card.tsxCardProps 인터페이스를 확인한 결과, category: string으로 필수 필드로 정의되어 있습니다. 따라서 NoCategoryBookmark 스토리에는 category를 전달해야 합니다.

해결 방안(택1):

  • CardProps를 분기형 유니온으로 리팩터링하여
    • type: 'remind' 일 때는 category: string 필수
    • type: 'bookmark' 일 때는 category?: string optional
  • 또는 스토리에 category: '' (또는 적절한 카테고리 값) 를 명시적으로 추가

위치:

  • packages/design-system/src/components/card/Card.tsx
    • 인터페이스 정의 (8번째 줄): category: string;
🤖 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.

Comment on lines 5 to 11
interface MyBookmarkCardProps {
title: string;
content?: string;
category: string;
imageUrl?: string;
date?: string;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

Comment on lines +24 to +31
<img src={imageUrl} className="h-full w-full object-cover" />
) : (
<img
src={chippiNoImage}
alt="이미지 없을 경우 logo"
className="h-[12rem]"
/>
)}
Copy link

Choose a reason for hiding this comment

The 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"
+          />
+        )}
  • 카드 썸네일이 장식적이면 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.

Suggested change
<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.

Copy link
Collaborator

@jjangminii jjangminii left a comment

Choose a reason for hiding this comment

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

카드 ui 너무 이쁘게 잘된거같아요 특히 프리탠다드가 적용돼서 그런가 더 이쁜거 같네요 ㅎㅎ
카드 타입별로 사용하는거 좋은 방식일 것 같은데 코멘트 한번만 확인해보시고
타입 분리에 대해 어떻게 생각하시는지 궁금합니다-!!

Comment on lines 5 to 11
interface MyBookmarkCardProps {
title: string;
content?: string;
category: string;
imageUrl?: string;
date?: string;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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;

타입 분리해서 사용하는거 좋을거같아요-!! 다만 이런식으로 타입별 필수,불가 필드를 분리해서 사용하는 방식은 어떤가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

헉. 제가 두 개 합친 Card로 두면서 interface가 참 애매하다고 생각했는데, type 키워드로 확장하는 방법이 있었군요!
반영했습니다~~ 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

@jllee000 jllee000 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!

단순 질문이 있는데!
리마인드 카드랑 북마크 카드의 큰 차이가 카운트다운 헤더날짜표시있냐 없냐 차이로 보이는데, 어차피 Card에서 타입에 따라 분기할거면, 헤더만 분기처리해서 (rounded만 조심하게 떼버리기) 하면 되는 방법으로 안하신 이유가 있었을까요??

Comment on lines 23 to 37
return (
<>
{type === 'remind' && (
<RemindCard
title={title}
content={content}
category={category}
imageUrl={imageUrl}
timeRemaining={timeRemaining}
/>
)}

{type === 'bookmark' && (
<MyBookmarkCard
title={title}
Copy link
Collaborator

Choose a reason for hiding this comment

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

카드라는 컴포넌트 하나로 묶여있긴 하지만
사실상 그냥 type에 따라 bookmarkCard / remindCard로 결국 따로 불러오는 것인데

그냥 아예 외부창에서 굳이 를 하는것과 하는 것의 차이가 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

처음에 디자인이 바뀌기 전에는 remindbookmark card의 요소 순서가 많이 달랐어요! 그래서 너무 많은 분기를 하는 것보다 아예 분기를 선택했는데, 중간에 바뀌어 버렸습니다...!! 그래서 말씀해주신 것처럼 하나의 컴포넌트에서 분기를 해도 괜찮을 것 같네요!

이후에 리팩토링 고려해보겠습니다 감사해요 👍

Comment on lines +52 to +54
<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>
Copy link
Collaborator

Choose a reason for hiding this comment

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

크게 상관없지만 이후에 카테고리 칩 갈아 끼우는 구간 TODO 표시해두면 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

Comment on lines +24 to +25
<span className="body2-m text-main400 mr-[0.2rem]">
{timeRemaining || '-'}
Copy link
Collaborator

Choose a reason for hiding this comment

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

요 - 의 의미는 무엇일까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

디자인 상으로는 없었는데 남은 시간이 다 지나서 remind에서 일반 카드 형태로 변경이 될 때 짧은 시간 동안 timeRemaining 값이 없어서 표시가 안되는 부분을 고려했습니다!
사용자에게 -를 보여주는 것이 어떨까 생각해서 임의로 설정해서, 이 부분은 디자이너 분들과 상의해서 한 번 더 수정할게요 👍

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 타입 export

RemindCard를 외부에서 직접 쓰지 않더라도 스토리/테스트/조합 컴포넌트에서 타입 재사용이 유용합니다. 위에서 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.

📥 Commits

Reviewing files that changed from the base of the PR and between d0126da and 9745742.

📒 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 래핑과 섹션 분리 명확

헤더(남은 시간) / 이미지 / 본문 영역을 명확히 구분해 가독성과 재사용성이 좋습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 9745742 and dc15864.

📒 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 빌드 단계에서 @/iconspackages/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 가드 포함)으로 변형 간 프롭 충돌을 방지한 점 좋아요. 외부 사용성 측면에서도 명확합니다.

Comment on lines +28 to +33
return (
<>
{type === 'remind' && <RemindCard {...props} />}

{type === 'bookmark' && <MyBookmarkCard {...props} />}
</>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

스프레드 전달 시 불필요/미정의 프롭 전파 위험(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}.

@constantly-dev constantly-dev merged commit 1dc8d24 into develop Aug 26, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Card 컴포넌트 구현

3 participants