0
0

More than 1 year has passed since last update.

フロントNext.js バックSpringBoot 商品一覧から商品を選択して買い物カゴに加える動きを実装する

Last updated at Posted at 2023-02-16

概要

BtoC向けEcshopの商品一覧から商品を選択して買いものカゴに加える動きの実装を行います。
記事の内容としましては、フロントエンドの実装方法が中心になります。

2つの記事を元に実装していきます。
繋げるだけだから簡単(なはず)
フロントNext.js バックエンドSpringBootRESTAPIで買い物カゴ実装
バック:SpringBoot フロント:React ページネーション付き商品一覧機能を実装する

ユースケース

杏林堂ネットスーパーの動きを参考にします。
以下gifアニメは杏林堂ネットスーパーを使っている様子

shop-kadai.gif

1.商品一覧機能で表示されている商品を購入ボタンを押す
2.ボタンを押すと、買い物カゴに商品が追加される。

開発環境

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

実装

GetMethodのAPIを実行したときのレスポンスの例

productResponse.json

productResponse.json
{
    "content": [
        {
            "id": 0,
            "name": "国産 豚肩ロース 100g",
            "description": "赤身と脂身が良いバランスで、とても食べやすい部位。",
            "inStock": true,
            "categoryId": 1,
            "price": 250.0,
            "taxRate": 0.08,
            "discountPercent": 0.0,
            "image": "1"
        },
        {
            "id": 1,
            "name": "アメリカ産 豚切りおとし 230g",
            "description": "",
            "inStock": true,
            "categoryId": 1,
            "price": 455.0,
            "taxRate": 0.08,
            "discountPercent": 0.0,
            "image": "1"
        },
        {
            "id": 2,
            "name": "国産 豚肉かたロース 150g",
            "description": "des",
            "inStock": true,
            "categoryId": 1,
            "price": 387.0,
            "taxRate": 0.08,
            "discountPercent": 0.0,
            "image": "2"
        }
    ],
    "pageNo": 0,
    "pageSize": 3,
    "totalElements": 55,
    "totalPages": 19,
    "last": false,
    "categoryId": 1,
    "categoryName": "食肉"
}

cartItemResponse.json

cartItemResponse.json
{
    "cartItemDtos": [
        {
            "id": 45,
            "customerId": 25,
            "productId": 34,
            "quantity": 1,
            "productName": "和牛かたスライス 230g",
            "priceWithoutTax": 2024.0,
            "priceWithTax": 2185.9202
        },
        {
            "id": 46,
            "customerId": 25,
            "productId": 32,
            "quantity": 3,
            "productName": "アメリカ産 牛タン 70g",
            "priceWithoutTax": 558.0,
            "priceWithTax": 602.64
        },
        {
            "id": 47,
            "customerId": 25,
            "productId": 6,
            "quantity": 3,
            "productName": "国産 豚肉スペアリブ 300g",
            "priceWithoutTax": 534.0,
            "priceWithTax": 576.72003
        }
    ],
    "productCost": 5300.0,
    "shippingCost": 0.0,
    "subTotal": 5300.0,
    "tax": 424.0,
    "total": 5724.0
}

実装のために作成したファイル一覧

Proudct

ファイル名 説明
paginationV2.tsx ページネーションのコンポーネント
productContentV2.tsx 商品の情報を表示するコンポーネント
productService.ts axiosで作ったapi処理が入っているファイル productドメインを対象
productResponse.d.ts 商品情報を取得するAPIを実行したときのレスポンスを定義
productView.tsx 商品一覧機能の完成形、productContentとpaginationを組み合わせたコンポーネント
product.d.ts 商品情報単体の型定義ファイル、productResponse上で使う。

CartItem

ファイル名 説明
cartItemServiceV2.ts axiosで作ったapi処理をが入っているファイル cartItemドメインを対象
cartMainItemV3.tsx 金額側のコンポーネントファイル 一つだけ生成
cartSubItemV3.tsx 商品のコンポーネントファイル 商品の数だけコンポーネントを生成する
cartItemDto.d.ts 1つの商品の型定義ファイル REST-APIの出力結果に合わせてる
cartItemResponse.d.ts Apiのレスポンスに関する型定義ファイル 商品の配列 金額情報を定義している
cartItemView.tsx 買い物カゴの完成形 cartMainとcartSubItemを組み合わせたコンポーネント

その他

ファイル名 説明
pages/develop/ProductAndCartItem/index.tsx ページのURL state、コンテキスト、ProductViewとCartItemViewの位置の定義を行っているファイル

