diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index bf36618c..4a1b845d 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -19,10 +19,9 @@ export default function Button({ themeColor = 'green-normal-01', lightColor, isSubmitting, - className, ...buttonProps }: ButtonProps) { - const { disabled } = buttonProps; + const { disabled, className } = buttonProps; const sizeClasses = SIZE[size]; const baseClasses = 'rounded-[12px] font-semibold cursor-pointer'; @@ -35,6 +34,10 @@ export default function Button({ : themeColor; const variantClasses = (() => { + if (disabled) { + return `text-gray-dark-02 bg-gray-normal-02`; + } + switch (fillType) { case 'solid': return `text-gray-white ${COLOR_SCHEMES[resolvedColor]['bg']}`; diff --git a/src/components/card/Card.stories.tsx b/src/components/card/Card.stories.tsx index 5da96dce..6965bd21 100644 --- a/src/components/card/Card.stories.tsx +++ b/src/components/card/Card.stories.tsx @@ -1,40 +1,72 @@ import type { Meta, StoryObj } from '@storybook/react'; import Card from './Card'; -import { mockMeeting } from './mock/mock'; const meta = { - title: 'Components/Card/Base', + title: 'Components/Card/Elements', component: Card, parameters: { layout: 'centered', }, - argTypes: { - isCanceled: { - control: 'boolean', - description: '모임 취소 여부', - }, - className: { - control: 'text', - description: '추가 스타일링을 위한 className', - }, - }, + tags: ['autodocs'], } satisfies Meta; export default meta; type Story = StoryObj; -export const Default: Story = { - args: { - isCanceled: false, - }, - render: (args) => ( - - - - - - alert('삭제되었습니다')} /> - - +export const Box: Story = { + render: () => ( + +

Card Box Content

+
+ ), +}; + +export const Image: Story = { + render: () => ( + alert('좋아요 클릭!')} + /> + ), +}; + +export const Title: Story = { + render: () => 모임 제목 예시, +}; + +export const Location: Story = { + render: () => 서울특별시 강남구, +}; + +export const DateTime: Story = { + render: () => 2024.03.15 (금) 19:00, +}; + +export const Overlay: Story = { + render: () => ( +
+ alert('삭제 버튼 클릭!')} /> +
+ ), +}; + +export const ImageWithLike: Story = { + render: () => ( + alert('좋아요 클릭!')} + /> + ), +}; + +export const BoxWithClick: Story = { + render: () => ( + alert('박스 클릭!')}> +

클릭 가능한 Card Box

+
), }; diff --git a/src/components/card/Card.test.tsx b/src/components/card/Card.test.tsx deleted file mode 100644 index 55ec033a..00000000 --- a/src/components/card/Card.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import Card from './Card'; -import '@testing-library/jest-dom'; - -describe('Card', () => { - describe('Card.Box', () => { - it('onClick 핸들러가 호출되어야 함', async () => { - const user = userEvent.setup(); - const handleClick = jest.fn(); - - render( - - 내용 - , - ); - - await user.click(screen.getByText('내용')); - expect(handleClick).toHaveBeenCalled(); - }); - }); - - describe('Card.Image', () => { - it('좋아요 버튼 클릭 시 onLikeClick이 호출되어야 함', async () => { - const user = userEvent.setup(); - const handleLikeClick = jest.fn(); - - render( - - - , - ); - - await user.click(screen.getByRole('button', { name: '좋아요' })); - expect(handleLikeClick).toHaveBeenCalled(); - }); - - it('좋아요 상태에 따라 적절한 aria-label이 표시되어야 함', () => { - const { rerender } = render( - - {}} /> - , - ); - - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-label', - '좋아요', - ); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-pressed', - 'false', - ); - - rerender( - - {}} /> - , - ); - - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-label', - '좋아요 취소', - ); - expect(screen.getByRole('button')).toHaveAttribute( - 'aria-pressed', - 'true', - ); - }); - }); - - describe('Card.Host', () => { - it('호스트 프로필 클릭 시 onClick이 호출되어야 함', async () => { - const user = userEvent.setup(); - const handleClick = jest.fn(); - - render( - - - , - ); - - await user.click(screen.getByRole('img')); - expect(handleClick).toHaveBeenCalled(); - }); - }); - - describe('Card.EndedOverlay', () => { - it('isCanceled가 true일 때만 오버레이가 표시되어야 함', () => { - const { rerender } = render( - - {}} /> - , - ); - - expect( - screen.queryByText(/호스트가 모임을 취소했어요/), - ).not.toBeInTheDocument(); - - rerender( - - {}} /> - , - ); - - expect( - screen.getByText(/호스트가 모임을 취소했어요/), - ).toBeInTheDocument(); - }); - - it('삭제하기 버튼 클릭 시 onDelete가 호출되어야 함', async () => { - const user = userEvent.setup(); - const handleDelete = jest.fn(); - - render( - - - , - ); - - await user.click(screen.getByText('삭제하기')); - expect(handleDelete).toHaveBeenCalled(); - }); - - it('삭제하기 버튼 클릭 시 이벤트 전파가 중단되어야 함', async () => { - const user = userEvent.setup(); - const handleDelete = jest.fn(); - const handleCardClick = jest.fn(); - - render( - - - , - ); - - await user.click(screen.getByText('삭제하기')); - expect(handleDelete).toHaveBeenCalled(); - expect(handleCardClick).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index 2f7869c4..f5af5e4a 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -1,57 +1,47 @@ 'use client'; import { createContext, useContext } from 'react'; -import Chip from '@/components/chip/Chip'; import ParticipantCounter from '../participant-counter/ParticipantCounter'; import AvatarGroup from '../avatar-group/AvatarGroup'; -import ConfirmedLabel from '../confirmed-label/ConfirmedLabel'; import ProgressBar from '../progress-bar/ProgressBar'; import Avatar from '../avatar/Avatar'; -import { LocationIcon, HostIcon, HeartIcon } from '../../../public/icons'; +import { + LocationIcon, + HostIcon, + HeartIcon, + RatingIcon, +} from '../../../public/icons'; import Image from 'next/image'; +import { twMerge } from 'tailwind-merge'; import { - CardContextType, - CardProps, CardBoxProps, - CardInfoProps, - CardStatusProps, - CardHostProps, + CardTitleProps, + CardLocationProps, + CardDateTimeProps, + CardOverlayProps, CardImageProps, - CardEndedOverlayProps, + CardProps, + DefaultClubCard, + HostedClubCard, + ParticipatedClubCard, + DetailedClubCard, + CardHostInfo, + CardContextType, } from './types'; +import ClubChip from '@/components/chip/club-chip/ClubChip'; +import Button from '@/components/button/Button'; const CardContext = createContext({ isCanceled: false }); -// 메인 Card -function Card({ - children, - isCanceled = false, - className, - ...props -}: CardProps) { - return ( - -
- {children} -
-
- ); -} - -// Box 컴포넌트 (CardInfo + CardStatus) -function CardBox({ - children, - className = '', - onClick, - ...props -}: CardBoxProps) { +// Box 컴포넌트 +function CardBox({ children, className = '', ...props }: CardBoxProps) { return (
{children} @@ -59,73 +49,84 @@ function CardBox({ ); } -// Info 컴포넌트 (모임에 관한 정보 - 제목, 위치, 날짜 등) -function CardInfo({ - title, - category, - location, - datetime, - isPast = false, +function CardImage({ + url, + alt = '모임 이미지', + isLiked, + onLikeClick, className, + isPast, ...props -}: CardInfoProps) { +}: CardImageProps) { return ( -
-
-

{title}

- -
-
-
- - {location} +
+ {alt} + {isLiked !== undefined && ( +
+
- {datetime} -
+ )}
); } -// Status 컴포넌트 (참가지 및 개설 여부 현황) -function CardStatus({ - currentParticipants, - maxParticipants, - isConfirmed = false, - confirmedVariant = 'confirmed', - isPast = false, - participants, +function CardTitle({ children, className, ...props }: CardTitleProps) { + return ( +

+ {children} +

+ ); +} + +function CardLocation({ + children, className, + textClassName, ...props -}: CardStatusProps) { +}: CardLocationProps) { return ( -
-
-
- - - {participants.map((participant, index) => ( - - ))} - -
- {isConfirmed && ( - +
+ + - + > + {children} +
); } +function CardDateTime({ children, className, ...props }: CardDateTimeProps) { + return ( + + {children} + + ); +} + // Overlay (모임 취소시 표시되는 오버레이) -function CardEndedOverlay({ onDelete }: CardEndedOverlayProps) { +function CardOverlay({ onDelete }: CardOverlayProps) { const { isCanceled } = useContext(CardContext); if (!isCanceled) return null; @@ -136,18 +137,21 @@ function CardEndedOverlay({ onDelete }: CardEndedOverlayProps) { }; return ( -
-
-

+

+
+

{'호스트가 모임을 취소했어요.'} +
+ {'새로운 모임을 찾아볼까요?'}

- {/* TODO:: 삭제 버튼 공통 컴포넌트 변경 필요 */} - + />
); @@ -158,9 +162,10 @@ function CardHost({ nickname, avatar, className, + isHost, onClick, ...props -}: CardHostProps) { +}: CardHostInfo) { return (
@@ -180,40 +185,431 @@ function CardHost({
- {nickname}님 + + {isHost ? '나' : `${nickname}님`} + 의 모임
); } -// Image 컴포넌트 (모임 이미지) -function CardImage({ - url, - alt = '모임 이미지', - isLiked = false, - onLikeClick, - className, - ...props -}: CardImageProps) { +function Card(props: CardProps) { + const renderCardContent = () => { + switch (props.variant) { + case 'defaultClub': + default: { + const { + imageUrl, + imageAlt, + title, + location, + datetime, + isLiked, + onLikeClick, + current, + max, + isPast, + isCanceled, + meetingType, + onClick, + onDelete, + status, + } = props as DefaultClubCard & { variant: 'defaultClub' }; + + return ( +
+ + + +
+
+ {title} + +
+
+ {location} + {datetime} +
+
+ +
+
+ + +
+ +
+
+ {isCanceled && } +
+ ); + } + + case 'participatedClub': { + const { + imageUrl, + imageAlt, + isLiked, + onLikeClick, + isCanceled, + onClick, + onDelete, + status, + meetingType, + title, + location, + datetime, + isPast, + onWriteReview, + onCancel, + } = props as ParticipatedClubCard & { variant: 'participatedClub' }; + + return ( +
+ + +
+
+
+ + +
+ +
+
+ {title} +
+ {location} + {datetime} +
+
+
+ {isPast ? ( +
+
+
+ {isCanceled && } +
+ ); + } + + case 'hostedClub': { + const { + imageUrl, + imageAlt, + onClick, + status, + meetingType, + isPast, + title, + location, + datetime, + onCancel, + reviewScore, + } = props as HostedClubCard & { variant: 'hostedClub' }; + + return ( +
+ + +
+
+ + +
+
+ {title} +
+ {location} + {datetime} +
+
+
+ {!isPast ? ( +
+
+
+
+ ); + } + + case 'detailedClub': { + const { + imageUrl, + imageAlt, + title, + location, + datetime, + isLiked, + onLikeClick, + meetingType, + current, + max, + isPast, + host, + status, + participants, + onClick, + isHost, + isParticipant, + hasWrittenReview, + onCancel, + onParticipate, + onCancelParticipation, + onWriteReview, + } = props as DetailedClubCard & { variant: 'detailedClub' }; + + const renderButton = () => { + if (isHost) { + return ( + -
-
-
- - ); -} - -export default FullCard; diff --git a/src/components/card/mock/mock.ts b/src/components/card/mock/mock.ts deleted file mode 100644 index 5d2f913b..00000000 --- a/src/components/card/mock/mock.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Meeting, FullMeeting } from '../types'; - -export const mockMeeting: Meeting = { - meetingInfo: { - title: '을지로에서 만나는 독서 모임', - category: '자유책', - location: '을지로 3가', - datetime: '12/14(토) 오전 10:00', - }, - participationStatus: { - currentParticipants: 17, - maxParticipants: 20, - isConfirmed: true, - confirmedVariant: 'confirmed', - participants: [ - { src: 'https://picsum.photos/seed/1/200', alt: '참가자1' }, - { src: 'https://picsum.photos/seed/2/200', alt: '참가자2' }, - { src: 'https://picsum.photos/seed/3/200', alt: '참가자3' }, - ], - }, - imageInfo: { - url: 'https://picsum.photos/seed/bookclub/800/450', - isLiked: true, - onLikeClick: () => alert('좋아요를 눌렀습니다!'), - }, - isPast: false, - isCanceled: false, - actions: { - onClick: () => alert('카드를 클릭했습니다!'), - onDelete: () => alert('모임을 삭제했습니다!'), - }, -}; - -export const mockFullMeeting: FullMeeting = { - ...mockMeeting, - hostInfo: { - nickname: '호스트', - onHostClick: () => alert('호스트 프로필을 클릭했습니다!'), - // 프로필 이미지 정보 - // avatar: { - // src: 'https://picsum.photos/seed/host/200', - // alt: '호스트 프로필 이미지', - // }, - }, - actions: { - onJoinClick: () => alert('참여하기를 클릭했습니다!'), - }, -}; - -export const mockPastMeeting = { - ...mockMeeting, - isPast: true, -}; - -export const mockPastFullMeeting = { - ...mockFullMeeting, - isPast: true, -}; - -export const mockCanceledMeeting = { - ...mockMeeting, - isCanceled: true, -}; - -export const mockCanceledFullMeeting = { - ...mockFullMeeting, - isCanceled: true, -}; diff --git a/src/components/card/my-club-card/MyClubCard.stories.tsx b/src/components/card/my-club-card/MyClubCard.stories.tsx deleted file mode 100644 index 47ee9add..00000000 --- a/src/components/card/my-club-card/MyClubCard.stories.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import MyClubCard from './MyClubCard'; - -const meta = { - title: 'Components/Card/MyClubCard', - component: MyClubCard, - parameters: { - layout: 'centered', - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -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) => ( -
- -
- ), -}; - -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, - }, - }, -}; diff --git a/src/components/card/my-club-card/MyClubCard.tsx b/src/components/card/my-club-card/MyClubCard.tsx deleted file mode 100644 index ffc5c651..00000000 --- a/src/components/card/my-club-card/MyClubCard.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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 ( - -
- - - {/* 첫 번째 줄: ClubChip + Chip */} -
-
- - -
- -
- - {/* 두 번째 줄: 모임 정보 */} -
-

- {meetingInfo.title} -

-
- {meetingInfo.location} - {meetingInfo.datetime} -
-
- - {/* 세 번째 줄: 버튼 */} -
-
- - -
-
-
- ); -} - -export default MyClubCard; diff --git a/src/components/card/simple-card/SimpleCard.stories.tsx b/src/components/card/simple-card/SimpleCard.stories.tsx deleted file mode 100644 index 05f42df9..00000000 --- a/src/components/card/simple-card/SimpleCard.stories.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import SimpleCard from './SimpleCard'; -import { mockMeeting } from '../mock/mock'; - -const meta = { - title: 'Components/Card/SimpleCard', - component: SimpleCard, - parameters: { - layout: 'centered', - }, - argTypes: { - meeting: { - control: 'object', - description: '모임 정보 (meetingInfo, participationStatus, imageInfo 등)', - }, - className: { - control: 'text', - description: '추가 스타일링을 위한 className', - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - meeting: mockMeeting, - }, - render: (args) => ( -
- -
- ), -}; - -export const Mobile: Story = { - ...Default, - parameters: { - viewport: { defaultViewport: 'mobile' }, - }, - render: (args) => ( -
- -
- ), -}; - -export const Desktop: Story = { - ...Default, - parameters: { - viewport: { defaultViewport: 'desktop' }, - }, - render: (args) => ( -
- -
- ), -}; diff --git a/src/components/card/simple-card/SimpleCard.tsx b/src/components/card/simple-card/SimpleCard.tsx deleted file mode 100644 index 933ba801..00000000 --- a/src/components/card/simple-card/SimpleCard.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { ComponentPropsWithoutRef } from 'react'; -import Card from '../Card'; -import { Meeting } from '@/components/card/types'; - -interface SimpleCardProps extends ComponentPropsWithoutRef<'article'> { - meeting: Meeting; -} - -function SimpleCard({ meeting, className, ...props }: SimpleCardProps) { - const { - meetingInfo, - participationStatus, - imageInfo, - isPast = false, - isCanceled = false, - actions, - } = meeting; - - return ( - -
-
- -
- - - - - -
-
- ); -} - -export default SimpleCard; diff --git a/src/components/card/types/actions.ts b/src/components/card/types/actions.ts deleted file mode 100644 index 834f3cb4..00000000 --- a/src/components/card/types/actions.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface SimpleActions { - onClick: () => void; - onDelete: () => void; -} - -export interface FullActions { - onJoinClick?: () => void; -} diff --git a/src/components/card/types/base.ts b/src/components/card/types/base.ts deleted file mode 100644 index 8f4db34a..00000000 --- a/src/components/card/types/base.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface BaseProps { - isPast?: boolean; - isCanceled?: boolean; -} - -export interface CardContextType { - isCanceled: boolean; -} diff --git a/src/components/card/types/card.ts b/src/components/card/types/card.ts new file mode 100644 index 00000000..710fd308 --- /dev/null +++ b/src/components/card/types/card.ts @@ -0,0 +1,75 @@ +import { + DefaultClubCard, + DetailedClubCard, + HostedClubCard, + ParticipatedClubCard, +} from '@/components/card/types/clubCard'; +import { ComponentPropsWithoutRef } from 'react'; + +interface CardBoxProps extends ComponentPropsWithoutRef<'div'> { + children: React.ReactNode; + className?: string; +} + +interface CardTitleProps extends ComponentPropsWithoutRef<'h3'> { + children: React.ReactNode; + className?: string; +} + +interface CardLocationProps extends ComponentPropsWithoutRef<'div'> { + children: React.ReactNode; + className?: string; + textClassName?: string; +} + +interface CardDateTimeProps extends ComponentPropsWithoutRef<'span'> { + children: React.ReactNode; + className?: string; +} + +interface CardImageProps extends ComponentPropsWithoutRef<'div'> { + url: string; + alt?: string; + isLiked?: boolean; + isPast?: boolean; + onLikeClick?: () => void; +} + +interface CardOverlayProps extends ComponentPropsWithoutRef<'div'> { + onDelete?: () => void; +} + +interface CardHostInfo extends ComponentPropsWithoutRef<'div'> { + nickname: string; + onHostClick?: (e: React.MouseEvent) => void; + isHost: boolean; + avatar?: { + src?: string; + alt?: string; + }; +} + +interface CardContextType { + isCanceled: boolean; +} + +type CardProps = { + variant?: 'defaultClub' | 'participatedClub' | 'hostedClub' | 'detailedClub'; +} & ( + | (DefaultClubCard & { variant?: 'defaultClub' }) + | (HostedClubCard & { variant?: 'hostedClub' }) + | (ParticipatedClubCard & { variant?: 'participatedClub' }) + | (DetailedClubCard & { variant?: 'detailedClub' }) +); + +export type { + CardBoxProps, + CardTitleProps, + CardLocationProps, + CardDateTimeProps, + CardImageProps, + CardOverlayProps, + CardHostInfo, + CardContextType, + CardProps, +}; diff --git a/src/components/card/types/clubCard.ts b/src/components/card/types/clubCard.ts new file mode 100644 index 00000000..5298db4b --- /dev/null +++ b/src/components/card/types/clubCard.ts @@ -0,0 +1,98 @@ +interface ClubCard { + // 이미지 정보 + imageUrl: string; + imageAlt?: string; + + // 모임 정보 + title: string; + location: string; + datetime: string; + meetingType: 'FREE' | 'FIXED'; + isPast: boolean; // 지난 모임인지 아닌지 + status: 'completed' | 'scheduled' | 'pending' | 'confirmed' | 'closed'; // 개설 현황 + + // 액션 (카드 클릭시 라우터 처리 등) + onClick: () => void; +} + +interface DefaultClubCard extends ClubCard { + // 찜 정보 + isLiked: boolean; + onLikeClick: () => void; + + // 취소 정보 (블러) + isCanceled: boolean; + onDelete: () => void; + + // 참가자 현황 + current: number; + max: number; +} + +interface ParticipatedClubCard extends ClubCard { + // 찜 정보 + isLiked: boolean; + onLikeClick: () => void; + + // 취소 정보 (블러) + isCanceled: boolean; + onDelete: () => void; + + // 버튼 액션 + onWriteReview: () => void; // 리뷰 작성 + onCancel: () => void; // 모임 취소 +} + +interface HostedClubCard extends ClubCard { + // 모임 취소 액션 + onCancel: () => void; + + // 리뷰 정보 + reviewScore?: number; +} + +interface DetailedClubCard extends ClubCard { + // 찜 정보 + isLiked: boolean; + onLikeClick: () => void; + + // 참가자 정보 + current: number; + max: number; + participants: ReadonlyArray<{ + readonly id?: string; + readonly name: string; + readonly profileImage?: string; + readonly profileImageAlt?: string; + }>; + + // 호스트 정보 + host: { + id?: string; + name: string; + profileImage?: string; + }; + + // 호스트 여부 + isHost: boolean; + + // 해당 모임의 참여자인지 여부 + isParticipant: boolean; + + // 리뷰 작성 여부 + hasWrittenReview?: boolean; + + // 액션 핸들러 + onCancel?: () => void; // 모임 취소 + onParticipate?: () => void; // 참여 + onCancelParticipation?: () => void; // 참여 취소 + onWriteReview?: () => void; // 리뷰 작성 +} + +export type { + ClubCard, + DefaultClubCard, + HostedClubCard, + ParticipatedClubCard, + DetailedClubCard, +}; diff --git a/src/components/card/types/components.ts b/src/components/card/types/components.ts deleted file mode 100644 index 93536406..00000000 --- a/src/components/card/types/components.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ComponentPropsWithoutRef, ReactNode } from 'react'; -import { BaseProps } from './base'; -import { - MeetingInfo, - HostInfo, - ParticipationStatus, - ImageInfo, -} from './meeting'; - -export interface CardProps - extends ComponentPropsWithoutRef<'article'>, - BaseProps { - children?: ReactNode; -} - -export interface CardBoxProps extends ComponentPropsWithoutRef<'div'> { - children: ReactNode; - onClick?: () => void; -} - -export interface CardInfoProps - extends Omit, keyof MeetingInfo>, - MeetingInfo, - BaseProps {} - -export interface CardStatusProps - extends ComponentPropsWithoutRef<'div'>, - ParticipationStatus, - BaseProps {} - -export interface CardHostProps - extends ComponentPropsWithoutRef<'div'>, - HostInfo {} - -export interface CardImageProps - extends ComponentPropsWithoutRef<'div'>, - ImageInfo {} - -export interface CardEndedOverlayProps extends ComponentPropsWithoutRef<'div'> { - onDelete?: () => void; -} diff --git a/src/components/card/types/index.ts b/src/components/card/types/index.ts index d34a7b38..cf51da62 100644 --- a/src/components/card/types/index.ts +++ b/src/components/card/types/index.ts @@ -1,5 +1,2 @@ -export * from './base'; -export * from './meeting'; -export * from './actions'; -export * from './components'; -export * from './models'; +export * from './card'; +export * from './clubCard'; diff --git a/src/components/card/types/meeting.ts b/src/components/card/types/meeting.ts deleted file mode 100644 index 4f80b06d..00000000 --- a/src/components/card/types/meeting.ts +++ /dev/null @@ -1,35 +0,0 @@ -export interface MeetingInfo { - title: string; - category: string; - location: string; - datetime: string; -} - -export interface HostInfo { - nickname: string; - onHostClick?: (e: React.MouseEvent) => void; - avatar?: { - src?: string; - alt?: string; - }; -} - -export interface ParticipantInfo { - src: string; - alt: string; -} - -export interface ParticipationStatus { - currentParticipants: number; - maxParticipants: number; - isConfirmed: boolean; - confirmedVariant: 'confirmed' | 'closed'; - participants: ParticipantInfo[]; -} - -export interface ImageInfo { - url: string; - alt?: string; - isLiked: boolean; - onLikeClick: () => void; -} diff --git a/src/components/card/types/models.ts b/src/components/card/types/models.ts deleted file mode 100644 index f44ca4d6..00000000 --- a/src/components/card/types/models.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BaseProps } from './base'; -import { - MeetingInfo, - ParticipationStatus, - ImageInfo, - HostInfo, -} from './meeting'; -import { SimpleActions, FullActions } from './actions'; - -export interface Meeting extends BaseProps { - meetingInfo: MeetingInfo; - participationStatus: ParticipationStatus; - imageInfo: ImageInfo; - actions: SimpleActions; -} - -export interface FullMeeting extends Omit { - hostInfo: HostInfo; - actions: FullActions; -} - -export interface ClubStatus { - isCompleted: boolean; - isConfirmed: boolean; -} - -export interface ClubMeeting extends BaseProps { - meetingInfo: MeetingInfo; - imageInfo: ImageInfo; - clubStatus: ClubStatus; - actions: SimpleActions; -} diff --git a/src/components/chip/club-chip/ClubChip.stories.tsx b/src/components/chip/club-chip/ClubChip.stories.tsx index 74f41fcc..e9281ee8 100644 --- a/src/components/chip/club-chip/ClubChip.stories.tsx +++ b/src/components/chip/club-chip/ClubChip.stories.tsx @@ -36,6 +36,18 @@ export const Confirmed: Story = { }, }; +export const FreeBook: Story = { + args: { + variant: 'FREE', + }, +}; + +export const FixedBook: Story = { + args: { + variant: 'FIXED', + }, +}; + export const AllStates: Story = { render: () => (
@@ -43,6 +55,9 @@ export const AllStates: Story = { + + +
), }; diff --git a/src/components/chip/club-chip/ClubChip.tsx b/src/components/chip/club-chip/ClubChip.tsx index 500baa9e..08e2e67e 100644 --- a/src/components/chip/club-chip/ClubChip.tsx +++ b/src/components/chip/club-chip/ClubChip.tsx @@ -1,52 +1,66 @@ import Chip from '../Chip'; import { twMerge } from 'tailwind-merge'; -type ClubChipVariant = 'completed' | 'scheduled' | 'pending' | 'confirmed'; +type ClubChipVariant = + | 'completed' + | 'scheduled' + | 'pending' + | 'confirmed' + | 'closed' + | 'FREE' + | 'FIXED'; const CLUB_CHIP_TEXT = { completed: '참여완료', scheduled: '참여예정', pending: '개설대기', confirmed: '개설확정', + closed: '모집마감', + FREE: '자유책', + FIXED: '지정책', } as const; +const CLUB_CHIP_VARIANT = { + completed: 'square-filled', + scheduled: 'square-filled', + pending: 'square-outlined', + confirmed: 'square-filled', + closed: 'square-filled', + FREE: 'rounded-filled', + FIXED: 'rounded-light', +} as const; + +const CLUB_CHIP_STYLE = { + completed: 'bg-gray-normal-01 text-gray-dark-02', + scheduled: 'bg-green-light-03 text-green-dark-01', + pending: 'border-blue-light-active text-blue-light-active', + confirmed: 'bg-blue-light text-blue-light-active', + closed: 'bg-blue-normal text-gray-white', + FREE: '', + FIXED: '', +} as const; + +const PAST_STATUS_STYLE = 'bg-gray-dark-02 text-gray-white'; +const PAST_BOOK_TYPE_STYLE = 'bg-gray-normal-01 text-gray-dark-02'; + interface ClubChipProps { variant: ClubChipVariant; className?: string; + isPast?: boolean; } -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'; - } - }; +function ClubChip({ variant, className, isPast = false }: ClubChipProps) { + const style = isPast + ? ['FREE', 'FIXED'].includes(variant) + ? PAST_BOOK_TYPE_STYLE + : PAST_STATUS_STYLE + : CLUB_CHIP_STYLE[variant]; return ( ); } diff --git a/src/features/bookclub/components/Main.tsx b/src/features/bookclub/components/Main.tsx index 6408c5e0..a09ef2e5 100644 --- a/src/features/bookclub/components/Main.tsx +++ b/src/features/bookclub/components/Main.tsx @@ -1,53 +1,54 @@ -import SimpleCard from '@/components/card/simple-card/SimpleCard'; -import { Meeting } from '@/components/card/types'; +// import SimpleCard from '@/components/card/simple-card/SimpleCard'; +// import { Meeting } from '@/components/card/types'; -const mockImgSrc = '/images/profile.png'; +// const mockImgSrc = '/images/profile.png'; -const mockMeeting: Meeting = { - meetingInfo: { - title: '을지로에서 만나는 독서 모임', - category: '자유책', - location: '을지로 3가', - datetime: '12/14(토) 오전 10:00', - }, - participationStatus: { - currentParticipants: 17, - maxParticipants: 20, - isConfirmed: true, - confirmedVariant: 'confirmed', - participants: [ - { - src: mockImgSrc, - alt: '참가자1', - }, - { - src: mockImgSrc, - alt: '참가자2', - }, - { - src: mockImgSrc, - alt: '참가자3', - }, - ], - }, - imageInfo: { - url: mockImgSrc, - isLiked: true, - onLikeClick: () => alert('좋아요를 눌렀습니다!'), - }, - isPast: false, - isCanceled: false, - actions: { - onClick: () => alert('카드를 클릭했습니다!'), - onDelete: () => alert('모임을 삭제했습니다!'), - }, -}; +// const mockMeeting: Meeting = { +// meetingInfo: { +// title: '을지로에서 만나는 독서 모임', +// category: '자유책', +// location: '을지로 3가', +// datetime: '12/14(토) 오전 10:00', +// }, +// participationStatus: { +// currentParticipants: 17, +// maxParticipants: 20, +// isConfirmed: true, +// confirmedVariant: 'confirmed', +// participants: [ +// { +// src: mockImgSrc, +// alt: '참가자1', +// }, +// { +// src: mockImgSrc, +// alt: '참가자2', +// }, +// { +// src: mockImgSrc, +// alt: '참가자3', +// }, +// ], +// }, +// imageInfo: { +// url: mockImgSrc, +// isLiked: true, +// onLikeClick: () => alert('좋아요를 눌렀습니다!'), +// }, +// isPast: false, +// isCanceled: false, +// actions: { +// onClick: () => alert('카드를 클릭했습니다!'), +// onDelete: () => alert('모임을 삭제했습니다!'), +// }, +// }; function Main() { return (
- - + {/* + */} +
hi
); } diff --git a/tailwind.config.ts b/tailwind.config.ts index c017cc5e..a9ac2ca6 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -38,7 +38,13 @@ const config: Config = { darker: '#003b33', }, blue: { - 'normal-01': '#007AFF', + light: '#d9ebff', + 'light-active': '#009cf4', + normal: '#007aff', + }, + red: { + pink: '#ff337e', + normal: '#dc2626', }, }, },