[React JS] 7. ์ํ ๋ํ ์ผ ํ์ด์ง
1. ์ํ ๋ํ ์ผ ํ์ด์ง ์์ฑํ๊ธฐ
App.jsx
<Route path="/product/:productId" element={<DetailProductPage />} />
CardItem์์ ๋๋ฅด๋ฉด ๋ฐฑ์๋ ํธ์ถ
Detailedpage
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import axiosInstance from "../../utils/axios";
import ProductImage from "./Sections/ProductImage";
import ProductInfo from "./Sections/ProductInfo";
const DetailedProductPage = () => {
const { productId } = useParams();
const [product, setproduct] = useState(null);
useEffect(() => {
async function fetchProduct() {
try {
const response = await axiosInstance.get(
`/products/${productId}?type=single`
);
console.log(response);
setproduct(response.data[0]);
} catch (error) {
console.error(error);
}
}
fetchProduct();
}, [productId]);
if (!product) return null;
return (
<section>
<div className="text-center">
<h1 className="p-4 text-2xl">{product.title}</h1>
</div>
<div className="flex gap-4">
{/* ProductImage */}
<div className="w-1/2">
<ProductImage product={product} />
</div>
{/* ProductInfo */}
<div className="w-1/2">
<ProductInfo procuct={product} />
</div>
</div>
</section>
);
};
export default DetailedProductPage;
ProductImage.jsx
import React, { useState, useEffect } from "react";
import ImageGallery from "react-image-gallery";
const ProductImage = ({ product }) => {
const [images, setimages] = useState([]);
useEffect(() => {
if (product?.images?.length > 0) {
let images = [];
product.images.map((imageName) => {
return images.push({
original: `${import.meta.env.VITE_SERVER_URL}/${imageName}`,
thumbnail: `${import.meta.env.VITE_SERVER_URL}/${imageName}`,
});
});
setimages(images);
}
}, [product]);
return <ImageGallery items={images} />;
};
export default ProductImage;
index.css
@import "react-image-gallery/styles/css/image-gallery.css";
๋ผ์ฐํฐ
router.get("/:productId", async (req, res, next) => {
const type = req.query.type;
let productIds = req.params.productId;
// productId๋ฅผ ์ด์ฉํด์ DB์์ productId์ ๊ฐ์ ์ํ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ด
try {
const product = await Product.find({ _id: { $in: productIds } }).populate(
"writer"
);
return res.status(200).send(product);
} catch (error) {
next(error);
}
});
2. ProductInfo ์ปดํฌ๋ํธ ์์ฑํ๊ธฐ
ProductInfo.jsx
import React from "react";
import { useDispatch } from "react-redux";
import { addToCart } from "../../../store/thunkFunctions";
const ProductInfo = ({ product }) => {
const dispatch = useDispatch();
const handleClick = () => {
dispatch(addToCart({ productId: product._id }));
};
return (
<div>
<p className="text-xl text-bold">Product Info</p>
<ul>
<li>
<span className="font-semibold text-gray-900">Price: </span>
{product.price} ์
</li>
<li>
<span className="font-semibold text-gray-900">Sold: </span>
{product.sold} ๊ฐ
</li>
<li>
<span className="font-semibold text-gray-900">Description: </span>
{product.description}
</li>
</ul>
<div className="mt-3">
<button
onClick={handleClick}
className="w-full px-4 py-2 text-white bg-black hover:bg-gray rounded-md"
>
Cart
</button>
</div>
</div>
);
};
export default ProductInfo;
thunkfunction.js
export const addToCart = createAsyncThunk(
"user/addToCart",
async (body, thunkAPI) => {
try {
const response = await axiosInstance.post("/users/cart", body);
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
);
๋ผ์ฐํฐ
router.post("/cart", auth, async (req, res, next) => {
try {
// ๋จผ์ User Collection์ ํด๋น ์ ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
const userInfo = await User.findOne({ _id: req.user._id });
// ๊ฐ์ ธ์จ ์ ๋ณด์์ ์นดํธ์๋ค ๋ฃ์ผ๋ ค๊ณ ํ๋ ์ํ์ด ์ด๋ฏธ ์๋์ง ํ์ธ
let duplicate = false;
userInfo.cart.forEach((item) => {
if (item.id === req.body.productId) {
duplicate = true;
}
});
// ์ํ์ด ์ด๋ฏธ ์์ ๋
if (duplicate) {
const user = await User.findOneAndUpdate(
{ _id: req.user._id, "cart.id": req.body.productId },
{ $inc: { "cart.$.quantity": 1 } },
{ new: true }
);
return res.status(201).send(user.cart);
}
// ์ํ์ด ์์ง ์์ ๋
else {
const user = await User.findOneAndUpdate(
{ _id: req.user._id },
{
$push: {
cart: {
id: req.body.productId,
quantity: 1,
date: Date.now(),
},
},
},
{ new: true }
);
return res.status(201).send(user.cart);
}
} catch (error) {
next(error);
}
});
userSlice.js
.addCase(addToCart.pending, (state) => {
state.isLoading = true;
})
.addCase(addToCart.fulfilled, (state, action) => {
state.isLoading = false;
state.userData.cart = action.payload;
toast.info('์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ๋์์ต๋๋ค.')
})
.addCase(addToCart.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
toast.error(action.payload);
})
3. ์ผํ์นดํธ ํ์ด์ง์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
- ์นดํธ ์์ ๋ค์ด๊ฐ์๋ ์ํ๋ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ฐ์ ธ์ค๊ธฐ
CartPage.js
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getCartItems } from "../../store/thunkFunctions";
const CartPage = () => {
const userData = useSelector((state) => state.user?.userData);
const dispatch = useDispatch();
useEffect(() => {
let cartItemIds = [];
if (userData?.cart && userData.cart.length > 0) {
userData.cart.forEach((item) => {
cartItemIds.push(item.id);
});
const body = {
cartItemIds,
userCart: userData.cart
};
dispatch(getCartItems(body));
}
return () => {
};
}, [dispatch, userData]);
return <div>CartPage</div>;
};
export default CartPage;
thunkfunction.js
export const getCartItems = createAsyncThunk(
"user/getCartItems",
async ({ cartItemIds, userCart }, thunkAPI) => {
try {
const response = await axiosInstance.get(
`/products/${cartItemIds}?type=array`
);
// product ๋ฐ์ดํฐ\ ์์ฒญ๋ณด๋ด์ ๊ฐ์ ธ์ค๋ฉด (response) cart์ ํฉ์ณ์ฃผ๋ ๊ฒ
userCart.forEach((cartItem) => {
response.data.forEach((productDetail, index) => {
if (cartItem.id === productDetail._id) {
response.data[index].quantity = cartItem.quantity;
}
});
});
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
);
product.js
router.get("/:productId", async (req, res, next) => {
const type = req.query.type;
let productIds = req.params.productId;
if (type === "array") {
// id=3123123, 213123,123123
// -> {'123213','123213',123123}
let ids = productIds.split(",");
productIds = ids.map((item) => {
return item;
});
}
// productId๋ฅผ ์ด์ฉํด์ DB์์ productId์ ๊ฐ์ ์ํ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ด
try {
const product = await Product.find({ _id: { $in: productIds } }).populate(
"writer"
);
return res.status(200).send(product);
} catch (error) {
next(error);
}
});
payload / userSlice
.addCase(getCartItems.pending, (state) => {
state.isLoading = true;
})
.addCase(getCartItems.fulfilled, (state, action) => {
state.isLoading = false;
state.cartDetail = action.payload;
})
.addCase(getCartItems.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
toast.error(action.payload);
});
4. ์ผํ์นดํธ ํ์ด์ง ์์ฑํ๊ธฐ
CartPage.js
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getCartItems } from "../../store/thunkFunctions";
const CartPage = () => {
const userData = useSelector((state) => state.user?.userData);
const cartDetail = useSelector((state) => state.user?.cartDetail);
const dispatch = useDispatch();
const [total, settotal] = useState(0);
useEffect(() => {
let cartItemIds = [];
if (userData?.cart && userData.cart.length > 0) {
userData.cart.forEach((item) => {
cartItemIds.push(item.id);
});
const body = {
cartItemIds,
userCart: userData.cart,
};
dispatch(getCartItems(body));
}
return () => {};
}, [dispatch, userData]);
useEffect(() => {
calculateTotal(cartDetail);
}, [cartDetail]);
const calculateTotal = (cartItems) => {
let total = 0;
cartItems.map((item) => (total += item.price * item.quantity));
settotal(total);
};
return (
<section>
<div className="text-center m-7">
<h2 className="text-2xl">My cart</h2>
</div>
{cartDetail?.length > 0 ? (
<>
<div className="mt-10">
<p>
<span className="font-bold">Total: </span>
{total}$
</p>
<button className="px-4 py-2 mt-5 text-white bg-black rounded-md hover:bg-gray-500">
Pay
</button>
</div>
</>
) : (
<p>์ฅ๋ฐ๊ตฌ๋๊ฐ ๋น์์ต๋๋ค..</p>
)}
</section>
);
};
export default CartPage;
CartTable
<CartTable products={cartDetail} onRemoveItem={handleRemoveCartItem}/>
import React from "react";
const CartTable = ({ products, onRemoveItem }) => {
const renderCartImage = (images) => {
if (images.length > 0) {
let image = images[0];
return `${import.meta.env.VITE_SERVER_URL}/${image}`;
}
};
const renderItems =
products.length > 0 &&
products.map((product) => (
<tr>
<td>
<img
className="w-[70px]"
alt="product"
src={renderCartImage(product.images)}
/>
</td>
<td>amount : {product.quantity}</td>
<td>{product.price} $</td>
<td>
<button onClick={() => onRemoveItem(product.id)}>delete</button>
</td>
</tr>
));
return (
<table className="w-full text-sm text-left text-gray-500">
<thead className="border-[1px]">
<tr>
<th>Photo</th>
<th>Amount</th>
<th>Price</th>
<th>Delete</th>
</tr>
</thead>
<tbody>{renderItems}</tbody>
</table>
);
};
export default CartTable;
---------
์ญ์ ๊ธฐ๋ฅ
const handleRemoveCartItem = (productId) => {
dispatch(removeCartItem(productId));
}
<CartTable products={cartDetail} onRemoveItem={handleRemoveCartItem}/>
<button onClick={() => onRemoveItem(product._id)}>delete</button>
thunkfunction.js
export const removeCartItem = createAsyncThunk(
"user/removeCartItem",
async (productId, thunkAPI) => {
try {
const response = await axiosInstance.delete(
`/users/cart?productId=${productId}`
);
// product ๋ฐ์ดํฐ ์์ฒญ๋ณด๋ด์ ๊ฐ์ ธ์ค๋ฉด (response) cart์์ ๋นผ์ฃผ๋ ๊ฒ
response.data.cart.forEach((cartItem) => {
response.data.productInfo.forEach((productDetail, index) => {
if (cartItem.id === productDetail._id) {
response.data.productInfo[index].quantity = cartItem.quantity;
}
});
});
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
);
users.js
router.delete("/cart", auth, async (req, res, next) => {
try {
//๋จผ์ cart์ ์ง์ฐ๋ ค๋ ์ํ ์ง์ฐ๊ธฐ
const userInfo = await User.findOneAndUpdate(
{ _id: req.user._id },
{
$pull: { cart: { id: req.query.productId } },
},
{ new: true }
);
const cart = userInfo.cart;
const array = cart.map((item) => {
return item.id;
});
const productInfo = await Product.find({ _id: { $in: array } }).populate(
"writer"
);
return res.json({
productInfo,
cart,
});
} catch (error) {
next(error);
}
});
5. ๊ฒฐ์ & ํ์คํ ๋ฆฌ
CartPage.js
const handlePaymentClick = () => {
dispatch(payProducts({ cartDetail }));
};
<button
className="px-4 py-2 mt-5 text-white bg-black rounded-md hover:bg-gray-500"
onClick={handlePaymentClick}
>
Thunkfunciton
export const payProducts = createAsyncThunk(
"user/payProducts",
async (body, thunkAPI) => {
try {
const response = await axiosInstance.post(`/users/payment`, body);
return response.data;
} catch (error) {
console.log(error);
return thunkAPI.rejectWithValue(error.response.data || error.message);
}
}
);
users.js
router.post("/payment", auth, async (req, res) => {
// User Collection ์์ History ํ๋ ์์ ๊ฐ๋จํ ๊ฒฐ์ ์ ๋ณด ๋ฃ์ด์ฃผ๊ธฐ
let history = [];
let transactionData = {};
req.body.cartDetail.forEach((item) => {
history.push({
dateOfPurchase: new Date().toISOString(),
name: item.title,
id: item._id,
price: item.price,
quantity: item.quantity,
paymentId: crypto.randomUUID(),
});
});
// Payment Collection์์ ์์ธํ ๊ฒฐ์ ์ ๋ณด ๋ฃ์ด์ฃผ๊ธฐ
transactionData.user = {
id: req.user._id,
name: req.user.name,
email: req.user.email,
};
transactionData.product = history;
// history์ ์ฅ๋ฐ๊ตฌ๋ ์ ๋ณด ๋ฃ์ด์ฃผ๊ณ ์นดํธ ๋น์์ฃผ๊ธฐ
await User.findOneAndUpdate(
{ _id: req.user._id },
{ $push: { history: { $each: history } }, $set: { cart: [] } }
);
// payment collection
const payment = new Payment(transactionData);
const paymentDocs = await payment.save();
let products = [];
paymentDocs.product.forEach((item) => {
products.push({ id: item.id, quantity: item.quantity });
});
async.eachSeries(
products,
async (item) => {
await Product.updateOne(
{ _id: item.id },
{
$inc: {
sold: item.quantity,
},
}
);
},
(err) => {
if (err) return res.status(500).send(err);
return res.sendStatus(200);
}
);
});
userSlice.js
router.post("/payment", auth, async (req, res) => {
// User Collection ์์ History ํ๋ ์์ ๊ฐ๋จํ ๊ฒฐ์ ์ ๋ณด ๋ฃ์ด์ฃผ๊ธฐ
let history = [];
let transactionData = {};
req.body.cartDetail.forEach((item) => {
history.push({
dateOfPurchase: new Date().toISOString(),
name: item.title,
id: item._id,
price: item.price,
quantity: item.quantity,
paymentId: crypto.randomUUID(),
});
});
// Payment Collection์์ ์์ธํ ๊ฒฐ์ ์ ๋ณด ๋ฃ์ด์ฃผ๊ธฐ
transactionData.user = {
id: req.user._id,
name: req.user.name,
email: req.user.email,
};
transactionData.product = history;
// history์ ์ฅ๋ฐ๊ตฌ๋ ์ ๋ณด ๋ฃ์ด์ฃผ๊ณ ์นดํธ ๋น์์ฃผ๊ธฐ
await User.findOneAndUpdate(
{ _id: req.user._id },
{ $push: { history: { $each: history } }, $set: { cart: [] } }
);
// payment collection
const payment = new Payment(transactionData);
const paymentDocs = await payment.save();
let products = [];
paymentDocs.product.forEach((item) => {
products.push({ id: item.id, quantity: item.quantity });
});
async.eachSeries(
products,
async (item) => {
await Product.updateOne(
{ _id: item.id },
{
$inc: {
sold: item.quantity,
},
}
);
},
(err) => {
if (err) return res.status(500).send(err);
return res.sendStatus(200);
}
);
});
history
import React from "react";
import { useSelector } from "react-redux";
import dayjs from 'dayjs'
const HistoryPage = () => {
const userData = useSelector((state) => state.user?.userData);
return (
<section>
<div className="text-center m-7">
<h2 className="text-2xl">History</h2>
</div>
<table className="w-full text-sm text-left text-gray-500">
<thead className="border-[1px]">
<tr>
<th>Payment Id</th>
<th>Price</th>
<th>Date of Purchase</th>
</tr>
</thead>
<tbody>
{userData?.history.map((item) => (
<tr className="border-b" key={item.id}>
<td>{item.id}</td>
<td>{item.price}</td>
<td>{item.quantity}</td>
<td>{dayjs(item.dateOfPurchase).format('YYYY-MM-DD HH:mm:ss')}</td>
</tr>
))}
</tbody>
</table>
</section>
);
};
export default HistoryPage;
'๐จโ๐ป Web Development > React JS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[React JS] 6. ์ํ ์์ฑ, ๋ฉ์ธ ํ์ด์ง ์์ฑํ๊ธฐ (0) | 2023.08.17 |
---|---|
[React JS] 5. ํ์๊ฐ์ /๋ก๊ทธ์ธ/๋ก๊ทธ์์ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ (0) | 2023.07.17 |
[React JS] 4. ๋ฐฑ์๋ ๊ธฐ๋ณธ๊ตฌ์กฐ ์์ฑํ๊ธฐ (0) | 2023.07.15 |
[React JS] [์ฐธ์กฐ] Redux (0) | 2023.06.22 |
[React JS] 3. ํ๋ก ํธ์๋ ๊ธฐ๋ณธ๊ตฌ์กฐ ์์ฑํ๊ธฐ (0) | 2023.06.18 |
์ต๊ทผ๋๊ธ