React

React로 커머스 사이트 만들기 (6): 관리자 페이지(1)

Jinmidnight 2023. 9. 24. 03:37
 

GitHub - jinmidnight01/powersell_frontend

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

github.com

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

 


로그인 페이지 (LoginPage.js)

 

로그인 POST

- URL에 /admin 입력시 자동으로 /login의 LoginPage로 이동

- ID와 PW가 승인될 경우 response.status 값 200과 함께 AdminPage로 이동

- 제어 컴포넌트로 로그인 객체 생성 및 POST

...
  // ID, PW
  const [inputs, setInputs] = useState({
    id: "",
    password: "",
  });

  const { id, password } = inputs;

  // input 객체 생성
  const onChange = (e) => {
    const { value, name } = e.target;
    setInputs({
      ...inputs,
      [name]: value,
    });
  };

  // REST API 4-1: login
  const handleClick = (e) => {
    e.preventDefault();

    axios
    .post(`${hostURL}/api/check`, inputs)
    .then((response) => {
      // 로그인 승인이 됐을 경우
      if (response.data.result === 'True') {
        navigator("/admin", { state: response.status });
      }
      // 로그인 승인이 안 됐을 경우
      else {
        alert("아이디 또는 비밀번호가 일치하지 않습니다.");
      }
    })
    .catch((error) => {
      console.log(error);
    });
  };
...

 


관리자 페이지: 주문 목록 (AdminPage.js, OrderListPage.js)

 

AdminPage.js

- useLocation을 통해 response.status 값을 받아와서 한 번 더 로그인 승인 확인

- 상단 헤더의 토글 버튼으로 주문목록 페이지와 상품/후기 목록 페이지 사이를 전환

- 하위 컴포넌트 OrderListPage와 ProductListPage는 toggle status에 따라 출력

...
  const [title, setTitle] = useState("주문 목록");
  const navigate = useNavigate();
  const location = useLocation();

  // prevent direct access
  useEffect(() => {
    // 로그인 정보가 틀릴 경우 로그인 페이지로 이동
    if (location.state !== 200) {
      navigate("/login");
    }
  }, [location.state, navigate]);
...
return
...
  {/* toggle button */}
  <label className={styles.switch}>
    <input type="checkbox" onClick={() => { title === "주문 목록" ? setTitle("상품/후기 목록") : setTitle("주문 목록") }}></input>
    <span className={styles.slider} id={styles.round}></span>
  </label>
...
  {/* Order List Page */}
  <div style={title === "주문 목록" ? { display: "block" } : { display: "none" }}>
    <OrderListPage status={location.state} />
  </div>

  {/* Product List Page */}
  <div style={title === "상품/후기 목록" ? { display: "block" } : { display: "none" }}>
    <ProductListPage status={location.state} />
  </div>
...

 

전 주문 목록 GET

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

- useState를 통해 result 상태를 GET한 전 주문 목록으로 set

- useState를 통해 전 주문정보를 GET할 경우 loading 상태 false로 변경

- reloadFlag 값이 dependency로서 변할 경우 전 주문 목록을 다시 불러옴 (배송 상태 수정 시 활용)

...
  // REST API 1-1: get all orders
  useEffect(() => {
    if (props.status === 200) {
      axios
        .get(`${hostURL}/api/admin/orders`)
        .then((response) => {
          setResult(response.data);
          setIsLoading(false);
        })
        .catch((error) => {
          console.log(error);
        });
    }
  }, [props.status, reloadFlag]);
...

 

주문 목록 검색

- 주문 목록을 필터할 기준들을 담고 있는 객체 생성

- filter된 주문 목록을 mapping하여 나열

- mapping할 때 key는 orderId로 설정

...
  // 회차, 배송상태, 상품명, 상품개수, 주문자명, 전화번호
  const [inputs, setInputs] = useState({
    testOrderSelected: "전체",
    statusSelected: "배송상태",
    productSelected: "상품명",
    countSelected: "상품개수",
    searchName: "",
    searchNumber: "",
  });

  const { testOrderSelected, statusSelected, productSelected, countSelected, searchName, searchNumber } = inputs;
  
  // input 객체 생성
  const onChange = (e) => {
    const { value, name } = e.target;
    setInputs({
      ...inputs,
      [name]: value,
    });
  };
...
  // filtered result
  const filteredResult = [...result]
    // 최신 순 정렬
    .sort((a, b) => {
      if (a.orderDate > b.orderDate) return -1;
      if (a.orderDate < b.orderDate) return 1;
      return 0;
    })
    // 회차 기준
    .filter((order) => {
      switch (testOrderSelected) {
        case "4회차":
          return (
            order.orderDate >= "2023-09-18T20:59:00" &&
            order.orderDate <= "2023-09-24T23:59:59"
          );
        case "3회차":
          return (
            order.orderDate >= "2023-09-11T20:59:00" &&
            order.orderDate <= "2023-09-17T23:59:59"
          );
        case "2회차":
          return (
            order.orderDate >= "2023-09-04T20:59:00" &&
            order.orderDate <= "2023-09-10T23:59:59"
          );
        case "1회차":
          return (
            order.orderDate >= "2023-08-28T20:59:00" &&
            order.orderDate <= "2023-09-03T23:59:59"
          );
        default:
          return true;
      }
    })
    // 배송상태 기준
    .filter((order) => {
      if (statusSelected === "배송상태") {
        return true;
      }
      return order.orderStatus === filterDict.statusDict[statusSelected];
    })
    // 상품명 기준
    .filter((order) => {
      if (productSelected === "상품명") {
        return true;
      }
      return order.item.name === filterDict.productDict[productSelected];
    })
    // 상품개수 기준
    .filter((order) => {
      if (countSelected === "상품개수") {
        return true;
      }
      return order.count === filterDict.countDict[countSelected];
    })
    // 주문자명 검색
    .filter((order) => {
      if (searchName === "") {
        return true;
      }
      return order.name
        .toString()
        .replace(" ", "")
        .toLocaleLowerCase()
        .includes(searchName.toLocaleLowerCase().replace(" ", ""));
    })
    // 주문자 전화번호 검색
    .filter((order) => {
      if (searchNumber === "") {
        return true;
      }
      return order.number
        .toString()
        .replace(" ", "")
        .toLocaleLowerCase()
        .includes(searchNumber.toLocaleLowerCase().replace(" ", ""));
    });
