Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion src/app/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
--spacing-m: 16px;
--spacing-s: 14px;
--spacing-xs: 12px;
--spacing-xxs: 8px;
--spacing-xxs: 10px;
--spacing-xxxs: 8px;

/* Margin (Primitive/Scale) */
--margin-l: 24px;
Expand Down
22 changes: 20 additions & 2 deletions src/pages/playground.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BannerCard } from '@shared/ui/BannerCard/BannerCard';
import { Button } from '@shared/ui/Button/Button';
import { Card } from '@shared/ui/Card/Card';
import { Checkbox } from '@shared/ui/Checkbox/Checkbox';
Expand All @@ -11,12 +12,13 @@ export default function Playground() {
<div className="flex flex-col gap-6 p-8">
<h1 className="typo-title-2">UI Playground</h1>

{/* Button */}
{/* Header */}
<section className="flex flex-col gap-4">
<h2 className="typo-body-1">Header</h2>
<Header />
</section>

{/* Button */}
<section className="flex flex-col gap-4">
<h2 className="typo-body-1">Button</h2>
<p className="typo-caption-2 text-gray-600">
Expand Down Expand Up @@ -97,7 +99,6 @@ export default function Playground() {
{/* Card */}
<section className="flex flex-col gap-4">
<h2 className="typo-body-1">Card</h2>

<Card
imageSrc="/iphone11.png"
titleText="Title 인데 제목이 정말 길 경우에는 두줄 까지만 보이고, 뒤엔 점 처리"
Expand All @@ -106,6 +107,23 @@ export default function Playground() {
/>
</section>

{/* BannerCard */}
<section className="flex flex-col gap-4">
<h2 className="typo-body-1">Banner Card</h2>
<p className="typo-caption-2 text-gray-600">배너 카드</p>
<BannerCard
title={
<>
중고 전자기기
<br />
구매하기
</>
}
description="설명 최대 길이 2줄"
buttonText="바로가기"
/>
</section>

{/* TextField 예시 */}
<section className="flex flex-col gap-4">
<h2 className="typo-body-1">TextField</h2>
Expand Down
9 changes: 9 additions & 0 deletions src/shared/assets/icons/banner/gear.svg

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이미지를 자세히보니 워터마크가 보이는 것 같아요
다른 이미지를 사용해야 할 것 같습니다 피그마에 요청하였으니 추후에 수정 부탁드립니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

저도 보면서 뭐지 했는데 감사합니다.! ㅎ

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/shared/assets/icons/banner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as GearIcon } from './gear.svg';
64 changes: 64 additions & 0 deletions src/shared/ui/BannerCard/BannerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { GearIcon } from '@shared/assets/icons/banner';
import { Button } from '@shared/ui/Button/Button';
import { bannerCardVariants } from './BannerCard.variants';
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import type { VariantProps } from 'tailwind-variants';

const { base, frame, textWrapper, title, description, imageWrapper, image } = bannerCardVariants();

export type BannerCardProps = Omit<ComponentPropsWithoutRef<'div'>, 'title'> &
VariantProps<typeof bannerCardVariants> & {
title: ReactNode;
description: string;
buttonText?: string;
onClick?: () => void;
};

export const BannerCard = ({
title: titleContent = (
<>
중고 전자기기
<br />
구매하기
</>
),
description: descriptionText = '설명 최대 길이 2줄',
buttonText = '바로가기',
onClick,
className,
...props
}: BannerCardProps) => {
return (
<div
data-testid="banner-card"
className={base({ className })}
role="button"
aria-label="banner-card"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick?.();
}
}}
{...props}
>
Comment on lines +32 to +46

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

버튼 클릭 시 stopPropagation()으로 부모 이벤트 멈추고, 다시 onClick() 메소드를 호출하고 있는 상황 같은데 바로가기를 눌렀을 때 이동할 것인지 카드 전체를 눌렀을 때 이동할 것인지 고려하고 한쪽은 onClick() 지워주시면 좋을 것 같아요!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

뭔가 카드 클릭, 버튼 클릭하여 이동하는 것을 모두 허용하게 하려 해서 약간 꼬였던 것 같습니다! 감사합니다!

<div className={frame()}>
<div className={textWrapper()}>
<p className={title()}>{titleContent}</p>

<p className={description()}>{descriptionText}</p>

<Button size="auto" className="w-[104px]">
{buttonText}
</Button>
</div>
</div>

<div className={imageWrapper()}>
<img src={GearIcon} alt="" className={image()} />
</div>
</div>
);
};
72 changes: 72 additions & 0 deletions src/shared/ui/BannerCard/BannerCard.variants.ts

