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

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

# 図
## 参考図
杏林堂のネットスーパーの買い物カゴをイメージして実装していきます。赤線部分
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/e29ce5e6-5acb-32ec-2ef1-48cdcb3bc18e.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|上記２コンポーネントを呼び出すファイル state、コンテキストの定義を行っている　
cartItemDto.d.ts|１つの商品の型定義ファイル　REST-APIの出力結果に合わせてる
cartItemResponse.d.ts|Apiのレスポンスに関する型定義ファイル　商品の配列　金額情報を定義している

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

https://qiita.com/RYA234/items/c41632ef5101f0db0b5d



## コード（長い！！）
### cartItemService
```ts: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

```tsx: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

```tsx: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

```tsx: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

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

```ts: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/dd62f615-2531-27d8-e565-30b7b08960f7.gif)



## 新しい商品を買い物カゴに追加する
![shop-add.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/79ad0bcd-5dc7-95ea-a79a-56d212aeb391.gif)



## 買い物カゴに入っている商品１つを削除する
![shop-remove.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/13e3cae3-a4f7-3656-24f4-7f47f4824549.gif)



# 次やること

商品一覧から買い物カゴに商品を加える
この動きを再現したいですね。
商品IDを渡せば実装できる？（まだ確認していない）
![shop-kadai.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/d6b5f806-7d39-7013-0c4a-cdf78cd0b3a7.gif)

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

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

https://www.kyorindo-netsuper.jp/