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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/design-system/src/assets/chippi_no_image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions packages/design-system/src/components/card/BaseCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface BaseCardProps {
children: React.ReactNode;
}
Comment on lines +1 to +3
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).


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;
124 changes: 124 additions & 0 deletions packages/design-system/src/components/card/Card.stories.tsx
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
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.

37 changes: 37 additions & 0 deletions packages/design-system/src/components/card/Card.tsx
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
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}.

);
};

export default Card;
63 changes: 63 additions & 0 deletions packages/design-system/src/components/card/MyBookmarkCard.tsx
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
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.

</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;
66 changes: 66 additions & 0 deletions packages/design-system/src/components/card/RemindCard.tsx
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
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 값이 없어서 표시가 안되는 부분을 고려했습니다!
사용자에게 -를 보여주는 것이 어떨까 생각해서 임의로 설정해서, 이 부분은 디자이너 분들과 상의해서 한 번 더 수정할게요 👍

</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
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.

👍

</div>
</BaseCard>
);
};

export default RemindCard;
9 changes: 5 additions & 4 deletions packages/design-system/src/components/index.ts
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';
1 change: 1 addition & 0 deletions packages/design-system/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
17 changes: 16 additions & 1 deletion packages/tailwind-config/shared-styles.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css');
@import 'tailwindcss';

/* Head */
Expand Down Expand Up @@ -230,8 +231,22 @@

@layer base {
html {
font-family: system-ui, sans-serif;
font-size: 62.5%;
padding: 0;
border: 0;
vertical-align: baseline;
}

body {
font-family:
'Pretendard',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
sans-serif;
}
}

Expand Down
Loading