実装の方針

データ取得をするApiのpttpレスポンスをStateにして、Contextで子コンポーネントに渡しています。
httpレスポンスが変化するたびにレンダリングが行われるので、適切なタイミングで

現時点ではNext.jsの機能は活用できていないです。ページ機能ぐらいだけです。
実質的にTypeScript+Reactで実装しています。(よくない)

コンポーネントの分け方はこんな感じでとらえてます。
image.png

コード(長いので先に「作成したファイル一覧」を御覧ください)

paginationV2

paginationV2.tsx
import { setHttpClientAndAgentOptions } from "next/dist/server/config";
import { useState, useEffect, useCallback, EventHandler, SetStateAction, Dispatch, useContext } from "react";
import { MouseEventHandler } from "react";
import { mainContext } from './index';

/**
 * @remarks ページネーション コンポーネント
 * 			親コンポーネントに現在のページ数を渡します。
 * 			RestApiを呼ぶ関数は親コンポーネント側で行います。
 *  		           
 * @param totalPage: 総ページ数:固定値として使われる想定
 * @param pageNo:現在のページ番号を取得する useState[pageNo, setPageNo]の1つ目の引数
 * @param setPageNo:現在のページ番号を更新する useState[page,setPage]の2つ目の引数
 */
export default function PaginationV2(props:{totalPage:number ,pageNo:number, setPageNo:Dispatch<SetStateAction<number>>}) {
  let currentPageNo = 1;
  const totalPage: number = 10;
  let paginationContent: JSX.Element[] = [];

  // 前へボタンを押したときの処理
  const backHandler = () => {
	props.setPageNo( props.pageNo - 1)
  };

  // 次へボタンを押したときの処理
  const nextHandler = () => {
	props.setPageNo(props.pageNo + 1)
	console.log(props.pageNo);
  };

  // ページ番号を押したときの処理
  const selectNumberHandler = (pg: number) => {
	props.setPageNo(pg)
  };

  return (
    <>
      <div className="Layout">
		{/* 前のページに戻る-ページ数が最初の場合は表示されない */}
	    <div className="Child">
        {props.pageNo != 1 ? 
            <button className="Font" onClick={backHandler}>前へ</button>
			:<div className="Font"> &emsp; &emsp;</div>  // 「前へ」ボタンが非表示でもレイアウトを維持するための処置。(何もしないと崩れる)
		}
		</div>
		{/*  即時関数を使って1からtotalNumberの数字を作成する。クリックすると押した番号のデータを取得するrestapiを呼ぶ関数を実行する */}
        {(() => {
          for (let i = 0; i < props.totalPage; i++) {
            paginationContent.push(
              <>  
                <button
				className= { props.pageNo== i+1 ?  'SelectedChild' :'Child'} 	  
				  onClick={()=>selectNumberHandler(i+1)}
                >
                  {i + 1}
                </button>
              </>
            );
          }
		  
          return <div>{paginationContent}</div>;
        })()}

		{/* 次のページへ進む-ページ数が最後の場合は表示されない */}
		<div className="Child">
        {props.pageNo != props.totalPage ? 
            <button className="Font" onClick={nextHandler}>次へ</button>
        	:<div className="Font"> &emsp; &emsp;</div>  // レイアウトを維持するために空欄を入れる
		}
	</div>
        &emsp;
      </div>
      <style jsx>{`
        // 大枠 要素を横並びにする
        .Layout {
          display: flex;
          flex-wrap: wrap;
          align-items: center;
        }
        // 子の幅 間隔を決定する
        .Child {
          flex-basis: auto;
          align-items: center;
          background-color: white;
          margin: 3px;
          border: none;
        }
		// 現在のページを明示するために背景色を変える 透明な青色とする
		.SelectedChild{
			flex-basis: auto;
			align-items: center;
			background-color: #1c5bcf85;
			margin: 5px;
			border: none;
		}
		
        // 文字のフォントサイズ 背景色設定
        .Font {
          align-items: center;
          background-color: white;
          text-align: center;
        }
        // マウスオーバー時の背景色は透明な灰色
        button:hover {
          background-color: #1c5bcf85;
        }
		// 子要素のボタン感を無くすために 色と境界線を無くす
        button {
          background-color: white;
          border: none;
        }
      `}</style>
    </>
  );
}


productContent

