[Next13 + TypeScript ] 예약 취소 구현하기

ezi·2023년 9월 5일
0

📢 해당 포스트는 스타트업 프로젝트를 진행하며, 직접 작성한 코드를 기록용으로 작성함을 명시합니다.

🐝 무엇을 구현한 것인가?

예약을 한 상품을 취소하도록 구현하였습니다.

🐝 무엇을 구현하였는가?

  1. 예약 취소할 상품에 대한 정보 불러오기 (api)
  2. 예약한 예약자에 대한 정보 불러오기 (api)
  3. 결제 금액 데이터 불러오기 (api)
  4. 수수료 데이터 불러오기 (api)
  5. 최종 환불 금액 데이터 불러오기 (api)
  6. 환불 규정 동의 체크박스 체크 안되어있을 시, 모달창1(체크 확인) 띄우기
  7. 환불 규정 동의 체크박스 체크 되어있을 시, 모달창2(예약 취소) 띄우기
  8. 모달창2에서 예약취소 확인 버튼 눌렀을 때 예약 취소되도록 하기 (api)

전체 코드

'use client';

import PaymentDetail from '@/components/common/PaymentDetail';
import Checkbox from '@/components/common/Checkbox';
import Btn from '@/components/common/Btn';
import { useEffect, useState } from 'react';
import {
  CancelButtonWrap,
  CancelContentsSpanData,
  CancelContentsSpanTitle,
  CancelContentsWrap,
  CancelPaymentDetailWrap,
  CancelWholeWrap,
  CheckFont,
  CheckboxWrap,
  GreyLine,
  InfoCancelTitle,
  InfoCancelWrap,
  ModalStyle,
  ModalWrap,
  ReservationCancelTextTitle,
  ReservationCancelWrap,
  TotalPayText,
  TotalPayWrap,
} from './styles';
import GreyRule from './greyrule';
import { CancelModal } from '@/app/reservationcancel/components/CancelModal';
import { CheckModal } from '@/app/reservationcancel/components/CheckModal';
import instance from '@/apis/instance';

