-
{item.name}
-
{item.price.toLocaleString()}원
-
-
- {item.favoriteCount}
+
+
+
0 ? item.images[0] : defaultImg} className="itemCardThumbnail" />
+
+
{item.name}
+
{item.price.toLocaleString()}원
+
+
+ {item.favoriteCount}
+
-
+
);
}
diff --git a/src/hooks/useProductDetails.js b/src/hooks/useProductDetails.js
new file mode 100644
index 000000000..df2b516c2
--- /dev/null
+++ b/src/hooks/useProductDetails.js
@@ -0,0 +1,49 @@
+import { useState, useEffect } from "react";
+import { fetchProductById, fetchProductCommentById } from "../api/product";
+import { HttpException } from "../utils/exceptions";
+
+export function useProductDetails(productId) {
+ const [item, setItem] = useState(null);
+ const [comments, setComments] = useState([]);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const getProductById = async (productId) => {
+ try {
+ const data = await fetchProductById(productId);
+ setItem(data);
+ } catch (error) {
+ if (error instanceof HttpException) {
+ setError(error.message);
+ } else {
+ setError("알 수 없는 오류가 발생했습니다.");
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getProductCommentById = async (productId) => {
+ try {
+ const { list } = await fetchProductCommentById(productId);
+ setComments(list);
+ } catch (error) {
+ if (error instanceof HttpException) {
+ setError(error.message);
+ } else {
+ setError("알 수 없는 오류가 발생했습니다");
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (productId) {
+ getProductById(productId);
+ getProductCommentById(productId);
+ }
+ }, [productId]);
+
+ return { item, comments, error, loading };
+}
diff --git a/src/pages/ItemDetailPage/ItemDetailPage.css b/src/pages/ItemDetailPage/ItemDetailPage.css
new file mode 100644
index 000000000..df3c129fb
--- /dev/null
+++ b/src/pages/ItemDetailPage/ItemDetailPage.css
@@ -0,0 +1,35 @@
+.item-detail-wrapper {
+ margin: 0 auto;
+ padding: 45px 0;
+ max-width: 1200px;
+ width: 100%;
+}
+
+.back-to-list {
+ display: block;
+ padding: 12px 43px;
+ margin: 50px auto;
+ background-color: #3692ff;
+ border: none;
+ border-radius: 40px;
+ color: #f3f4f6;
+ font-size: 18px;
+ font-weight: 600;
+ text-align: center;
+ transition: background-color 0.3s ease;
+}
+
+.back-to-list:hover {
+ background-color: #2a7ae4;
+}
+
+.back-to-list-img {
+ padding-left: 5px;
+ vertical-align: middle;
+}
+
+@media (max-width: 1199px) {
+ .item-detail-wrapper {
+ padding: 45px 20px;
+ }
+}
diff --git a/src/pages/ItemDetailPage/ItemDetailPage.js b/src/pages/ItemDetailPage/ItemDetailPage.js
new file mode 100644
index 000000000..973cd64a7
--- /dev/null
+++ b/src/pages/ItemDetailPage/ItemDetailPage.js
@@ -0,0 +1,73 @@
+import "./ItemDetailPage.css";
+import ic_back from "../../assets/icons/ic_back.svg";
+import { useParams } from "react-router-dom";
+import { useProductDetails } from "../../hooks/useProductDetails";
+import { useNavigate } from "react-router-dom";
+import Header from "../../components/ui/Header/Header";
+import ProductInfo from "../../components/section/ProductInfo/ProductInfo";
+import InquiryForm from "../../components/section/InquiryForm/InquiryForm";
+import CommentList from "../../components/section/CommentList/CommentList";
+
+function ItemDetailPage() {
+ const navigate = useNavigate();
+ const { productId } = useParams();
+ const { item, comments, error, loading } = useProductDetails(productId);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!item) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ItemDetailPage;
diff --git a/src/utils/formatRelativeTime.js b/src/utils/formatRelativeTime.js
new file mode 100644
index 000000000..81f82482d
--- /dev/null
+++ b/src/utils/formatRelativeTime.js
@@ -0,0 +1,27 @@
+export function formatRelativeTime(dateString) {
+ const now = new Date();
+ const past = new Date(dateString);
+ const diffInSeconds = Math.floor((now - past) / 1000);
+
+ if (diffInSeconds < 0) {
+ return "방금 전";
+ }
+
+ const intervals = [
+ { label: "년", seconds: 365 * 24 * 60 * 60 },
+ { label: "개월", seconds: 30 * 24 * 60 * 60 },
+ { label: "일", seconds: 24 * 60 * 60 },
+ { label: "시간", seconds: 60 * 60 },
+ { label: "분", seconds: 60 },
+ { label: "초", seconds: 1 },
+ ];
+
+ for (let interval of intervals) {
+ const count = Math.floor(diffInSeconds / interval.seconds);
+ if (count >= 1) {
+ return `${count}${interval.label} 전`;
+ }
+ }
+
+ return "방금 전";
+}