diff --git a/public/images/i-back.png b/public/images/i-back.png new file mode 100644 index 000000000..6c5cbec22 Binary files /dev/null and b/public/images/i-back.png differ diff --git a/public/images/i-detail-like.png b/public/images/i-detail-like.png new file mode 100644 index 000000000..abf0a75bc Binary files /dev/null and b/public/images/i-detail-like.png differ diff --git a/public/images/i-menu.png b/public/images/i-menu.png new file mode 100644 index 000000000..04a53ff53 Binary files /dev/null and b/public/images/i-menu.png differ diff --git a/src/api.js b/src/api.js index cb50f5505..186ff29da 100644 --- a/src/api.js +++ b/src/api.js @@ -1,7 +1,21 @@ -export async function getProducts({ page, pageSize, order }) { - const apiUrl = process.env.REACT_APP_API_URL; +// 상품 API +export async function getProducts({ + page = 1, + pageSize = 20, + order = "recent", +}) { + const apiUrl = process.env.REACT_APP_BASE_URL; const query = `page=${page}&pageSize=${pageSize}&orderBy=${order}`; const response = await fetch(`${apiUrl}.vercel.app/products?${query}`); const body = await response.json(); return body; } + +// 댓글 API +export async function getComments({ productId, limit = 3 }) { + const apiUrl = process.env.REACT_APP_BASE_URL; + const query = `${productId}/comments?limit=${limit}`; + const response = await fetch(`${apiUrl}.vercel.app/products/${query}`); + const body = await response.json(); + return body; +} diff --git a/src/components/AddItem.js b/src/components/AddItem.js index d54c9720c..471a43a69 100644 --- a/src/components/AddItem.js +++ b/src/components/AddItem.js @@ -36,25 +36,29 @@ function AddItem() { setInputFileUrl(null); }; - const validAddItem = () => { + const validAddItemVal = () => { // 유효성 검사 if ( - formData.name !== "" && - formData.info !== "" && - formData.price !== "" && - formData.tag !== "" + formData.name.trim() !== "" && + formData.info.trim() !== "" && + formData.price.trim() !== "" && + formData.tag.trim() !== "" ) { + return true; + } + + return false; + }; + + useEffect(() => { + // input 데이터 변경 될때마다 유효성 검사 실행 + if (validAddItemVal()) { setAddItemBtn("on"); setBtnDisabled(false); } else { setAddItemBtn(""); setBtnDisabled(true); } - }; - - useEffect(() => { - // input 데이터 변경 될때마다 유효성 검사 실행 - validAddItem(); }, [formData]); return ( diff --git a/src/components/AllProduct.js b/src/components/AllProduct.js index 1a5c4a9e7..0a171386f 100644 --- a/src/components/AllProduct.js +++ b/src/components/AllProduct.js @@ -16,14 +16,14 @@ const responsivePageSize = () => { } }; -const recent = "recent"; -const favorite = "favorite"; +const RECENT = "recent"; +const FAVORITE = "favorite"; function AllProduct() { const [products, setProducts] = useState([]); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); - const [order, setOrder] = useState(recent); + const [order, setOrder] = useState(RECENT); const [dropArrow, setDropArrow] = useState(""); const [dropDisplay, setDropDisplay] = useState("none"); const [orderTxt, setOrderTxt] = useState("최신순"); @@ -44,7 +44,7 @@ function AllProduct() { setOrderTxt(menuTxt); setDropArrow(""); setDropDisplay("none"); - setOrder(recent); + setOrder(RECENT); }; const handleBestOrder = (e) => { @@ -52,7 +52,7 @@ function AllProduct() { setOrderTxt(menuTxt); setDropArrow(""); setDropDisplay("none"); - setOrder(favorite); + setOrder(FAVORITE); }; const pageNumClick = (page) => { @@ -116,22 +116,26 @@ function AllProduct() { return (
  • -
    - {product.name} -
    -
    -

    {product.name}

    -

    {productPrice}원

    -
    - - {product.favoriteCount} + +
    + {product.name}
    -
    +
    +

    {product.name}

    +

    {productPrice}원

    +
    + + {product.favoriteCount} +
    +
    +
  • ); })} diff --git a/src/components/App.js b/src/components/App.js index 6b55f1e83..628c823cc 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -9,6 +9,7 @@ import "../css/reset.css"; import "../css/style.css"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import NotFoundPage from "./NotFoundPage"; +import ProductDetail from "./ProductDetail"; function App() { return ( @@ -20,7 +21,10 @@ function App() { } /> } /> } /> - } /> + + } /> + } /> + } /> } /> diff --git a/src/components/BestProduct.js b/src/components/BestProduct.js index 1302d01ba..a6467b213 100644 --- a/src/components/BestProduct.js +++ b/src/components/BestProduct.js @@ -1,3 +1,4 @@ +import { Link } from "react-router-dom"; import { getProducts } from "../api.js"; import { useEffect, useState } from "react"; @@ -51,21 +52,23 @@ function BestProduct() { .replace(/\B(? -
    - {product.name} -
    -
    -

    {product.name}

    -

    {productPrice}원

    -
    - - {product.favoriteCount} + +
  • +
    + {product.name}
    -
  • - +
    +

    {product.name}

    +

    {productPrice}원

    +
    + + {product.favoriteCount} +
    +
    + + ); })} diff --git a/src/components/Header.js b/src/components/Header.js index 10f94e732..bed423e48 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -24,8 +24,8 @@ function Header() { { - let { list } = await getProducts(options); - setProducts(list); - }; - - useEffect(() => { - handleLoad({ pageSize, order }); - }, []); - return ( <>
    - - + + +
    ); diff --git a/src/components/ProductDetail.js b/src/components/ProductDetail.js new file mode 100644 index 000000000..b157b6900 --- /dev/null +++ b/src/components/ProductDetail.js @@ -0,0 +1,194 @@ +import { Link, Navigate, useParams } from "react-router-dom"; +import { getProducts } from "../api.js"; +import { getComments } from "../api.js"; +import { useEffect, useState } from "react"; + +function ProductDetail() { + const { productId } = useParams(); + const [products, setProducts] = useState([]); + const [comments, setComments] = useState([]); + const [commentLimit, setCommentLimit] = useState(3); + const [loading, setLoading] = useState(true); + const [inquiryTxt, setInquiryTxt] = useState(""); + const [inquiryBtn, setInquiryBtn] = useState({ + disabled: "true", + btnClass: "", + }); + + const productIdFind = (id) => { + return products.find((product) => product.id === id); + }; + + const foundProduct = productIdFind(Number(productId)); + + const validInquiry = () => { + return inquiryTxt.trim() !== ""; + }; + + // 상품 가져오기 + useEffect(() => { + const handleLoad = async () => { + try { + const { list } = await getProducts(1); + setProducts(list); + } catch (error) { + console.log("Error message:", error); + } finally { + setLoading(false); // 로딩 완료 + } + }; + + handleLoad(); + }, []); + + // 상품 댓글 가져오기 + useEffect(() => { + const handleLoad = async (option) => { + try { + const { list } = await getComments(option); + setComments(list); + } catch (error) { + console.log("Error message:", error); + } finally { + setLoading(false); // 로딩 완료 + } + }; + + handleLoad({ productId, commentLimit }); + }, [commentLimit]); + + // 문의하기 유효성 검사 + useEffect(() => { + if (validInquiry()) { + setInquiryBtn({ + disabled: "false", + btnClass: "on", + }); + } else { + setInquiryBtn({ + disabled: "true", + btnClass: "", + }); + } + }, [inquiryTxt]); + + // 로딩중 + if (loading) { + return ( +
    +

    상품 정보를 찾고 있습니다.

    +
    + ); + } + + // 상품을 찾지 못했을 경우 + if (!foundProduct) { + return ( +
    +

    상품을 찾지 못했습니다.

    + 중고마켓 페이지로 가기 +
    + ); + } + + // 상품 이미지 경로 확인후 잘못된 이미지면 기본 이미지로 적용 + const imgChk = foundProduct.images.join("").includes("jpeg"); // 이미지 경로에 jpeg가 있는지 확인 + const imgUrl = imgChk ? foundProduct.images : "/images/card01-small.png"; + + return ( +
    +
    +
    + {foundProduct.name} +
    +
    +
    +
    +

    {foundProduct.name}

    + +
    + +

    {foundProduct.price.toLocaleString("ko-KR")}원

    +
    +

    + 상품소개 + {foundProduct.description} +

    + 상품 태그 +
      + {foundProduct.tags.map((tag, i) => { + return
    • # {tag}
    • ; + })} +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +