0
0

More than 1 year has passed since last update.

フロントNext.js バックエンドSpringBootRESTAPIで買い物カゴ実装

Last updated at Posted at 2023-02-12

概要

BtoC向けECショップの買い物カゴ機能を実装します。
内容はフロントエンド側の実装方法が中心です。
バックエンドは既に出来ているものとして話を進めます。

参考図

杏林堂のネットスーパーの買い物カゴをイメージして実装していきます。赤線部分
image.png

ユースケース

以下のユースケースを想定しています。
1.買い物カゴに商品が追加する
2.買い物かごに入っている商品の個数を変更する
3.買い物かご中の商品を一つ削除できる

開発環境

OS:windows10

バックエンド側:
IDE:IntelliJ Community
spring-boot-starter-parent 2.75
java : 11

データベース
mysql:8.0.29
クライアントソフト: MySQL Workbench

フロントエンド:
IDE:VScode

├── @types/node@18.11.15
├── @types/react-dom@18.0.9
├── @types/react@18.0.26
├── axios@1.2.1
├── eslint-config-next@13.0.6
├── eslint@8.29.0
├── next-auth@4.18.7
├── next@13.0.6
├── react-bootstrap@2.7.0
├── react-dom@18.2.0
├── react@18.2.0
├── styled-components@5.3.6
├── styled-jsx@5.1.1
└── typescript@4.9.4

実装

レスポンス

買い物カゴをGETした時のhttpレスポンス

Response.json
{
    "cartItemDtos": [
        {
            "id": 34,
            "customerId": 25,
            "productId": 1,
            "quantity": 1,
            "productName": "アメリカ産 豚切りおとし 230g",
            "priceWithoutTax": 455.0,
            "priceWithTax": 491.40002
        },
        {
            "id": 59,
            "customerId": 25,
            "productId": 5,
            "quantity": 1,
            "productName": "アメリカ産 豚肉かたロースかたまり 300g",
            "priceWithoutTax": 474.0,
            "priceWithTax": 511.92
        }
    ],
    "productCost": 929.0,
    "shippingCost": 300.0,
    "subTotal": 1229.0,
    "tax": 74.31999,
    "total": 1303.32
}

作成したファイルまとめ

ファイル名 説明
cartItemServiceV2.ts axiosで作ったapi処理をが入っているファイル
cartMainItemV2.tsx 金額側のコンポーネントファイル 一つだけ生成
cartSubItemV2.tsx 商品のコンポーネントファイル 商品の数だけコンポーネントを生成する
index.tsx 上記2コンポーネントを呼び出すファイル state、コンテキストの定義を行っている 
cartItemDto.d.ts 1つの商品の型定義ファイル REST-APIの出力結果に合わせてる
cartItemResponse.d.ts Apiのレスポンスに関する型定義ファイル 商品の配列 金額情報を定義している

ログイン系の説明はしません。別の記事に書かれています。

コード(長い!!)

cartItemService

cartItemServiceV2.ts
import axios, { AxiosResponse } from 'axios';
import { response } from 'msw';
import { CartItem } from '../types/cartItem';

/**
 * 
 * @remarks SpringBootで作ったAPIを呼ぶ関数
 * 
 * 
 * @param jwtAccessKey 
 * @returns 
 */
export   function  addProductCart2(productId:number,quantity:number, jwtAccessKey:string){
	return  axios.post(`http://127.0.0.1:5000/api/cart/add`
	+ `?productId=` +productId
	+ `&quantity=` +quantity
	,{},
	{ headers : {
		'Authorization': `Bearer ${jwtAccessKey}`,
		'Request-Method' : 'POST',
		'Content-Type': 'application/json',
		'Access-Control-Allow-Origin': 'http://127.0.0.1:5000/*',
		'Access-Control-Allow-Headers': 'accept, accept-language, content-language, content-type',
		'Access-Control-Allow-Credentials': 'true'
	}}
	).then((response)=>{
		console.log("cartItems is added");
	})
	.catch((error) => {
        console.log("cartItems are not set");
      });
}
 export  const  getCartItems2  = async (jwtAccessKey : string) => {
     return  await axios.get(`http://127.0.0.1:5000/api/cart/all`,
		{
			headers: {
				'Authorization': `Bearer ${jwtAccessKey}`
			}
		}
	)
		.then((response) => {
			{
				 const cartItems: CartItem[] =  response.data;
				console.log(cartItems);
				console.log("service is ended");
				return response.data as AxiosResponse<CartItem[]>;
			}
		})
		.catch((error) => {
			console.log("cartItems are not set");
			 return error;
		});
	
}