productContentV2.tsx
import Image from 'next/image';
import { useContext, useState,useEffect } from 'react';
import { mainContext } from '.';
import * as CartItemService2 from "../../../service/cartItemServiceV2";
import { CartItemResponse } from '../../../types/cartItem/cartItemResponse';
interface Props{
	id:number;
	productName:string;
	priceWithoutTax:number;
	priceIncludingTax:string;
	imageURL:string;
}
/**
 * 
 * @param props 商品情報 
 * @returns 商品情報を表示するコンポーネント
 */

export default function ProductContentV2(props : Props){
	// 個数のプルダウンメニューの値
	const[tempNum,setTempNum] = useState<number>(1);
	// 買い物カゴ情報をAPIから取得した後、値をを更新するため利用
	const {setCartItemsResponse}: any = useContext(mainContext);
	// 商品情報の取得に利用
	const {productResponse}:any = useContext(mainContext)
	
	// 商品情報を再取得したときに個数の値を1にする
	useEffect(()=>{
		setTempNum(1)
	},[productResponse])

	// 購入ボタンを押したとき、買い物かごに商品を追加して、買い物かご情報を取得する。
	const addProductHandler = async (event: React.MouseEvent<HTMLButtonElement>) => {
		await addCartItem();
		await getCartItem();
	}

	// 個数のプルダウンリストを変更したときに実行される。”一時的”に値を収納する
	const onChangeHandler = async(e: React.ChangeEvent<HTMLSelectElement>) => {
		await setTempNum(Number(e.target.value))
	}
  // 新しい商品を買い物カゴに追加する
  const addCartItem = async () => {
    const response = await CartItemService2.addProductCart2(
		props.id,tempNum,
      localStorage.getItem("accessToken") as unknown as string
    );
  };

  // ユーザーの買い物カゴ情報を取得する
  const getCartItem = async () => {
    let response: CartItemResponse = await CartItemService2.getCartItems2(
      localStorage.getItem("accessToken") as unknown as string
    );
	 setCartItemsResponse(response);
  };
	return(
		<>
			<div className = 'ContentPadding ContentLayout'>
				<Image src={props.imageURL}
				width={160}
				height={150}
				alt='logo' />
				<div>{props.productName}</div>
				<div className='PriceWithoutTaxAndQuantityLayout'>
					<div>{`税抜` + props.priceWithoutTax  +`円`}</div>
					<div>数量</div>
					<div>
						<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>
					</div>
				</div>
				<div className='PriceIncludingTaxAndBuyButtonLayout'>
				<div>{`税込` + props.priceIncludingTax  +`円`}</div>
				<button onClick={addProductHandler} >購入</button>
				</div>
			</div>

			<style jsx>{`				
				// 大枠の空白部分を担当
				.ContentPadding{
					padding: 20px;
					border:1px solid black;
				}
				// 大枠のレイアウト(縦並び)		
				.ContentLayout{
					position:relative;
					flex:1;
					width:200px;
					height:350px;
				}
				// 税抜き価格と数量のレイアウト(横並び-等間隔)
				.PriceWithoutTaxAndQuantityLayout{
					display:flex;
					justify-content:space-between;
				}
				// 税込み価格と購入ボタンのレイアウト(横並び-等間隔)
				.PriceIncludingTaxAndBuyButtonLayout{
					display:flex;
					justify-content:space-between;
				}
			`}</style>	
		</>
	)
}



productService

productService.tsx
import axios, { AxiosResponse } from 'axios';
import { Product } from '../types/product/product';
import { ProductResponse } from '../types/product/productResponse';


export const getProductsByCategoryId2 = async ( pageNo : number,pageSize:number, categoryId:number) =>{
	return await axios.get(`http://127.0.0.1:5000/api/products?`
					+`pageNo=`+ pageNo
					+`&pageSize=`+ pageSize
					+`&category=`+ categoryId,
					)
					.then((response)=>{
						return response.data as AxiosResponse<ProductResponse>;
					})
					.catch((error)=>{
						return error;
					});
					
};

productResponse

productResponse.d.ts


export type ProductResponse = {
	content : Product[] 
	pageNo : number 
	pageSize : number
	totalElements : number
	totalPages : number 
	isLast : boolean
	categoryId : number
	categoryName : string
}

productView

productView.tsx

import { ProductResponse } from '../../../types/product/productResponse';
import { createContext, SetStateAction, useContext, useEffect, useState } from 'react';
import { mainContext } from '.';
import * as productServiceV2 from '../../../service/productServiceV2';
import { Product } from '../../../types/product/product';
import ProductContentV2 from './productContentV2';
import PaginationV2 from './paginationV2';


