Version: 2.0.0 Last Updated: 2026-02-05
Card 컴포넌트 패밀리는 다양한 레이아웃과 콘텐츠를 위한 카드 기반 UI를 제공합니다. 기본 Card 컴포넌트와 slot composition 패턴, 그리고 특화된 ProductCard, GridCard, FeedCard, ProfileHeaderCard를 포함합니다.
파일 위치:
packages/web/lib/design-system/card.tsx(기본 Card)packages/web/lib/design-system/product-card.tsxpackages/web/lib/design-system/grid-card.tsxpackages/web/lib/design-system/feed-card.tsxpackages/web/lib/design-system/profile-header-card.tsx
시각적 참고: decoded.pen
// Card 기본
import {
Card, CardHeader, CardContent, CardFooter, CardSkeleton
} from "@/lib/design-system";
// 특화 카드
import {
ProductCard, GridCard, FeedCardBase, ProfileHeaderCard
} from "@/lib/design-system";
// Variants import
import { cardVariants } from "@/lib/design-system";
// TypeScript types
import type {
CardProps, ProductCardProps, GridCardProps, FeedCardBaseProps, ProfileHeaderCardProps
} from "@/lib/design-system";기본 카드 컨테이너입니다. Slot composition 패턴 (CardHeader, CardContent, CardFooter)과 variant 시스템을 지원합니다.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "elevated" | "outline" | "ghost" |
"default" |
카드 스타일 변형 |
size |
"sm" | "md" | "lg" |
"md" |
패딩 크기 |
interactive |
boolean |
false |
Interactive 상태 (cursor-pointer, hover:shadow-lg) |
className |
string |
- | 추가 CSS 클래스 |
children |
ReactNode |
- | 카드 내용 |
| ...rest | HTMLAttributes<HTMLDivElement> |
- | 표준 div 속성 |
| Variant | Style | Usage |
|---|---|---|
default |
배경, 테두리, 작은 그림자 (shadow-sm) | 기본 카드 |
elevated |
배경, 테두리, 중간 그림자 (shadow-md) | 강조 카드 |
outline |
배경, 두꺼운 테두리 (border-2) | 아웃라인 카드 |
ghost |
투명 배경, 테두리 없음 | 배경 없는 카드 |
| Size | Padding | Usage |
|---|---|---|
sm |
12px (p-3) | 작은 카드 (ProductCard, GridCard) |
md |
16px (p-4) | 기본 카드 |
lg |
24px (p-6) | 큰 카드 (프로필, 설정) |
import { Card } from "@/lib/design-system";
function BasicCard() {
return (
<Card>
<p>Basic card content</p>
</Card>
);
}import { Card } from "@/lib/design-system";
function CardVariants() {
return (
<>
<Card variant="default" size="md">Default</Card>
<Card variant="elevated" size="lg">Elevated</Card>
<Card variant="outline" size="sm">Outline</Card>
<Card variant="ghost">Ghost</Card>
</>
);
}import { Card } from "@/lib/design-system";
function InteractiveCard() {
return (
<Card interactive onClick={() => console.log('clicked')}>
{/* cursor-pointer + hover:shadow-lg 자동 적용 */}
Click me
</Card>
);
}헤더 섹션 (제목, 부제목, 메타 정보)
Props: HTMLAttributes<HTMLDivElement>
Usage:
import { Card, CardHeader, Heading, Text } from "@/lib/design-system";
<Card>
<CardHeader>
<Heading variant="h4">Card Title</Heading>
<Text variant="small" textColor="muted">Subtitle</Text>
</CardHeader>
</Card>메인 콘텐츠 섹션
Props: HTMLAttributes<HTMLDivElement>
Usage:
import { Card, CardContent, Text } from "@/lib/design-system";
<Card>
<CardContent>
<Text>Main content goes here</Text>
</CardContent>
</Card>푸터 섹션 (액션 버튼, 메타 정보)
Props: HTMLAttributes<HTMLDivElement>
Usage:
import { Card, CardFooter } from "@/lib/design-system";
<Card>
<CardFooter>
<button className="btn-primary">Action</button>
<button className="btn-secondary">Cancel</button>
</CardFooter>
</Card>import { Card, CardHeader, CardContent, CardFooter, Heading, Text } from "@/lib/design-system";
function CompleteCard() {
return (
<Card variant="elevated" size="lg">
<CardHeader>
<Heading variant="h3">Article Title</Heading>
<Text variant="small" textColor="muted">
By Author Name · 2 days ago
</Text>
</CardHeader>
<CardContent>
<Text>
This is the main article content with multiple paragraphs...
</Text>
</CardContent>
<CardFooter>
<button className="btn-primary">Read More</button>
<button className="btn-ghost">Share</button>
</CardFooter>
</Card>
);
}Card 로딩 상태를 위한 스켈레톤 컴포넌트입니다.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
CardProps['variant'] | "default" |
카드 variant (Card와 동일) |
size |
CardProps['size'] | "md" |
카드 size (Card와 동일) |
showHeader |
boolean |
true |
헤더 스켈레톤 표시 |
showContent |
boolean |
true |
콘텐츠 스켈레톤 표시 |
showFooter |
boolean |
true |
푸터 스켈레톤 표시 |
aspectRatio |
"4/5" | "1/1" | "16/9" |
- | 이미지 플레이스홀더 비율 |
className |
string |
- | 추가 CSS 클래스 |
import { CardSkeleton } from "@/lib/design-system";
// 기본 스켈레톤
<CardSkeleton />
// 이미지가 있는 스켈레톤
<CardSkeleton aspectRatio="4/5" />
// 커스터마이징
<CardSkeleton
variant="elevated"
size="lg"
showFooter={false}
aspectRatio="16/9"
/>제품 정보를 표시하는 카드입니다. 이미지, 브랜드, 제품명, 가격, 뱃지를 지원하며, ProductCard 레이아웃에 최적화되어 있습니다.
| Prop | Type | Default | Description |
|---|---|---|---|
imageUrl |
string |
- | 제품 이미지 URL |
brand |
string |
required | 브랜드 이름 |
name |
string |
required | 제품명 |
price |
string | number |
- | 가격 (₩ 자동 추가) |
originalPrice |
string | number |
- | 원가 (취소선 표시) |
badge |
"TOP" | "NEW" | "BEST" | "SALE" |
- | 뱃지 (왼쪽 상단) |
link |
string |
required | 제품 링크 |
aspectRatio |
"1/1" | "3/4" | "4/5" |
"1/1" |
이미지 비율 |
onClick |
() => void |
- | 클릭 핸들러 (link 대신 사용 가능) |
className |
string |
- | 추가 CSS 클래스 |
| Badge | Color | Usage |
|---|---|---|
TOP |
Primary (blue) | 인기 상품 |
NEW |
Blue-500 | 신상품 |
BEST |
Amber-500 | 베스트셀러 |
SALE |
Destructive (red) | 할인 상품 |
import { ProductCard } from "@/lib/design-system";
<ProductCard
imageUrl="/product.jpg"
brand="Nike"
name="Air Max 90 Triple White"
price={129000}
link="/items/123"
/><ProductCard
imageUrl="/product.jpg"
brand="Adidas"
name="Ultraboost 22"
price={159000}
originalPrice={199000}
badge="SALE"
link="/items/456"
aspectRatio="4/5"
/>import { ProductCard } from "@/lib/design-system";
function ProductGrid({ products }) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{products.map(product => (
<ProductCard
key={product.id}
imageUrl={product.image}
brand={product.brand}
name={product.name}
price={product.price}
badge={product.badge}
link={`/items/${product.id}`}
/>
))}
</div>
);
}import { ProductCardSkeleton } from "@/lib/design-system";
// 로딩 상태
<ProductCardSkeleton aspectRatio="4/5" />
// 그리드 로딩
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[...Array(8)].map((_, i) => (
<ProductCardSkeleton key={i} />
))}
</div>갤러리 레이아웃을 위한 카드입니다. 전체 이미지와 선택적 오버레이 콘텐츠를 지원하며, 다양한 aspect ratio를 제공합니다.
| Prop | Type | Default | Description |
|---|---|---|---|
imageUrl |
string |
- | 이미지 URL |
aspectRatio |
"1/1" | "3/4" | "4/3" | "4/5" | "16/9" |
"4/5" |
이미지 비율 |
overlay |
ReactNode |
- | 오버레이 콘텐츠 (뱃지, 카운트 등) |
link |
string |
- | 링크 |
onClick |
() => void |
- | 클릭 핸들러 |
priority |
boolean |
false |
이미지 우선 로딩 (above-fold) |
alt |
string |
required | 이미지 alt 텍스트 |
className |
string |
- | 추가 CSS 클래스 |
import { GridCard } from "@/lib/design-system";
<GridCard
imageUrl="/gallery.jpg"
alt="Gallery image"
aspectRatio="4/5"
link="/gallery/123"
/><GridCard
imageUrl="/photo.jpg"
alt="Photo"
overlay={
<div className="absolute bottom-2 right-2 bg-black/50 px-2 py-1 rounded">
<span className="text-white text-xs">12 items</span>
</div>
}
link="/collections/456"
/>import { GridCard } from "@/lib/design-system";
function Gallery({ images }) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{images.map((image, index) => (
<GridCard
key={image.id}
imageUrl={image.url}
alt={image.alt}
aspectRatio="1/1"
priority={index < 4} // 첫 4개만 우선 로딩
link={`/images/${image.id}`}
/>
))}
</div>
);
}import { GridCardSkeleton } from "@/lib/design-system";
<GridCardSkeleton aspectRatio="1/1" />소셜 피드를 위한 카드입니다. 이미지 콘텐츠와 오버레이, 선택적 푸터를 지원하며, Instagram 스타일 (4:5 비율)을 기본으로 합니다.
| Prop | Type | Default | Description |
|---|---|---|---|
imageUrl |
string |
- | 이미지 URL |
aspectRatio |
"4/5" | "1/1" | "16/9" |
"4/5" |
이미지 비율 |
overlay |
ReactNode |
- | 오버레이 (우하단에 자동 위치) |
link |
string |
- | 링크 |
onClick |
() => void |
- | 클릭 핸들러 |
priority |
boolean |
false |
이미지 우선 로딩 |
alt |
string |
required | 이미지 alt 텍스트 |
children |
ReactNode |
- | 푸터 콘텐츠 (p-4 패딩 자동 적용) |
className |
string |
- | 추가 CSS 클래스 |
import { FeedCardBase } from "@/lib/design-system";
<FeedCardBase
imageUrl="/feed-image.jpg"
alt="Feed item"
link="/posts/123"
/>import { FeedCardBase, Text } from "@/lib/design-system";
<FeedCardBase
imageUrl="/feed-image.jpg"
alt="Feed item"
overlay={
<span className="px-2 py-1 bg-black/50 rounded text-white text-xs">
5 items
</span>
}
link="/posts/123"
>
<div className="space-y-1">
<Text variant="small" className="font-semibold">@username</Text>
<Text variant="caption" textColor="muted">2 hours ago</Text>
</div>
</FeedCardBase>import { FeedCardBase } from "@/lib/design-system";
function FeedGrid({ posts }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{posts.map(post => (
<FeedCardBase
key={post.id}
imageUrl={post.image}
alt={post.title}
link={`/posts/${post.id}`}
>
<div>
<p className="font-semibold">{post.author}</p>
<p className="text-sm text-muted-foreground">{post.timestamp}</p>
</div>
</FeedCardBase>
))}
</div>
);
}import { FeedCardBaseSkeleton } from "@/lib/design-system";
<FeedCardBaseSkeleton showFooter={true} />프로필 헤더를 표시하는 카드입니다. 아바타, 이름, username, 바이오, 통계, 액션 버튼을 지원합니다.
| Prop | Type | Default | Description |
|---|---|---|---|
avatarUrl |
string |
- | 아바타 이미지 URL (없으면 이니셜 표시) |
displayName |
string |
required | 사용자 이름 |
username |
string |
required | @username |
bio |
string |
- | 바이오 (최대 2줄) |
actions |
ReactNode |
- | 액션 버튼 (설정, 로그아웃 등) |
stats |
Array<{ label: string; value: string | number }> |
- | 통계 (Posts, Followers 등) |
onAvatarClick |
() => void |
- | 아바타 클릭 핸들러 |
className |
string |
- | 추가 CSS 클래스 |
import { ProfileHeaderCard } from "@/lib/design-system";
<ProfileHeaderCard
avatarUrl="/avatar.jpg"
displayName="John Doe"
username="@johndoe"
bio="Product designer and coffee enthusiast"
/>import { ProfileHeaderCard } from "@/lib/design-system";
<ProfileHeaderCard
avatarUrl="/avatar.jpg"
displayName="John Doe"
username="@johndoe"
bio="Product designer and coffee enthusiast"
stats={[
{ label: "Posts", value: 42 },
{ label: "Followers", value: 1234 },
{ label: "Following", value: 567 }
]}
actions={
<>
<button className="btn-primary">Edit Profile</button>
<button className="btn-ghost">Settings</button>
</>
}
/>// avatarUrl이 없으면 자동으로 이니셜 표시
<ProfileHeaderCard
displayName="John Doe"
username="@johndoe"
// avatarUrl 없음 → "J" 표시
/>import { ProfileHeaderCardSkeleton } from "@/lib/design-system";
<ProfileHeaderCardSkeleton showStats={true} />// 반응형 2-4 컬럼 그리드
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<ProductCard />
<ProductCard />
<ProductCard />
</div>
// 1-3 컬럼 피드 그리드
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<FeedCardBase />
<FeedCardBase />
<FeedCardBase />
</div>// Link로 감싸기
<Link href="/items/123">
<Card interactive>
<CardContent>Clickable card</CardContent>
</Card>
</Link>
// onClick 핸들러
<Card interactive onClick={() => handleClick()}>
<CardContent>Clickable card</CardContent>
</Card>{isLoading ? (
<div className="grid grid-cols-4 gap-4">
{[...Array(8)].map((_, i) => (
<ProductCardSkeleton key={i} />
))}
</div>
) : (
<div className="grid grid-cols-4 gap-4">
{products.map(product => (
<ProductCard key={product.id} {...product} />
))}
</div>
)}Card 컴포넌트는 다음 토큰을 사용합니다:
colors.card: 카드 배경colors.cardForeground: 카드 텍스트colors.border: 카드 테두리shadows.sm,shadows.md,shadows.lg: 카드 그림자borderRadius.lg: 카드 모서리spacing[3],spacing[4],spacing[6]: 카드 패딩
자세한 내용: tokens.md
- Design Tokens - Color, Spacing, Shadow 토큰
- Typography Components - Heading, Text (CardHeader에서 사용)
- Design Patterns - 카드 레이아웃 패턴
- decoded.pen - 시각적 레퍼런스
Note: 모든 카드 컴포넌트는 기본 Card를 기반으로 구축되었습니다. 일관된 스타일링과 interactive 상태를 위해 항상 디자인 시스템 카드를 사용하세요.