EtoC
OrderPage 만들기 본문
작업을 시작하기에 앞서 어제 발생한 map함수에대한 걱정이 실화가 되었다.
그저 웃음만 나온다..허허허
이번에 들어오는 데이터는 아래와 같다.
{
"data": [
{
"id": "363",
"sub_category_id": "600*1200*20",
"surface_type_id": "HARD MATT",
"name": "P_1_CTN_01",
"weight": "30",
"type": "천장용",
"price": "10000.00",
"sell_counts": "5",
"image_url": "images/6.jpg"
},
{
"id": "2",
"sub_category_id": 2,
"surface_type_id": 2,
"name": "SDH211",
"weight": "60",
"price": "30000.00",
"sell_counts": "3",
"image_url": "images/11.jpg"
},
{
"id": "3",
"sub_category_id": 3,
"surface_type_id": 3,
"name": "SDdasdsa",
"weight": "90",
"price": "300000.00",
"sell_counts": "7",
"image_url": "images/11.jpg"
}
]
}
{
"user": [
{
"id": 123,
"name": "김최이서",
"email": "asdf@naver.com",
"phoneNumber": "0104032321",
"payWay": "Point",
"receiveMan": "배송받을 사람",
"deliveryLocation": "서울시 송파구 어쩌구 저쩌구",
"deliveryRequest": "경비실에 맡겨주세요"
}
]
}
제품상세페이지를 만들때 백엔드와 더 소통해야겠다고 느꼈는데 이번에는 소통을 너무 많이했다.
처음에 구상한 주문페이지는 아래의 이미지와 같았는데 백엔드 한분과 논의하며 바꾸다보니
간단했던 주문페이지가
장바구니 기능이 포함된 주문페이지로 완성되었다.
버튼 컴포넌트를 분리하여 재사용하였고 총무게에대한 버튼 기능 제한과 알림문까지 나타나게한것과 사용자가 주문전에 한번더 체크할 수 있도록 구현했다며 굉장히 뿌듯해했다.
물건을 다 취소했을 경우에는 물품이 없다는 표시를 보여주도록까지 구현을 했는데
멘토님의 장바구니와 기능이 겹치는점 유저에게 너무 관대한점으로인해 코드는 장바구니를 담당하는팀원에게 드렸다.
결국 완성된건 아래처럼 get요청으로 데이터를 받아온것을 띄워서 최종 확인을 하게하는것과
주문정보를 post 요청하는 페이지로 작성하였다.
완성된 주문 페이지
어려웠던 점
1. 버튼 컴포넌트가 이상하게 작동했던점
처음에 컴포넌트를 분리해서 사용했을때는 잘 작동하는것을 확인했다.
그런데 바뀌는 숫자값에따라 표시되는 무게와 가격이 달라지고 조건에따라 disabled를 주면서 이상하게 작동했다.
처음에는 NaN이 그리고 그뒤에 1이 그리고 1에 1을더해서 2가 그뒤로는 1111이찍혔다.
이전의 코드를 찍지못해서 정확하지는 않은데 복합적인 문제였다.
당시 사용자가 버튼으로만 수량을 조절하지않고 숫자를 직접 입력하게하고싶어 문자열로 만들어서 NaN이떴었고,
Number로 감싸지 않아서 숫자들이 문자열로 더해졌다.
외부에서 countNumber, setCount, isDisabled를 받아오기만하고 내부적으로 상태를 관리하지않았다.
그래서 버튼컴포넌트를 불러왔을때 상태가 변동되지않아 무게와 가격이 변동되지않았다.
useEffect와 useState를 제대로 알지못해서 팀원도 고생하고 나도 부끄러웠다..
2. grid 와 flex
생각했던것은 이미지 똑같은 높이와 넓이로 옆에 2열 3행으로 제품의 정보를 보여주기였다.
css를 공부하다 grid를 알게되서 한번 사용해봤는데 들어오는 값에따라 크기가 달라져서 원하는 모양이 나오지 않았다.
지금 생각해보면 각 열마다 사이즈를 다르게 주지않고 단순히 1fr을 반복해서 여러개 만들게 했으니 안되는거였던거같다.
결국 flex로 화면을 구성했는데 제대로 쓸 줄 알았다면 두번 작업하지 않았을거같아 아쉽다.
전체 코드
//order.js
import React, { useState, useEffect } from "react";
import "./Order.scss";
import Count from "../../components/Count/Count";
import { Link, useNavigate } from "react-router-dom";
const Order = () => {
const [items, setItems] = useState([]);
const [originalItems, setOriginalItems] = useState([]);
const [users, setUsers] = useState([]);
const [weights, setTotalWeight] = useState(0);
const [prices, setTotalPrice] = useState(0);
const navigate = useNavigate();
//카트에 GET, PATCH
useEffect(() => {
fetch("./data/order.json")
.then((res) => res.json())
.then((data) => {
setItems(data.data);
setOriginalItems(data.data);
setTotalWeight(calculateTotalWeight(data.data));
setTotalPrice(calculateTotalPrice(data.data));
});
}, []);
useEffect(() => {
fetch("./data/user.json")
.then((res) => res.json())
.then((data) => setUsers(data.user));
}, []);
useEffect(() => {
if (items.length === 0) {
setTotalWeight(0);
setTotalPrice(0);
}
}, [items]);
const setCountArray = (itemId, count) => {
setItems((prevItems) => {
const updatedItems = prevItems.map((item) => {
if (item.id === itemId) {
const originalItem = originalItems.find((i) => i.id === itemId);
const updatedItem = { ...item, count: count };
updatedItem.weight = originalItem.weight * count;
updatedItem.price = Number(originalItem.price) * count;
return updatedItem;
}
return item;
});
setTotalWeight(calculateTotalWeight(updatedItems));
setTotalPrice(calculateTotalPrice(updatedItems));
return updatedItems;
});
};
const calculateTotalWeight = (items) => {
return items.reduce((total, item) => total + Number(item.weight), 0);
};
const calculateTotalPrice = (items) => {
return items.reduce((total, item) => total + Number(item.price), 0);
};
const [inputValues, setInputValues] = useState({});
const handleInputValue = (e) => {
const { name, value } = e.target;
setInputValues((prevValues) => ({
...prevValues,
[name]: value,
}));
};
const totalWeight = weights;
const showAlert = totalWeight > 1000;
const onRemove = (itemId) => {
setItems((prevItems) => {
const itemsFilter = prevItems.filter((data) => data.id !== itemId);
setTotalWeight(calculateTotalWeight(itemsFilter));
setTotalPrice(calculateTotalPrice(itemsFilter));
return itemsFilter;
});
};
//order에 POST
const postProduct = () => {
fetch("api/oder/주문번호", {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({
address: inputValues.address,
total_price: prices,
total_weight: weights,
}),
})
.then((res) => {
return res.json();
})
.then((data) => {
if (data.message === "CREATE_USER_SUCCESS!") {
navigate("/orderResult");
} else if (data.message === "백엔드 메세지") {
alert("구매에 실패하였습니다.");
}
});
};
return (
<div className="order">
{users.map((el) => (
<div className="buyerInfo" key={el.id}>
<h1>주문자 정보</h1>
<p className="userInfo"> {el.name} </p>
<div className="shippingAddress">
<h2>주문을 어디로 보내시겠습니까?</h2>
<div className="orderTypeName"> 이름</div>
<input
className="name input"
name="name"
placeholder="받으실 분 성함을 적어주세요"
onChange={handleInputValue}
/>
<div className="orderTypeName">주소</div>
<input
className="address input"
placeholder="배송 상세주소를 적어주세요"
name="address"
onChange={handleInputValue}
/>
<div className="orderTypeName">배송 요청</div>
<input
className="detailAddress input"
placeholder="경비실에 맡겨주세요"
name="memo"
onChange={handleInputValue}
/>
</div>
</div>
))}
<div className="perchaseBox">
<div className="payBox">
<div className="indexBox">
<p className="resultText"> 최종가격</p>
<p className="resultText"> 총 무게</p>
</div>
<div className="final">
<p className="resultText">{prices} 원</p>
<p className="resultText">{weights} KG</p>
</div>
</div>
{items.length === 0 && (
<div className="orderEmpty">
<p>구매할 물품이 없습니다.</p>
</div>
)}
{items.map((el) => (
<div className="total" key={el.id}>
<div className="item">
<img
src={el.image_url}
alt="perchaseProduct"
className="perchaseImage"
/>
<div className="perchaseOption">
<p className="descriptionOption">{el.name}</p>
<p className="descriptionOption">{el.surface_type_id}</p>
<p className="descriptionOption">{el.sub_category_id}</p>
</div>
<div className="perchaseOption">
<Count
className="count"
count={el.sell_counts}
setCount={(newCount) => setCountArray(el.id, newCount)}
isDisabled={showAlert}
/>
<p className="descriptionOption">{el.weight} KG</p>
<p className="descriptionOption">{Number(el.price)} 원</p>
</div>
<div className="deleteBox">
<button
type="submit"
className="deleteButton"
onClick={() => onRemove(el.id)}
>
✕
</button>
</div>
</div>
</div>
))}
<div className="buttonBox">
<button
type="submit"
className="payment"
onClick={() => {
navigate("/orderResult");
}}
disabled={showAlert}
>
결제하기
</button>
{items.length === 0 && (
<button
className="gotoHome"
onClick={() => {
navigate("/");
}}
>
홈으로
</button>
)}
{showAlert && (
<div className="alertTextBox">
<p className="alertText">
무게는 1000Kg 이상 구매하실 수 없습니다.
</p>
</div>
)}
</div>
</div>
</div>
);
};
export default Order;
//order.scss
.order {
padding-top: 5em;
display: flex;
justify-content: space-between;
height: 100vh;
.buyerInfo {
padding: 3em 5em;
h1 {
margin-bottom: 1em;
font-size: 1.7em;
}
.userInfo {
font-size: 1.3em;
margin-bottom: 0.5em;
}
.shippingAddress {
display: flex;
flex-direction: column;
padding-top: 4em;
width: 40em;
h2 {
font-size: 1.7em;
margin-bottom: 1.5em;
}
.orderTypeName {
margin-top: 1em;
color: #9888;
}
.input {
font-size: 1em;
width: 100%;
height: 4em;
border: none;
border-bottom: 1px solid rgb(180, 180, 180);
background-color: transparent;
&:focus {
outline: none;
}
}
}
}
.perchaseBox {
width: 50%;
height: 100vh;
padding: 3em;
background-color: rgb(246, 239, 223);
.payBox {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-right: 1em;
padding-bottom: 1em;
border-bottom: 1px solid grey;
.resultText {
font-size: 1.3em;
padding-bottom: 0.7em;
}
.final {
text-align: end;
}
}
.orderEmpty {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
margin: 0 auto;
height: 20em;
font-size: 1em;
}
.total {
.indexBox {
border-bottom: 1px solid grey;
}
.item {
display: flex;
flex-direction: row;
justify-content: space-between;
border-bottom: 1px solid rgb(180, 180, 180);
padding: 1em 0;
.perchaseImage {
width: 5.4em;
}
}
.perchaseOption {
display: grid;
grid-template-rows: auto;
text-align: center;
align-items: center;
width: 1500px;
margin: 0 auto;
.count {
margin: 0;
align-items: center;
justify-content: center;
}
}
.deleteBox {
.deleteButton {
font-size: 1em;
color: #333;
background-color: transparent;
border: none;
}
}
}
.buttonBox {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1em;
align-items: center;
.alertTextBox {
.alertText {
margin-top: 2em;
color: #ff0000;
}
}
.payment {
background-color: #333;
border: 0;
color: white;
margin-top: 3em;
width: 20em;
height: 4em;
position: relative;
}
.gotoHome {
background-color: #333;
border: 0;
color: white;
margin-top: 3em;
width: 20em;
height: 4em;
position: absolute;
z-index: 1;
}
}
}
}
//count.js
import React, { useState, useEffect } from "react";
import "./Count.scss";
const Count = ({ countNumber, setCount, isDisabled }) => {
const [count, setInternalCount] = useState(Number(countNumber) || 1);
useEffect(() => {
setInternalCount(Number(countNumber) || 1);
}, [countNumber]);
const decrease = () => {
if (count <= 1) {
return;
} else {
const newCount = count - 1;
setInternalCount(newCount);
setCount(newCount);
}
};
const increase = () => {
const newCount = count + 1;
setInternalCount(newCount);
setCount(newCount);
};
return (
<div className="count">
<div className="countInput">
<button onClick={decrease}>-</button>
<div className="countInputText">{count}</div>
<button onClick={increase} disabled={isDisabled}>
+
</button>
</div>
</div>
);
};
export default Count;
//count.scss
.count {
display: flex;
height: 3em;
gap: 10px;
justify-content: flex-start;
margin: 1.5em 0;
&Input {
display: flex;
width: fit-content;
height: 100%;
padding: 8px;
border-radius: 0.5em;
gap: 8px;
button {
border: none;
background-color: transparent;
cursor: pointer;
font-size: 1.3em;
}
&Text {
width: 4em;
height: 2em;
border-width: 0 1px;
text-align: center;
padding-top: 0.5em;
border: 1px solid rgb(199, 199, 199);
border-radius: 0.5em;
}
}
.resetBtn {
width: 48px;
height: 100%;
border: 1px solid black;
border-radius: 8px;
background-color: transparent;
cursor: pointer;
}
}
2023-07-04