React

React로 커머스 사이트 만들기 (4): 구매 Modal & 주문완료 페이지

Jinmidnight 2023. 9. 24. 01:16

 

 

GitHub - jinmidnight01/powersell_frontend

Contribute to jinmidnight01/powersell_frontend development by creating an account on GitHub.

github.com

본 내용의 코드는 전부 위 링크에 있으니 참고바랍니다

 


구매 Modal (Modal.js)

 

주문 POST

1. 제어 컴포넌트로 주문 input 객체 생성

...
  // 수량, 주문자명, 지역번호, 전화번호, 우편 번호, 주소, 상세 주소, 비밀번호
  const [inputs, setInputs] = useState({
    quantity: 1,
    name: "",
    preNum: "010",
    phoneNumber: "",
    zipCode: "",
    address: "",
    dongho: "",
    pw: "",
  });

  const { quantity, name, preNum, phoneNumber, zipCode, address, dongho, pw } = inputs;

  // input 객체 생성
  const onChange = (e) => {
    let { value, name } = e.target;
    if (name === "phoneNumber" || name === "pw") {
      value = value.replace(/[^0-9]/g, "");
    }
    setInputs({
      ...inputs,
      [name]: value,
    });
  };
...
return
...
  // 주문자명
  <input
    type="text"
    name="name"
    value={name}
    onChange={onChange}
  />
...

2. input 객체 POST

- input 값 형식 확인 후 주문 POST 진행

- 주문 POST 시 loading 실행

- 주문 성공 시 pw값과 response.data인 successData를 OrderSuccessPage로 넘겨줌과 동시에 이동

- 선착순으로 인한 주문 실패 시 OrderFailPage로 이동

...
  // REST API 1-2: post order
  function handleSubmit(e) {
    e.preventDefault();
    // 전화번호에 '-'가 포함되어 있는지 확인
    const hasDash = phoneNumber.includes("-");
    // 전화번호, 비밀번호 길이 확인
    const flag1 = phoneNumber.length !== 7 && phoneNumber.length !== 8;
    const flag2 = pw.length !== 4;

    if (
      !name ||
      !preNum ||
      !phoneNumber ||
      !zipCode ||
      !address ||
      !dongho ||
      !pw
    ) {
      alert("※ 모든 정보를 입력해주세요.");
      return;
    } else if (hasDash) {
      alert("(-)를 제외한 숫자만 입력해주세요.");
      return;
    } else if (flag1 || flag2) {
      alert("※ 아래 양식을 지켜주세요\n\nㆍ전화번호: 7~8자리ㆍ비밀번호: 4자리");
      return;
    } else {
      const number = preNum + phoneNumber;
      const submitInputs = {
        itemId: props.product.itemId,
        count: quantity,
        name: name,
        number: number,
        zipcode: zipCode,
        address: address,
        dongho: dongho,
        pw: pw,
      };

      // 주문 POST 시 loading 실행
      setClicked(true);
      props.setPosting(true);

      // 주문 POST
      axios
        .post(`${hostURL}/api/orders`, submitInputs)
        .then((response) => {
          document.body.style.overflow = "auto";
          const successData = response.data;
          // 주문 성공시 OrderSuccessPage로 이동
          navigate("/ordersuccess", {
            state: { successData: successData, pw: pw },
          });
        })
        .catch((error) => {
          // 선착순 구매로 인한 주문 실패 시 OrderFailPage로 이동
          if (error.response && error.response.status === 400) {
            navigate("/orderfail");
          } else {
            console.log(error);
          }
        });
    }
  }
...

 

수량 증가/감소 함수

- 주문 수량 증가/감소 버튼 클릭시 작동

...
  // 수량 증가 함수
  function increaseQuantity(e) {
    e.preventDefault();
    if (quantity < 2) {
      setInputs({
        ...inputs,
        quantity: quantity + 1
      });
    }
  }

  // 수량 감소 함수
  function decreaseQuantity(e) {
    e.preventDefault();
    if (quantity > 1) {
      setInputs({
        ...inputs,
        quantity: quantity - 1
      });
    }
  }
...
return
...
  <div className={styles.quantity_selector}>
    // 수량 감소 버튼
    <button
      className={styles.quantity_button}
      onClick={decreaseQuantity}
    >
      -
    </button>

    // 수량
    <span>{quantity}</span>

    // 수량 증가 버튼
    <button
      className={styles.quantity_button}
      onClick={increaseQuantity}
    >
      +
    </button>
  </div>
...

 

주소 찾기 API: DaumPostcode

 

 

- kakao에서 제공하는 DaumPostcode API를 활용

- 주소찾기 버튼을 클릭하면 modalState가 true가 되면서 DaumPostcode가 작동

- DaumPostcode에서 선택한 주소를 input 객체의 address와 zipCode 속성 값으로 부여

# DaumPostcode 라이브러리 설치
npm install react-daum-postcode
import DaumPostcode from "react-daum-postcode";
...
  // 주소 찾기 모달 오픈 여부
  const [modalState, setModalState] = useState("");

  // onCompletePost 함수: address, zipCode 값 설정
  const onCompletePost = (data) => {
    setInputs({
      ...inputs,
      address: data.address,
      zipCode: data.zonecode,
    });
    setModalState(false);
  };
