2024년 11월 18일
구현 계획
장바구니 페이지 틀 만들기 + 체크 아이콘 버튼 만들기
// App.tsx
... 생략 ...
,{
path: "/carts",
element: <Layout><Cart ></Cart></Layout>
}
... 생략 ...
// pages / Cart.tsx
import styled from 'styled-components'
import Title from '../components/common/Title'
import CartItem from '../components/cart/CartItem';
import { useCart } from '../hooks/useCart';
const Cart = () => {
const { carts } = useCart();
return (
<>
<Title size="large">장바구니</Title>
<CartStyle>
<div className="content">
{
carts.map((item) => (<CartItem key={item.id} cart={item} />))
}
</div>
<div className="summary">
</div>
</CartStyle>
</>
)
}
const CartStyle = styled.div`
`;
export default Cart
// api / carts.api.ts
... 생략 ...
export const fetchCart = async () => {
const response = await httpClient.get<Cart[]>("/carts");
return response.data;
}
// hooks / useCart.ts
import { useEffect, useState } from 'react'
import { Cart } from '../models/cart.model'
import { fetchCart } from '../api/carts.api';
export const useCart = () => {
const [carts, setCarts] = useState<Cart[]>([]);
const [isEmpty, setIsEmpty] = useState(true);
useEffect(()=>{
fetchCart().then((carts)=>{
setCarts(carts);
setIsEmpty(carts.length === 0);
})
}, [])
return { carts, isEmpty };
}
// components / cart / CartIem.tsx
import styled from 'styled-components'
import { Cart } from '../../models/cart.model';
import Button from '../common/Button';
import Title from '../common/Title';
import { formatNumber } from '../../utils/format';
import CheckIconButton from './CheckIconButton';
interface Props {
cart: Cart;
}
const CartItem = ({cart}:Props) => {
return (
<CartItemStyle>
<div className="info">
<CheckIconButton />
<div>
<Title size="large" color="text">{cart.title}</Title>
<p className="summary">{cart.summary}</p>
<p className="price">{formatNumber(cart.price)} 원</p>
<p className="quantity">{cart.quantity} 권</p>
</div>
</div>
<Button size="medium" scheme="normal">장바구니 삭제</Button>
</CartItemStyle>
)
}
const CartItemStyle = styled.div`
display: flex;
justify-content: space-between;
align-items: start;
border: 1px solid ${({theme}) => theme.colors.border};
border-radius: ${({theme}) => theme.borderRadius.default};
padding: 12px;
p {
padding: 0 0 8px 0;
margin: 0;
}
`;
export default CartItem
// components / cart / CheckIconButton.tsx
import styled from 'styled-components'
import { FaRegCircle, FaRegCheckCircle } from "react-icons/fa";
interface Props {
isChecked: boolean;
onCheck: () => void;
}
const CheckIconButton = ({isChecked, onCheck} : Props) => {
return (
<CheckIconButtonStyle onClick={onCheck}>
{
isChecked ? (<FaRegCheckCircle />) : (<FaRegCircle />)
}
</CheckIconButtonStyle>
)
}
const CheckIconButtonStyle = styled.button`
background: none;
border: 0;
cursor: pointer;
svg {
width : 24px;
height: 24px;
}
`;
export default CheckIconButton
장바구니 아이템 체크 로직 만들기
루프 되고 있는 아이템들을 체크하는데는 여러 전략이 있음
체크 상태인 장바구니 아이템 보여주기
// pages / Cart.tsx
import styled from 'styled-components'
import Title from '../components/common/Title'
import CartItem from '../components/cart/CartItem';
import { useCart } from '../hooks/useCart';
import { useState } from 'react';
const Cart = () => {
const { carts } = useCart();
const [ checkedItems, setCheckedItems ] = useState<number[]>([1]);
// 아이템의 아이디만 넣을 것이기 때문에 넘버 배열타입으로 지정해줌
// checkedItems의 상태에 따라 CartItem, CheckIconButton까지 상태가 전달되어야 함
return (
<>
<Title size="large">장바구니</Title>
<CartStyle>
<div className="content">
{
carts.map((item) => (<CartItem key={item.id} cart={item}
checkedItems={checkedItems} />))
}
</div>
<div className="summary">
</div>
</CartStyle>
</>
)
}
const CartStyle = styled.div``;
export default Cart
// components / cart /cartItem
import styled from 'styled-components'
import { Cart } from '../../models/cart.model';
import Button from '../common/Button';
import Title from '../common/Title';
import { formatNumber } from '../../utils/format';
import CheckIconButton from './CheckIconButton';
import { useMemo } from 'react';
interface Props {
cart: Cart;
checkedItems: number[];
}
const CartItem = ({ cart, checkedItems }:Props) => {
// checkedItems 목록에 id가 있는지 판단 = checked
const isChecked = useMemo(() => {
return checkedItems.includes(cart.id);
}, [checkedItems, cart.id]);
return (
<CartItemStyle>
<div className="info">
<CheckIconButton isChecked={isChecked} />
<div>
<Title size="large" color="text">{cart.title}</Title>
<p className="summary">{cart.summary}</p>
<p className="price">{formatNumber(cart.price)} 원</p>
<p className="quantity">{cart.quantity} 권</p>
</div>
</div>
<Button size="medium" scheme="normal">장바구니 삭제</Button>
</CartItemStyle>
)
}
const CartItemStyle = styled.div`
display: flex;
justify-content: space-between;
align-items: start;
border: 1px solid ${({theme}) => theme.colors.border};
border-radius: ${({theme}) => theme.borderRadius.default};
padding: 12px;
p {
padding: 0 0 8px 0;
margin: 0;
}
`;
export default CartItem
체크버튼 눌렀을 때 체크/언체크 되게 만들기
// components / cart / CartItem.tsx
import styled from 'styled-components'
import { Cart } from '../../models/cart.model';
import Button from '../common/Button';
import Title from '../common/Title';
import { formatNumber } from '../../utils/format';
import CheckIconButton from './CheckIconButton';
import { useMemo } from 'react';
interface Props {
cart: Cart;
checkedItems: number[];
onCheck:(id:number) => void;
}
const CartItem = ({ cart, checkedItems, onCheck }:Props) => {
// checkedItems 목록에 id가 있는지 판단 = checked
const isChecked = useMemo(() => {
return checkedItems.includes(cart.id);
}, [checkedItems, cart.id]);
const handleCheck = () => {
onCheck(cart.id);
}
return (
<CartItemStyle>
<div className="info">
<CheckIconButton isChecked={isChecked} onCheck={handleCheck}/>
... 생략 ...
// pages / Cart.tsx
import styled from 'styled-components'
import Title from '../components/common/Title'
import CartItem from '../components/cart/CartItem';
import { useCart } from '../hooks/useCart';
import { useState } from 'react';
const Cart = () => {
const { carts } = useCart();
const [ checkedItems, setCheckedItems ] = useState<number[]>([]);
const handleCheckItem = (id:number) => {
if(checkedItems.includes(id)){
// 언체크
setCheckedItems(checkedItems.filter((itemId) => itemId !== id))
} else {
// 체크
setCheckedItems([ ...checkedItems, id]);
return;
}
}
return (
<>
<Title size="large">장바구니</Title>
<CartStyle>
<div className="content">
{
carts.map((item) => (<CartItem key={item.id} cart={item}
checkedItems={checkedItems} onCheck={handleCheckItem}/>))
}
</div>
... 생략 ...
// api / carts.api.ts
... 생략 ...
export const deleteCart = async (cartId: number) => {
const response = await httpClient.delete(`/carts/${cartId}`);
return response.data;
}
// hooks / useCart.ts
... 생략 ...
const deleteCartItem = (id:number) => {
deleteCart(id).then(() => {
setCarts(carts.filter((cart) => cart.id !== id));
})
}
useEffect(()=>{
fetchCart().then((carts)=>{
setCarts(carts);
setIsEmpty(carts.length === 0);
})
}, [])
return { carts, isEmpty, deleteCartItem };
}
// pages / Cart.tsx
... 생략 ...
const Cart = () => {
const { carts, deleteCartItem } = useCart();
... 생략 ...
const handleItemDelete = (id:number) => {
// 삭제
deleteCartItem(id);
}
return (
<>
<Title size="large">장바구니</Title>
<CartStyle>
<div className="content">
{
carts.map((item) => (<CartItem key={item.id} cart={item}
checkedItems={checkedItems} onCheck={handleCheckItem}
onDelete={handleItemDelete}/>))
}
</div>
... 생략 ...
// components / cart / CartItem.tsx
... 생략 ...
interface Props {
cart: Cart;
checkedItems: number[];
onCheck:(id:number) => void;
onDelete: (id:number) => void;
}
const CartItem = ({ cart, checkedItems, onCheck, onDelete }:Props) => {
const isChecked = useMemo(() => {
return checkedItems.includes(cart.id);
}, [checkedItems, cart.id]);
const handleCheck = () => {
onCheck(cart.id);
}
const handleDelete = () => {
showConfirm("정말 삭제하시겠습니까?", () => {
onDelete(cart.id);
});
}
return (
... 생략 ...
<Button size="medium" scheme="normal"
onClick={handleDelete}>장바구니 삭제</Button>
... 생략 ...
// hooks / useAlert.ts
import { useCallback } from 'react'
export const useAlert = () => {
const showAlert = useCallback((message: string) => {
window.alert(message);
}, []);
const showConfirm = useCallback((message: string, onConfirm: () => void) =>
{
if(window.confirm(message)) {
onConfirm();
}
}, []);
return {showAlert, showConfirm};
}
// components / common / Empty.tsx
import styled from 'styled-components'
import Title from '../common/Title';
interface Props {
icon?: React.ReactNode;
title: string;
description?: React.ReactNode;
}
const Empty = ({ icon, title, description }:Props) => {
return (
<EmptyStyle>
{
icon && (<div className="icon">{icon}</div>)
}
<Title size="large" color="secondary">
{title}
</Title>
{
description && (<p>{description}</p>)
}
</EmptyStyle>
)
}
const EmptyStyle = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
padding: 120px 0;
.icon {
svg {
font-size: 4rem;
fill: #ccc;
}
}
`;
export default Empty
// components / books / BooksEmpty.tsx
import { FaSmileWink } from 'react-icons/fa';
import { Link } from 'react-router-dom';
import Empty from '../common/Empty';
const BooksEmpty = () => {
return (
<Empty title="검색 결과가 없습니다." icon={<FaSmileWink />} description={<Link to="/books">전체 검색 결과로 이동</Link>}/>
)
}
export default BooksEmpty
// pages . Cart.tsx
... 생략 ...
return (
<>
<Title size="large">장바구니</Title>
<CartStyle>
{
carts.length !== 0 &&
(
<>
<div className="content">
{
carts.map((item) => (<CartItem key={item.id} cart={item} checkedItems={checkedItems} onCheck={handleCheckItem} onDelete={handleItemDelete}/>))
}
</div>
<div className="summary">
<CartSummary totalPrice={totalPrice} totalQuantity={totalQuantity}/>
</div>
</>
)
}
{
carts.length === 0 && <Empty title="장바구니가 비었습니다." icon={<FaShoppingCart />} description={<>장바구니를 채워보세요.</>} />
}
</CartStyle>
</>
)
}
... 생략 ...
// components / cart / CartSummary.tsx
import styled from 'styled-components';
import { formatNumber } from '../../utils/format';
interface Props {
totalQuantity: number;
totalPrice: number;
}
const CartSummary = ({totalQuantity, totalPrice} : Props) => {
return (
<CartSummaryStyle>
<h1>주문 요약</h1>
<dl>
<dt>총 수량</dt>
<dd>{totalQuantity} 권</dd>
</dl>
<dl>
<dt>총 금액</dt>
<dd>{formatNumber(totalPrice)} 원</dd>
</dl>
</CartSummaryStyle>
)
}
const CartSummaryStyle = styled.div`
border: 1px solid ${({theme}) => theme.colors.border};
border-radius: ${({theme}) => theme.borderRadius.default};
padding: 12px;
width: 240px;
h1 {
font-size: 1.5rem;
margin-bottom: 12px;
}
dl {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
dd {
font-weight: 700;
}
}
`;
export default CartSummary
// pages / Cart.tsx
... 생략 ...
const totalQuantity = useMemo(() => {
return carts.reduce((acc, cart) => {
if(checkedItems.includes(cart.id)) {
return acc + cart.quantity;
}
return acc;
}, 0);
}, [carts, checkedItems]);
const totalPrice = useMemo(() => {
return carts.reduce((acc, cart) => {
if(checkedItems.includes(cart.id)) {
return acc + cart.price * cart.quantity;
}
return acc;
}, 0);
}, [carts, checkedItems]);
.... 생략 ...
<div className="summary">
<CartSummary totalPrice={totalPrice} totalQuantity={totalQuantity}/>
</div>
... 생략 ...
// models / order.model.ts
... 생략 ...
export interface OrderSheet {
items: number[];
totalQuantity: number;
totalPrice: number;
firstBookTitle: string;
delivery: {
address: string;
receiver: string;
contact: string;
};
}
// App.tsx
... 생략 ...
{
path: "/order",
element: <Layout><Order /></Layout>
}
... 생략 ...
// pages / Order.tsx
import { useLocation } from 'react-router-dom';
import styled from 'styled-components'
const Order = () => {
const location = useLocation();
const orderDataFromCart = location.state;
console.log(orderDataFromCart);
return (
<OrderStyle>Order</OrderStyle>
)
}
const OrderStyle = styled.div`
`;
export default Order
// pages / Cart.tsx
... 생략 ...
const navigate = useNavigate();
... 생략 ...
const totalQuantity = useMemo(() => {
return carts.reduce((acc, cart) => {
if(checkedItems.includes(cart.id)) {
return acc + cart.quantity;
}
return acc;
}, 0);
}, [carts, checkedItems]);
const totalPrice = useMemo(() => {
return carts.reduce((acc, cart) => {
if(checkedItems.includes(cart.id)) {
return acc + cart.price * cart.quantity;
}
return acc;
}, 0);
}, [carts, checkedItems]);
const handleOrder = () => {
if(checkedItems.length === 0) {
showAlert("주문할 상품을 선택해주세요.");
return;
}
// 주문서 작성으로 데이터 전달
const orderData: Omit<OrderSheet, "delivery"> = {
items: checkedItems,
totalPrice,
totalQuantity,
firstBookTitle: carts[0].title,
}
showConfirm("주문하시겠습니까?", () => {
navigate("/order", { state: orderData });
// orderData가 페이지 이동시에 같이 전달됨
})
}
... 생략 ...
<div className="summary">
<CartSummary totalPrice={totalPrice} totalQuantity={totalQuantity}/>
<Button size="large" scheme="primary" onClick={handleOrder}>주문하기</Button>
</div>
... 생략 ...
// order.model.ts
export interface Order {
id: number;
createdAt: string;
address: string;
receiver: string;
contact: string;
totalQuantity: number;
totalPrice: number;
}
export interface OrderSheet {
items: number[];
totalQuantity: number;
totalPrice: number;
firstBookTitle: string;
delivery: {
address: string;
receiver: string;
contact: Delivery;
};
}
export interface Delivery {
address: string;
receiver: string;
contact: string;
}
// pages / Order.tsx
import { useLocation } from 'react-router-dom';
import Title from '../components/common/Title';
import { CartStyle } from './Cart';
import CartSummary from '../components/cart/CartSummary';
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';
interface DeliveryForm extends Delivery {
addressDetail : string;
}
const Order = () => {
const location = useLocation();
const orderDataFromCart = location.state;
const { totalQuantity, totalPrice, firstBookTitle } = orderDataFromCart;
const { register, handleSubmit, formState: {errors},} = useForm<DeliveryForm>();
const handlePay = (data:DeliveryForm) => {
const orderData: OrderSheet = {
...orderDataFromCart,
delivery: {
...data,
address : `${data.address} ${data.addressDetail}`
}
}
// 서버로 넘겨준다
console.log(orderData);
}
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>
<Button size="medium" scheme="normal">주소 찾기</Button>
</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 totalPrice={totalPrice} totalQuantity={totalQuantity}/>
<Button size="large" scheme="primary" onClick={handleSubmit(handlePay)}>결제하기</Button>
</div>
</CartStyle>
</>
)
}
export default Order
// pages . Cart.tsx
... 생략 ...
.order-info {
h1 {
padding: 0 0 24px 0;
}
border: 1px solid ${({theme}) => theme.colors.border};
border-radius: ${({theme}) => theme.borderRadius.default};
padding: 12px;
}
.delivery {
fieldset {
border: 0;
margin: 0;
padding: 0 0 12px 0;
display: flex;
justify-content: start;
gap: 8px;
label {
width: 80px;
}
.input {
flex: 1;
input {
width: 100%;
}
}
}
.error-text {
color: red;
margin: 0;
padding: 0 0 12px 0;
text-align: right;
}
}
`;
export default Cart
// components / order / FindAddressButton.tsx
import styled from 'styled-components'
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";
const FindAddressButton = ({onCompleted}: Props) => {
// 스크립트 로드
// 핸들러
// 입력
const handleOpen = () => {
new window.daum.Postcode({
oncomplete: (data:any) => {
console.log(data);
},
}).open();
}
useEffect(()=> {
const script = document.createElement("script");
script.src = SCRIPT_URL;
script.async = true;
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
}
},[]);
return (
<FindAddressButtonStyle>
<Button type="button" size="medium" scheme="normal"
onClick={handleOpen}>주소 찾기</Button>
</FindAddressButtonStyle>
)
}
const FindAddressButtonStyle = styled.div`
`;
export default FindAddressButton
// src / window.d.ts
interface Window {
daum: {
Postcode: any;
}
}
// components / order / FindAddressButton.tsx
import styled from 'styled-components'
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";
const FindAddressButton = ({onCompleted}: Props) => {
// 스크립트 로드
// 핸들러
// 입력
const handleOpen = () => {
new window.daum.Postcode({
oncomplete: (data:any) => {
onCompleted(data.address as string);
},
}).open();
}
... 생략 ...
<Button type="button" size="medium" scheme="normal"
onClick={handleOpen}>주소 찾기</Button>
// pages / Order.tsx
... 생략 ...
<FindAddressButton onCompleted={(address)=>{
console.log("전달된 주소", address)}}/>
... 생략 ...
// pages / Order.tsx
... 생략 ...
const { register, handleSubmit, setValue, formState: {errors},} =
useForm<DeliveryForm>();
... 생략 ...
<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>}
... 생략 ...
// pages / Order.tsx
... 생략 ...
const Order = () => {
const { showAlert, showConfirm } = useAlert();
const navigate = useNavigate();
... 생략 ...
const handlePay = (data:DeliveryForm) => {
const orderData: OrderSheet = {
...orderDataFromCart,
delivery: {
...data,
address : `${data.address} ${data.addressDetail}`
}
}
// 서버로 넘겨준다
showConfirm("주문을 진행하시겠습니까?", () => {
order(orderData).then(()=> {
showAlert("주문이 처리되었습니다");
navigate('/orderlist');
});
})
}
... 생략 ...
<div className="summary">
<CartSummary totalPrice={totalPrice} totalQuantity={totalQuantity}/>
<Button size="large" scheme="primary"
onClick={handleSubmit(handlePay)}>결제하기</Button>
</div>
... 생략 ....
구현 계획
주문 내역 페이지 만들기
// pages / OrderList.tsx
import styled from 'styled-components'
import Title from '../components/common/Title';
import { useOrder } from '../hooks/useOrders';
const OrderList = () => {
const { orders } = useOrder();
console.log(orders);
return (
<>
<Title size="large">주문 내역</Title>
<OrderListStyle>
<table>
<thead>
<tr>
<th>id</th>
<th>주문일자</th>
<th>주소</th>
<th>수령인</th>
<th>전화번호</th>
<th>전화번호</th>
<th>대표상품명</th>
<th>수량</th>
<th>금액</th>
</tr>
</thead>
<tbody>
<tr></tr>
</tbody>
</table>
</OrderListStyle>
</>
)
}
const OrderListStyle = styled.div`
`;
export default OrderList
// App.tsx
... 생략 ...
{
path: "/orderlist",
element: <Layout><OrderList /></Layout>
}
... 생략 ...
// api / order.api.ts
... 생략 ...
export const fetchOrders = async () => {
const response = await httpClient.get<Order[]>("/orders");
return response.data;
}
// hooks / useOrders.ts
import { useEffect, useState } from 'react'
import { Order } from '../models/order.model';
import { fetchOrders } from '../api/order.api';
export const useOrder = () => {
const [orders, setOrders] = useState<Order[]>([]);
useEffect(()=>{
fetchOrders().then((orders) => {
setOrders(orders);
})
}, []);
return { orders };
}
// pages / OrderList.tsx
import styled from 'styled-components'
import Title from '../components/common/Title';
import { useOrder } from '../hooks/useOrders';
import { formatNumber, formatDate } from '../utils/format';
const OrderList = () => {
const { orders } = useOrder();
return (
<>
<Title size="large">주문 내역</Title>
<OrderListStyle>
<table>
<thead>
<tr>
<th>id</th>
<th>주문일자</th>
<th>주소</th>
<th>수령인</th>
<th>전화번호</th>
<th>대표상품명</th>
<th>수량</th>
<th>금액</th>
</tr>
</thead>
<tbody>
{
orders.map((order)=>(
<tr key={order.id}>
<td>{order.id}</td>
<td>{formatDate(order.created_at, "YYYY.MM.DD")}</td>
<td>{order.address}</td>
<td>{order.receiver}</td>
<td>{order.contact}</td>
<td>{order.book_title}</td>
<td>{order.total_quantity} 권</td>
<td>{formatNumber(order.total_price)} 원</td>
</tr>
))
}
</tbody>
</table>
</OrderListStyle>
</>
)
}
const OrderListStyle = styled.div`
padding: 24px 0 0 0;
table {
width: 100%;
border-collapse: collapse;
border-top: 1px solid ${({theme}) => theme.colors.border};
border-bottom: 1px solid ${({theme}) => theme.colors.border};
th, td {
padding: 16px;
border-bottom: 1px solid ${({theme}) => theme.colors.border};
text-align: center;
}
}
`;
export default OrderList
// models /order.model.ts
... 생략 ...
export interface OrderDetailItem {
book_id: number;
title: string;
author: string;
price: number;
quantity: number;
}
... 생략 ...
// api / order.api.ts
... 생략 ...
export const fetchOrder = async (orderId: number) => {
const response = await httpClient.get<OrderDetailItem[]>(`/orders/${orderId}`);
return response.data;
}
// hooks / useOrders.ts
import { useEffect, useState } from 'react'
import { Order } from '../models/order.model';
import { fetchOrder, fetchOrders } from '../api/order.api';
export const useOrder = () => {
const [orders, setOrders] = useState<Order[]>([]);
const [selectedItemId, setSelectedItemId] = useState<number | null>(null);
useEffect(()=>{
fetchOrders().then((orders) => {
setOrders(orders);
})
}, []);
const selectOrderItem = (orderId: number) => {
fetchOrder(orderId).then((orderDetail)=>{
console.log("orderDetail", orderDetail);
})
}
return { orders, selectedItemId, selectOrderItem };
}
// pages / OrderList.tsx
... 생략 ...
const OrderList = () => {
const { orders, selectedItemId, selectOrderItem } = useOrder();
... 생략 ...
<td>
<Button size="small" scheme="normal" onClick={()=>{
selectOrderItem(order.id)
}}>자세히</Button>
</td>
... 생략 ...
// models / order.model.ts
... 생략 ...
export interface OrderListItem extends Order {
detail?: OrderDetailItem[];
}
// hooks / useOrders.ts
import { useEffect, useState } from 'react'
import { OrderListItem } from '../models/order.model';
import { fetchOrder, fetchOrders } from '../api/order.api';
export const useOrder = () => {
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
import styled from 'styled-components';
import Title from '../components/common/Title';
import { useOrder } from '../hooks/useOrders';
import { formatNumber, formatDate } from '../utils/format';
import Button from '../components/common/Button';
import React from 'react';
const OrderList = () => {
const { orders, selectedItemId, selectOrderItem } = useOrder();
return (
<>
<Title size="large">주문 내역</Title>
<OrderListStyle>
<table>
<thead>
<tr>
<th>id</th>
<th>주문일자</th>
<th>주소</th>
<th>수령인</th>
<th>전화번호</th>
<th>대표상품명</th>
<th>수량</th>
<th>금액</th>
<th></th>
</tr>
</thead>
<tbody>
{
orders.map((order)=>(
<React.Fragment key={order.id}>
<tr>
<td>{order.id}</td>
<td>{formatDate(order.created_at, "YYYY.MM.DD")}</td>
<td>{order.address}</td>
<td>{order.receiver}</td>
<td>{order.contact}</td>
<td>{order.book_title}</td>
<td>{order.total_quantity} 권</td>
<td>{formatNumber(order.total_price)} 원</td>
<td>
<Button size="small" scheme="normal" onClick={() => {
selectOrderItem(order.id)
}}>자세히</Button>
</td>
</tr>
{
selectedItemId === order.id && (
<tr>
<td></td>
<td colSpan={8}>
<ul className='detail'>
{
order?.detail && order.detail.map((item) => (
<li key={item.book_id}>
<div>
<span>{item.book_id}</span>
<span>{item.author}</span>
<span>{formatNumber(item.price)} 원</span>
</div>
</li>
))
}
</ul>
</td>
</tr>
)
}
</React.Fragment>
))
}
</tbody>
</table>
</OrderListStyle>
</>
)
}
const OrderListStyle = styled.div`
padding: 24px 0 0 0;
table {
width: 100%;
border-collapse: collapse;
border-top: 1px solid ${({theme}) => theme.colors.border};
border-bottom: 1px solid ${({theme}) => theme.colors.border};
th, td {
padding: 16px;
border-bottom: 1px solid ${({theme}) => theme.colors.border};
text-align: center;
}
.detail {
margin: 0;
li {
list-style: square;
text-align: left;
div {
display: flex;
padding: 8px 12px;
gap: 8px;
}
}
}
}
`;
export default OrderList