
๐ฏ ์ฅ๋ฐ๊ตฌ๋ ๋ชฉ๋ก๊ณผ ์ฃผ๋ฌธ์ ์์ฑ ํ์ด์ง๋ฅผ ์ ์ํฉ๋๋ค.
import styled from 'styled-components';
import Title from '../components/common/Title';
import CartItem from '../components/cart/CartItem';
import { useCart } from '../hooks/useCart';
import { useMemo, useState } from 'react';
import Empty from '../components/common/Empty';
import { FaShoppingCart } from 'react-icons/fa';
import CartSummary from '../components/cart/CartSummary';
import Button from '../components/common/Button';
import { useAlert } from '../hooks/useAlert';
import { OrderSheet } from '../models/order.model';
import { useNavigate } from 'react-router-dom';
import { Theme } from '../style/theme';
export default function Cart() {
const { carts, isEmpty, deleteCartItem } = useCart();
const [checkedItems, setCheckedItems] = useState<number[]>([]);
const { showAlert, showConfirm } = useAlert();
const navigate = useNavigate();
const handleCheck = (id: number) => {
if (checkedItems.includes(id)) {
setCheckedItems(checkedItems.filter((item) => item !== id));
} else {
setCheckedItems([...checkedItems, id]);
}
};
const handleItemDelete = (id: number) => {
deleteCartItem(id);
};
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 });
});
};
return (
<>
<Title size='large'>์ฅ๋ฐ๊ตฌ๋</Title>
<StyledCart>
{!isEmpty && (
<>
<div className='content'>
{carts.map((cart) => (
<CartItem
key={cart.id}
cart={cart}
checkedItems={checkedItems}
onCheck={handleCheck}
onDelete={handleItemDelete}
/>
))}
</div>
<div className='summary'>
<CartSummary
totalPrice={totalPrice}
totalQuantity={totalQuantity}
/>
<Button size='large' scheme='primary' onClick={handleOrder}>
์ฃผ๋ฌธํ๊ธฐ
</Button>
</div>
</>
)}
{isEmpty && (
<Empty
icon={<FaShoppingCart />}
title='์ฅ๋ฐ๊ตฌ๋๊ฐ ๋น์์ต๋๋ค.'
description='์ฅ๋ฐ๊ตฌ๋๋ฅผ ์ฑ์๋ณด์ธ์.'
/>
)}
</StyledCart>
</>
);
}
export const StyledCart = styled.div<{ theme: Theme }>`
display: flex;
gap: 24px;
justify-content: space-between;
padding: 24px 0 0 0;
.content {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.summary {
display: flex;
flex-direction: column;
gap: 24px;
}
.order-info {
h1 {
padding: 0 0 24px 0;
}
border: 1px solid ${({ theme }) => theme.color.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;
}
}
`;
useCart() : carts(์ฅ๋ฐ๊ตฌ๋ ๋ชฉ๋ก), isEmpty(๋น์๋์ง ์ฌ๋ถ), deleteCartItem()(์ญ์ ํจ์)๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
useState<number[]>([]) : ์ฒดํฌ๋ ์ํ์ ID๋ฅผ ์ซ์๋ก ๋ ๋ฐฐ์ด๋ก ๊ด๋ฆฌํฉ๋๋ค.
useAlert() : showAlert()์ showConfirm()์ผ๋ก ์๋ฆผ์ฐฝ์ด๋ ํ์ธ์ฐฝ์ ๋์๋๋ค.
useNavigate() : React Router์์ ์ ๊ณตํ๋ ํ
์ผ๋ก, ํ์ด์ง ์ด๋์ ๋์ต๋๋ค.
handleCheck : ์ฌ์ฉ์๊ฐ ์ฒดํฌ๋ฐ์ค๋ฅผ ๋๋ฅด๋ฉด ํด๋น ์ํ ID๋ฅผ checkedItems์์ ์ถ๊ฐํ๊ฑฐ๋ ์ ๊ฑฐํฉ๋๋ค.
handleItemDelete : ํ
์ ํตํด ๊ฐ์ ธ์จ deleteCartItem()(์ญ์ ํจ์)๋ฅผ ์ด์ฉํด์ ์ํ์ ์ญ์ ํฉ๋๋ค.
totalQuantity : ์ฅ๋ฐ๊ตฌ๋๋ชฉ๋ก๊ณผ ์ฒดํฌ๋ ๋ชฉ๋ก์ด ๋ฐ๋๋ฉด, ์ฅ๋ฐ๊ตฌ๋ ์์ดํ
์ด ์ฒดํฌ๋ ์์ดํ
์ด๋ฉด ์๋์ ๊ณ์ฐํฉ๋๋ค.
๐ค
useMemo()๋ ์ด๋ค ํ ์ด์ง?import { useMemo } from 'react'; const ๋ณ์๋ช = useMemo(() => { // "ํจ์" }, ["์์กด์ฑ ๋ฐฐ์ด"]);์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง๋ ๋, ๋ถํ์ํ ๊ณ์ฐ์ ๋ฐฉ์งํ๊ธฐ ์ํ ํ ์ ๋๋ค.
"์์กด์ฑ ๋ฐฐ์ด"์ด ๋ฐ๋ ๋๋ง๋ค "ํจ์"๊ฐ ์คํ๋๊ณ , ๋ณดํต ๊ณ์ฐ ๋น์ฉ์ด ํฐ ์ฐ์ฐ์ด๋ ๋ฐ์ดํฐ ํํฐ๋ง์ ์ํํฉ๋๋ค.
totalPrice : ์ฅ๋ฐ๊ตฌ๋๋ชฉ๋ก๊ณผ ์ฒดํฌ๋ ๋ชฉ๋ก์ด ๋ฐ๋๋ฉด, ์ฅ๋ฐ๊ตฌ๋ ์์ดํ
์ด ์ฒดํฌ๋ ์์ดํ
์ด๋ฉด ๊ฐ๊ฒฉ์ ๊ณ์ฐํฉ๋๋ค.
handleOrder : ์ฒดํฌ๋ ์ํ์ด ์์ผ๋ฉด ๊ฒฝ๊ณ ์ฐฝ์ ๋์ฐ๊ณ , ์ฒดํฌ๋ ์ํ์ด ์๋ค๋ฉด, ํ์ธ์ฐฝ ๋์ด ๋ค์ /order ํ์ด์ง๋ก ์ด๋ํ๋ฉด์ orderData ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ๋๊น๋๋ค.
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 { Theme } from '../../style/theme';
import CheckIconButton from './CheckIconButton';
import { useMemo } from 'react';
import { useAlert } from '../../hooks/useAlert';
interface Props {
cart: Cart;
checkedItems: number[];
onCheck: (id: number) => void;
onDelete: (id: number) => void;
}
export default function CartItem({
cart,
checkedItems,
onCheck,
onDelete,
}: Props) {
const { showConfirm } = useAlert();
const isChecked = useMemo(() => {
return checkedItems.includes(cart.id);
}, [checkedItems, cart.id]);
const handleCheck = () => {
onCheck(cart.id);
};
const handleDelete = () => {
showConfirm('์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น?', () => {
onDelete(cart.id);
});
};
return (
<StyledCartItem>
<div className='info'>
<div className='check'>
<CheckIconButton isChecked={isChecked} onCheck={handleCheck} />
</div>
<div>
<Title size='medium' 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' onClick={handleDelete}>
์ฅ๋ฐ๊ตฌ๋ ์ญ์
</Button>
</StyledCartItem>
);
}
const StyledCartItem = styled.div<{ theme: Theme }>`
display: flex;
justify-content: space-between;
align-items: start;
border: 1px solid ${({ theme }) => theme.color.border};
border-radius: ${({ theme }) => theme.borderRadius.default};
padding: 12px;
.info {
display: flex;
align-items: start;
flex: 1;
.check {
width: 40px;
flex-shrink: 0;
margin-top: 3px;
}
}
p {
margin: 0;
padding: 0 0 8px 0;
}
`;
useAlert() : showConfirm()์ผ๋ก ํ์ธ์ฐฝ์ ๋์๋๋ค.
isChecked : checkedItems ์ cart.id ๊ฐ ๋ฐ๋ ๋ ๋ง๋ค checkedItems ์์ ์ด cart.id๊ฐ ๋ค์ด์๋์ง ํ์ธํฉ๋๋ค.
handleCheck : ์ฌ์ฉ์๊ฐ ์ฒดํฌ๋ฐ์ค๋ฅผ ํด๋ฆญํ๋ฉด onCheck(cart.id)๋ฅผ ํธ์ถํฉ๋๋ค.
๐ฃ๏ธ ์ด๋ป๊ฒ ์ฐ๊ฒฐ๋๋ ๊ฑธ๊น?
CheckIconButtonํด๋ฆญ โhandleCheck()์คํ โonCheck(cart.id)์คํ โCart.tsx์handleCheck(id)์คํ โcheckedItems์ํ๊ฐ ๋ณ๊ฒฝ
handleDelete : ํ์ธ์ฐฝ์ ๋์ด ํ, onDelete(cart.id)๋ฅผ ํธ์ถํฉ๋๋ค.import { FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
import styled from 'styled-components';
interface Props {
isChecked: boolean;
onCheck: () => void;
}
export default function CheckIconButton({ isChecked, onCheck }: Props) {
return (
<StyledCheckIconButton onClick={onCheck}>
{isChecked ? <FaRegCheckCircle /> : <FaRegCircle />}
</StyledCheckIconButton>
);
}
const StyledCheckIconButton = styled.button`
background: none;
border: 0;
cursor: pointer;
svg {
width: 24px;
height: 24px;
}
`;
isChecked : true๋ฉด ์ฒดํฌ๋ ๋๊ทธ๋ผ๋ฏธ๋ฅผ ๋ณด์ฌ์ฃผ๊ณ , false๋ฉด ์ฒดํฌ ์ ๋ ๋๊ทธ๋ผ๋ฏธ๋ฅผ ๋ณด์ฌ์ค๋๋ค.
onCheck : ๋ฒํผ ํด๋ฆญ ์ ์คํ๋๋ ํจ์๋ก CartItem์์ ๋๊ฒจ์ค handleCheck๋ฅผ ๋ปํฉ๋๋ค.
import styled from 'styled-components';
import { formatNumber } from '../../utils/format';
import { Theme } from '../../style/theme';
interface Props {
totalQuantity: number;
totalPrice: number;
}
export default function CartSummary({ totalQuantity, totalPrice }: Props) {
return (
<StyledCartSummary>
<h1>์ฃผ๋ฌธ ์์ฝ</h1>
<dl>
<dt>์ด ์๋</dt>
<dd>{totalQuantity} ๊ถ</dd>
</dl>
<dl>
<dt>์ด ๊ธ์ก</dt>
<dd>{formatNumber(totalPrice)} ์</dd>
</dl>
</StyledCartSummary>
);
}
const StyledCartSummary = styled.div<{ theme: Theme }>`
border: 1px solid ${({ theme }) => theme.color.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;
}
}
`;
Cart.tsx์์ ๊ณ์ฐํ totalQuantity์ totalPrice๋ฅผ ๋ฐ์์์ ์์ฝ ์ ๋ณด๋ฅผ ๋ณด์ฌ์ค๋๋ค.
<dl> ํ๊ทธ๋ definition list๋ฅผ ๋งํ๊ณ , ์ ๋ณด์ ์ ๋ชฉ(<dt>)๊ณผ ๋ด์ฉ(<dd>) ์ง์ผ๋ก ๊ตฌ์ฑ๋ ๋ชฉ๋ก์
๋๋ค.
์ถ์ฒ: mdn

import { useLocation, useNavigate } from 'react-router-dom';
import { StyledCart } from './Cart';
import Title from '../components/common/Title';
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';
import FindAddressButton from '../components/order/FindAddressButton';
import { order } from '../api/order.api';
import { useAlert } from '../hooks/useAlert';
interface DeliveryForm extends Delivery {
addressDetail: string;
}
export default function Order() {
const { showAlert, showConfirm } = useAlert();
const navigate = useNavigate();
const location = useLocation();
const orderDataFromCart = location.state;
const { totalPrice, totalQuantity, 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>
<StyledCart>
<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 totalPrice={totalPrice} totalQuantity={totalQuantity} />
<Button
size='large'
scheme='primary'
onClick={handleSubmit(handlePay)}
>
๊ฒฐ์ ํ๊ธฐ
</Button>
</div>
</StyledCart>
</>
);
}
orderDataFromCart : Cart.tsx์์ navigate('/order', { state: orderData })๋ก ๋๊ธด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ต๋๋ค.
items, totalPrice, totalQuantity, firstBookTitle ์ ๋ณด๊ฐ ๋ด๊ฒจ์์ต๋๋ค. register๋ก input ์ฐ๊ฒฐ, errors๋ก ์๋ฌ ์ฒ๋ฆฌ, setValue๋ ์ฃผ์ ์ฐพ๊ธฐ ํ ๊ฐ ๋ฃ์ ๋ ์ฌ์ฉํฉ๋๋ค.
๐ค
useForm()๋ ์ด๋ค ํ ์ด์ง?React Hook Form ์์ ์ ๊ณตํ๋ ํผ ์ํ ๊ด๋ฆฌ์ฉ ํ ์ ๋๋ค.
HTML<form>์์์ ์ ๋ ฅ๊ฐ๋ค์ ํธํ๊ฒ ๋ค๋ฃจ๊ณ , ๊ฒ์ฆ๋ ์ฝ๊ฒ ํ ์ ์๋๋ก ๋์์ค๋๋ค.<InputText {...register('address', { required: true })} />
- input์ ๋ฑ๋ก(register)ํด์ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ฒ ํด์ค๋๋ค.
onClick={handleSubmit(handlePay)}
- ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๋จผ์ ํ๊ณ , ํต๊ณผํ๋ฉด
handlePay์คํํฉ๋๋ค.- โจ
handlePayํจ์๋ง ๋๊ธฐ๋ฉด ๊ทธ ์์data: DeliveryForm์ด ์๋์ผ๋ก ์ ๋ฌ๋ฉ๋๋ค.{errors.address && <p>์ฃผ์๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.</p>}
- ์ ํจ์ฑ ๊ฒ์ฌ ์คํจ ์ ์๋ฌ ์ ๋ณด๋ฅผ ํ์ํฉ๋๋ค.
<FindAddressButton onCompleted={(address) => { setValue('address', address); }} />
- ์ฃผ์ ๊ฒ์ํด์ ์ฐพ์ ์ฃผ์๋ฅผ ํผ ํ๋์ ์ง์ ๋ฃ์ด์ค ๋ ์ฌ์ฉํฉ๋๋ค.
handlePay : ๋ฐฐ์ก ์ ๋ณด์ ์ฅ๋ฐ๊ตฌ๋ ๋ฐ์ดํฐ๋ฅผ ํฉ์ณ์ orderData๋ฅผ ๋ง๋ค๊ณ API (order()) ํธ์ถ ํ ์ฑ๊ณตํ๋ฉด ์ฑ๊ณต ๋ฉ์ธ์ง๋ฅผ ๋ณด์ฌ์ฃผ๊ณ /orderlist ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.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';
export default function FindAddressButton({ onCompleted }: Props) {
const handleOpen = () => {
new window.daum.Postcode({
oncompleted: (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>
);
}
onCompleted : ์ฃผ์ ์ ํ์ด ์๋ฃ๋์์ ๋ ์คํํ ์ฝ๋ฐฑ ํจ์๋ก Order.tsx์์ setValue('address', ์ฃผ์)๋ก ์ฐ๊ฒฐํฉ๋๋ค.
handleOpen : window.daum.Postcode()๋ ๋ค์์์ ์ ๊ณตํ๋ ์ฐํธ๋ฒํธ ๊ฒ์ ํ์
์
๋๋ค.
oncompleted ์ฝ๋ฐฑ์ด ์คํ๋๊ณ , ๊ทธ ์์์ ์ ํํ ์ฃผ์(data.address)๋ฅผ onCompleted()๋ก ์ ๋ฌํด์ค๋๋ค.useEffect : ์ปดํฌ๋ํธ๊ฐ ์ฒ์ ๋ง์ดํธ๋ ๋์๋ง ๋ค์ ์ฃผ์ ๊ฒ์ ์คํฌ๋ฆฝํธ๋ฅผ ๋ถ๋ฌ์ต๋๋ค. document.head์ ์ง์ <script> ํ๊ทธ๋ฅผ ์ฝ์
ํ๊ณ , ์ปดํฌ๋ํธ๊ฐ ์ฌ๋ผ์ง ๋๋ ์ ๊ฑฐํฉ๋๋ค.
๐ฌ ๋ค์ ์ฐํธ๋ฒํธ ์๋น์ค ์ด์ฉ ๋ฐฉ๋ฒ
1. cdn ๋งํฌ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
<head>ํ๊ทธ์<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>๋ฅผ ์ฝ์ ํฉ๋๋ค.2. ๊ธ๋ก๋ฒํ๊ฒ ํ์ ์ ์ถ๊ฐํฉ๋๋ค.
// window.d.ts interface Window { daum: { Postcode: any; }; }
window.daum์ Daum ์ฃผ์ ๊ฒ์ API๊ฐ ์คํฌ๋ฆฝํธ๋ฅผ ํตํด ๋ฐํ์์ ์ ์ญ์ผ๋ก ์ฃผ์ ๋๋ ๊ฐ์ฒด์ด๋ฏ๋ก ์ปดํ์ผ ์๋ฌ๊ฐ ๋๊ธฐ ๋๋ฌธ์ ํ์ ์ ์ ์ํด์ค๋๋ค.3.
oncomeplete: ํ์ ์์ ๊ฒ์๊ฒฐ๊ณผ ํญ๋ชฉ์ ํด๋ฆญํ์๋ ์คํํ ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค.new window.daum.Postcode({ oncomplete: function(data) { //data๋ ์ฌ์ฉ์๊ฐ ์ ํํ ์ฃผ์ ์ ๋ณด๋ฅผ ๋ด๊ณ ์๋ ๊ฐ์ฒด // ํ์ ์์ ๊ฒ์๊ฒฐ๊ณผ ํญ๋ชฉ์ ํด๋ฆญํ์๋ ์คํํ ์ฝ๋๋ฅผ ์์ฑํ๋ ๋ถ๋ถ์ ๋๋ค. } }).open();

import styled from 'styled-components';
import Title from '../components/common/Title';
import { useOrders } from '../hooks/useOrders';
import { Theme } from '../style/theme';
import { formatDate, formatNumber } from '../utils/format';
import Button from '../components/common/Button';
import React from 'react';
export default function OrderList() {
const { orders, selectedItemId, selectOrderItem } = useOrders();
return (
<>
<Title size='large'>์ฃผ๋ฌธ ๋ด์ญ</Title>
<StyledOrderList>
<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.createdAt, 'YYYY.MM.DD')}</td>
<td>{order.address}</td>
<td>{order.receiver}</td>
<td>{order.contact}</td>
<td>{order.bookTitle}</td>
<td>{order.totalQuantity} ๊ถ</td>
<td>{order.totalPrice} ์</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.bookId}>
<div>
<span>{item.title}</span>
<span>{item.author}</span>
<span>{formatNumber(item.price)} ์</span>
</div>
</li>
))}
</ul>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</StyledOrderList>
</>
);
}
const StyledOrderList = styled.div<{ theme: Theme }>`
padding: 24px 0 0 0;
table {
width: 100%;
border-collapse: collapse;
border-top: 1px solid ${({ theme }) => theme.color.border};
border-bottom: 1px solid ${({ theme }) => theme.color.border};
}
th,
td {
padding: 16px;
border-bottom: 1px solid ${({ theme }) => theme.color.border};
text-align: center;
}
.detail {
margin: 0;
li {
list-style: square;
text-align: left;
div {
display: flex;
padding: 8px 12px;
gap: 8px;
}
}
}
`;
useOrders()๋ผ๋ ์ปค์คํ
ํ
์ ์ฌ์ฉํด์ orders(์ฃผ๋ฌธ ๋ฆฌ์คํธ ๋ฐฐ์ด), selectedItemId(์ ํ๋ ์ฃผ๋ฌธ์ id), selectOrderItem(์ฃผ๋ฌธ ์์ธ ์ ๋ณด ๋ถ๋ฌ์ค๋ ํจ์)๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
์ฒซ๋ฒ์งธ <tr> ํ๊ทธ๋ ์ฃผ๋ฌธ ์์ฝ ์ ๋ณด๋ฅผ ํ๊ธฐํ๊ณ , ๋ ๋ฒ์งธ <tr> ํ๊ทธ๋ ํด๋น ์ฃผ๋ฌธ์ด ์ ํ๋ ๊ฒฝ์ฐ์๋ง ์์ธ ์์ดํ
๋ค ์ถ๋ ฅํฉ๋๋ค.
<table>์ ํ์ ์์, <thead>๋ ํ
์ด๋ธ์ ์ ๋ชฉ ์์ญ,<tr>์ ํ ์ค, <th>์ ์ ๋ชฉ์ฉ ์
, <tbody>์ ๋ด์ฉ ์์ญ, <td>์ ์ค์ ๋ฐ์ดํฐ ๋ค์ด๊ฐ๋ ์นธ์ ๋งํฉ๋๋ค.
โจ map์ผ๋ก ๋๋ฆด ๋์๋
key๊ฐ์ด ํ์ํ๊ธฐ ๋๋ฌธ์React.Fragment๋ฅผ ์จ์<tr>2๊ฐ๋ฅผ ๋ฌถ์ด์ฃผ์ด์ผ ํฉ๋๋ค.

์ค๋ช ์์ด ๋์ด๊ฐ๋ ๋ถ๋ถ๋ค์ ์ ์ฐ๋์ง ์ ๋ฆฌํ๋ฉด์ ์ ์ผ๋ ๋ ์ดํด๊ฐ ์ฝ๊ฒ ๋๊ณ ์ ๊ทธ๋ ๊ฒ ์ ์๋์ง ๋ ๊น๋ํ๊ฒ ์ดํด๊ฐ ๋๋ ๊ฒ ๊ฐ๋ค.