반응형

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