const ReservationCancelUI = ({ id }: { id: number }) => {
  const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);
  const [checkModalOpen, setCheckModalOpen] = useState<boolean>(false);
  const [priceRefund, setPriceRefund] = useState(null);
  const [refundPercent, setRefundPercent] = useState();

  const [checked, setChecked] = useState<boolean>(false);
  const [submit, setSubmit] = useState<boolean>(false);
  const [paymentDetail, setPaymentDetail] = useState(null);

  const handleConfirmModalOpen = () => {
    setConfirmModalOpen(true);
  };

  const handleConfirmModalClose = () => {
    setConfirmModalOpen(false);
  };

  const handleCheckModalOpen = () => {
    setCheckModalOpen(true);
  };

  const handleCheckModalClose = () => {
    setCheckModalOpen(false);
  };

  const handleClick = () => {
    setChecked(!checked);
  };

  const alertClick = () => {
    if (checked) {
      setSubmit(!submit);
      handleConfirmModalOpen();
    } else {
      handleCheckModalOpen();
    }
  };

  const getAccessTokenFromLocalStorage = () =>
    localStorage.getItem('accessToken');

  const RefundPrice = () => {
    const accessToken = getAccessTokenFromLocalStorage();

    instance
      .get(`/api/v1/payment/refund/${id}/user/estimate`, {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      })
      .then((response) => {
        // console.log(response);
        setPriceRefund(response.data.data);
        console.log(priceRefund);
      })
      .catch((error) => {
        console.log(error, '실패하였습니다');
      });
  };

  const RefundPercent = () => {
    const accessToken = getAccessTokenFromLocalStorage();

    instance
      .get(`/api/v1/payment/refund/${id}/user/estimate/percent`, {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      })
      .then((response) => {
        // console.log(response);
        setRefundPercent(response.data.data.refundPercent);
        console.log(refundPercent);
      })
      .catch((error) => {
        console.log(error, '실패하였습니다');
      });
  };

  const DetailPayment = () => {
    const accessToken = getAccessTokenFromLocalStorage();

    instance
      .get(`/api/v1/payment/refund/${id}/page`, {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      })
      .then((response) => {
        // console.log(response);
        setPaymentDetail(response.data.data);
        console.log(paymentDetail);
      })
      .catch((error) => {
        console.log(error, '실패하였습니다');
      });
  };

  useEffect(() => {
    RefundPrice();
    RefundPercent();
    DetailPayment();
  }, []);

  return (
    <>
      <ReservationCancelWrap>
        <ReservationCancelTextTitle>예약 취소하기</ReservationCancelTextTitle>
        <CancelPaymentDetailWrap>
          <PaymentDetail
            title={paymentDetail?.exhibitionTitle}
            location={paymentDetail?.exhibitionLocation}
            nickname={paymentDetail?.hostNickname}
            host={paymentDetail?.hostNickname}
            imgUrl={`https:${paymentDetail?.exhibitionPreImg}`}
            // category={dummy.category}
            date={paymentDetail?.exhibitionStockDatetime}
            name={paymentDetail?.userName}
            phoneNumber={paymentDetail?.userEmail}
            job={paymentDetail?.userJobName}
            company={paymentDetail?.userCompanyName}
          />
        </CancelPaymentDetailWrap>
        <InfoCancelWrap>
          <InfoCancelTitle>환불 안내</InfoCancelTitle>
          <CancelWholeWrap>
            <CancelContentsWrap>
              <CancelContentsSpanTitle>결제금액</CancelContentsSpanTitle>
              <CancelContentsSpanTitle>취소 수수료</CancelContentsSpanTitle>
            </CancelContentsWrap>
            <CancelContentsWrap>
              <CancelContentsSpanData>
                {priceRefund?.paymentAmount}</CancelContentsSpanData>
              <CancelContentsSpanData>
                {priceRefund?.feeAmount}</CancelContentsSpanData>
            </CancelContentsWrap>
          </CancelWholeWrap>
          <GreyLine />
          <TotalPayWrap>
            <TotalPayText>최종 환불 금액</TotalPayText>
            <TotalPayText>{priceRefund?.refundAmount}</TotalPayText>
          </TotalPayWrap>
        </InfoCancelWrap>
        <GreyRule />
        <CheckboxWrap>
          <CheckFont>위 환불 금액 및 규정을 모두 확인하였습니다.</CheckFont>
          <Checkbox checked={checked} onChange={handleClick} />
        </CheckboxWrap>
        <CancelButtonWrap>
          <Btn
            text="취소하기"
            disabled={false}
            size="middle"
            state="orange"
            onClick={alertClick}
          />
          {submit && (
            <CancelModal
              isOpen={confirmModalOpen}
              isClose={handleConfirmModalClose}
              // paymentId={paymentId}
              refundPercent={refundPercent}
              id={id}
            />
          )}
          {checkModalOpen && (
            <CheckModal
              isOpen={checkModalOpen}
              isClose={handleCheckModalClose}
            />
          )}
        </CancelButtonWrap>
      </ReservationCancelWrap>
    </>
  );
};

export default ReservationCancelUI;

🍯 localstorage에 있는 accessToken 가져와서 저장하기

const getAccessTokenFromLocalStorage = () =>
    localStorage.getItem('accessToken');

왜 ?
로컬스토리지에 저장된 accessToken으로 해당 회원이 로그인된 상태임을 알 수 있고,
따라서 회원에 맞는 데이터 (결제 데이터 등)을 가져올 수 있기 때문에

🍯 instance 생성

axios instance 생성 & 이유

🍯 1. 예약 취소할 상품에 대한 정보 불러오기 (api)

 const DetailPayment = () => {
    const accessToken = getAccessTokenFromLocalStorage();

    instance
      .get(`/api/v1/payment/refund/${id}/page`, {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
      })
      .then((response) => {
        // console.log(response);
        setPaymentDetail(response.data.data);
        console.log(paymentDetail);
      })
      .catch((error) => {
        console.log(error, '실패하였습니다');
      });
  };

