데브코스

[15주차 - DAY3] 도서 정보 사이트 - 주문서

미안하다 강림이 좀 늦었다 2024. 6. 5. 14:46

 

 

주문서 작성

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 항목 존재 유무를 확인하여 백엔드로 불필요한 요청을 보내지 않을 수 있다.