원티드 프리온보딩 FE 5주차 팀프로젝트 - 호텔 예약 사이트

dev_sang·2022년 9월 12일
0

담당 기능

  • 호텔 예약/삭제 버튼과 버튼 클릭 시에 로컬 스토리지에 저장/삭제 기능 개발
  • 호텔 저장 확인 안내 모달창 제작과 중복 저장 시 안내 처리
  • 예약(저장)된 호텔 리스트 확인 페이지 UI 제작
  • 목 서버 (json server사용) 세팅

➕ 팀 리딩, 프로젝트 기획, 문서 관리, 일정 조율

시연 영상

예약하기 버튼 클릭, '확인' 클릭 시 저장된 호텔 목록 페이지로 이동


중복 저장(예약)일 경우 alert창으로 알림


예약취소 버튼 클릭 시 로컬스토리지에서 삭제

상세 설명

Card.tsx

호텔 목록에 들어갈 카드를 구현했다.

HotelList 컴포넌트로부터에서 호텔 이름과 주소, 리뷰, 가격, 예약여부에 대한 데이터를 props로 받아온다.

  • 받아오는 psrops의 개수가 많기 때문에 구조분해할당을 이용
// Card. tsx
import { Hotel } from 'types/types';
import { priceToString } from '../utils/priceToString';
import BookingButton from '../components/bookedList/BookingButton';
import Rating from './Rating';
import { useRecoilValue } from 'recoil';
import { HideCard } from 'store/global';

interface PropsType {
  data: Hotel;
  isBooked: boolean;
}

const IMAGE_URL =
  'https://images.unsplash.com/photo-1535827841776-24afc1e255ac?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1335&q=80';

const BookedHotels: Hotel[] = [];

const Card = ({ data, isBooked }: PropsType) => {
  const { hotel_name: hotelName, address, rating, review, price } = data;

  return (
    <div
      key={hotelName}
      className="flex justify-between w-full mt-5 mb-5 bg-white rounded-lg h-80 max-w-7xl border-slate-300 drop-shadow-md dark:bg-gray-500"
    >
      <div className="w-1/3 h-full">
        <img
          src={IMAGE_URL}
          alt="hotel_image"
          className="w-full h-full bg-cover rounded-l-lg"
        />
      </div>
      <section className="flex flex-col w-full ml-2 mr-6 mt-9 h-1/2">
		 
          ... (생략)
        
          <BookButton newHotel={data} isBooked={isBooked} />
        </div>
      </section>
    </div>
  );
};

export default Card;

BookButton.tsx

예약 버튼 컴포넌트에서 예약 기능과(로컬스토리지에 저장) 예약 확인 모달을 띄우는 기능에 해당하는 로직을 구현했다.

handleClickBook

  • 예약 버튼이 클릭된 호텔을 parameter로 받는다.
  • isExisting 에서 저장된 호텔인 지 확인한다.
  • 이미 저장된 호텔인 경우 alert창을 띄운다.
  • 새로 저장될 호텔인 경우 로컬스토리지에 저장하고 changeModal 함수에 true값(BOOK_HOTEL)을 보낸다.
  • 로컬스토리지 저장 시 BookedHotels 배열에 할당된 호텔 데이터를 저장한다.

handleClickCancel

로컬스토리지에서 특정 데이터 삭제 로직

  • 예약취소 버튼이 클릭된 호텔(targetHotel)을 매개변수로 받는다.
  • 기존 로컬 스토리지에 저장된 호텔 배열에 filter 매서드를 적용하여 targetHotel을 제외한 나머지 호텔 데이터들이 할당된 newBookedHotels 배열을 만든다.
  • 로컬 스토리지에 newBookedHotels배열을 저장한다.
  • changeModal함수에 false값 (CANCEL_HOTEL)을 보낸다.

changeModal

  • 리코일 전역 상태값을 업데이트하는 함수
  • isBooked의 값이 true일 경우 예약 버튼과 예약 페이지로 이동할 것인 지 확인하는 내용의 모달이 뜬다.
  • isBooked의 값이 false일 경우 예약취소 버튼과 예약 취소를 확인하는 내용의 모달이 뜬다.
  • 해당 전역 상태값은 Confirm.tsx 컴포넌트에서도 사용됩니다. 모달의 내용을 바꾸기 위해 사용했다.
  • toggle 매서드를 호출합니다. 호출 시 useConfirm.tsxisShown의 상태값이 true로 바뀌고 모달창이 나타난다.

다른 팀원분께서 모달을 위해 useConfirm 커스텀 훅과 Confirm (모달)에 해당하는 기능을 만들어두셨고 해당 코드를 사용하려고 하다보니 로직이 조금 복잡해졌다.

Confirm.tsx 컴포넌트는 아래 사진의 버튼 부분(빨간 박스)에 해당하는 기능으로 사용된다.

// BookButton.tsx
import React from 'react';
import { Hotel } from 'types/types';
import { useConfirm } from '../../hooks/useConfirm';
import ConfirmContent from '../result/ConfirmContent';
import { Confirm } from '../../common/Confirm';
import { useSetRecoilState } from 'recoil';
import { IsBookedButton } from 'store/global';

interface PropsType {
  newHotel: Hotel;
  isBooked: boolean;
}

const BookedHotels: Hotel[] = [];

const BOOK_HOTEL = true;
const CANCEL_HOTEL = false;