${id} 를 보면, 동적라우팅 을 사용하여 구현하였다.

이전에 만들어 두었던 getAccessTokenFromLocalStorage 함수를 사용하여

accessToken 변수에 저장해주고

instance 를 사용하여 axios get을 해주었다.

헤더에 Authorization: Bearer ${accessToken} 넣어주고 , (Bearer)

 const [paymentDetail, setPaymentDetail] = useState(null);

setPaymentDetail() 를 사용하여 paymentDetail 변수에 원하는 데이터 값을 넣어주었다.

아래와 같이

paymentDetail? : paymentDetail이 있다면
paymentDetail?.exhibitionTitle : paymentDetail의 exhibitionTitle 데이터를 가져온다.

 <CancelPaymentDetailWrap>
          <PaymentDetail
            title={paymentDetail?.exhibitionTitle}
            location={paymentDetail?.exhibitionLocation}
            nickname={paymentDetail?.hostNickname}
            host={paymentDetail?.hostNickname}
            imgUrl={`https:${paymentDetail?.exhibitionPreImg}`}
            // category={dummy.category}
            date={paymentDetail?.exhibitionStockDatetime}
            name={paymentDetail?.userName}
            phoneNumber={paymentDetail?.userEmail}
            job={paymentDetail?.userJobName}
            company={paymentDetail?.userCompanyName}
          />
        </CancelPaymentDetailWrap>

🍯 2. 예약한 예약자에 대한 정보 불러오기 (api)

🍯 3. 결제 금액 데이터 불러오기 (api)

🍯 4. 수수료 데이터 불러오기 (api)

🍯 5. 최종 환불 금액 데이터 불러오기 (api)

는 1번과 동일하게 처리해주면 된다.


🍯 useEffect()

  useEffect(() => {
    RefundPrice();
    RefundPercent();
    DetailPayment();
  }, []);

useEffect 를 사용하여 컴포넌트가 처음 마운트 될 때

  1. RefundPrice: 이 함수는 API를 호출하여 환불 금액에 관한 정보를 가져옵니다.
  2. RefundPercent: 이 함수는 API를 호출하여 환불 금액의 백분율을 가져옵니다.
  3. DetailPayment: 이 함수는 API를 호출하여 결제에 관한 상세 정보를 가져옵니다.

🍯 6. 환불 규정 동의 체크박스 체크 안되어있을 시, 모달창1(체크 확인) 띄우기

체크 모달창 전체 코드

import { useEffect } from 'react';
import {
  Overlay,
  Wrapper,
  Title,
  CancelMessageText,
  CancelMessageBox,
  CheckBtnWrap,
} from '../index.styles';
import Btn from '@/components/common/Btn';
import { CheckModalProps } from '../index.types';

export const CheckModal = ({ isOpen, isClose }: CheckModalProps) => {
  useEffect(() => {
    if (isOpen) document.body.style.overflow = 'hidden';
    else document.body.removeAttribute('style');
  }, [isOpen]);

  return (
    <Overlay $isOpen={isOpen}>
      <Wrapper>
        <Title>예약 약관 동의</Title>

        <CancelMessageBox>
          <CancelMessageText>예약 확인을 체크해주세요.</CancelMessageText>
          <CheckBtnWrap>
            <Btn
              text="확인"
              state="orange"
              disabled={false}
              size="small"
              onClick={isClose}
            />
          </CheckBtnWrap>
        </CancelMessageBox>
      </Wrapper>
    </Overlay>
  );
};

🍯 7. 환불 규정 동의 체크박스 체크 되어있을 시, 모달창2(예약 취소) 띄우기

예약 취소 확인 모달창 전체 코드