...
  return (
    <div className={styles.order_main}>
      {/* 필터 목록 */}
      <div className={styles.firstSelectBox}>

        {/* 회차 */}
        <select onChange={onChange} name="testOrderSelected" value={testOrderSelected}>
          {Object.keys(filterDict.testOrderDict).map((testOrder) => (
            <option value={testOrder} key={testOrder}>
              {testOrder}
            </option>
          ))}
        </select>

        {/* 배송상태 */}
        <select onChange={onChange} name="statusSelected" value={statusSelected}>
          {Object.keys(filterDict.statusDict).map((status) => (
            <option value={status} key={status}>
              {status}
            </option>
          ))}
        </select>
...
        {/* 검색 결과 */}
        {filteredResult.map((order) => (...))}
...

 

주문 목록 요약

- 주문 개수, 상품 개수, 비용, 매출에 관한 정보를 요약하여 출력

- 세 자리마다 쉼표 표시

...
  // product count
  let productCount = 0;
  for (let i = 0; i < filteredResult.length; i++) {
    productCount += filteredResult[i].count;
  }

  // total cost
  let cost = 0;
  for (let i = 0; i < filteredResult.length; i++) {
    cost += filteredResult[i].item.originalPrice * filteredResult[i].count;
  }

  // total revenue
  let revenue = 0;
  for (let i = 0; i < filteredResult.length; i++) {
    revenue += filteredResult[i].orderPrice;
  }
...
return
...
  {/* order & payment count */}
  <div className={styles.orderCount}>
    <span>
      주문: {filteredResult.length}개 / 상품: {productCount}개
    </span>
  </div>
  <div className={styles.paymentCount}>
    <span>
      비용: {cost.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')}원 / 매출: {revenue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')}원
    </span>
  </div>
...

 

배송 상태 PATCH

- props를 orderId, orderStatus, reloadFlag, setReloadFlag로 지닌 하위 컴포넌트 StatusButton 생성

- orderStatus에 따라 배송 상태 버튼 style을 다르게 설정

// OrderListPage.js
return
...
  {/* delivery */}
  <StatusButton
    orderId={order.orderId}
    orderStatus={order.orderStatus}
    reloadFlag={reloadFlag}
    setReloadFlag={setReloadFlag}
  />
...
// StatusButton.js
...
  const { orderId, orderStatus, reloadFlag, setReloadFlag } = props;
...
  // intial button style
  const initialStyle = {
    WAITING: ButtonStyle,
    DELIVERING: ButtonStyle,
    ARRIVED: ButtonStyle,
    CANCELED: ButtonStyle
  };

  switch (orderStatus) {
    case "WAITING":
      initialStyle.WAITING = clickedButtonStyle;
      break;
    case "DELIVERING":
      initialStyle.DELIVERING = clickedButtonStyle;
      break;
    case "ARRIVED":
      initialStyle.ARRIVED = clickedButtonStyle;
      break;
    case "CANCELED":
      initialStyle.CANCELED = clickedButtonStyle;
      break;
    default:
      break;
  }

  const [styleInput, setStyleInput] = useState(initialStyle);
  const { WAITING, DELIVERING, ARRIVED, CANCELED } = styleInput;
...

- 다른 배송 상태 버튼 클릭 시 배송 상태 변경

- 이에 따라 모든 배송 상태 버튼들의 style도 같이 변동

- reloadFlag 값이 변함에 따라 전 주문 목록 reload

// StatusButton.js
...
  // REST API 1-4: change order status
  const onClick = (e) => {
    // 버튼 Style 초기화
    setStyleInput({
      WAITING: ButtonStyle,
      DELIVERING: ButtonStyle,
      ARRIVED: ButtonStyle,
      CANCELED: ButtonStyle
    });

    // 배송 상태 수정
    axios
    .patch(`${hostURL}/api/admin/orders/${orderId}`, { status: e.target.value })
    .then((response) => {
      // 클릭된 배송 상태만 Style 변동
      setStyleInput((prev) => ({...prev, [e.target.value]: clickedButtonStyle}));
      // reloadFlag가 변함에 따라 전 주문 목록 reload
      reloadFlag === 0 ? setReloadFlag(1) : setReloadFlag(0);
    })
    .catch((error) => {
      console.log(error);
    });
  };

  return (
    <div style={orderDelivery}>
      <button
        onClick={onClick}
        style={WAITING}
        value="WAITING"
      >
        입금대기
      </button>
      <button
        onClick={onClick}
        style={DELIVERING}
        value="DELIVERING"
      >
        배송중
      </button>
      <button
        onClick={onClick}
        style={ARRIVED}
        value="ARRIVED"
      >
        배송완료
      </button>
      <button
        onClick={onClick}
        style={CANCELED}
        value="CANCELED"
      >
        주문취소
      </button>
    </div>
  );
...