[React JS] 6. ์ํ ์์ฑ, ๋ฉ์ธ ํ์ด์ง ์์ฑํ๊ธฐ
1. Shop App์ ์ํ ์์ค ์ฝ๋ ์ถ๊ฐํ๊ธฐ
RegisterPage.js
UploadProductPage.js
DetailProductPagejs
CartPage.js
HistoryPage.js
์ถ๊ฐ
App.jsx์ route ์ถ๊ฐ
Model ์์ฑ
- Product
- Payment
NavItem์ ์ถ๊ฐ
- cart
- upload
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
import { logoutUser } from "../../../store/thunkFunctions";
import { AiOutlineShoppingCart } from "react-icons/ai";
const routes = [
{ to: "/login", name: "๋ก๊ทธ์ธ", auth: false },
{ to: "/register", name: "ํ์๊ฐ์
", auth: false },
{ to: "/product/upload", name: "์
๋ก๋", auth: true },
{
to: "/user/cart",
name: "์นดํธ",
auth: true,
icon: <AiOutlineShoppingCart style={{ fontSize: "1.4rem" }} />,
},
{ to: "", name: "๋ก๊ทธ์์", auth: true },
{ to: "/history", name: "์ฃผ๋ฌธ๋ชฉ๋ก", auth: true },
];
const NavItem = ({ mobile }) => {
const isAuth = useSelector((state) => state.user?.isAuth);
const dispatch = useDispatch();
const navigate = useNavigate();
const handleLogout = () => {
dispatch(logoutUser()).then(() => {
navigate("/login");
});
};
return (
<ul
className={`text-md justify-center w-full flex gap-4 ${
mobile && "flex-col bg-gray-900 h-full"
} items-center`}
>
{routes.map(({ to, name, auth, icon }) => {
if (isAuth !== auth) return null;
if (name === "๋ก๊ทธ์์") {
return (
<li
key={name}
className="py-2 text-center border-b-4 cursor-pointer"
>
<Link onClick={handleLogout}>{name}</Link>
</li>
);
} else if (icon) {
return (
<li
className="relative py-2 text-center border-b-4 cursor-pointer"
key={name}
>
<Link to={to}>
{icon}
<span className="absolute top-0 inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -right-3">
{1}
</span>
</Link>
</li>
);
} else {
return (
<li
key={name}
className="py-2 text-center border-b-4 cursor-pointer"
>
<Link to={to}>{name}</Link>
</li>
);
}
})}
</ul>
);
};
export default NavItem;
ํจํค์ง ์ค์น
frontend: npm i react-dropzone react-image-gallery react-responsive-carousel backend: npm i async multer |
2. ์ํ ์ ๋ก๋ ํ์ด์ง UI ์์ฑํ๊ธฐ
UploadProductPage.js
import React from "react";
const continents = [
{ key: 1, value: "Africa" },
{ key: 2, value: "Europe" },
{ key: 3, value: "Asia" },
{ key: 4, value: "North America" },
{ key: 5, value: "South America" },
{ key: 6, value: "Australia" },
{ key: 7, value: "Antarctica" },
];
const UploadProductPage = () => {
return (
<section>
<div className="text-center m-7">
<h1>์์ ์ํ ์
๋ก๋</h1>
</div>
<form className="mt-6">
<div className="mt-4">
<label htmlFor="title">์ด๋ฆ</label>
<input
className="w-full px-4 py-2 bg-white border rounded-md border-spacing-4"
name="title"
id="title"
/>
</div>
<div className="mt-4">
<label htmlFor="description">์ค๋ช
</label>
<input
className="w-full px-4 py-2 bg-white border rounded-md"
name="description"
id="description"
/>
</div>
<div className="mt-4">
<label htmlFor="price">๊ฐ๊ฒฉ</label>
<input
className="w-full px-4 py-2 bg-white border rounded-md"
type="number"
name="price"
id="price"
/>
</div>
<div className="mt-4">
<label htmlFor="continents">์ง์ญ</label>
<select
className="w-full px-4 mt-2 bg-white border rounded-md"
name="continents"
id="continents"
>
{continents.map((item) => (
<option key={item.key} value={item.key}>
{item.value}
</option>
))}
</select>
</div>
<div className="mt-4">
<button className="w-full px-4 text-white bg-black rounded-md hover:bg-gray-700 py-2">
์์ฑํ๊ธฐ
</button>
</div>
</form>
</section>
);
};
export default UploadProductPage;
3. ์ํ ์ ๋ก๋ ํ์ด์ง ๊ธฐ๋ฅ ์์ฑํ๊ธฐ
State ์์ฑ
state : ์๋ฅผ ๋ค์ด ํผ์ ์์ฑํด์ฃผ๋ ๊ฒ๋ค์ ๊ด๋ฆฌ
const [product, setProduct] = useState({
title: "",
description: "",
price: 0,
continents: 1,
images: [],
});
ํธ๋ค๋ฌ ํจ์ ์์ฑ
const handleChange = (event) => {
const { name, value } = event.target;
setProduct((prevState) => ({
...prevState,
[name]: value,
}));
};
state์์ userdata ๊ฐ์ ธ์ค๊ธฐ (id) -> auth ๋ก ๋ง๋ค์ด์ ธ์์
const userData = useSelector(state => state.user?.userData);
const navigate = useNavigate();
submit
const handleSubmit = async (event) => {
event.preventDefault();
//์ด๋ ๊ฒ ํด๋ ๋๊ณ
const { title, description, price, images, continents } = product;
const body = {
writer: userData.id,
// ์ด๋ ๊ฒ ํด๋ ๋จ
...product,
};
try {
await axiosInstance.post("/products", body);
navigate("/");
} catch (error) {
console.error(error);
}
};
๋ฐฑ๋จ ๋ผ์ฐํฐ
router.post("/", auth, async (req, res, next) => {
try {
const product = new Product(req.body);
await product.save();
return res.sendStatus(200);
} catch (error) {
next(error);
}
});
4. ํ์ผ ์ ๋ก๋ ์ปดํฌ๋ํธ ์์ฑํ๊ธฐ
ํ์ผ์ ๋ก๋ ์ปดํฌ๋ํธ ์์ฑ
components > FileUpload.jsx
์ด๋ฏธ์ง ๋๋กญ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
npm i install react-dropzone |
UploadProductPage.jsx
<FileUpload images={product.images} onImageChange={handleImages}/>
const handleImages = (newImages) => {
setProduct((prevState) => ({
...prevState,
images: newImages,
}));
};
FileUpload.jsx
import React from "react";
import Dropzone from "react-dropzone";
import axiosInstance from "../utils/axios";
const FileUpload = ({ onImageChange, images }) => {
const handleDrop = async (files) => {
let formData = new FormData();
const config = {
header: { "content-type": "multipart/form-data" },
};
formData.append("file", files[0]);
try {
const response = await axiosInstance.post(
"/products/image",
formData,
config
);
onImageChange([...images, response.data.fileName]);
} catch (error) {
console.error(error);
}
};
const handleDelete = (image) => {
const currentIndex = images.indexOf(image);
let newImages = [...images];
newImages.splice(currentIndex, 1);
onImageChange(newImages);
};
return (
<div className="flex gap-4">
<Dropzone onDrop={handleDrop}>
{({ getRootProps, getInputProps }) => (
<section className="min-w-[300px] border flex itmes-center justify-center">
<div {...getRootProps()}>
<input {...getInputProps()} />
<p className="text-3xl">+</p>
</div>
</section>
)}
</Dropzone>
<div className="flex-grow h-[300px] borader flex items-center justify-center overflow-x-scroll overflow-y-hidden">
{images.map((image) => (
<div key={image} onClick={() => handleDelete(image)}>
<img
className="min-w-[300px] h-[300px]"
src={`${import.meta.env.VITE_SERVER_URL}/${image}`}
alt={image}
/>
</div>
))}
</div>
</div>
);
};
export default FileUpload;
multer ์ฌ์ฉ
products router
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/");
},
filename: function (req, file, cb) {
cb(null, `${Date.now()}_${file.originalname}`);
},
});
const upload = multer({ storage: storage }).single("file");
router.post("/image", auth, async (req, res, next) => {
upload(req, res, (err) => {
if (err) {
return req.status(500).send(err);
}
return res.json({ fileName: res.req.file.filename });
});
});
5. Landing Page ์์ฑํ๊ธฐ
์ปดํฌ๋ํธ ์์ฑ (CheckBox, RadioBox, SearchInput, CardItem)
State ์์ฑ
const limit = 4;
const [products, setproducts] = useState([]);
const [skip, setskip] = useState(0);
const [hasMore, sethasMore] = useState(false);
const [filters, setfilters] = useState({
continents: [],
price: [],
});
fetch ํจ์ ์ฌ์ฉํด์ ๋ฐฑ์๋ ์์ฒญํด db์์ product ๊ฐ์ ธ์ด
const fetchProducts = async (
skip,
limit,
loadMore = false,
filters = {},
searchTerm = ""
) => {
const params = {
skip,
limit,
filters,
searchTerm,
};
try {
const response = await axiosInstance.get("/products", { params });
setproducts(response.data.products);
} catch (error) {
console.error(error);
}
};
์ด๋ useEffect dependency array์ ๋น ๊ฐ์ ๋ฃ์ด์ ์ปดํฌ๋ํธ๊ฐ ๋ง์ดํธ๋ ํ ํ๋ฒ๋ง ๋ ๋๋ง๋ ์ ์๊ฒ ํด์ค ๊ฒ
useEffect(() => {
fetchProducts({ skip, limit });
}, []);
๋ฐฑ์๋ ๋ผ์ฐํฐ ์ถ๊ฐ
router.get("/", async (req, res, next) => {
try {
const product = await Product.find().populate('writer');
return res.sendStatus(200).json({
products
});
} catch (error) {
next(error);
}
});
CardItem component
import React from "react";
import { Link } from "react-router-dom";
import ImageSlider from "../../../components/ImageSlider";
const CardItem = ({ product }) => {
return (
<div className="border-[1px] border-gray-300">
<ImageSlider images={product.images} />
<Link to={`/product/${product._id}`}>
<p className="p-1">{product.title}</p>
<p className="p-1">{product.continents}</p>
<p className="p-1 text-xs text-gray-500">{product.price}์</p>
</Link>
</div>
);
};
export default CardItem;
ImageSlider
import React from "react";
import "react-responsive-carousel/lib/styles/carousel.min.css";
import { Carousel } from "react-responsive-carousel";
const ImageSlider = ({ images }) => {
return (
<Carousel autoPlay showThumbs={false} infiniteLoop>
{images.map((image) => (
<div key={image}>
<img
src={`${import.meta.env.VITE_SERVER_URL}/${image}`}
alt={image}
className="w-full max-h-[150px]"
/>
<p className="legend">view product</p>
</div>
))}
</Carousel>
);
};
export default ImageSlider;
<๋๋ณด๊ธฐ ๊ธฐ๋ฅ>
skip : skip + limit (4๊ฐ ์คํต, ์ฒ์ 4๊ฐ๋ ๋ณด์ฌ์ง๊ฒ)
limit : 4
loadmore : true
filters (state) -> ํํฐ ์กฐ๊ฑด์ผ๋ก ๋๋ณด๊ธฐ ๊ฐ์ ธ์ค๋ ๊ฒ
(skip๊ณผ limit์ mongodb์์ ์ ๊ณต)
๋๋ณด๊ธฐ ํด๋ฆญํ๋ฉด ์คํ๋ handleLoadMore
const handleLoadMore = () => {
const body = {
skip: skip + limit,
limit,
loadMore: true,
filters,
};
fetchProducts(body);
setskip(skip + limit);
};
๋ผ์ฐํฐ ์์
router.get("/", async (req, res, next) => {
const order = req.query.order ? req.query.order : "desc";
const sortBy = req.query.sortBy ? req.query.sortBy : "_id";
const limit = req.query.limit ? Number(req.query.limit) : 20;
const skip = req.query.skip ? Number(req.query.skip) : 0;
try {
const products = await Product.find()
.populate("writer")
.sort([[sortBy, order]])
.skip(skip)
.limit(limit);
const productsTotal = await Product.countDocuments();
const hasMore = skip + limit < productsTotal ? true : false;
return res.status(200).json({
products,
hasMore,
});
} catch (error) {
next(error);
}
});
fetchProducts ์์
const fetchProducts = async ({
skip,
limit,
loadMore = false,
filters = {},
searchTerm = "",
}) => {
const params = {
skip,
limit,
filters,
searchTerm,
};
try {
const response = await axiosInstance.get("/products", { params });
if (loadMore) {
setproducts([...products, ...response.data.products]);
} else {
setproducts(response.data.products);
}
sethasMore(response.data.hasMore)
} catch (error) {
console.error(error);
}
};
<CheckBox Filter ๊ธฐ๋ฅ>
LandingPage
<CheckBox
continents={continents}
checkedContinents={filters.continents}
onFilters={(filters) => handleFilters(filters, "continents")}
/>
filterData
const continents = [
{
"_id": 1,
"name": "Africa",
},
{
"_id": 2,
"name": "Europe",
},
{
"_id": 3,
"name": "Asia",
},
{
"_id": 4,
"name": "North America",
},
{
"_id": 5,
"name": "South America",
},
{
"_id": 6,
"name": "Australia",
},
{
"_id": 7,
"name": "Antarctica",
},
];
const prices = [
{
"_id": 1,
"name": "All",
"array": [],
},
{
"_id": 2,
"name": "0~9$",
"array": [0, 9],
},
{
"_id": 3,
"name": "10~99$",
"array": [10, 99],
},
{
"_id": 4,
"name": "100~999$",
"array": [100, 999],
},
{
"_id": 5,
"name": "1000~9999$",
"array": [1000, 9999],
},
{
"_id": 6,
"name": "10000~99999$",
"array": [10000, 99999],
},
];
export {
continents,
prices
}
CheckBox
import React from "react";
const CheckBox = ({ continents, checkedContinents, onFilters }) => {
const handleToggle = (continentId) => {
// ํ์ฌ ๋๋ฅธ CheckBox๊ฐ ์ด๋ฏธ ๋๋ฅธ checkbox์ธ์ง ํ์ธ
const currentIndex = checkedContinents.indexOf(continentId);
const newChecked = [...checkedContinents];
if (currentIndex === -1) {
newChecked.push(continentId);
} else {
// ์ ์ฒด Checked๋ State์์ ํ์ฌ ๋๋ฅธ CheckBox๊ฐ ์๋ค๋ฉด ์ ๊ฑฐ
newChecked.splice(currentIndex, 1);
}
onFilters(newChecked);
};
return (
<div className="p-2 mb-3 bg-gray-100 rounded-md">
{continents?.map(continent => (
<div key={continent._id}>
<input
type="checkbox"
onChange={() => handleToggle(continent._id)}
checked={
checkedContinents.indexOf(continent._id) === -1 ? false : true
}
/>{" "}
<label>{continent.name}</label>
</div>
))}
</div>
);
};
export default CheckBox;
LandingPage functions
const handleFilters = (newFilteredData, category) => {
const newFilters = { ...filters };
newFilters[category] = newFilteredData;
showFilteredResults(newFilters);
setfilters(newFilters);
};
const showFilteredResults = (filters) => {
const body = {
skip: 0,
limit,
filters,
};
fetchProducts(body);
setskip(0);
};
add option in router
router.get("/", async (req, res, next) => {
const order = req.query.order ? req.query.order : "desc";
const sortBy = req.query.sortBy ? req.query.sortBy : "_id";
const limit = req.query.limit ? Number(req.query.limit) : 20;
const skip = req.query.skip ? Number(req.query.skip) : 0;
let findArgs = {};
for(let key in req.query.filters){
if(req.query.filters[key].length > 0){
findArgs[key] = req.query.filters[key];
}
}
try {
const products = await Product.find(findArgs)
.populate("writer")
.sort([[sortBy, order]])
.skip(skip)
.limit(limit);
const productsTotal = await Product.countDocuments(findArgs);
const hasMore = skip + limit < productsTotal ? true : false;
return res.status(200).json({
products,
hasMore,
});
} catch (error) {
next(error);
}
});
<RadioBox ๊ฐ๊ฒฉ ๊ธฐ๋ฅ>
<RadioBox
prices={prices}
checkedPrice={filters.price}
onFilters={(filters) => handleFilters(filters, "price")}
/>
import React from "react";
const RadioBox = ({ prices, checkedPrice, onFilters }) => {
return (
<div className="p-2 mb-3 bg-gray-100 rounded-md">
{prices?.map((price) => (
<div key={price._id}>
<input
checked={checkedPrice === price.array}
onChange={(e) => onFilters(e.target.value)}
type="radio"
id={price._id}
value={price._id}
/>{" "}
<label htmlFor={price._id}>{price.name}</label>
</div>
))}
</div>
);
};
export default RadioBox;
for (let key in req.query.filters) {
if (req.query.filters[key].length > 0) {
if (key === "price") {
findArgs[key] = {
//Greater than equal
$gte: req.query.filters[key][0],
//Less than equal
$lte: req.query.filters[key][1],
};
} else {
findArgs[key] = req.query.filters[key];
}
}
}
<< ๊ฒ์๊ธฐ๋ฅ ๋ง๋ค๊ธฐ >>
searchInput
import React from "react";
const SearchInput = ({ searchTerm, onSearch }) => {
return (
<input
className="p-2 border border-gray-300 rounded-md"
type="text"
placeholder="search title"
onChange={onSearch}
value={searchTerm}
/>
);
};
export default SearchInput;
LandingPage
<SearchInput searchTerm={searchTerm} onSearch={handleSearchTerm} />
const handleSearchTerm = (event) => {
const body = {
skip: 0,
limit,
filters,
searchTerm: event.target.value,
};
setskip(0);
setsearchTerm(event.target.value);
fetchProducts(body);
};
๋ผ์ฐํฐ
if (term) {
findArgs["$text"] = { $search: term };
}
๋ชจ๋ธ ์ธ๋ฑ์ค ์ถ๊ฐ
productSchema.index(
{
title: "text",
description: "text",
},
{
weights: {
title: 5,
description: 1,
},
}
);
'๐จโ๐ป Web Development > React JS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[React JS] 7. ์ํ ๋ํ ์ผ ํ์ด์ง (0) | 2023.09.11 |
---|---|
[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 |
์ต๊ทผ๋๊ธ