export const updateQuantity2 = async (productId:number,quantity:number,jwtAccessKey:string) =>{
	return await axios.put(`http://127.0.0.1:5000/api/cart/update`
	+ `?productId=` +productId
	+ `&quantity=` +quantity,
	{},
	{ headers : {
			'Authorization': `Bearer ${jwtAccessKey}`,
			'Request-Method' : 'PUT',
			'Content-Type': 'application/json',
			'Access-Control-Allow-Origin': 'http://127.0.0.1:5000/*',
			'Access-Control-Allow-Headers': 'accept, accept-language, content-language, content-type',
			'Access-Control-Allow-Credentials': 'true'
	}}
	)
	.then((response) => {
        console.log(response.data);
      })
      .catch((error) => {
        console.log("update is failed");
      });
}
export const removeProduct2 = async (productId:number, jwtAccessKey:string) =>{
	return await axios.delete(`http://127.0.0.1:5000/api/cart/remove`
		+`?productId=`+ productId,
		{ headers : {
				'Authorization': `Bearer ${jwtAccessKey}`
		}}
	)
	.then((response) => {
        console.log(response.data);
      })
	.catch((error) => {
        console.log(error);
      });
	;
	}


cartMainItemV2

cartMainItemV2.tsx

import { useContext, useState } from "react";
import {SubCartItemContext } from '../../pages/develop/cartItem/withApi';
// ショッピングカートの金額情報を表示するコンポーネント
export default function CartMainItemV2(this :any) {	
	const {cartItemsResponse} : any= useContext(SubCartItemContext)
	console.log("cartItemsResponse is" + cartItemsResponse)
	return (
		<div className="Layout" key={cartItemsResponse}>		
			<div className="Title">買い物カゴの中身</div>
			<button className="PurchaseButton">決済へ</button>
			<div className="MoneyInfomation">
				<div>合計額:</div>
				{/* cartItemsResponse.totalだとエラーになる「TypeError: Cannot read properties of undefined」 */}
				<div>{cartItemsResponse?.total}</div>
			</div>
			<div className="MoneyInfomation">
				<div>商品小計:</div>
				<div>{cartItemsResponse?.productCost}</div>
			</div>
			<div className="MoneyInfomation">
				<div>配送料:</div>
				<div>{cartItemsResponse?.shippingCost}</div>
			</div>
			<div className="MoneyInfomation">
				<div>消費税:</div>
				<div>{cartItemsResponse?.tax}</div>
			</div>
			<style jsx>{`
				footer{
				}
				.Layout{
					display: flex;
					flex-direction: column;
					width:200px;
					aligh-items: center;
					border: 1px solid black;
				}
				.Title{
					align-self: center;
				}	
				.PurchaseButton{
					width:100px;
					align-self: center;
				}
				.MoneyInfomation{
					display: flex;
					flex-direction: row;
					justify-content:space-around;
				}
			`}</style>
		</div>
	)
}

cartSubItemV2

cartSubItemV2.tsx
import React, { useContext, useState } from "react";
import { SubCartItemContext } from "../../pages/develop/cartItem/withApi";
import * as cartItemServiceV2 from "../../service/cartItemServiceV2";
import { CartItemResponse } from "../../types/cartItem/cartItemResponse";