...
return
...
  <label>주소</label>
  // 팝업창 열기
  <button
    type="button"
    onClick={() => {setModalState(true);}}
  >
    주소찾기
  </button>
...
  // DaumPostcode 팝업창
  <div>
    {modalState ? (
      <DaumPostcode
        autoClose={false}
        onComplete={onCompletePost}
        focusInput={true}
      ></DaumPostcode>
    ) : null}
  </div>
...

 

시간에 따라 구매 버튼 실시간 변화

 

- 웹 socket 없이 구매버튼이 OPEN 시간에 맞춰 자동으로 켜지고, CLOSE 시간에 맞춰 자동으로 꺼짐

- input 값을 써놓은 채 EVENT 시간에 맞춰 구매하기 가능

- 정석적인 방법은 아니지만, setInterval 함수를 통해 간단하게 구현해볼 수 있음

...
  // 현재 시간 구하기
  const nowTime = () => {
    const today = new Date();
    const year = today.getFullYear();
    const month = ("0" + (today.getMonth() + 1)).slice(-2);
    const day = ("0" + today.getDate()).slice(-2);
    const hours = ("0" + today.getHours()).slice(-2);
    const minutes = ("0" + today.getMinutes()).slice(-2);
    const seconds = ("0" + today.getSeconds()).slice(-2);

    const dateTimeString =
      year +
      "-" +
      month +
      "-" +
      day +
      " " +
      hours +
      ":" +
      minutes +
      ":" +
      seconds;
    return dateTimeString;
  };

  // props로 받은 product의 startDate, endDate를 state로 관리
  const product = props.product;
  useEffect(() => {
    setStartDate(product.startDate);
    setEndDate(product.endDate);

    // 현재 시간이 바뀔 때마다 openStatus, closeStatus를 업데이트
    setInterval(() => {
      if (startDate === "") return;
      setOpenStatus(startDate < nowTime());
      if (endDate === "") return;
      setCloseStatus(endDate < nowTime());
    }, 1);
    setIsLoading(false);
  }, [props.product.itemId, product.startDate, product.endDate, startDate, endDate]);
...
return
...
  {isloading ? (
    <div style={{ display: "flex", justifyContent: "center" }}>
      <img
        style={{ margin: "37px 0" }}
        src={spinner}
        alt="로딩 중..."
        width="10%"
      />
    </div>
  ) : (
    // 로딩 종료 후 버튼 세팅
    // 마감 이후, 오픈 이후 및 마감 전, 오픈 전으로 나누어 세팅
    <input
      onClick={handleSubmit}
      className={`${styles.submit_button} ${closeStatus || !openStatus ? styles.negative_button : "" }`}
      type="submit"
      value={
        closeStatus
          ? "오픈 준비 중입니다"
          : openStatus
            ? "구매하기"
            : startDate.slice(5, 7) +
            "월 " +
            startDate.slice(8, 10) +
            "일 " +
            startDate.slice(11, 13) +
            "시 " +
            startDate.slice(14, 16) +
            "분 OPEN"
      }
      disabled={closeStatus || !openStatus}
    />
  )}
...

 


주문완료 페이지 (OrderSuccessPage.js)

 

useLocation을 통해 response.data 받기

- useEffect를 통해 최초 렌더링 시에만 작동

- useState를 통해 successData 상태를 전달받은 주문정보로 set

...
  const location = useLocation();
  const navigate = useNavigate();
  const [successData, setSuccessData] = useState(null);
  const [pw, setPw] = useState(null);

  // response data
  useEffect(() => {
    // prevent direct access
    if (location.state === null) {
      navigate("/");
    } else {
      setSuccessData(location.state.successData);
      setPw(location.state.pw);
    }
  }, [location, navigate]);
...

 

클립보드 복사 API: CopyToClipboard

- CopyToClipboard API를 통해 계좌번호 클릭시 복사

# CopyToClipboard 라이브러리 설치
npm install react-copy-to-clipboard
import { CopyToClipboard } from "react-copy-to-clipboard";
...
return
...
  <span>송금계좌</span>:{" "}
  <CopyToClipboard
    text="3333277508505"
    onCopy={() => alert("계좌가 복사되었습니다")}
  >
    <span>
      <span
        style={{
          textDecoration: "underline",
        }}
      >
        카카오뱅크 3333277508505
      </span>
      <img
        src={copyText}
        alt="copy"
        width={14}
        style={{ cursor: "pointer", marginLeft: "7px" }}
      />
    </span>
  </CopyToClipboard>
...

 

주문 정보 출력

- successData 객체의 속성 값 활용

return
...
  <ul>
    <li>
      <span>주문일시</span>:{" "}
      {successData.orderDate.replace("T", " ").slice(0, 24)}
    </li>
    <li>
      <span>주문자</span>: {successData.name}
    </li>
    <li>
      <span>전화번호</span>: {successData.number}
    </li>
    <li>
      <span>주소</span>: {successData.address} {successData.dongho}
    </li>
    <li>
      <span>비밀번호</span>: {pw.slice(0, 2)}xx
    </li>
  </ul>
...