Skip to content
2 changes: 1 addition & 1 deletion public/icons/RatingIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function RatingIcon({
checked = false,
...props
}: RatingIconProps) {
const heartColor = checked ? '#EA580C' : '#D1D5DB';
const heartColor = checked ? '#00a991' : '#d8d9db';
return (
<svg
width={width}
Expand Down
78 changes: 74 additions & 4 deletions src/components/written-review/WrittenReview.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,92 @@
import type { Meta, StoryObj } from '@storybook/react';
import WrittenReview from './WrittenReview';

// 컴파운드 컴포넌트의 모든 props를 포함하는 타입 정의
interface StoryProps {
ratingCount: number;
comment: string;
profileImage: string;
userName: string;
createdAt: string;
clubImage: string;
clubName: string;
clubImageAlt: string;
location: string;
}

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

export default meta;
type Story = StoryObj<typeof WrittenReview>;
type Story = StoryObj<StoryProps>;

export const CreatedReview: Story = {
export const FindClubReview: Story = {
render: (args) => (
<WrittenReview>
<div className="flex flex-col gap-y-2">
<WrittenReview.Rating ratingCount={args.ratingCount} />
<WrittenReview.Comment text={args.comment} />
<WrittenReview.UserProfile
profileImage={args.profileImage}
userName={args.userName}
createdAt={args.createdAt}
/>
</div>
</WrittenReview>
),
args: {
ratingCount: 4,
comment:
'따듯하게 느껴지는 공간이에요 :) 평소에 달램 이용해보고 싶었는데 이렇게 같이 달램 생기니까 너무 좋아요! 프로그램이 더 많이 늘어났으면 좋겠어요.',
'아침부터 자기발전을 위한 시간을 가져서 좋았어요. 각자의 길 위에서 달려가는 생생한 순간을 공유해주셔서 감사합니다!',
profileImage:
'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg',
userName: '다람쥐',
createdAt: '2024.01.25',
},
};

export const MypageReview: Story = {
render: (args) => (
<div className="w-[311px] md:w-full">
<WrittenReview>
<div className="flex flex-col items-center gap-x-6 sm:items-start sm:gap-y-2 md:flex-row">
<WrittenReview.ClubImage
src={args.clubImage}
alt={args.clubImageAlt}
/>
<div className="flex flex-col justify-start gap-y-1">
<WrittenReview.Rating ratingCount={args.ratingCount} />
<WrittenReview.ClubInfo
clubName={args.clubName}
location={args.location}
/>
<WrittenReview.Comment text={args.comment} />
<WrittenReview.UserProfile
createdAt={args.createdAt}
className="gap-x-0"
/>
</div>
</div>
</WrittenReview>
</div>
),
args: {
profileImage:
'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg',
userName: '럽윈즈올',
userName: '다람쥐',
ratingCount: 4,
comment:
'아침부터 자기발전을 위한 시간을 가져서 좋았어요. 각자의 길 위에서 달려가는 생생한 순간을 공유해주셔서 감사합니다!',
createdAt: '2024.01.25',
clubImage:
'https://cdn.pixabay.com/photo/2024/02/17/00/18/cat-8578562_1280.jpg',
clubName: '달램핏 오피스 스트레칭',
clubImageAlt: '달램핏 이미지',
location: '을지로 3가',
},
};
55 changes: 0 additions & 55 deletions src/components/written-review/WrittenReview.test.tsx

This file was deleted.

132 changes: 90 additions & 42 deletions src/components/written-review/WrittenReview.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,107 @@
'use client';

import Image from 'next/image';
import RatingDisplay from '../rating-display/RatingDisplay';
import { useState, useEffect } from 'react';

// 디자인 확정시, 기본 이미지 변경
const defaultProfileImage =
'https://cdn.pixabay.com/photo/2018/02/12/10/45/heart-3147976_1280.jpg';

interface WrittenReviewProps {
ratingCount: number;
comment: string;
profileImage?: string;
userName: string;
createdAt: string;
import { LocationIcon } from '../../../public/icons';
import {
ClubImageProps,
ClubInfoProps,
CommentProps,
RatingProps,
UserProfileProps,
} from './types/writtenReview';

// 기본 이미지 (변경될 수 있음)
const defaultProfileImage = '/images/profile.png';
const defaultClubImage = '/images/profile.png';

function handleImageError(
event: React.SyntheticEvent<HTMLImageElement>,
defaultSrc: string,
) {
event.currentTarget.src = defaultSrc;
}

export default function WrittenReview({
ratingCount,
comment,
profileImage,
userName,
createdAt,
}: WrittenReviewProps) {
const [imgSrc, setImgSrc] = useState(profileImage || defaultProfileImage);
children,
}: {
children: React.ReactNode;
}) {
return (
<article className="flex w-full max-w-[948px] flex-col items-start sm:justify-center">
{children}
<hr className="border-t-1 mt-4 w-full border-gray-normal-01" />
</article>
);
}

useEffect(() => {
setImgSrc(profileImage || defaultProfileImage);
}, [profileImage]);
function Rating({ ratingCount }: RatingProps) {
return <RatingDisplay ratingCount={ratingCount} />;
}

const handleImageError = () => {
setImgSrc(defaultProfileImage);
};
function Comment({ text }: CommentProps) {
return (
<p className="max-w-full flex-wrap text-sm font-medium text-gray-darker">
{text}
</p>
);
}

function UserProfile({
profileImage,
userName,
createdAt,
className,
}: UserProfileProps) {
return (
<article className="flex flex-col items-start">
<RatingDisplay ratingCount={ratingCount} />
<p className="mb-2 mt-[10px] flex-wrap text-sm font-medium text-gray-700">
{comment}
</p>
<div className="flex items-center">
<div className={`flex items-center gap-x-[6px] ${className}`}>
{profileImage && (
<Image
width={24}
height={24}
src={imgSrc}
src={profileImage || defaultProfileImage}
alt={`${userName}'s profile picture`}
className="h-6 w-6 rounded-full"
onError={handleImageError}
className="h-6 w-6 rounded-full object-cover"
onError={(e) => handleImageError(e, defaultProfileImage)}
/>
<p className="flex h-[1em] items-center border-r-2 border-r-gray-700 px-2 text-xs font-medium text-gray-700">
{userName}
</p>
<p className="ml-3 text-xs font-medium text-gray-500">{createdAt}</p>
)}
<p className="flex h-[1em] items-center text-xs font-semibold text-gray-darker">
{userName}
</p>
<p className="text-xs font-medium text-gray-normal-03">{createdAt}</p>
</div>
);
}

function ClubImage({ src, alt }: ClubImageProps) {
return (
<Image
src={src}
alt={alt || 'Club image'}
width={0} // 너비를 Tailwind로 관리하므로 설정하지 않음
height={0} // 비율을 유지하며 높이는 자동으로 설정됨
sizes="(max-width: 744px) 311px, 280px"
className="h-[156px] w-[311px] rounded-3xl object-cover md:w-[280px]"
onError={(e) => handleImageError(e, defaultClubImage)}
/>
);
}

function ClubInfo({ clubName, location }: ClubInfoProps) {
return (
<div className="flex items-center gap-x-2">
<p className="text-sm font-bold">{clubName}</p>
<div className="flex items-center">
<LocationIcon />
<span className="text-sm font-semibold text-gray-dark-03">
{location}
</span>
</div>
<hr className="mt-4 w-full border-t-2 border-dashed border-gray-200" />
</article>
</div>
);
}

// WrittenReview의 자식 컴포넌트 연결
WrittenReview.Rating = Rating;
WrittenReview.Comment = Comment;
WrittenReview.UserProfile = UserProfile;
WrittenReview.ClubInfo = ClubInfo;
WrittenReview.ClubImage = ClubImage;
24 changes: 24 additions & 0 deletions src/components/written-review/types/writtenReview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface UserProfileProps {
profileImage?: string;
userName?: string;
createdAt: string;
className?: string;
}

export interface ClubImageProps {
src: string;
alt?: string;
}

export interface ClubInfoProps {
clubName: string;
location: string;
}

export interface CommentProps {
text: string;
}

export interface RatingProps {
ratingCount: number;
}