export default function ProductView(){
	// 商品情報のhttpレスポンス
	const {productResponse,setProductResponse}:any = useContext(mainContext)
	// ページ番号
	const [pageNo,setPageNo] = useState<number>(1)
	
	// 1ページあたりの商品数
	const pageSize : number = 10;
	
	// 検証のためにカテゴリーIDを1に固定
	const categoryId : number = 1;
	
	// pageNoが変わるとgetProductByCategoryが実行される
	useEffect(() => {
		getProductByCategory()
	},[pageNo])
	
	// ページネーション化されたカテゴリー毎の商品情報たちを取得する。
	const getProductByCategory = async () => {
		const response : ProductResponse= await productServiceV2.getProductsByCategoryId2(pageNo - 1,pageSize,categoryId)
		setProductResponse(response)
	}
	return(
		<>
			<div className="Layout">
			{
				productResponse?.content.map((product: Product,index:number)=>{
					return(
						<div className="Child" key={index}>
								<ProductContentV2 productName={productResponse.content[index].name} priceWithoutTax={productResponse.content[index].price} priceIncludingTax={(productResponse.content[index].price * (1 + productResponse.content[index].taxRate)).toFixed() as string} imageURL={'/sampleProduct1.JPG'} id={productResponse.content[index].id}/>
						</div>
					)			
				})
			}
			</div>
			<div className="PaginationPosition">
			      	<PaginationV2 totalPage={productResponse?.totalPages as number} pageNo={pageNo} setPageNo={setPageNo}/>
			</div>
		<style jsx>{`				
				// 大枠
				.Layout{
					display:flex;
					flex-wrap:wrap;					
					background-color:white;
				}
				// 子の幅を指定 これしないとレイアウトが崩れる
				.Child{
					flex-basis:200px;
					//align-self:stretch;
				}
				.PaginationPosition{
					margin:auto;
					width:50%
				}
			`}</style>		
		</>
	)
}

product.d

.product.d.ts

/**
 *  @remarks 商品情報です ページネーション化されたprooduct.d.tsで使われます。
 * 
 *  @author RYA234 
 * 
 */
export type Product = {
	id:number
	name:string
	description:string
	inStock:boolean
	categoryId:number
	price:number
	taxRate:number
	discountPercent:number
	image:string
}

cartItemService

cartItemServiceV2.tsx
import axios, { AxiosResponse } from 'axios';
import { response } from 'msw';


/**
 * 
 * @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);
      });
	;
	}




cartMainItem

cartMainItemV3.tsx

import { useContext, useState } from "react";
import {mainContext } from './index';
// ショッピングカートの金額情報を表示するコンポーネント
export default function CartMainItemV3(this :any) {	
	
	const {cartItemsResponse} : any= useContext(mainContext)
	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.toFixed()}</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.toFixed()}</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>
	)
}


cartSubItemV

cartSubItemV3.tsx
import React, { useContext, useState } from "react";
import {mainContext } from "./index";
import * as cartItemServiceV2 from "../../../service/cartItemServiceV2";
import { CartItemResponse } from "../../../types/cartItem/cartItemResponse";

// ショッピングカートの商品部分のコンポーネント
export default function CartSubItemV3(props:{index:number}) {

  const { cartItemsResponse, setCartItemsResponse}: any = useContext(mainContext);
  // 数量を変えたときに一時的に収納する値 修正ボタンを押したときこの値に入っている値を処理をする
  let [tempNum, setTempNum] = useState<number>(cartItemsResponse?.cartItemDtos[props.index]?.quantity); 

  // 「削除」ボタンを押した時の処理 コンポーネントを削除して情報の更新
  const onDeleteButtonHandler = async (event: React.MouseEvent<HTMLButtonElement>) => {
    await removeCartItem();
    await getCartItem();
  };

  // 「修正」ボタン押した時の処理-数量を変更して描画更新
  const onQuantityChangeButtonHandler = async (
    event: React.MouseEvent<HTMLButtonElement>
  ) => {
    await updateCartItem();
    await getCartItem();
  };

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

   // データベースからユーザーの買い物カゴの商品情報を取得する関数
   const getCartItem = async () => {
    let response: CartItemResponse = await cartItemServiceV2.getCartItems2(
      localStorage.getItem("accessToken") as unknown as string
    );
    setCartItemsResponse(response);
  };

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

  // 買い物カゴに入った数量を変更
  const updateCartItem = async () => {
    await cartItemServiceV2.updateQuantity2(
      cartItemsResponse.cartItemDtos[props.index].productId,
      tempNum,
      localStorage.getItem("accessToken") as string
    );
  };


  return (
    <div className="Layout">
      <div>{cartItemsResponse?.cartItemDtos[props.index].productName}</div>
      <div>税抜{cartItemsResponse?.cartItemDtos[props.index].priceWithoutTax}</div>
      <div className="IncludingTax">
        税込{cartItemsResponse?.cartItemDtos[props.index].priceWithTax.toFixed()}</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>
  );
}


cartItemDto

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

cartItemReponse

cartItemResponse.d.ts

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

cartItemView

cartItemView.tsx
import { CartItemResponse } from "../../../types/cartItem/cartItemResponse";
import { useContext } from 'react';
import { mainContext } from './index';
import CartMainItemV3 from "./cartMainItemV3";
import CartSubItemV3 from './cartSubItemV3';
import { CartItemDto } from '../../../types/cartItem/cartItemDto';

// 買い物カゴの金額情報と買い物カゴに入れた商品情報たちを束ねてるコンポーネント
export default function CartItemView(){
// carItemのResponse Contextで子コンポーネントに渡す
  const {cartItemsResponse} : any =useContext(mainContext);
// 新しい商品を買い物カゴに追加する
  return (
    <div>
      {/* MainItemsの実装 金額関係 */}
        <CartMainItemV3 />
      {/* SubItemsの実装 買い物カゴにいれた商品 */}
      {cartItemsResponse?.cartItemDtos.map((cartItemDtos:CartItemDto, index:number) => {
        return (
			<>
            	<CartSubItemV3 index={index} />  
			</>
		);
      })}
    </div>
  );
}    