import { useEffect, useState } from 'react';
import {
  Overlay,
  Wrapper,
  Title,
  CancelMessageText,
  CancelMessageBox,
  CancelBtnWrap,
} from '../index.styles';
import Btn from '@/components/common/Btn';
import { CancelModalProps } from '../index.types';
import instance from '@/apis/instance';
import { useRouter } from 'next/navigation';
import { CancelConfirmModal } from '../CancelConfirmModal';

export const CancelModal = ({
  isOpen,
  isClose,
  refundPercent,
  id,
}: CancelModalProps) => {
  const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);

  const router = useRouter();

  const handleConfirmModalOpen = () => {
    setConfirmModalOpen(true);
  };

  const handleConfirmModalClose = () => {
    setConfirmModalOpen(false);
  };

  const getAccessTokenFromLocalStorage = () =>
    localStorage.getItem('accessToken');

  const CancelRequest = () => {
    const accessToken = getAccessTokenFromLocalStorage();
    instance
      .post(
        `/api/v1/payment/refund/${id}/user`,
        {},
        {
          // 빈 객체를 요청 데이터로 전달
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      )
      .then((response) => {
        console.log('취소 성공', response);
        router.push('/mypages');
      })
      .catch((error) => {
        console.log(error, '실패하였습니다');
      });
  };

  useEffect(() => {
    if (isOpen) document.body.style.overflow = 'hidden';
    else document.body.removeAttribute('style');
  }, [isOpen]);

  const handleConfirm = () => {
    CancelRequest();
    handleConfirmModalOpen();
    isClose();
  };

  return (
    <Overlay $isOpen={isOpen}>
      <Wrapper>
        <Title>예약 취소하기</Title>

        <CancelMessageBox>
          <CancelMessageText>
            지금 예약을 취소하시면 {refundPercent}% 환불 가능합니다. <br></br>
            예약 취소하시겠습니까?
          </CancelMessageText>

          <CancelBtnWrap>
            <Btn
              text="취소"
              state="white"
              disabled={false}
              size="small"
              onClick={isClose}
            />
            <Btn
              text="확인"
              state="orange"
              disabled={false}
              size="small"
              onClick={handleConfirm}
            />
            {confirmModalOpen && (
              <CancelConfirmModal
                isOpen={confirmModalOpen}
                isClose={handleConfirmModalClose}
              />
            )}
          </CancelBtnWrap>
        </CancelMessageBox>
      </Wrapper>
    </Overlay>
  );
};

확인 버튼을 누르면 handleConfirm 함수가 작동하며

 const handleConfirm = () => {
    CancelRequest();
    handleConfirmModalOpen();
    isClose();
  };
  1. 취소 api 작동
  2. 해당 모달창 닫기
  3. 취소 완료되었다는 모달창 띄우기
    위의 세가지가 수행되도록 하였다.

이렇게 하면, 예약 취소 페이지 구현 완료이다 !

배운점

  1. axios를 사용할 때 post만 정보를 담아서 보낸다고 생각했는데,
headers: {
          Authorization: `Bearer ${accessToken}`,
        },

와 같이 get을 사용할 때에도 헤더에 정보를 담다는 것.

  1. 동적라우팅을 하기 위해서
 //page.tsx
import ReservationCancelUI from '@/components/mypages/reservationcancel';

const ReservationCancelPage = ({
  params: { id },
}: {

  params: { id: number };

}) => <ReservationCancelUI id={id} />;
export default ReservationCancelPage;
//ReservationCancelUI
ReservationCancelUI 에선 ({ id }: { id: number }) 를 사용하면

const ReservationCancelUI = ({ id }: { id: number }) => {
  ... 
}  

params 를 사용하여 변수값을 넘겨준다는 것.

  1. api 통신을 통해 필요한 데이터를 먼저 가지고 오기 위해서 useEffect를 사용한 것.
  2. 페이지 마다 파라미터를 넘겨주고 가져오며 사용한 것

아쉬운 점

모달창을 따로 구현하였는데

재사용하며 사용할 수 있지 않았을까? 하는 아쉬운이 있다.

profile
차곡차곡

0개의 댓글