선택된 상품에 대한 상태관리를 하기 위해 checkedCartState를 recoil로 관리한다.
▼ src/recoils/cart.ts
export const checkedCartState = atom<Cart[]>({
key: "cartState",
default: [],
});
});
Pick<Cart, "imageUrl" | "title" | "price">
▼ src/components/cart/itemData.tsx
const ItemData = ({
imageUrl,
title,
price,
}: Pick<Cart, "imageUrl" | "title" | "price">) => {
return (
<div>
<img className="cart-item_img" src={imageUrl} />
<p className="cart-item_title">{title}</p>
<p className="cart-item_price">{price}원</p>
</div>
);
};
▼ src/components/cart/willPay.tsx
const WillPay = () => {
const navigate = useNavigate();
const checkedItems = useRecoilValue(checkedCartState);
const totalPrice = checkedItems.reduce((res, { price, amount }) => {
res += price * amount;
return res;
}, 0);
const handleSubmit = () => {
if (checkedItems.length) {
navigate("/payment");
} else {
alert("결제 할 상품이 없어요.");
}
};
return (
<div className="cart-willpay">
<ul>
{checkedItems.map(({ imageUrl, price, title, amount, id }) => (
<li key={id}>
<ItemData
imageUrl={imageUrl}
price={price}
title={title}
key={id}
/>
<p>수량 :{amount}</p>
<p>금액 :{price * amount} </p>
</li>
))}
</ul>
<span>총 예상결제액 : {totalPrice}원 </span>
<button onClick={handleSubmit}>결제하기</button>
</div>
);
};
▼ src/components/cart/index.tsx
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
const checkedItems = checkboxRefs.reduce<Cart[]>((res, ref, i) => {
if (ref.current!.checked) res.push(items[i]);
return res;
}, []);
setCheckedCartData(checkedItems);
}, [items, formData]);
const [formData, setFormData] = useState<FormData>();
const setAllcheckedFromItems = () => {
if (!formRef.current) return;
const data = new FormData(formRef.current);
const selectedCount = data.getAll("select-item").length;
const allchecked = selectedCount === items.length;
formRef.current.querySelector<HTMLInputElement>(
".cart_select-all"
)!.checked = allchecked;
};
useEffect(() => {
checkedCartData.forEach((item) => {
const itemRef = checkboxRefs.find(
(ref) => ref.current!.dataset.id === item.id
);
if (itemRef) itemRef.current!.checked = true;
});
setAllcheckedFromItems();
}, []);
const setItemsCheckedFromAll = (targetInput: HTMLInputElement) => {
const allchecked = targetInput.checked;
checkboxRefs.forEach((inputElem) => {
inputElem.current!.checked = allchecked;
});
};
const handleCheckboxChanged = (e?: SyntheticEvent) => {
if (!formRef.current) return;
const targetInput = e?.target as HTMLInputElement;
if (targetInput && targetInput.classList.contains("cart_select-all")) {
setItemsCheckedFromAll(targetInput);
} else {
setAllcheckedFromItems();
}
const data = new FormData(formRef.current);
setFormData(data);
};
https://ko.legacy.reactjs.org/docs/portals.html
ReactDOM.createPortal(child, container)
최상단 index.html의 body에 id가 modal인 div를 추가한다. 이 곳에 Portal을 통해 모달 컴포넌트가 렌더링된다.
▼ index.html
<body>
<div id="root"></div>
<div id="modal"></div>
</body>
modal 컴포넌트에 childrun이 ReactNode이고, id가 modal인(위에서 정의) 요소를 createPortal하여 리턴하는 컴포넌트 ModalPortal을 정의한다.
▼ src/components/payment/modal.tsx
const ModalPortal = ({ children }: { children: ReactNode }) => {
return createPortal(children, document.getElementById("modal")!);
};
const PaymentModal = ({ show, proceed, cancel}: { show: boolean; proceed: () => void; cancel: () => void;
}) => {
return (
show && (
<ModalPortal>
<div className={`modal ${show ? "show" : ""}`}>
<div className="modal_inner">
<p>정말 결제하시겠습니까?</p>
<div>
<button onClick={proceed}>예</button>
<button onClick={cancel}>아니오</button>
</div>
</div>
</div>
</ModalPortal>
)
);
};
▼ src/graphql/payment.ts
export const EXECUTE_PAY = gql`
mutation EXCUTE_PAY($info: [string]) {
payInfo(info: $info)
}
▼ src/mocks/handlers.ts
graphql.mutation(EXECUTE_PAY, ({ variables: ids }, res, ctx) => {
ids.forEach((id: string) => {
delete cartData[id];
});
return res(ctx.data(ids));
}),
type PaymentInfos = string[];
const Payment = () => {
const { mutate: executePay } = useMutation((payInfos: PaymentInfos) =>
graphqlFetcher(EXECUTE_PAY, payInfos)
);
const proceed = () => {
const payInfos = checkedCartData.map(({ id }) => id);
executePay(payInfos);
setCheckedCartData([]);
alert("결제가 완료되었습니다.");
navigate("/products", { replace: true });
};