1. cartSlice 작성
import jwtAxios from "../util/jwtUtil";
import { API_SERVER_HOST } from "./todoApi";
const host = `${API_SERVER_HOST}/api/cart`
export const getCartItems = async () => {
const res = await jwtAxios.get(`${host}/items`)
return res.data
}
export const postChangeCart = async (cartItem) => {
const res = await jwtAxios.post(`${host}/change`, cartItem)
return res.data
}
src\api\cartApi.js
HTTP 요청을 보내는 API 함수를 정의
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { getCartItems } from "../api/cartApi";
export const getCartItemsAsync = createAsyncThunk('getCartItemsAsync', () => {
return getCartItems()
})
export const postChangeCartAsync = createAsyncThunk('postChangeCartAsync', (param)=>{
return postChangeCart(param)
})
const initState = []
const cartSlice = createSlice({
name:'cartSlice',
initialState: initState,
extraReducers: (builder) => {
builder.addCase(getCartItemsAsync.fulfilled, (state, action) => {
console.log("getCartItemsAsync.fulfilled")
console.log(action.payload)
return action.payload
})
.addCase(postChangeCartAsync.fulfilled, (state, action) => {
console.log("postChangeCartAsync.fulfilled")
return action.payload
})
}
})
export default cartSlice.reducer
src\slices\cartSlice.js
장바구니 데이터를 Redux 상태로 관리
import { configureStore } from "@reduxjs/toolkit";
import loginSlice from "./slices/loginSlice"
import cartSlice from "./slices/cartSlice"
export default configureStore({
reducer: {
"loginSlice": loginSlice,
"cartSlice": cartSlice
}
});
src\store.js에 cartSlice 추가
2. 장바구니용 컴포넌트 처리
import React, { useEffect } from 'react'
import useCustomLogin from "../../hooks/useCustomLogin"
import { useDispatch, useSelector } from 'react-redux'
import { getCartItemsAsync } from "../../slices/cartSlice"
function CartComponent() {
const {isLogin, loginState} = useCustomLogin()
const dispatch = useDispatch()
const cartItems = useSelector(state => state.cartSlice)
useEffect(() => {
if(isLogin) {
dispatch(getCartItemsAsync())
}
}, [isLogin]);
return (
<div className='w-full'>
{isLogin ?
<div className='flex'>
<div className='m-2 font-extrabold'>{loginState.nickname}'s cart</div>
<div className='bg-orange-600 w-9 text-center text-white font-bold rounded-full m-2'>{cartItems.length}</div>
</div>
:
<div></div>}
</div>
)
}
export default CartComponent
src\components\menus\CartComponent.js
로그인을 하면 사이드바에 닉네임이 함께 출력되고
user1로 로그인했을 때 장바구니 목록에 2개 항목이 존재하는 것을 확인할 수 있다.
3. 장바구니 수량 변경 및 커스텀 훅
import React, { useEffect } from 'react'
import useCustomLogin from "../../hooks/useCustomLogin"
import useCustomCart from '../../hooks/useCustomCart'
function CartComponent() {
const {isLogin, loginState} = useCustomLogin()
const {refreshCart, cartItems} = useCustomCart()
useEffect(() => {
if(isLogin) {
refreshCart()
}
}, [isLogin]);
return (
<div className='w-full'>
{isLogin ?
<div className='flex'>
<div className='m-2 font-extrabold'>{loginState.nickname}'s cart</div>
<div className='bg-orange-600 w-9 text-center text-white font-bold rounded-full m-2'>{cartItems.length}</div>
</div>
:
<div></div>}
</div>
)
}
export default CartComponent
src\components\menus\CartComponent.js
import { useDispatch, useSelector } from "react-redux"
import { getCartItemsAsync, postChangeCartAsync } from "../slices/cartSlice"
const useCustomCart = () => {
const cartItems = useSelector(state => state.cartSlice)
const dispatch = useDispatch()
const refreshCart = () => {
dispatch(getCartItemsAsync())
}
const changeCart = (param) => {
dispatch(postChangeCartAsync(param))
}
return {cartItems, refreshCart, changeCart}
}
export default useCustomCart
src\hooks\useCustomCart.js
커스텀 훅 작성
장바구니(cart) 관련 상태와 기능을 다른 컴포넌트에서 재사용할 수 있게 함
import React, { useEffect } from 'react'
import useCustomLogin from "../../hooks/useCustomLogin"
import useCustomCart from '../../hooks/useCustomCart'
import CartItemComponent from "../cart/CartItemComponent"
function CartComponent() {
const {isLogin, loginState} = useCustomLogin()
const {refreshCart, cartItems} = useCustomCart()
useEffect(() => {
if(isLogin) {
refreshCart()
}
}, [isLogin]);
return (
<div className='w-full'>
{isLogin ?
<div className="flex flex-col">
<div className='w-full flex'>
<div className='font-extrabold text-2xl w-4/5'>{loginState.nickname}'s cart</div>
<div className='bg-orange-600 text-center text-white font-bold w-1/5 rounded-full m-1'>
{cartItems.length}
</div>
</div>
<div>
<ul>
{Array.isArray(cartItems) && cartItems.map((item, index) => (
<li key={index}><CartItemComponent {...item} /></li>
))}
</ul>
</div>
</div>
:
<div></div>
}
</div>
)
}
export default CartComponent
src\components\menus\CartComponent.js
import React from 'react'
function CartItemComponent({cino, pname, price, qty, imageFile}) {
return (
<div>
<div>{cino} --- {pname}</div>
</div>
)
}
export default CartItemComponent
src\components\cart\CartItemComponent.js
상품 번호와 상품명이 함께 출력되도록 구현
import React from 'react'
import { API_SERVER_HOST } from '../../api/todoApi'
const host = API_SERVER_HOST
function CartItemComponent({cino, pname, price, pno, qty, imageFile}) {
const handleClickQty = (amount) => {
}
return (
<li key={cino} className="border-2">
<div className="w-full border-2">
<div className=" m-1 p-1 ">
<img src={`${host}/api/products/view/s_${imageFile}`}/>
</div>
<div className="justify-center p-2 text-xl ">
<div className="justify-end w-full">
</div>
<div>Cart Item No: {cino}</div>
<div>Pno: {pno}</div>
<div>Name: {pname}</div>
<div>Price: {price}</div>
<div className="flex ">
<div className="w-2/3">
Qty: {qty}
</div>
<div>
<button
className="m-1 p-1 text-2xl bg-orange-500 w-8 rounded-lg"
onClick={() => handleClickQty(1)}
>
+
</button>
<button
className="m-1 p-1 text-2xl bg-orange-500 w-8 rounded-lg"
onClick={() => handleClickQty(-1)}
>
-
</button>
</div>
</div>
<div>
<div className="flex text-white font-bold p-2 justify-center">
<button
className="m-1 p-1 text-xl text-white bg-red-500 w-8 rounded-lg"
onClick={() => handleClickQty(-1 * qty)}
>
X
</button>
</div>
<div className='font-extrabold border-t-2 text-right m-2 pr-4'>
{qty * price} 원
</div>
</div>
</div>
</div>
</li>
);
}
export default CartItemComponent;
src\components\cart\CartItemComponent.js
버튼과 이미지 등 저장되어 있는 정보가 출력된다.
이전에 pno 설정이 서버에 작성되어 있지 않아 CartItemListDTO와 CartItemRepository에 pno를 추가했다.
목록에서 Products 클릭하면 오류가 났었는데 productsApi 코드 수정해서 해결했다,,
import axios from "axios"
import { API_SERVER_HOST } from "./todoApi"
import jwtAxios from "../util/jwtUtil"
const host = `${API_SERVER_HOST}/api/products`
export const postAdd = async (product) => {
const header = {headers: {"Content-Type": "multipart/form-data"}}
const res = await jwtAxios.post(`${host}/`, product, header)
return res.data
}
export const getList = async ( pageParam ) => {
const {page,size} = pageParam
const res = await jwtAxios.get(`${host}/list`, {params: {page:page,size:size }})
return res.data
}
export const getOne = async (tno) => {
const res = await jwtAxios.get(`${host}/${tno}` )
return res.data
}
export const putOne = async (pno, product) => {
const header = {headers: {"Content-Type": "multipart/form-data"}}
const res = await jwtAxios.put(`${host}/${pno}`, product, header)
return res.data
}
export const deleteOne = async (pno) => {
const res = await jwtAxios.delete(`${host}/${pno}`)
return res.data
}
src\api\productsApi.js
import React, { useEffect } from 'react'
import useCustomLogin from "../../hooks/useCustomLogin"
import useCustomCart from '../../hooks/useCustomCart'
import CartItemComponent from "../cart/CartItemComponent"
function CartComponent() {
const {isLogin, loginState} = useCustomLogin()
const {refreshCart, cartItems, changeCart} = useCustomCart()
useEffect(() => {
if(isLogin) {
refreshCart()
}
}, [isLogin]);
return (
<div className='w-full'>
{isLogin ?
<div className="flex flex-col">
<div className='w-full flex'>
<div className='font-extrabold text-2xl w-4/5'>{loginState.nickname}'s cart</div>
<div className='bg-orange-600 text-center text-white font-bold w-1/5 rounded-full m-1'>
{cartItems.length}
</div>
</div>
<div>
<ul>
{cartItems.map( item =>
<CartItemComponent {...item}
key={item.cino}
changeCart={changeCart}
email = {loginState.email}
/>)}
</ul>
</div>
</div>
:
<div></div>
}
</div>
)
}
export default CartComponent
src\components\menus\CartComponent.js
- useCustomLogin: 사용자의 로그인 상태와 로그인 정보를 가져옴
- useCustomCart: 카트의 항목을 가져오고, 카트를 업데이트하는 기능을 제공
import React from 'react'
import { API_SERVER_HOST } from '../../api/todoApi'
const host = API_SERVER_HOST
function CartItemComponent({cino, pname, price, pno, qty, imageFile, changeCart, email}) {
const handleClickQty = (amount) => {
changeCart({email:email, cino:cino, pno,pno, qty:qty + amount})
}
return (
<li key={cino} className="border-2">
<div className="w-full border-2">
<div className=" m-1 p-1 ">
<img src={`${host}/api/products/view/s_${imageFile}`}/>
</div>
<div className="justify-center p-2 text-xl ">
<div className="justify-end w-full">
</div>
<div>Cart Item No: {cino}</div>
<div>Pno: {pno}</div>
<div>Name: {pname}</div>
<div>Price: {price}</div>
<div className="flex ">
<div className="w-2/3">
Qty: {qty}
</div>
<div>
<button
className="m-1 p-1 text-2xl bg-orange-500 w-8 rounded-lg"
onClick={() => handleClickQty(1)}
>
+
</button>
<button
className="m-1 p-1 text-2xl bg-orange-500 w-8 rounded-lg"
onClick={() => handleClickQty(-1)}
>
-
</button>
</div>
</div>
<div>
<div className="flex text-white font-bold p-2 justify-center">
<button
className="m-1 p-1 text-xl text-white bg-red-500 w-8 rounded-lg"
onClick={() => handleClickQty(-1 * qty)}
>
X
</button>
</div>
<div className='font-extrabold border-t-2 text-right m-2 pr-4'>
{qty * price} 원
</div>
</div>
</div>
</div>
</li>
);
}
export default CartItemComponent;
src\components\cart\CartItemComponent.js
- handleClickQty: 수량을 변경하는 함수
- changeCart 함수를 호출하여 새로운 수량 값을 부모로 전달
- 수량을 증가/감소시키는 +, - 버튼 및 항목을 삭제하는 X 버튼
- 최종 가격 계산(qty * price)
4. 상품 조회에서 장바구니 추가
src\components\products\ReadComponent.js
'Add Cart' 버튼 추가
import React, { useEffect, useState } from "react";
import { API_SERVER_HOST } from "../../api/todoApi";
import { getOne } from "../../api/productsApi";
import FetchingModal from "../common/FetchingModal";
import useCustomMove from "../../hooks/useCustomMove";
import useCustomCart from "../../hooks/useCustomCart";
import useCustomLogin from "../../hooks/useCustomLogin";
const initState = {
pno: 0,
pname: "",
pdesc: "",
price: 0,
uploadFileNames: [],
};
const host = API_SERVER_HOST;
function ReadComponent({ pno }) {
const [product, setProduct] = useState(initState);
//fetching
const [fetching, setFetching] = useState(false);
//화면 이동 함수
const { moveToList, moveToModify, page, size } = useCustomMove();
//현재 사용자의 장바구니 아이템들
const { cartItems, changeCart } = useCustomCart()
const {loginState} = useCustomLogin()
useEffect(() => {
setFetching(true);
getOne(pno).then((data) => {
console.log(data);
setProduct(data);
setFetching(false);
});
}, [pno]);
const handleClickAddCart = () => {
let qty = 1
const addedItem = cartItems.filter(item => item.pno === parseInt(pno))[0]
if(addedItem){
if(window.confirm('이미 추가된 상품입니다. 추가하시겠습니까?') === false){
return
}
qty = addedItem.qty + 1
}
changeCart({email: loginState.email, qty:qty, pno:pno})
}
return (
<div className="border-2 border-sky-200 mt-10 m-2 p-4">
{fetching ? <FetchingModal /> : <></>}
<div className="flex justify-center mt-10">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">PNO</div>
<div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
{product.pno}
</div>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">PNAME</div>
<div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
{product.pname}
</div>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">PRICE</div>
<div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
{product.price}
</div>
</div>
</div>
<div className="flex justify-center">
<div className="relative mb-4 flex w-full flex-wrap items-stretch">
<div className="w-1/5 p-6 text-right font-bold">PDESC</div>
<div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
{product.pdesc}
</div>
</div>
</div>
<div className="w-full justify-center flex flex-col m-auto items-center">
{product.uploadFileNames.map((imgFile, i) => (
<img
alt="product"
key={i}
className="p-4 w-1/2"
src={`${host}/api/products/view/${imgFile}`}
/>
))}
</div>
<div className="flex justify-end p-4">
<button
type="button"
className="inline-block rounded p-4 m-2 text-xl w-32 text-white bg-green-500"
onClick={handleClickAddCart}
>
Add Cart
</button>
<button
type="button"
className="inline-block rounded p-4 m-2 text-xl w-32 text-white bg-red-500"
onClick={() => moveToModify(pno)}
>
Modify
</button>
<button
type="button"
className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
onClick={() => moveToList({ page, size })}
>
List
</button>
</div>
</div>
);
}
export default ReadComponent;
src\components\products\ReadComponent.js
없던 상품에서 add cart 버튼을 누르면 장바구니에 상품이 추가되고,
이미 추가된 상품에서 add cart 버튼을 다시 누르면 위와 같은 확인창이 뜬다.
확인 버튼을 누르면 장바구니에서 수량과 가격이 증가한다.
'Web' 카테고리의 다른 글
12. React Query와 Recoil (0) | 2024.11.11 |
---|---|
10. 장바구니 API 만들기 (0) | 2024.11.05 |
9. 리액트 소셜 로그인 (0) | 2024.10.31 |
8. 리덕스 툴킷 (0) | 2024.10.30 |
7. 시큐리티와 API 서버 (0) | 2024.10.27 |