## Milestone1.
### BE
* product.controller.js
```javascript=
// isDeleted false 인것만 가져오기
productController.getProducts = async (req, res) => {
try {
const { page, name } = req.query;
let response = { status: "success" };
const cond = name
? { name: { $regex: name, $options: "i" }, isDeleted: false }
: { isDeleted: false };
let query = Product.find(cond);
if (page) {
query = query.skip((page - 1) * PAGE_SIZE).limit(5);
const totalItemNum = await Product.find(cond).count();
const totalPageNum = Math.ceil(totalItemNum / PAGE_SIZE);
response.totalPageNum = totalPageNum;
}
const productList = await query.exec();
response.data = productList;
res.status(200).json(response);
} catch (error) {
return res.status(400).json({ status: "fail", error: error.message });
}
};
// 실제 삭제 로직
productController.deleteProduct = async (req, res) => {
try {
const productId = req.params.id;
const product = await Product.findByIdAndUpdate(
{ _id: productId },
{ isDeleted: true }
);
if (!product) throw new Error("No item found");
res.status(200).json({ status: "success" });
} catch (error) {
return res.status(400).json({ status: "fail", error: error.message });
}
};
module.exports = productController;
```
* product.api.js
```javascript=
router.delete(
"/:id",
authController.authenticate,
authController.checkAdminPermission,
productController.deleteProduct
);
```
### FE
* productAction.js
```javascript=
const deleteProduct = (id) => async (dispatch) => {
try {
dispatch({ type: types.PRODUCT_DELETE_REQUEST });
const response = await api.delete(`/product/${id}`);
if (response.status !== 200) throw new Error(response.error);
dispatch({
type: types.PRODUCT_DELETE_SUCCESS,
});
dispatch(commonUiActions.showToastMessage("상품 삭제 완료", "success"));
dispatch(getProductList({ page: 1 }));
} catch (error) {
dispatch({ type: types.PRODUCT_DELETE_FAIL, payload: error.error });
dispatch(commonUiActions.showToastMessage(error.error, "error"));
}
};
```
* AdminProduct
```javascript=
const deleteItem = (id) => {
dispatch(productActions.deleteProduct(id));
};
```
## Milestone2. 랜딩페이지
### FE
* ProductAll.js
```javascript=
import React, { useEffect } from "react";
import ProductCard from "../component/ProductCard";
import { Row, Col, Container } from "react-bootstrap";
import { useSearchParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { productActions } from "../action/productAction";
import { commonUiActions } from "../action/commonUiAction";
const ProductAll = () => {
const dispatch = useDispatch();
const productList = useSelector((state) => state.product.productList);
const [query, setQuery] = useSearchParams();
const name = query.get("name");
useEffect(() => {
dispatch(
productActions.getProductList({
name,
})
);
}, [query]);
return (
<Container>
<Row>
{productList.length > 0 ? (
productList.map((item) => (
<Col md={3} sm={12} key={item._id}>
<ProductCard item={item} />
</Col>
))
) : (
<div className="text-align-center empty-bag">
{name === "" ? (
<h2>등록된 상품이 없습니다!</h2>
) : (
<h2>{name}과 일치한 상품이 없습니다!`</h2>
)}
</div>
)}
</Row>
</Container>
);
};
export default ProductAll;
```
* ProductCard.js
```javascript=
import React from "react";
import { useNavigate } from "react-router-dom";
import { currencyFormat } from "../utils/number";
const ProductCard = ({ item }) => {
const navigate = useNavigate();
const showProduct = (id) => {
navigate(`/product/${id}`);
};
return (
<div className="card" onClick={() => showProduct(item._id)}>
<img src={item?.image} alt={item?.image} />
<div>{item?.name}</div>
<div>₩ {currencyFormat(item?.price)}</div>
</div>
);
};
export default ProductCard;
```
## Milestone3. 상품상세페이지
### FE
* ProductDetail.js (다음에 할 카트와 코드가 좀 섞여있습니다. 참고용으로만 보세요)
```javascript=
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Container, Row, Col, Button, Dropdown } from "react-bootstrap";
import { useDispatch, useSelector } from "react-redux";
import { productActions } from "../action/productAction";
import { ColorRing } from "react-loader-spinner";
import { cartActions } from "../action/cartAction";
import { commonUiActions } from "../action/commonUiAction";
import { currencyFormat } from "../utils/number";
import "../style/productDetail.style.css";
const ProductDetail = () => {
const dispatch = useDispatch();
const selectedProduct = useSelector((state) => state.product.selectedProduct);
const loading = useSelector((state) => state.product.loading);
const error = useSelector((state) => state.product.error);
const [size, setSize] = useState("");
const { id } = useParams();
const navigate = useNavigate();
useEffect(() => {
dispatch(productActions.getProductDetail(id));
}, [id]);
if (loading || !selectedProduct)
return (
<ColorRing
visible={true}
height="80"
width="80"
ariaLabel="blocks-loading"
wrapperStyle={{}}
wrapperClass="blocks-wrapper"
colors={["#e15b64", "#f47e60", "#f8b26a", "#abbd81", "#849b87"]}
/>
);
return (
<Container className="product-detail-card">
<Row>
<Col sm={6}>
<img src={selectedProduct.image} className="w-100" alt="image" />
</Col>
<Col className="product-info-area" sm={6}>
<div className="product-info">{selectedProduct.name}</div>
<div className="product-info">
₩ {currencyFormat(selectedProduct.price)}
</div>
<div className="product-info">{selectedProduct.description}</div>
<Dropdown
className="drop-down size-drop-down"
title={size}
align="start"
onSelect={(value) => selectSize(value)}
>
// <Dropdown.Toggle
// className="size-drop-down"
// variant={sizeError ? "outline-danger" : "outline-dark"}
// id="dropdown-basic"
// align="start"
// >
// {size === "" ? "사이즈 선택" : size.toUpperCase()}
// </Dropdown.Toggle>
<Dropdown.Menu className="size-drop-down">
{Object.keys(selectedProduct.stock).length > 0 &&
Object.keys(selectedProduct.stock).map((item) =>
selectedProduct.stock[item] > 0 ? (
<Dropdown.Item eventKey={item}>
{item.toUpperCase()}
</Dropdown.Item>
) : (
<Dropdown.Item eventKey={item} disabled={true}>
{item.toUpperCase()}
</Dropdown.Item>
)
)}
</Dropdown.Menu>
</Dropdown>
// <div className="warning-message">
// {sizeError && "사이즈를 선택해주세요."}
// </div>
<Button variant="dark" className="add-button" onClick={addItemToCart}>
추가
</Button>
</Col>
</Row>
</Container>
);
};
export default ProductDetail;
```
* productAction.js
```javascript=
const getProductDetail = (id) => async (dispatch) => {
try {
dispatch({ type: types.GET_PRODUCT_DETAIL_REQUEST });
const response = await api.get(`/product/${id}`);
if (response.status !== 200) throw new Error(response.error);
dispatch({
type: types.GET_PRODUCT_DETAIL_SUCCESS,
payload: response.data.data,
});
} catch (error) {
dispatch({ type: types.GET_PRODUCT_DETAIL_FAIL, payload: error.error });
dispatch(commonUiActions.showToastMessage(error.error, "error"));
}
};
```
### BE
* product.controller.js
```javascript=
productController.getProductById = async (req, res) => {
try {
const productId = req.params.id;
const product = await Product.findById(productId);
if (!product) throw new Error("No item found");
res.status(200).json({ status: "success", data: product });
} catch (error) {
return res.status(400).json({ status: "fail", error: error.message });
}
};
```
* product.api.js
```javascript=
router.get("/:id", productController.getProductById);
```