주문서 작성
api/order.api.ts
주문 요청을 보내는 함수를 다음과 같이 정의한다.
export const order = async (orderData: OrderSheet) => {
const response = await httpClient.post('/orders', orderData);
return response.data;
}
components/order/FindAddressButton.tsx
주소를 찾는 버튼이다.
daum의 api를 사용하며, onCompleted로 전달되는 함수는 주소 InputText에 주소를 찾은 결과를 set 하는 함수이다.
import Button from "../common/Button";
import { useEffect } from "react";
interface Props {
onCompleted: (address: string) => void;
}
const SCRIPT_URL = '//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js';
function FindAddressButton({ onCompleted }: Props) {
const handleOpen = () => {
new window.daum.Postcode({
oncomplete: (data: any) => {
onCompleted(data.address as string);
}
}).open();
}
useEffect(() => {
const script = document.createElement('script');
script.src = SCRIPT_URL;
script.async = true;
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
}
}, [])
return (
<Button
type="button"
size="small"
scheme="normal"
onClick={handleOpen}
>
주소 찾기
</Button>
)
}
export default FindAddressButton;
window.d.ts
타입스크립트가 고려되지 않은 라이브러리를 사용할 때는 window.d.ts 파일을 만들어서 타입을 정의할 수 있다.
interface Window {
daum: {
Postcode: any;
};
}
pages/Order.tsx
location을 이용해서 이전 페이지인 장바구니 페이지로부터 체크된 아이템 아이디들, 총 가격, 총 수량, 첫 번째 책 제목을 받는다. 주문하기 버튼이 눌리면 alert 창을 띄우고, 백엔드 단으로 주문 요청을 보낸다.
...
interface DeliveryForm extends Delivery {
addressDetail: string;
}
function Order() {
const { showAlert, showConfirm } = useAlert();
const navigate = useNavigate();
const location = useLocation();
const orderDataFromCart = location.state;
const { totalQuantity, totalPrice, firstBookTitle } = orderDataFromCart;
const {
register,
handleSubmit,
setValue,
formState: { errors }
} = useForm<DeliveryForm>();
const handlePay = (data: DeliveryForm) => {
const orderData: OrderSheet = {
...orderDataFromCart,
delivery: {
...data,
address: `${data.address} ${data.addressDetail}`
}
}
showConfirm('주문을 진행하시겠습니까?', () => {
order(orderData).then(() => {
showAlert('주문이 처리되었습니다.');
navigate('/orderList');
})
})
}
return (
<>
<Title size="large">주문서 작성</Title>
<CartStyle>
<div className="content">
<div className="order-info">
<Title size="medium" color="text">
배송 정보
</Title>
<form className="delivery">
<fieldset>
<label>주소</label>
<div className="input">
<InputText
inputType="text"
{...register('address', { required: true })}
/>
</div>
<FindAddressButton
onCompleted={(address) => { setValue('address', address) }}
/>
</fieldset>
{
errors.address &&
<p className="error-text">
주소를 입력해 주세요.
</p>
}
<fieldset>
<label>상세 주소</label>
<div className="input">
<InputText
inputType="text"
{...register('addressDetail', { required: true })}
/>
</div>
</fieldset>
{
errors.addressDetail &&
<p className="error-text">
상세 주소를 입력해 주세요.
</p>
}
<fieldset>
<label>수령인</label>
<div className="input">
<InputText
inputType="text"
{...register('receiver', { required: true })}
/>
</div>
</fieldset>
{
errors.receiver &&
<p className="error-text">
수령인을 입력해 주세요.
</p>
}
<fieldset>
<label>전화번호</label>
<div className="input">
<InputText
inputType="text"
{...register('contact', { required: true })}
/>
</div>
</fieldset>
{
errors.contact &&
<p className="error-text">
전화번호를 입력해 주세요.
</p>
}
</form>
</div>
<div className="order-info">
<Title size="medium" color="text">
주문 상품
</Title>
<strong>
{firstBookTitle} 등 총 {totalQuantity} 권
</strong>
</div>
</div>
<div className="summary">
<CartSummary
totalQuantity={totalQuantity}
totalPrice={totalPrice}
/>
<Button
size="large"
scheme="primary"
onClick={handleSubmit(handlePay)}
>
결제하기
</Button>
</div>
</CartStyle>
</>
)
}
export default Order;
주문 내역
api/order.api.ts
주문 내역을 요청하는 함수와 주문 상세 내역을 요청하는 함수를 정의한다.
export const fetchOrders = async () => {
const response = await httpClient.get<Order[]>('/orders');
return response.data;
}
export const fetchOrder = async (orderId: number) => {
const response = await httpClient.get<OrderDetailItem[]>(`/orders/${orderId}`);
return response.data;
}
hooks/useOrder.ts
기본적으로 주문 내역들은 모두 받아온다.
주문 내역 상세 보기가 클릭되면 selectOrderItem이 실행되는데, 원래 각 주문 내역에는 detail 항목이 없다. detail이 없으면 주문 상세 내역을 백엔드로 요청해서 detail을 추가하고, detail이 있는 경우에는 다시 백엔드로 요청을 보내지 않아도 되므로 요청을 방어한다.
...
export const useOrders = () => {
const [orders, setOrders] = useState<OrderListItem[]>([]);
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
useEffect(() => {
fetchOrders().then((orders) => {
setOrders(orders);
});
}, []);
const selectOrderItem = (orderId: number) => {
if (orders.filter((item) => item.id === orderId)[0].detail) {
setSelectedItemId(orderId);
return;
}
fetchOrder(orderId).then((orderDetail) => {
setSelectedItemId(orderId);
setOrders(
orders.map((item) => {
if (item.id === orderId) {
return {
...item,
detail: orderDetail
}
}
return item;
})
);
});
}
return { orders, selectedItemId, selectOrderItem };
}
pages/OrderList.tsx
모든 주문 내역을 보여주는 페이지다.
selectedItemId는 자세히 보기가 눌린 주문 내역의 id이므로 selectedItemId와 주문 id가 일치하면 주문 상세 내역을 보여준다.
...
function OrderList() {
const { orders, selectedItemId, selectOrderItem } = useOrders();
return (
<div>
<Title size="large">주문 내역</Title>
<OrderListStyle>
<table>
<thead>
<tr>
...
</tr>
</thead>
<tbody>
{
orders.map((order) => (
<React.Fragment key={order.id}>
<tr>
...
</tr>
{
selectedItemId === order.id &&
<tr>
<td></td>
<td colSpan={8}>
<ul className="detail">
{
order?.detail &&
order.detail.map((item) => (
<li key={item.bookId}>
<div>
<span>{item.bookId}</span>
<span>{item.author}</span>
<span>{formatNumber(item.price)} 원</span>
</div>
</li>
))
}
</ul>
</td>
</tr>
}
</React.Fragment>
))
}
</tbody>
</table>
</OrderListStyle>
</div >
)
}
...
배운 점
- 타입스크립트가 고려되지 않은 라이브러리를 사용할 때는 window.d.ts 파일을 만들어서 타입을 정의할 수 있다.
- <></>에 키를 넣으려면 React.Fragment 태그를 사용하면 된다.
- 각 주문 내역의 detail 항목 존재 유무를 확인하여 백엔드로 불필요한 요청을 보내지 않을 수 있다.
'데브코스' 카테고리의 다른 글
[15주차 - DAY5] 도서 정보 사이트 - 리팩토링 (0) | 2024.06.07 |
---|---|
[15주차 주간 발표] Flex, Grid (0) | 2024.06.05 |
[15주차 - DAY2] 도서 정보 사이트 - 장바구니 목록 (0) | 2024.06.04 |
[15주차 - DAY1] 도서 정보 사이트 - 도서 상세 페이지 (0) | 2024.06.03 |
[14주차 - DAY5] 도서 정보 사이트 - 로그인과 도서 목록 페이지 (0) | 2024.05.31 |