pages/develop/ProductAndCartItem/index

pages/develop/ProductAndCartItem/index.tsx

import { createContext, useState,useEffect } from "react";
import { CartItemResponse } from "../../../types/cartItem/cartItemResponse";
import { Customer } from "../../../types/customer";
import { ProductResponse } from "../../../types/product/productResponse";
import * as CustomerService from "../../../service/customerService";
import * as CartItemService2 from "../../../service/cartItemServiceV2";
import ProductView from "./productView";
import CartItemView from "./cartItemView";

export const mainContext  = createContext(0);
// 商品一覧機能と買い物カゴ機能をつなげる方法を確認するページ
// http://127.0.0.1:3000/develop/cartItemAndProduct
export default function Index() {
  // 商品情報のhttpレスポンス
  const [productResponse, setProductResponse] = useState<ProductResponse>();
  // 買い物カゴ情報のhttpレスポンス
  const [cartItemsResponse, setCartItemsResponse] = useState<CartItemResponse>();
  // 初回時 ログインしてユーザーの買い物かご情報を取得する。
 useEffect(() => {
	loginAndGetCartItem();
		},[]);

  // ログインするときに必要なユーザー情報
  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 () => {
    const response = await CustomerService.signIn2(loginRequestBody);
    localStorage.setItem("accessToken", response);
    await getCartItem();
  };
  // ユーザーのカート情報を取得する
  const getCartItem = async () => {
    let response: CartItemResponse = await CartItemService2.getCartItems2(
      localStorage.getItem("accessToken") as unknown as string
    );
	 setCartItemsResponse(response);
  };
  return(
  	<div className="Layout">
		<div className="ProductViewPosition">
			<mainContext.Provider value={{ productResponse, setProductResponse,setCartItemsResponse }}>
				<ProductView />
			</mainContext.Provider>
		</div>

		<div className="CartItemViewPosition">
		<mainContext.Provider value={{ cartItemsResponse, setCartItemsResponse }}>
			<CartItemView />
		</mainContext.Provider>

		</div>

		<style jsx>{`
			.Layout{
				display:flex;
				flex-direction:row;
			}				
			// ProductViewは中心に置く
			.ProductViewPosition{
				display:flex;
				flex-basis:1000px;
				flex-wrap:wrap;					
				// width:500px;

			}
			// CartItemViewは右側に置く
			.CartItemViewPosition{
				//align-self:stretch;
			}
		`}</style>
  
  	</div>
	);
}


実装後の確認

左側はViewを確認するためブラウザ、右側はDBを確認するためMySqlWorkBenchです。

商品一覧から商品を追加すると、買い物カゴに追加される。

ブラウザとDBともに商品が加えられていることを確認できます。期待した動きができています。

product_add.gif

次やること

商品カテゴリーメニューを実装して、カテゴリー毎に商品を表示できるようにする。
以下杏林堂ネットスーパーの参考図
product_category.gif

参考

杏林堂ネットスーパー

0
0
0

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