INtiful / SootheWithMe

같이 달램
https://soothe-with-me.vercel.app/
0 stars 0 forks source link

feat: Bottom Floating Bar 컴포넌트 UI 작성 #36

Closed hakyoung12 closed 3 weeks ago

hakyoung12 commented 3 weeks ago

✏️ 작업 내용

-Bottom Floating Bar 컴포넌트 UI 작성

📷 스크린샷

참가자 UI

PC화면

스크린샷 2024-09-09 오후 3 49 22

태블릿 화면

스크린샷 2024-09-09 오후 3 50 08

모바일 화면

스크린샷 2024-09-09 오후 4 03 35

참여 불가능할 때

스크린샷 2024-09-09 오후 4 09 06

주최자 UI

PC화면

스크린샷 2024-09-09 오후 4 04 04

태블릿 화면

스크린샷 2024-09-09 오후 4 04 36

모바일 화면

스크린샷 2024-09-09 오후 4 04 46

✍️ 사용법

      <BottomFloatingBar
        user={user} // 사용자 정보
        createdBy={createdBy} // 모임을 생성한 사용자 ID
        participantCount={participantCount} // 현재 참여자 수
        capacity={capacity} // 모임의 최대 수용 인원
        registrationEnd={registrationEnd} // 등록 마감일
        canceledAt={canceledAt} // 모임 취소일
        participantsData={participantsData} // 참여자 데이터
      />

위와 같이 props를 내려주어 사용할 수 있습니다.

🎸 기타

실제 api 결과에 따라 props를 변경할 가능성이 있습니다.

yunsung-hodoopapa commented 1 week ago

전반적으로 Bottom Floating Bar 컴포넌트에 사용되는 ParticipationButton 컴포넌트의 가독성이 좋지 않습니다. 어떻게 하면 조금 더 가독성있게 만들 수 있을까요?

가장 쉽게 예를 들면 아래와 같이 switch case 문을 사용해볼 것 같아요.

'use client';

import Button from '../Button/Button';
import { onCancel, onShare, onJoin, onWithdraw } from './Mock';

interface Participant {
  User: {
    id: number;
  };
}

// @todo api 연결 후 Props 수정
interface ParticipationButtonProps {
  isHost: boolean;
  user: { name: string; id: number };
  participantCount: number;
  capacity: number;
  registrationEnd: string;
  canceledAt: null | string;
  participantsData: Participant[];
}

const ParticipationButton = ({
  user,
  isHost,
  participantCount,
  capacity,
  registrationEnd,
  canceledAt,
  participantsData,
}: ParticipationButtonProps) => {
  const getButtonProps = (): { name: string; variant: 'white' | 'default' | 'gray'; action?: () => void; disabled?: boolean } => {
    const isFull = participantCount === capacity;
    const isRegistrationEnded = new Date() > new Date(registrationEnd);
    const hasParticipated = participantsData.some(
      (participant) => participant.User.id === user.id
    );
    const isCancelled = Boolean(canceledAt);
    const isParticipationDisabled = isFull || isRegistrationEnded || isCancelled;

    switch (true) {
      case isHost:
        return {
          name: '취소하기',
          variant: 'white',
          action: onCancel,
          disabled: isRegistrationEnded || isCancelled,
        };
      case hasParticipated:
        return {
          name: '참여 취소하기',
          variant: 'white',
          action: onWithdraw,
        };
      case isParticipationDisabled:
        return {
          name: '참여하기',
          variant: 'gray',
          action: undefined,
          disabled: true,
        };
      default:
        return {
          name: '참여하기',
          variant: 'default',
          action: onJoin,
        };
    }
  };

  const { name, variant, action, disabled } = getButtonProps();

  return (
    <div className={`h-44 w-[115px] ${isHost && 'w-full md:w-[115px]'}`}>
      <Button
        name={name}
        type="button"
        onClick={action}
        variant={variant}
        disabled={disabled}
      />
    </div>
  );
};

export default ParticipationButton;

조금 더 나아가서 타입스크립트를 사용한다면 ts-pattern을 사용할 수 있는데요,

ts-pattern을 사용하면 switch 문을 더 선언적으로 작성하고, TypeScript 타입 시스템을 더 강력하게 활용할 수 있습니다.

'use client';

import Button from '../Button/Button';
import { onCancel, onShare, onJoin, onWithdraw } from './Mock';
import { match } from 'ts-pattern';

interface Participant {
  User: {
    id: number;
  };
}

// @todo api 연결 후 Props 수정
interface ParticipationButtonProps {
  isHost: boolean;
  user: { name: string; id: number };
  participantCount: number;
  capacity: number;
  registrationEnd: string;
  canceledAt: null | string;
  participantsData: Participant[];
}

const ParticipationButton = ({
  user,
  isHost,
  participantCount,
  capacity,
  registrationEnd,
  canceledAt,
  participantsData,
}: ParticipationButtonProps) => {
  const isFull = participantCount === capacity;
  const isRegistrationEnded = new Date() > new Date(registrationEnd);
  const hasParticipated = participantsData.some(
    (participant) => participant.User.id === user.id
  );
  const isCancelled = Boolean(canceledAt);
  const isParticipationDisabled = isFull || isRegistrationEnded || isCancelled;

  const { name, variant, action, disabled } = match({ isHost, hasParticipated, isParticipationDisabled })
    .with({ isHost: true }, () => ({
      name: '취소하기',
      variant: 'white' as const,
      action: onCancel,
      disabled: isRegistrationEnded || isCancelled,
    }))
    .with({ hasParticipated: true }, () => ({
      name: '참여 취소하기',
      variant: 'white' as const,
      action: onWithdraw,
    }))
    .with({ isParticipationDisabled: true }, () => ({
      name: '참여하기',
      variant: 'gray' as const,
      action: undefined,
      disabled: true,
    }))
    .otherwise(() => ({
      name: '참여하기',
      variant: 'default' as const,
      action: onJoin,
    }));

  return (
    <div className={`h-44 w-[115px] ${isHost && 'w-full md:w-[115px]'}`}>
      <Button
        name={name}
        type="button"
        onClick={action}
        variant={variant}
        disabled={disabled}
      />
    </div>
  );
};

export default ParticipationButton;
yunsung-hodoopapa commented 1 week ago

리팩토링 방향 : 조금 더 선언적으로 컴포넌트 작성하기