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
79 changes: 79 additions & 0 deletions src/components/card/my-club-card/MyClubCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from '@storybook/react';
import MyClubCard from './MyClubCard';

const meta = {
title: 'Components/Card/MyClubCard',
component: MyClubCard,
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof MyClubCard>;

export default meta;
type Story = StoryObj<typeof MyClubCard>;

const mockClubMeeting = {
meetingInfo: {
title: '을지로에서 만나는 독서 모임',
location: '을지로 3가',
datetime: '12/14(토) 오전 10:00',
category: '자유책',
},
imageInfo: {
url: 'https://picsum.photos/seed/bookclub/800/450',
isLiked: true,
onLikeClick: () => alert('좋아요를 눌렀습니다!'),
},
clubStatus: {
isCompleted: false,
isConfirmed: true,
},
actions: {
onClick: () => alert('카드를 클릭했습니다!'),
onDelete: () => alert('모임을 삭제했습니다!'),
},
};

export const Default: Story = {
args: {
meeting: mockClubMeeting,
},
render: (args) => (
<div className="w-[344px]">
<MyClubCard {...args} />
</div>
),
};

export const Completed: Story = {
args: {
meeting: {
...mockClubMeeting,
clubStatus: {
...mockClubMeeting.clubStatus,
isCompleted: true,
},
},
},
};

export const Pending: Story = {
args: {
meeting: {
...mockClubMeeting,
clubStatus: {
...mockClubMeeting.clubStatus,
isConfirmed: false,
},
},
},
};

export const Canceled: Story = {
args: {
meeting: {
...mockClubMeeting,
isCanceled: true,
},
},
};
68 changes: 68 additions & 0 deletions src/components/card/my-club-card/MyClubCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ComponentPropsWithoutRef } from 'react';
import Card from '../Card';
import ClubChip from '@/components/chip/club-chip/ClubChip';
import Button from '@/components/button/Button';
import { ClubMeeting } from '@/components/card/types';
import Chip from '@/components/chip/Chip';

interface MyClubCardProps extends ComponentPropsWithoutRef<'article'> {
meeting: ClubMeeting;
}