// ショッピングカートの商品のコンポーネント
export default function CartSubItemV2(this: any) {
  const { cartItemsResponse, setCartItemsResponse, index }: any = useContext(SubCartItemContext);
  // 数量を変えたときに一時的に収納する値 修正ボタンを押したときこの値に入っている値を処理をする
  let [tempNum, setTempNum] = useState<number>(
    cartItemsResponse.cartItemDtos[index].quantity
  );

  const getCartItem = async () => {
    let response: CartItemResponse = await cartItemServiceV2.getCartItems2(
      localStorage.getItem("accessToken") as unknown as string
    );
    setCartItemsResponse(response);
  };

  // 「削除」ボタンを押した時の挙動 コンポーネントを削除して情報の更新
  const onDeleteButtonHandler = async (
    event: React.MouseEvent<HTMLButtonElement>
  ) => {
    console.log(
      `${cartItemsResponse.cartItemDtos[index].productName}を削除しました。`
    );
    // DELETEのAPIを叩く
    await removeCartItem();
    // GETのAPIを叩く
    await getCartItem();
  };

  // 「修正」ボタン押した時の挙動-コンポーネントを数量を変更して情報の更新
  const onQuantityChangeButtonHandler = async (
    event: React.MouseEvent<HTMLButtonElement>
  ) => {
    await updateCartItem();
    await getCartItem();
  };

  // 商品を一つ削除する
  const removeCartItem = async () => {
    await cartItemServiceV2.removeProduct(
      cartItemsResponse.cartItemDtos[index].productId,
      localStorage.getItem("accessToken") as string
    );
    console.log(removeCartItem);
  };

  // 数量を変更します。
  const updateCartItem = async () => {
    await cartItemServiceV2.updateQuantity2(
      cartItemsResponse.cartItemDtos[index].productId,
      tempNum,
      localStorage.getItem("accessToken") as string
    );
  };

  // 数量変更したときに”一時的”に値を保持する。
  const onChangeHandler = (e: React.FormEvent<HTMLSelectElement>) => {
    setTempNum(e.currentTarget.value as unknown as number);
    console.log(
      `商品名:${cartItemsResponse.cartItemDtos[index].productName}のセレクトボックス表示が${tempNum} に変更されました。)`
    );
  };

  return (
    <div className="Layout">
      <div>{cartItemsResponse.cartItemDtos[index].productName}</div>
      <div>税抜{cartItemsResponse.cartItemDtos[index].priceWithoutTax}</div>
      <div className="IncludingTax">
        税込{cartItemsResponse.cartItemDtos[index].priceWithTax}</div>
      <div className="SubLayout">
        <select
          value={tempNum}
          onChange={(e: React.FormEvent<HTMLSelectElement>) =>
            onChangeHandler(e)
          }
        >
          <option value={tempNum}>{tempNum}</option>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
          <option value="4">4</option>
          <option value="5">5</option>
        </select>
        <button
          className="ModifyButton"
          onClick={onQuantityChangeButtonHandler}
        >
          修正
        </button>
        <button className="DeleteButton" onClick={onDeleteButtonHandler}>
          削除
        </button>
      </div>
      <style jsx>{`
        .Layout {
          display: flex;
          flex-direction: column;
          width: 200px;
          aligh-items: center;
          border: 1px solid black;
        }
        .SubLayout {
          display: flex;
          flex-direction: row;
          column-gap: 10px;
        }
        .IncludingTax {
          color: red;
        }
        .ModifyButton {
          background-color: green;
          color: white;
        }
        .DeleteButton {
          background-color: red;
          color: white;
        }
      `}</style>
    </div>
  );
}

index

index.tsx
import { createContext, useEffect, useState } from "react";
import CartMainItemV2 from "../../../../component/cartItem/cartMainItemV2";
import CartSubItemV2 from "../../../../component/cartItem/cartSubItemV2";
import { Customer } from "../../../../types/customer";
import * as CustomerService from "../../../../service/customerService";
import * as CartItemService2 from "../../../../service/cartItemServiceV2";
import { CartItemResponse } from "../../../../types/cartItem/cartItemResponse";

// 買い物カゴ機能の実装
// Apiと接続して動作するか確認する
// http://127.0.0.1:3000/develop/cartItem/withApi

