1. 기능
🎯 어떤 순서로 기능을 만들 것인지는 비즈니스 우선 순위에 따라 결정
2. 화면
상품 상세 페이지 - /products/{id}
회원 가입 완료 페이지 - /signup/complete
주문 완료 페이지 - /order/complete
3. 주문 페이지
🎯 주문 페이지와 주문 완료 페이지를 추가해서 전체 흐름을 만들기
OrderPage
생성
로직 자체는 장바구니와 매우 유사함
OrderForm
컴포넌트 생성
역시 장바구니와 유사
주소에 /order
를 강제로 입력해서 들어오는 것은 고려하지 않음
const Container = styled.div`
h2 {
font-size: 4rem;
}
`;
type OrderFormProps = {
cart: Cart;
};
export default function OrderForm({ cart }: OrderFormProps) {
return (
<Container>
<h2>주문</h2>
<Table
lineItems={cart.lineItems}
totalPrice={cart.totalPrice}
/>
{/* TODO: 배송지 입력 */}
{/* TODO: 결제 */}
</Container>
);
}
OrderCompletePage
생성
SignupCompletePage
와 마찬가지로 간단하게 구성
export default function OrderCompletePage() {
return (
<div>
<p>주문이 완료되었습니다.</p>
<p><Link to="/orders">주문 목록 확인</Link></p>
</div>
);
}
routes에 페이지 추가
{ path: '/order', element: <OrderPage /> },
{ path: '/order/complete', element: <OrderCompletePage /> },
CartView
에 버튼 추가
장바구니 페이지에서 주문 페이지로 갈 수 있도록 버튼 추가
export default function CartView({ cart }: CartViewProps) {
const navigate = useNavigate();
const handleClick = () => {
navigate('/order');
};
return (
<div>
{/**/}
<Button onClick={handleClick}>주문하기</Button>
</div>
);
}
4. 배송지 정보 입력
🎯 받는 사람 이름, 주소, 전화번호를 입력 받는 폼 구현하기
OrderFormStore
를 만들어서 관련 정보를 다룰 수 있도록 준비
주소는 크게 두 부분으로 나뉨 - 상위 주소, 상세 주소
상세 주소가 아닌 상위 주소를 지정할 때 우편번호를 함께 지정
OrderFormStore Hook 생성
@singleton()
@Store()
export default class OrderFormStore {
name = '';
address1 = '';
address2 = '';
postalCode = '';
phoneNumber = '';
get valid() {
return !!this.name.trim()
&& !!this.address1.trim()
&& !!this.address2.trim()
&& !!this.postalCode.trim()
&& !!this.phoneNumber.trim();
}
@Action()
changeName(name: string) {
this.name = name;
}
@Action()
changeAddress1(address1: string, postalCode: string) {
this.address1 = address1;
this.postalCode = postalCode;
}
@Action()
changeAddress2(address2: string) {
this.address2 = address2;
}
@Action()
changePhoneNumber(phoneNumber: string) {
this.phoneNumber = phoneNumber.replace(/[^0-9]/g, '');
}
}
지번/도로명 주소가 우편번호와 매칭되기 때문에 동시에 사용
useOrderFormStore Hook 생성
OrderForm
에서 쓸 수 있는 ShippingForm
생성
const Container = styled.div`
h3 {
font-size: 2rem;
}
input {
width: 50rem;
}
`;
const PostalCodeField = styled.div`
> div {
display: inline-block;
margin-right: 1rem;
}
input {
width: 10rem;
}
`;
export default function ShippingForm() {
const [{
name, address1, address2, postalCode, phoneNumber,
}, store] = useOrderFormStore();
const handleClickSearchPostalCode = () => {
// TODO: 우편번호 검색
};
const handleChangeName = (value: string) => {
store.changeName(value);
};
// TODO: 우편번호 검색할 때 사용
const handleChangeAddress1 = (value: {
address: string;
postalCode: string;
}) => {
store.changeAddress1(value.address, value.postalCode);
};
const handleChangeAddress2 = (value: string) => {
store.changeAddress2(value);
};
const handlePhonNumber = (value: string) => {
store.changePhoneNumber(value);
};
return (
<Container>
<h3>받는 사람</h3>
<TextBox
label="이름"
placeholder="받는 분 이름"
value={name}
onChange={handleChangeName}
/>
<PostalCodeField>
<TextBox
label="우편번호"
value={postalCode}
readOnly
/>
<Button onClick={handleClickSearchPostalCode}>
우편번호 검색
</Button>
</PostalCodeField>
<TextBox
label="주소"
value={address1}
readOnly
/>
<TextBox
label="상세 주소"
value={address2}
onChange={handleChangeAddress2}
/>
<TextBox
label="전화번호"
type="tel"
value={phoneNumber}
onChange={handlePhonNumber}
/>
</Container>
);
}
TextBox
에 readOnly 속성 추가
readOnly 속성을 추가하고 onChange를 옵셔널하게 만들기
OrderForm.tsx
의 TODO: 배송지 입력에 <ShippingForm />
추가
type TextBoxProps = {
label: string;
placeholder?: string;
type?: 'text' | 'number' | 'password' | 'tel'; // ...and more types...
value: string;
onChange?: (value: string) => void;
readOnly?: boolean;
}
export default function TextBox({
label, placeholder = undefined, type = 'text', value,
onChange = undefined, readOnly = false,
}: TextBoxProps) {
const id = useRef(`textbox-${Math.random().toString().slice(2)}`);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!onChange) {
return;
}
onChange(event.target.value);
};
return (
<Container>
<label htmlFor={id.current}>
{label}
</label>
<input
id={id.current}
type={type}
placeholder={placeholder}
value={value}
onChange={handleChange}
readOnly={readOnly}
/>
</Container>
);
}
5. 우편번호 검색
우편번호 검색 기능
Daum 우편번호 서비스
index.html
파일에 스크립트 태그 추가
⚠️ script의 순서에 주의할 것
로딩이 먼저 완료될 수 있도록 위에 작성하기
<script src="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script type="module" src="src/main.tsx"></script>
타입 지정
전역으로 얻는 daum 객체의 타입을 알 수 있도록 daum.postcode.d.ts
파일 생성
declare namespace daum {
export type PostcodeResult = {
address: string;
zonecode: string;
}
export class Postcode {
constructor({ oncomplete, width, height }: {
oncomplete: (data: PostcodeResult) => void;
width: string;
height: string;
});
embed(element: HTMLElement | null);
}
}
팝업 모달로 사용할 경우 open 으로 바꾸면 됨
AddressSearch
컴포넌트 생성
우편번호 검색 모달(modal)을 보여주는 AddressSearch
컴포넌트 만들기
useRef 훅을 사용하면 DOM 객체에 직접 접근할 수 있음
const Container = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, .5);
div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
height: 80%;
max-width: 50rem;
background: ${(props) => props.theme.colors.background};
}
`;
type AddressSearchProps = {
close: ()=> void;
changeAddress: ({ address, postalCode }: {
address: string;
postalCode: string;
}) => void;
}
export default function AddressSearch({
close, changeAddress,
}: AddressSearchProps) {
const refElement = useRef<HTMLDivElement>(null);
useEffectOnce(() => {
new daum.Postcode({
oncomplete(data) {
const { address, zonecode: postalCode } = data;
changeAddress({ address, postalCode });
close();
},
width: '100%',
height: '100%',
}).embed(refElement.current);
});
return (
<Container id="address-search-container" onClick={close}>
<div ref={refElement} />
</Container>
);
}
ShippingForm
에서 AddressSearch
를 활용하도록 구현
const {
value: searching, setTrue: openSearch, setFalse: closeSearch,
} = useBoolean();
// …(중략)…
const handleClickSearchPostalCode = () => {
openSearch();
};
// …(중략)…
{searching && (
<AddressSearch
close={closeSearch}
changeAddress={handleChangeAddress1}
/>
)}
codecept를 이용한 팁
개발할 때 매번 배송 정보 입력 폼을 임의로 작성하는 것이 번거로움
💡 E2E 테스트로 모든 input을 채워넣고 마지막에 wait로 대기 시간을 길게 주면 그동안 페이지에서 개발자 도구를 열어 디버깅 하는 등의 행위를 할 수 있음
🚧 개발이 끝나면 삭제할 것