Skip to content

Commit c6a8c33

Browse files
authored
Merge pull request #25 from Leets-Official/23-feat/BannerCard-컴포넌트-구현
[Feat] BannerCard 컴포넌트 제작
2 parents 51e4027 + d2ae872 commit c6a8c33

10 files changed

Lines changed: 206 additions & 3 deletions

File tree

src/app/styles/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
--spacing-m: 16px;
6666
--spacing-s: 14px;
6767
--spacing-xs: 12px;
68-
--spacing-xxs: 8px;
68+
--spacing-xxs: 10px;
69+
--spacing-xxxs: 8px;
6970

7071
/* Margin (Primitive/Scale) */
7172
--margin-l: 24px;

src/pages/playground.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BannerCard } from '@shared/ui/BannerCard/BannerCard';
12
import { Button } from '@shared/ui/Button/Button';
23
import { Card } from '@shared/ui/Card/Card';
34
import { Checkbox } from '@shared/ui/Checkbox/Checkbox';
@@ -11,12 +12,13 @@ export default function Playground() {
1112
<div className="flex flex-col gap-6 p-8">
1213
<h1 className="typo-title-2">UI Playground</h1>
1314

14-
{/* Button */}
15+
{/* Header */}
1516
<section className="flex flex-col gap-4">
1617
<h2 className="typo-body-1">Header</h2>
1718
<Header />
1819
</section>
1920

21+
{/* Button */}
2022
<section className="flex flex-col gap-4">
2123
<h2 className="typo-body-1">Button</h2>
2224
<p className="typo-caption-2 text-gray-600">
@@ -97,7 +99,6 @@ export default function Playground() {
9799
{/* Card */}
98100
<section className="flex flex-col gap-4">
99101
<h2 className="typo-body-1">Card</h2>
100-
101102
<Card
102103
imageSrc="/iphone11.png"
103104
titleText="Title 인데 제목이 정말 길 경우에는 두줄 까지만 보이고, 뒤엔 점 처리"
@@ -106,6 +107,23 @@ export default function Playground() {
106107
/>
107108
</section>
108109

110+
{/* BannerCard */}
111+
<section className="flex flex-col gap-4">
112+
<h2 className="typo-body-1">Banner Card</h2>
113+
<p className="typo-caption-2 text-gray-600">배너 카드</p>
114+
<BannerCard
115+
title={
116+
<>
117+
중고 전자기기
118+
<br />
119+
구매하기
120+
</>
121+
}
122+
description="설명 최대 길이 2줄"
123+
buttonText="바로가기"
124+
/>
125+
</section>
126+
109127
{/* TextField 예시 */}
110128
<section className="flex flex-col gap-4">
111129
<h2 className="typo-body-1">TextField</h2>

src/shared/assets/icons/banner/gear.svg

Lines changed: 9 additions & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as GearIcon } from './gear.svg';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { GearIcon } from '@shared/assets/icons/banner';
2+
import { Button } from '@shared/ui/Button/Button';
3+
import { bannerCardVariants } from './BannerCard.variants';
4+
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
5+
import type { VariantProps } from 'tailwind-variants';
6+
7+
const { base, frame, textWrapper, title, description, imageWrapper, image } = bannerCardVariants();
8+
9+
export type BannerCardProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> &
10+
VariantProps<typeof bannerCardVariants> & {
11+
title: ReactNode;
12+
description: string;
13+
buttonText?: string;
14+
onClick?: () => void;
15+
};
16+
17+
export const BannerCard = ({
18+
title: titleContent = (
19+
<>
20+
중고 전자기기
21+
<br />
22+
구매하기
23+
</>
24+
),
25+
description: descriptionText = '설명 최대 길이 2줄',
26+
buttonText = '바로가기',
27+
onClick,
28+
className,
29+
...props
30+
}: BannerCardProps) => {
31+
return (
32+
<div
33+
data-testid="banner-card"
34+
className={base({ className })}
35+
role="button"
36+
aria-label="banner-card"
37+
tabIndex={0}
38+
onClick={onClick}
39+
onKeyDown={(e) => {
40+
if (e.key === 'Enter' || e.key === ' ') {
41+
e.preventDefault();
42+
onClick?.();
43+
}
44+
}}
45+
{...props}
46+
>
47+
<div className={frame()}>
48+
<div className={textWrapper()}>
49+
<p className={title()}>{titleContent}</p>
50+
51+
<p className={description()}>{descriptionText}</p>
52+
53+
<Button size="auto" className="w-[104px]">
54+
{buttonText}
55+
</Button>
56+
</div>
57+
</div>
58+
59+
<div className={imageWrapper()}>
60+
<img src={GearIcon} alt="" className={image()} />
61+
</div>
62+
</div>
63+
);
64+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { tv } from 'tailwind-variants';
2+
3+
export const bannerCardVariants = tv({
4+
base: [
5+
'group',
6+
'relative',
7+
'flex',
8+
'flex-col',
9+
'justify-end',
10+
'items-start',
11+
12+
'w-[427px]',
13+
'h-[384px]',
14+
15+
'px-[var(--padding-xl)]',
16+
'py-[27px]',
17+
18+
'gap-[var(--spacing-xxs)]',
19+
20+
'rounded-[var(--radius-xl)]',
21+
22+
'bg-white',
23+
'bg-[linear-gradient(0deg,rgba(4,217,181,0)_0%,rgba(0,179,155,0.2)_71.11%)]',
24+
25+
'cursor-pointer',
26+
'outline-none',
27+
],
28+
29+
slots: {
30+
frame: [
31+
'flex',
32+
'flex-col',
33+
'items-start',
34+
35+
'gap-[var(--spacing-xs)]',
36+
37+
'self-stretch',
38+
39+
'h-[330px]',
40+
],
41+
42+
textWrapper: [
43+
'relative',
44+
'z-10',
45+
'flex',
46+
'flex-col',
47+
'items-start',
48+
49+
'gap-[var(--spacing-xs)]',
50+
51+
'self-stretch',
52+
],
53+
54+
title: ['typo-title-3', 'text-black'],
55+
56+
description: ['typo-body-1', 'text-black', 'overflow-hidden', 'text-ellipsis', 'line-clamp-2'],
57+
58+
imageWrapper: ['absolute', 'right-0', 'bottom-0', 'z-0', 'pointer-events-none'],
59+
60+
image: [
61+
'w-[213px]',
62+
'h-[213px]',
63+
'object-cover',
64+
65+
'transition-all',
66+
'duration-300',
67+
68+
'group-hover:w-[307px]',
69+
'group-hover:h-[307px]',
70+
],
71+
},
72+
});

src/shared/ui/BannerCard/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { BannerCard } from './BannerCard';
2+
export type { BannerCardProps } from './BannerCard';

src/shared/ui/Card/index.ts

Whitespace-only changes.

src/shared/ui/Profile/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Profile } from './Profile';
2+
export type { ProfileProps } from './Profile';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { BannerCard } from '@shared/ui/BannerCard';
2+
import { render, screen } from '@testing-library/react';
3+
import { describe, it, expect, vi } from 'vitest';
4+
5+
describe('BannerCard', () => {
6+
it('배너 카드 렌더링', () => {
7+
render(<BannerCard />);
8+
expect(screen.getByTestId('banner-card')).toBeInTheDocument();
9+
});
10+
11+
it('타이틀 텍스트 렌더링', () => {
12+
render(<BannerCard />);
13+
expect(screen.getByText(/ /)).toBeInTheDocument();
14+
expect(screen.getByText(//)).toBeInTheDocument();
15+
});
16+
17+
it('버튼 렌더링', () => {
18+
render(<BannerCard />);
19+
expect(screen.getByRole('button', { name: '바로가기' })).toBeInTheDocument();
20+
});
21+
22+
it('버튼 클릭 시 onClick 호출', () => {
23+
const onClick = vi.fn();
24+
render(<BannerCard onClick={onClick} />);
25+
26+
screen.getByRole('button', { name: '바로가기' }).click();
27+
expect(onClick).toHaveBeenCalledTimes(1);
28+
});
29+
30+
it('className 전달', () => {
31+
render(<BannerCard className="custom-class" />);
32+
expect(screen.getByTestId('banner-card')).toHaveClass('custom-class');
33+
});
34+
});

0 commit comments

Comments
 (0)