概要
BtoC向けECショップの買い物カゴ機能を実装します。
内容はフロントエンド側の実装方法が中心です。
バックエンドは既に出来ているものとして話を進めます。
図
参考図
杏林堂のネットスーパーの買い物カゴをイメージして実装していきます。赤線部分
ユースケース
以下のユースケースを想定しています。
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レスポンス
{
"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
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
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
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
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
export type CartItemDto={
id:number
customerId:number
productId:number
quantity:number
productName:string
priceWithoutTax:number
priceWithTax:number
}
cartItemResponse
// CartItemを取得した時のレスポンスの型定義
export type CartItemResponse = {
// 各々商品の情報
cartItemDtos : CartItemDto[]
// 商品の合計額(税抜き)
productCost:number
// 配送料(4000円だと無料)
shippingCost:number
// ???
subTotal:number
// 消費税の合計額
tax:number
// お客様が支払う金額
total:number
}
完成後の挙動を確認する(gifアニメ)
左側はブラウザ(viewを確認するため) 右側はMySqlWorkbench(DBを確認するため)です。
顧客のidは25です。
商品の数量を変更する
新しい商品を買い物カゴに追加する
買い物カゴに入っている商品1つを削除する
次やること
商品一覧から買い物カゴに商品を加える
この動きを再現したいですね。
商品IDを渡せば実装できる?(まだ確認していない)
実装時に気になった点
買い物カゴの情報変更する際にapi処理するのが微妙だと思った。バックエンド側の負担が大きくなる懸念がある。
cookie localstorageに一時的に保存したほうが良いのでは?
参考
杏林堂ネットスーパーの買い物カゴ