// Stateをグローバル化するためにContextを作成
// StateはCartMainItemV2とCartSubItemV2にわたす
export const SubCartItemContext = createContext();

export default function Index() {
  // ログイン処理は初回時のみ実行
  useEffect(() => {
    loginAndGetCartItem();
  }, []);

  // carItemのResponse Contextで子コンポーネントに渡す
  const [cartItemsResponse, setCartItemsResponse] =
    useState<CartItemResponse>();

  // ログインするときに必要なユーザー情報
  const loginRequestBody: Customer = {
    email: "aaaba@gmail.com",
    password: "test",
    append: function (arg0: string, inputEmail: string): unknown {
      throw new Error("Function not implemented.");
    },
  };

  // ログインしてカートアイテムの情報を取得する
  const loginAndGetCartItem = async () => {
    //await login2(loginRequestBody);
    const response = await customerService.signIn2(loginRequestBody);
    localStorage.setItem("accessToken", response);
    await getCartItem();
  };

  // 新しい商品を追加する
  const addHandler = async () => {
    await addCartItem();
    await getCartItem();
  };

  // ユーザーのカート情報を取得する
  const getCartItem = async () => {
    let response: CartItemResponse = await cartItemService2.getCartItems2(
      localStorage.getItem("accessToken") as unknown as string
    );
    // console.log("getCartItem" + response);
    setCartItemsResponse(response);
  };

  // 新しい商品を買い物カゴに追加する
  const addCartItem = async () => {
    // 商品ID:5 数量:4の商品情報
		const productId : number = 5 
		const quantity : number = 4 
    const response = await cartItemService2.addProductCart2(
      productId,quantity,
      localStorage.getItem("accessToken") as unknown as string
    );
  };

  return (
    <div>
      {/* MainItemsの実装 金額関係 */}
      <SubCartItemContext.Provider key={-1} value={{ cartItemsResponse }}>
        <CartMainItemV2 />
      </SubCartItemContext.Provider>

      {/* SubItemsの実装 買い物カゴにいれた商品 */}
      {cartItemsResponse?.cartItemDtos.map((cartItems, index) => {
        let subItemvalue = {
          cartItemsResponse,
          setCartItemsResponse,
          index,
        };
        return (
          <SubCartItemContext.Provider key={index} value={subItemvalue}>
            <CartSubItemV2 />
          </SubCartItemContext.Provider>
        );
      })}
      <button onClick={addHandler}>商品を追加する</button>
    </div>
  );
}

cartItemDto

cartItemDto.d.ts
export type CartItemDto={
	id:number
	customerId:number
	productId:number
	quantity:number
	productName:string
	priceWithoutTax:number
	priceWithTax:number
	
}

cartItemResponse

cartItemResponse.d.ts

// CartItemを取得した時のレスポンスの型定義
export type CartItemResponse = {
	// 各々商品の情報
	cartItemDtos : CartItemDto[]
	// 商品の合計額(税抜き)
	productCost:number
	// 配送料(4000円だと無料)
	shippingCost:number
	// ???
	subTotal:number
	// 消費税の合計額
	tax:number
	// お客様が支払う金額
	total:number
}

完成後の挙動を確認する(gifアニメ)

左側はブラウザ(viewを確認するため) 右側はMySqlWorkbench(DBを確認するため)です。
顧客のidは25です。

商品の数量を変更する

shop-update.gif

新しい商品を買い物カゴに追加する

shop-add.gif

買い物カゴに入っている商品1つを削除する

shop-remove.gif

次やること

商品一覧から買い物カゴに商品を加える
この動きを再現したいですね。
商品IDを渡せば実装できる?(まだ確認していない)
shop-kadai.gif

実装時に気になった点

買い物カゴの情報変更する際にapi処理するのが微妙だと思った。バックエンド側の負担が大きくなる懸念がある。
cookie localstorageに一時的に保存したほうが良いのでは?

参考

杏林堂ネットスーパーの買い物カゴ

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0