@KyeongJooni KyeongJooni Jan 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

정의된 디자인 토큰에 있는 값들은 디자인 토큰 값을 사용하면 좋을 것 같아요!
그리고 현재 Card와 BannerCard 두 컴포넌트에서 tv() + slots만 사용 중인데, variants 없이 tv() 쓰는 건 오버엔지니어링이라고 개인적으로 생각하고 있어요 개인적인 생각이기 때문에 참고만 해주세요! 😄

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { tv } from 'tailwind-variants';

export const bannerCardVariants = tv({
base: [
'group',
'relative',
'flex',
'flex-col',
'justify-end',
'items-start',

'w-[427px]',
'h-[384px]',

'px-[var(--padding-xl)]',
'py-[27px]',

'gap-[var(--spacing-xxs)]',

'rounded-[var(--radius-xl)]',

'bg-white',
'bg-[linear-gradient(0deg,rgba(4,217,181,0)_0%,rgba(0,179,155,0.2)_71.11%)]',

'cursor-pointer',
'outline-none',
],

slots: {
frame: [
'flex',
'flex-col',
'items-start',

'gap-[var(--spacing-xs)]',

'self-stretch',

'h-[330px]',
],

textWrapper: [
'relative',
'z-10',
'flex',
'flex-col',
'items-start',

'gap-[var(--spacing-xs)]',

'self-stretch',
],

title: ['typo-title-3', 'text-black'],

description: ['typo-body-1', 'text-black', 'overflow-hidden', 'text-ellipsis', 'line-clamp-2'],

imageWrapper: ['absolute', 'right-0', 'bottom-0', 'z-0', 'pointer-events-none'],

image: [
'w-[213px]',
'h-[213px]',
'object-cover',

'transition-all',
'duration-300',

'group-hover:w-[307px]',
'group-hover:h-[307px]',
],
},
});
2 changes: 2 additions & 0 deletions src/shared/ui/BannerCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { BannerCard } from './BannerCard';
export type { BannerCardProps } from './BannerCard';
Empty file added src/shared/ui/Card/index.ts
Empty file.
2 changes: 2 additions & 0 deletions src/shared/ui/Profile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Profile } from './Profile';
export type { ProfileProps } from './Profile';
34 changes: 34 additions & 0 deletions tests/unit/shared/ui/BannerCard/BannerCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { BannerCard } from '@shared/ui/BannerCard';
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';

describe('BannerCard', () => {
it('배너 카드 렌더링', () => {
render(<BannerCard />);
expect(screen.getByTestId('banner-card')).toBeInTheDocument();
});

it('타이틀 텍스트 렌더링', () => {
render(<BannerCard />);
expect(screen.getByText(/중고 전자기기/)).toBeInTheDocument();
expect(screen.getByText(/구매하기/)).toBeInTheDocument();
});

it('버튼 렌더링', () => {
render(<BannerCard />);
expect(screen.getByRole('button', { name: '바로가기' })).toBeInTheDocument();
});

it('버튼 클릭 시 onClick 호출', () => {
const onClick = vi.fn();
render(<BannerCard onClick={onClick} />);

screen.getByRole('button', { name: '바로가기' }).click();
expect(onClick).toHaveBeenCalledTimes(1);
});

it('className 전달', () => {
render(<BannerCard className="custom-class" />);
expect(screen.getByTestId('banner-card')).toHaveClass('custom-class');
});
});
Loading