function MyClubCard({ meeting, className, ...props }: MyClubCardProps) {
const {
meetingInfo,
imageInfo,
clubStatus,
isCanceled = false,
actions,
} = meeting;

return (
<Card isCanceled={isCanceled} className={className} {...props}>
<div className="flex flex-col gap-4">
<Card.Image {...imageInfo} />
<Card.Box className="relative flex-1" onClick={actions?.onClick}>
{/* 첫 번째 줄: ClubChip + Chip */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ClubChip
variant={clubStatus.isCompleted ? 'completed' : 'scheduled'}
/>
<ClubChip
variant={clubStatus.isConfirmed ? 'confirmed' : 'pending'}
/>
</div>
<Chip text={meetingInfo.category} />
</div>

{/* 두 번째 줄: 모임 정보 */}
<div className="mt-4">
<h3 className="text-xl font-semibold text-gray-black">
{meetingInfo.title}
</h3>
<div className="mt-1 flex items-center gap-1.5 text-sm text-gray-dark-03">
<span>{meetingInfo.location}</span>
<span>{meetingInfo.datetime}</span>
</div>
</div>

{/* 세 번째 줄: 버튼 */}
<div className="mt-4">
<Button
text="참여 취소하기"
size="medium"
fillType="outline"
themeColor="green-normal-01"
onClick={actions?.onDelete}
/>
</div>

<Card.EndedOverlay onDelete={actions?.onDelete} />
</Card.Box>
</div>
</Card>
);
}

export default MyClubCard;
12 changes: 12 additions & 0 deletions src/components/card/types/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,15 @@ export interface FullMeeting extends Omit<Meeting, 'actions'> {
hostInfo: HostInfo;
actions: FullActions;
}

export interface ClubStatus {
isCompleted: boolean;
isConfirmed: boolean;
}

export interface ClubMeeting extends BaseProps {
meetingInfo: MeetingInfo;
imageInfo: ImageInfo;
clubStatus: ClubStatus;
actions: SimpleActions;
}
8 changes: 4 additions & 4 deletions src/components/chip/Chip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import Chip from './Chip';

const meta = {
title: 'Components/Chip',
title: 'Components/Chip/Base',
component: Chip,
parameters: {
layout: 'centered',
Expand All @@ -29,7 +29,7 @@ export const RoundedLight: Story = {
export const SquareOutlined: Story = {
args: {
text: '오프라인',
variant: 'square-light',
variant: 'square-outlined',
},
};

Expand All @@ -54,13 +54,13 @@ export const AllStates: Story = {
<div className="flex gap-2">
<Chip text="모집중" variant="rounded-filled" />
<Chip text="1월 7일" variant="rounded-light" />
<Chip text="오프라인" variant="square-light" />
<Chip text="오프라인" variant="square-outlined" />
<Chip text="마감" variant="square-filled" />
</div>
<div className="flex gap-2">
<Chip text="자유책" variant="rounded-filled" isPast={true} />
<Chip text="1월 7일" variant="rounded-light" isPast={true} />
<Chip text="오프라인" variant="square-light" isPast={true} />
<Chip text="오프라인" variant="square-outlined" isPast={true} />
<Chip text="마감" variant="square-filled" isPast={true} />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/chip/Chip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Chip from '@/components/chip/Chip';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';

describe('TextChip', () => {
describe('Chip', () => {
it('텍스트가 올바르게 렌더링되어야 한다', () => {
render(<Chip text="테스트" />);
const chip = screen.getByText('테스트');
Expand Down
14 changes: 8 additions & 6 deletions src/components/chip/Chip.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { twMerge } from 'tailwind-merge';

const CHIP_VARIANTS = {
'rounded-filled': 'rounded-full bg-green-light-02 text-green-dark-01',
'rounded-light': 'rounded-full bg-green-normal-01 text-gray-white',
'square-light':
'rounded border border-gray-normal-02 bg-gray-light-01 text-gray-dark-01',
'square-filled': 'rounded bg-green-dark-01 text-gray-dark-01',
'rounded-filled':
'rounded-full bg-green-light-02 text-green-dark-01 font-semibold',
'rounded-light':
'rounded-full bg-green-normal-01 text-gray-white font-semibold',
'square-outlined':
'rounded-md border border-green-normal-01 bg-gray-white text-green-normal-01',
'square-filled': 'rounded-md bg-green-normal-01 text-gray-white px-2',
} as const;

type ChipVariant = keyof typeof CHIP_VARIANTS;
Expand All @@ -24,7 +26,7 @@ function Chip({
className,
}: ChipProps) {
const baseStyles =
'inline-flex items-center justify-center px-2.5 py-1 text-sm font-semibold';
'inline-flex items-center justify-center px-2.5 py-1 text-sm font-medium';

const combinedClassName = twMerge(
baseStyles,
Expand Down
48 changes: 48 additions & 0 deletions src/components/chip/club-chip/ClubChip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/react';
import ClubChip from './ClubChip';

const meta = {
title: 'Components/Chip/ClubChip',
component: ClubChip,
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof ClubChip>;

export default meta;
type Story = StoryObj<typeof ClubChip>;

export const Completed: Story = {
args: {
variant: 'completed',
},
};

export const Scheduled: Story = {
args: {
variant: 'scheduled',
},
};

export const Pending: Story = {
args: {
variant: 'pending',
},
};

export const Confirmed: Story = {
args: {
variant: 'confirmed',
},
};

export const AllStates: Story = {
render: () => (
<div className="flex gap-2">
<ClubChip variant="completed" />
<ClubChip variant="scheduled" />
<ClubChip variant="pending" />
<ClubChip variant="confirmed" />
</div>
),
};
29 changes: 29 additions & 0 deletions src/components/chip/club-chip/ClubChip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ClubChip from './ClubChip';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';

describe('ClubChip', () => {
it('variant에 따라 올바른 텍스트가 렌더링되어야 한다', () => {
render(<ClubChip variant="completed" />);
expect(screen.getByText('참여완료')).toBeInTheDocument();
});

it('모든 variant가 올바르게 렌더링되어야 한다', () => {
const { rerender } = render(<ClubChip variant="completed" />);
expect(screen.getByText('참여완료')).toBeInTheDocument();

rerender(<ClubChip variant="scheduled" />);
expect(screen.getByText('참여예정')).toBeInTheDocument();

rerender(<ClubChip variant="pending" />);
expect(screen.getByText('개설대기')).toBeInTheDocument();

rerender(<ClubChip variant="confirmed" />);
expect(screen.getByText('개설확정')).toBeInTheDocument();
});

it('className prop으로 스타일을 오버라이드할 수 있어야 한다', () => {
render(<ClubChip variant="completed" className="custom-class" />);
expect(screen.getByText('참여완료')).toHaveClass('custom-class');
});
});
54 changes: 54 additions & 0 deletions src/components/chip/club-chip/ClubChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Chip from '../Chip';
import { twMerge } from 'tailwind-merge';

type ClubChipVariant = 'completed' | 'scheduled' | 'pending' | 'confirmed';

const CLUB_CHIP_TEXT = {
completed: '참여완료',
scheduled: '참여예정',
pending: '개설대기',
confirmed: '개설확정',
} as const;

interface ClubChipProps {
variant: ClubChipVariant;
className?: string;
}

function ClubChip({ variant, className }: ClubChipProps) {
const getChipVariant = () => {
switch (variant) {
case 'completed':
return 'square-filled';
case 'scheduled':
return 'square-filled';
case 'pending':
return 'square-outlined';
case 'confirmed':
return 'square-filled';
}
};

const getCustomClassName = () => {
switch (variant) {
case 'completed':
return 'bg-gray-normal-01 text-gray-dark-02';
case 'scheduled':
return 'bg-green-normal-01 text-gray-white';
case 'pending':
return 'border-blue-normal-01 text-blue-normal-01';
case 'confirmed':
return 'bg-blue-normal-01 text-gray-white';
}
};

return (
<Chip
text={CLUB_CHIP_TEXT[variant]}
variant={getChipVariant()}
className={twMerge(getCustomClassName(), className)}
/>
);
}

export default ClubChip;
3 changes: 3 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const config: Config = {
'dark-03': '#004c41',
darker: '#003b33',
},
blue: {
'normal-01': '#007AFF',
},
},
},
},
Expand Down
Loading