[React / ts] Book Store 만들기 - ⑩ 주문 처리(주소 검색 구현)
주소를 입력할 때 자주 볼 수 있는 화면이다.
위 창에서 주소를 검색한 뒤 주소를 클릭하면 검색된 주소 정보가 입력된다.
https://postcode.map.daum.net/guide
Daum 우편번호 서비스
우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Daum 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.
postcode.map.daum.net
위 서비스는 다음 링크에서 보다 자세히 안내되어 있으며 무료로 제약없이 사용할 수 있다.
이를 활용하여 주문을 처리하는 페이지를 구현해보자.
// FindAddressButton.tsx
import Button from "../common/Button";
import { useEffect } from "react";
import { SCRIPT_URL } from "../../constants/address";
interface FindAddressButtonProps {
onCompleted: (address: string) => void;
}
const FindAddressButton: React.FC<FindAddressButtonProps> = ({
onCompleted,
}) => {
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="medium" scheme="normal" onClick={handleOpen}>
주소 찾기
</Button>
);
};
export default FindAddressButton;
주소 찾기 기능을 수행하는 FindAddressButton.tsx이다.
버튼을 클릭할 시 handleOpen이 실행되고, new window.daum.Postcode()가 실행된다.
그런데 타입스크립트에서는 daum의 타입이 지정되어있지 않다는 에러가 발생할 것이다.
// /src/window.d.ts
interface Window {
daum: {
Postcode: any;
};
}
그럴 때는 src폴더에 window.d.cs파일을 생성한 뒤 Window라는 interface를 지정해주고 daum, Postcode의 타입을 any로 지정해주면 해결할 수 있다.
이제 window.daum.Postcode()는 주소 검색 라이브러리를 호출하게 된다.
그리고 선택된 주소는 onCompleted()를 통해 부모에게 넘겨주게 된다.
useEffect(() => {
const script = document.createElement("script");
script.src = SCRIPT_URL;
script.async = true;
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
}, []);
useEffect()에서는 SCRIPT_URL(주소 검색 URL)을 <script>태그로 동적으로 추가한다.
그리고 종료될 때 스크립트를 제거하여 필요할 때만 주소 검색 스크립트를 로딩하도록 하였다.
// Order.tsx
import { useLocation, useNavigate } from "react-router-dom";
import Title from "../components/common/Title";
import CartSummary from "../components/cart/CartSummary";
import { CartStyle } from "./Cart";
import Button from "../components/common/Button";
import InputText from "../components/common/InputText";
import { useForm } from "react-hook-form";
import { Delivery, OrderSheet } from "../models/order.model";
import FindAddressButton from "../components/order/FindAddressButton";
import { useAlert } from "../hooks/useAlert";
import { order } from "../api/order.api";
interface DeliveryForm extends Delivery {
addressDetail: string;
}
const Order = () => {
const location = useLocation();
const orderDataFromCart = location.state;
const { totalCounts, totalPrice, firstBookTitle } = orderDataFromCart;
const { showAlert, showConfirm } = useAlert();
const navigate = useNavigate();
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm<DeliveryForm>();
const handlePay = (data: DeliveryForm) => {
const orderData: OrderSheet = {
...orderDataFromCart,
delivery: {
...data,
address: `${data.address} ${data.addressDetail}`,
},
};
console.log(orderData);
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.address && (
<p className="error-text">상세 주소를 입력해주세요</p>
)}
<fieldset>
<label>수령인</label>
<div className="input">
<InputText
inputType="text"
{...register("receiver", { required: true })}
/>
</div>
</fieldset>
{errors.address && (
<p className="error-text">수령인을 입력해주세요</p>
)}
<fieldset>
<label>전화번호</label>
<div className="input">
<InputText
inputType="text"
{...register("contact", { required: true })}
/>
</div>
</fieldset>
{errors.address && (
<p className="error-text">전화번호를 입력해주세요</p>
)}
</form>
</div>
<div className="order-info">
<Title size="medium" color="text">
주문 상품
</Title>
<strong>
{firstBookTitle} 등 {totalCounts}권
</strong>
</div>
</div>
<div className="summary">
<CartSummary totalCounts={totalCounts} totalPrice={totalPrice} />
<Button
size="large"
scheme="primary"
onClick={handleSubmit(handlePay)}
>
결제하기
</Button>
</div>
</CartStyle>
</>
);
};
export default Order;