diff --git a/src/apis/commentApi.js b/src/apis/commentApi.js new file mode 100644 index 00000000..deba3fbd --- /dev/null +++ b/src/apis/commentApi.js @@ -0,0 +1,20 @@ +import { instance } from "./instance"; + +export const getComments = async ({ + productId, + limit = "3", + cursor = null, +} = {}) => { + if (!productId) { + throw new Error("productId가 필요합니다."); + } + try { + const params = { limit, cursor }; + const { data } = await instance.get(`products/${productId}/comments`, { + params, + }); + return data; + } catch (error) { + throw new Error(`데이터 불러오기 실패: ${error.message}`); + } +}; diff --git a/src/apis/detailApi.js b/src/apis/detailApi.js new file mode 100644 index 00000000..904a364a --- /dev/null +++ b/src/apis/detailApi.js @@ -0,0 +1,11 @@ +import { instance } from "./instance"; + +export const getItemDetail = async (productId) => { + try { + const id = Number(productId); + const { data } = await instance.get(`products/${id}`); + return data; + } catch (error) { + throw new Error(`데이터 불러오기 실패: ${error.message}`); + } +}; diff --git a/src/assets/icons/icon_return.jsx b/src/assets/icons/icon_return.jsx new file mode 100644 index 00000000..880b5e10 --- /dev/null +++ b/src/assets/icons/icon_return.jsx @@ -0,0 +1,17 @@ +export default function ReturnIcon() { + return ( + + + + + ); +} diff --git a/src/assets/icons/icon_vertical-ellipsis.jsx b/src/assets/icons/icon_vertical-ellipsis.jsx new file mode 100644 index 00000000..37b738a1 --- /dev/null +++ b/src/assets/icons/icon_vertical-ellipsis.jsx @@ -0,0 +1,15 @@ +export default function VerticalEllipsis() { + return ( + + + + + + ); +} diff --git a/src/assets/images/no-comments.png b/src/assets/images/no-comments.png new file mode 100644 index 00000000..e7287b95 Binary files /dev/null and b/src/assets/images/no-comments.png differ diff --git a/src/assets/utils.js b/src/assets/utils.js new file mode 100644 index 00000000..c33d87e7 --- /dev/null +++ b/src/assets/utils.js @@ -0,0 +1,18 @@ +export function formatToTimeAgo(date) { + const dayInMs = 1000 * 60 * 60 * 24; + const time = new Date(date).getTime(); + const now = new Date().getTime(); + const diff = Math.round((time - now) / dayInMs); + + const formatter = new Intl.RelativeTimeFormat("ko"); + + return formatter.format(diff, "days"); +} + +export function formatDateToYMD(dateString) { + const date = new Date(dateString); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); // 0-indexed + const day = String(date.getDate()).padStart(2, "0"); + return `${year}.${month}.${day}`; +} diff --git a/src/components/AllProductList.jsx b/src/components/AllProductList.jsx index cbc4175e..12a40829 100644 --- a/src/components/AllProductList.jsx +++ b/src/components/AllProductList.jsx @@ -1,3 +1,4 @@ +import { Link } from "react-router-dom"; import HeartIcon from "../assets/icons/icon_heart"; import noImage from "../assets/images/no-image.png"; @@ -20,7 +21,8 @@ export default function AllProductList({ data = [] }) { if (index >= display.desktop) hiddenClass += " xl:hidden"; return ( -
@@ -39,7 +41,7 @@ export default function AllProductList({ data = [] }) { {item.favoriteCount}

-
+ ); })} diff --git a/src/components/BestProducts.jsx b/src/components/BestProducts.jsx index 9a627115..d6de92ff 100644 --- a/src/components/BestProducts.jsx +++ b/src/components/BestProducts.jsx @@ -1,5 +1,5 @@ +import { Link } from "react-router-dom"; import HeartIcon from "../assets/icons/icon_heart"; -import ProductList from "./AllProductList"; export default function BestProductList({ data = [] }) { const display = { @@ -27,7 +27,8 @@ export default function BestProductList({ data = [] }) { return (
{filteredData.map((item) => ( -
@@ -38,7 +39,7 @@ export default function BestProductList({ data = [] }) { {item.favoriteCount}

-
+ ))}
); diff --git a/src/components/Nav.jsx b/src/components/Nav.jsx index 1fff8130..3d0d6ac7 100644 --- a/src/components/Nav.jsx +++ b/src/components/Nav.jsx @@ -13,7 +13,7 @@ export default function Nav() { className=" max-w-[1920px] mx-auto - px-[2rem] sm:px-[5rem] md:px-[10rem] lg:px-[20rem] + px-[2rem] sm:px-[2rem] md:px-[2rem] lg:px-[20rem] py-4 flex justify-between " diff --git a/src/pages/ProductDetail.jsx b/src/pages/ProductDetail.jsx new file mode 100644 index 00000000..f45d4bae --- /dev/null +++ b/src/pages/ProductDetail.jsx @@ -0,0 +1,244 @@ +import { Link, useParams } from "react-router-dom"; +import { getItemDetail } from "../apis/detailApi"; +import { useEffect, useState } from "react"; +import noImage from "../assets/images/no-image.png"; +import avatar from "../assets/images/avatar.png"; +import HeartIcon from "../assets/icons/icon_heart"; +import VerticalEllipsis from "../assets/icons/icon_vertical-ellipsis"; +import { getComments } from "../apis/commentApi"; +import { formatDateToYMD, formatToTimeAgo } from "../assets/utils"; +import ReturnIcon from "../assets/icons/icon_return"; +import noComment from "../assets/images/no-comments.png"; + +export default function ProductDetail() { + const [item, setItem] = useState(null); + const [comments, setComments] = useState([]); + const [openMenuId, setOpenMenuId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [editingCommentId, setEditingCommentId] = useState(null); + const [editedContent, setEditedContent] = useState(""); + const [commentText, setCommentText] = useState(""); + const { productId } = useParams(); + + const getDetailData = async () => { + try { + setIsLoading(true); + const data = await getItemDetail(productId); + setItem(data); + } catch (error) { + console.error("상품 상세 데이터 오류", error); + } finally { + setIsLoading(false); + } + }; + + const getCommentsData = async () => { + try { + setIsLoading(true); + const data = await getComments({ productId }); + setComments(data.list); + } catch (error) { + console.error("코멘트 데이터 오류", error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (productId) { + getDetailData(); + getCommentsData(); + } + }, [productId]); + + const handleEdit = (id, content) => { + setEditingCommentId(id); + setEditedContent(content); + setOpenMenuId(null); // 드롭다운 닫기 + }; + + if (isLoading) { + return

로딩 중..

; + } + + if (!item) { + return

상품 정보가 없습니다.

; + } + + return ( +
+
+
+ +
+
+
+
+

{item.name}

+ +
+

+ {item.price.toLocaleString()}원 +

+
+
+
상품 소개
+

+ {item.description} +

+
+
+
상품 태그
+
+ {item.tags && + item.tags.map((tag) => ( +
+ #{tag} +
+ ))} +
+
+
+
+ Owner Avatar +
+

{item.ownerNickname}

+

+ {formatDateToYMD(item.updatedAt)} +

+
+
+
+ +

{item.favoriteCount}

+
+
+
+
+
+

문의하기

+