const BookButton = ({ newHotel, isBooked }: PropsType) => {
  const setIsBooked = useSetRecoilState(IsBookedButton);
  const { isShown, toggle } = useConfirm();
  const modalContent = <ConfirmContent {...newHotel} />;
  const changeModal = (isBooked: boolean) => {
    if (isBooked) {
      setIsBooked(BOOK_HOTEL);
      toggle();
    }
    if (!isBooked) {
      setIsBooked(CANCEL_HOTEL);
      toggle();
    }
  };

  const handleClickBook = (newHotel: Hotel) => {
    const isExisting = BookedHotels.some(
      (e) => e.hotel_name === newHotel.hotel_name
    );
    if (isExisting) {
      alert('이미 저장된 호텔입니다.');
      return;
    }
    if (!isExisting) {
      BookedHotels.push(newHotel);
      changeModal(BOOK_HOTEL);
    }
    localStorage.setItem('hotels', JSON.stringify(BookedHotels));
  };

  const handleClickCancel = React.useCallback((targetHotel: Hotel) => {
    const localHotelData = JSON.parse(localStorage.getItem('hotels')!) ?? [];
    const newBookedHotels = localHotelData.filter(
      (hotel: Hotel) => hotel.hotel_name !== targetHotel.hotel_name
    );
    localStorage.setItem('hotels', JSON.stringify(newBookedHotels));
    changeModal(CANCEL_HOTEL);
  }, []);

  return (
    <>
      <div>
        {isBooked ? (
          <button
            className="self-end w-24 h-10 mt-2 text-base font-bold text-center text-white rounded bg-main hover:shadow-md"
            onClick={() => handleClickCancel(newHotel)}
          >
            예약 취소
          </button>
        ) : (
          <button
            className="self-end w-24 h-10 mt-2 text-base font-bold text-center text-white rounded bg-main hover:shadow-md"
            onClick={() => handleClickBook(newHotel)}
          >
            예약하기
          </button>
        )}
      </div>
      <Confirm
        isShown={isShown}
        hide={toggle}
        modalContent={modalContent}
        headerText="예약 완료!"
      />
    </>
  );
};

export default BookButton;


BookedList.tsx

예약 목록 확인 페이지 (로컬 스토리지에 저장된 목록 확인)

  • 로컬 스토리지에 저장된 값을 가져와 localHotelData에 할당
  • 과제 조건에 명시된 대로 로딩시간을 (timer) 주었다.
  • 디바이스 화면 크기에 따라 각각 BookedMobile, BookedPc 컴포넌트가 렌더링되도록 구현
// BookedList.tsx
import React from 'react';
import { Hotel } from 'types/types';
import BookedPc from './BookedPC';
import BookedMobile from './BookedMobile';

const BookedList = () => {
  const [isLoading, setIsLoading] = React.useState(true);
  const [localHotelData, setLocalHotelData] = React.useState<Hotel[]>([]);

  React.useEffect(() => {
    const localHotelData = JSON.parse(localStorage.getItem('hotels')!) ?? [];
    const timer = setTimeout(() => {
      setLocalHotelData(localHotelData as Hotel[]);
      setIsLoading(false);
    }, 500);

    return () => {
      clearTimeout(timer);
    };
  }, []);

  return (
    <>
      <div className="hidden lg:block md:hidden">
        <BookedPc hotel={localHotelData} isLoading={isLoading} />
      </div>
      <div className="md:block bolck lg:hidden">
        <BookedMobile hotel={localHotelData} isLoading={isLoading} />
      </div>
    </>
  );
};

export default BookedList;



모달 기능 추가

제가 메인으로 맡아 구현하지는 않았지만 함께 사용했던 컴포넌트입니다.
: Confirm.tsx, ConfirmContent.tsx, useConfirm.tsx

그려보자면 이런 식 (간략한 그림)

마간 기한이 임박해 새로 모달을 구현할 시간이 부족했고 기존에 다른 팀원분계서 구현해둔 모달 기능을 사용해야했다. BookButton컴포넌트에서 예약 확인, 취소 상태에 대한 전역 상태를 추적하고 이 전역 상태값을 ConfirmContent.tsxConfirm.tsx에 반영한다. true일 경우 호텔 예약 확인 모달, false일 경우 예약 취소 확인 모달로 설정한다. 그리고 useConfirm.tsx에서 모달 노출 상태값(isShown)을 관리하며 toggle 매서드 호출을 통해 isShwon값을 변경한다. isShowntrue일 경우 모달을 띄우고, false일 경우 모달을 숨긴다.

처음 접해보는 모달 설정 방식라 조금 당황하였으나 마감기한이 길지 않아 동작 방식을 빠르게 파악하는 것에 집중했다. 예약 확인 상태, 예약 취소 상태 두 값을 각각 다른 모달에 전달해야한다고만 생각했었으나 두 상태 값을 각각 true와 false 로, 그리고 전역 상태값으로 설정했더니 모달을 설정하기 수월했다. 모달을 만들기 위한 다른 코드들이 필요 없고 전역 상태값과 커스텀 Hook, 그리고 모달 내용과 버튼에 해당하는 컴포넌트만 있으면 모달이 완성되니 의외로 나름 편한 방법이라는 생각도 들었다.



profile
There is no reason for not trying.

0개의 댓글