## 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); ```