diff --git a/src/App.js b/src/App.js
index 74736b32..b39d2dee 100644
--- a/src/App.js
+++ b/src/App.js
@@ -3,26 +3,32 @@ import HomePage from "./pages/HomePage/HomePage";
import LoginPage from "./pages/LoginPage/LoginPage";
import MarketPage from "./pages/MarketPage/MarketPage";
import AddItemPage from "./pages/AddItemPage/AddItemPage";
+import ItemDetailPage from "./pages/ItemsDetailPage/ItemsDetailPage";
import CommunityFeedPage from "./pages/CommunityFeedPage/CommunityFeedPage";
+import { ThemeProvider } from "styled-components";
import Header from "./components/Layout/Header";
+import theme from "./styles/theme";
function App() {
return (
-
- {/* Global Navigation Bar */}
-
+
+
+ {/* Global Navigation Bar */}
+
-
-
- {/* React Router v6부터는 path="/" 대신 간단하게 `index`라고 표기하면 돼요 */}
- } />
- } />
- } />
- } />
- } />
-
-
-
+
+
+ {/* React Router v6부터는 path="/" 대신 간단하게 `index`라고 표기하면 돼요 */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
);
}
diff --git a/src/api/commentApi.js b/src/api/commentApi.js
new file mode 100644
index 00000000..7b827092
--- /dev/null
+++ b/src/api/commentApi.js
@@ -0,0 +1,12 @@
+//commentApi.js
+import axios from "axios";
+
+const BASE_URL = process.env.REACT_APP_BASE_URL;
+
+export async function getComments(productId) {
+ const response = await axios.get(
+ `${BASE_URL}/products/${productId}/comments?limit=3`
+ );
+
+ return response.data.list || [];
+}
diff --git a/src/api/itemApi.js b/src/api/itemApi.js
index 0a086253..5ebf24ec 100644
--- a/src/api/itemApi.js
+++ b/src/api/itemApi.js
@@ -14,3 +14,9 @@ export async function getProducts({ page, pageSize, orderBy, keyword }) {
throw new Error(`HTTP error: ${error.response?.status || error.message}`);
}
}
+
+export async function getProductInfo(productId) {
+ const response = await axios.get(`${BASE_URL}/products/${productId}`);
+
+ return response.data;
+}
diff --git a/src/assets/images/icons/backToListBtn.svg b/src/assets/images/icons/backToListBtn.svg
new file mode 100644
index 00000000..756c9556
--- /dev/null
+++ b/src/assets/images/icons/backToListBtn.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/images/icons/ic_kebab.svg b/src/assets/images/icons/ic_kebab.svg
new file mode 100644
index 00000000..63a0344c
--- /dev/null
+++ b/src/assets/images/icons/ic_kebab.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/images/icons/ic_user.svg b/src/assets/images/icons/ic_user.svg
new file mode 100644
index 00000000..0480454d
--- /dev/null
+++ b/src/assets/images/icons/ic_user.svg
@@ -0,0 +1,24 @@
+
diff --git a/src/assets/images/icons/img_default.svg b/src/assets/images/icons/img_default.svg
new file mode 100644
index 00000000..5304bee9
--- /dev/null
+++ b/src/assets/images/icons/img_default.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/components/Comments.jsx b/src/components/Comments.jsx
new file mode 100644
index 00000000..e03cf76c
--- /dev/null
+++ b/src/components/Comments.jsx
@@ -0,0 +1,45 @@
+import * as S from "./Comments.styles";
+import ic_user from "../assets/images/icons/ic_user.svg";
+import EditDropdown from "./common/EditDropdown";
+
+export default function Comments({ comments }) {
+ return (
+
+ {comments.map((comment) => (
+
+
+ {comment.content}
+
+
+
+
+
+
+ {comment.writer?.nickname || "알 수 없음"}
+
+ {getTimeAgo(comment.createdAt)}
+
+
+
+ ))}
+
+ );
+}
+
+function getTimeAgo(createdAt) {
+ if (!createdAt) return "방금 전";
+
+ const now = new Date();
+ const createdDate = new Date(createdAt);
+ const diffInMs = now - createdDate; // 밀리초 단위 차이
+ const diffInHours = diffInMs / (1000 * 60 * 60); // 시간 단위 변환
+
+ if (diffInHours < 1) {
+ return "방금 전";
+ } else if (diffInHours < 24) {
+ return `${Math.floor(diffInHours)}시간 전`; // 24시간 미만이면 "n시간 전"
+ } else {
+ const diffInDays = Math.floor(diffInHours / 24);
+ return `${diffInDays}일 전`; // 1일 이상이면 "n일 전"
+ }
+}
diff --git a/src/components/Comments.styles.jsx b/src/components/Comments.styles.jsx
new file mode 100644
index 00000000..a7743b5b
--- /dev/null
+++ b/src/components/Comments.styles.jsx
@@ -0,0 +1,55 @@
+import styled from "styled-components";
+import theme from "../styles/theme";
+
+export const CommentContainer = styled.div`
+ margin-top: 16px;
+`;
+
+export const InquiryItem = styled.div`
+ padding: 14px;
+ border-bottom: 1px solid ${theme.colors.Gray200};
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+`;
+
+export const CommentHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 10px;
+`;
+
+export const CommentText = styled.p`
+ font: ${theme.fonts.H6Regular};
+ color: ${theme.colors.Gray800};
+`;
+
+export const UserInfo = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 12px;
+`;
+
+export const ProfileImage = styled.img`
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+`;
+
+export const UserDetails = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+export const Nickname = styled.span`
+ font: ${theme.fonts.H8};
+ color: ${theme.colors.Gray600};
+`;
+
+export const TimeAgo = styled.span`
+ font-size: 12px;
+ color: ${theme.colors.Gray400};
+ margin-top: 4px;
+`;
diff --git a/src/components/ItemDetail.jsx b/src/components/ItemDetail.jsx
new file mode 100644
index 00000000..ee19a695
--- /dev/null
+++ b/src/components/ItemDetail.jsx
@@ -0,0 +1,91 @@
+// itemDetail.jsx
+import * as S from "./ItemDetail.styles";
+import { useEffect, useState } from "react";
+import { useParams } from "react-router-dom";
+import { getProductInfo } from "../api/itemApi";
+import img_default from "../assets/images/icons/img_default.svg";
+import ic_heart from "../assets/images/icons/ic_heart.svg";
+import ic_user from "../assets/images/icons/ic_user.svg";
+import EditDropdown from "./common/EditDropdown";
+
+export default function ItemDetail() {
+ const { productId } = useParams();
+ const [product, setProduct] = useState({
+ name: "",
+ description: "",
+ price: 0,
+ tags: [],
+ images: null,
+ favoriteCount: 0,
+ createdAt: "",
+ updatedAt: "",
+ ownerNickname: "",
+ });
+ const [isImgError, setIsImgError] = useState(false);
+
+ useEffect(() => {
+ getProductInfo(productId)
+ .then((result) => setProduct(result))
+ .catch((error) => console.error(error));
+ }, [productId]);
+
+ return (
+
+ {/* 아이템 이미지 */}
+ {product.images && !isImgError ? (
+ setIsImgError(false)}
+ onError={() => setIsImgError(true)}
+ />
+ ) : (
+
+
+
+ )}
+
+ {/* 아이템 정보 */}
+
+
+
+
+ {product.name}
+
+
+ {product.price.toLocaleString()}원
+
+
+
+ 상품소개
+ {product.description}
+
+
+ 상품태그
+
+ {product.tags.map((tag) => (
+ #{tag}
+ ))}
+
+
+
+
+
+
+ {/* 판매자 정보 */}
+
+
+
+ {product.ownerNickname}
+ {product.createdAt}
+
+
+ {/* 좋아요버튼 */}
+
+
+ {product.favoriteCount}
+
+
+
+
+ );
+}
diff --git a/src/components/ItemDetail.styles.jsx b/src/components/ItemDetail.styles.jsx
new file mode 100644
index 00000000..87f91321
--- /dev/null
+++ b/src/components/ItemDetail.styles.jsx
@@ -0,0 +1,173 @@
+//itemDetail.styles.jsx
+import styled from "styled-components";
+import theme from "../styles/theme";
+
+export const DetailContainer = styled.div`
+ max-width: 1200px;
+ width: 100%;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 24px;
+ padding-bottom: 40px;
+ border-bottom: 1px solid #e5e7eb;
+`;
+
+export const Image = styled.img`
+ max-width: 500px;
+ width: 100%;
+ height: auto;
+ aspect-ratio: 1/1;
+ border-radius: 16px;
+ background-color: ${theme.colors.Gray200};
+`;
+
+export const NoneImageContainer = styled.div`
+ max-width: 500px;
+ width: 100%;
+ flex-shrink: 0;
+ aspect-ratio: 1/1;
+ border-radius: 16px;
+ background-color: ${theme.colors.Gray200};
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+export const NoneImage = styled.img`
+ width: 60px;
+ height: 60px;
+`;
+
+export const Detail = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 62px;
+`;
+
+export const Header = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding-bottom: 16px;
+ margin-bottom: 16px;
+ border-bottom: 1px solid ${theme.colors.Gray200};
+`;
+
+export const TitleWrapper = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+export const Title = styled.h1`
+ font: ${theme.fonts.H2Bold};
+ color: ${theme.colors.Gray800};
+`;
+
+export const Price = styled.h1`
+ font: ${theme.fonts.H0};
+ color: ${theme.colors.Gray800};
+`;
+
+export const ItemInfo = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ font: ${theme.fonts.H5Regular};
+ color: ${theme.colors.Gray600};
+ min-height: 200px; /* 상품 정보 섹션 최소 높이 유지 */
+`;
+
+export const Label = styled.p`
+ font: ${theme.fonts.H5Bold};
+ color: ${theme.colors.Gray600};
+ padding-bottom: 16px;
+ max-height: 5em; /* 최대 5줄까지만 표시 */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 5; /* 5줄 이상이면 숨김 */
+ -webkit-box-orient: vertical;
+ white-space: normal; /* 줄바꿈 유지 */
+`;
+
+export const Content = styled.p`
+ font: ${theme.fonts.H5Regular};
+ color: ${theme.colors.Gray600};
+`;
+
+export const TagContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+`;
+
+export const TagWrapper = styled.div`
+ display: flex;
+ gap: 10px;
+`;
+
+export const Tag = styled.span`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 36px;
+ padding: 6px 12px 6px 16px;
+ border-radius: 26px;
+ background-color: ${theme.colors.Gray100};
+ color: ${theme.fonts.H5Regular};
+ gap: 10px;
+`;
+
+export const UserWrapper = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 16px 0;
+`;
+
+export const SellerContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+`;
+
+export const SellerInfo = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+export const SellerName = styled.p`
+ font: ${theme.fonts.H7Regular};
+ color: ${theme.colors.Gray600};
+`;
+
+export const Updated = styled.p`
+ font: ${theme.fonts.H7Regular};
+ color: ${theme.colors.Gray400};
+`;
+
+export const LikeWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ border: 1px solid ${theme.colors.Gray200};
+ border-radius: 20px;
+ padding: 6px 12px;
+ background: none;
+ cursor: pointer;
+ transition: 0.2s;
+`;
+
+export const Like = styled.img`
+ width: 24px;
+ height: 24px;
+`;
+
+export const LikeCount = styled.p`
+ font: ${theme.fonts.H5Regular};
+ color: ${theme.colors.Gray500};
+`;
diff --git a/src/components/ProductInquiry.jsx b/src/components/ProductInquiry.jsx
new file mode 100644
index 00000000..15ee47ac
--- /dev/null
+++ b/src/components/ProductInquiry.jsx
@@ -0,0 +1,98 @@
+// ProductInquiry.jsx
+import { useState, useEffect } from "react";
+import styled from "styled-components";
+import theme from "../styles/theme";
+import Comments from "./Comments";
+
+export default function ProductInquiry({ productId }) {
+ const [comment, setComment] = useState("");
+ const [comments, setComments] = useState([]);
+
+ const handleKeyDown = (e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ setComment("");
+ }
+ };
+
+ useEffect(() => {
+ if (!productId) return;
+
+ fetch(
+ `https://panda-market-api.vercel.app/products/${productId}/comments?limit=10`
+ )
+ .then((res) => res.json())
+ .then((data) => {
+ setComments(data.list || []);
+ })
+ .catch(console.error);
+ }, [productId]);
+
+ return (
+
+ 문의하기
+
+ setComment(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+
+
+
+
+
+ );
+}
+
+const Container = styled.div`
+ padding: 16px;
+ width: 1200px;
+`;
+
+const Title = styled.h2`
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 12px;
+`;
+
+const FormContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ width: 100%;
+ position: relative;
+ gap: 24px;
+`;
+
+const Input = styled.textarea`
+ height: 104px;
+ width: 100%;
+ padding: 16px 24px;
+ border: none;
+ border-radius: 12px;
+ resize: none;
+ min-height: 80px;
+ font-size: 14px;
+ color: ${theme.colors.Gray400};
+ background-color: ${theme.colors.Gray100};
+ outline: none;
+`;
+
+const Button = styled.button.attrs(({ $active }) => ({
+ "data-active": $active || undefined,
+}))`
+ width: 74px;
+ height: 42px;
+ padding: 10px;
+ border: none;
+ border-radius: 12px;
+ background-color: ${({ $active }) =>
+ $active ? theme.colors.Primary200 : theme.colors.Gray400};
+ color: white;
+ font-size: 16px;
+ cursor: ${({ $active }) => ($active ? "pointer" : "default")};
+`;
diff --git a/src/components/common/EditDropdown.jsx b/src/components/common/EditDropdown.jsx
new file mode 100644
index 00000000..641bff31
--- /dev/null
+++ b/src/components/common/EditDropdown.jsx
@@ -0,0 +1,53 @@
+// EditDropdown.jsx
+import { useState } from "react";
+import styled from "styled-components";
+import theme from "../../styles/theme";
+import ic_kebab from "../../assets/images/icons/ic_kebab.svg";
+
+export default function EditDropdown({ sortOption, setSortOption }) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+ setIsOpen(!isOpen)}>
+
+
+
+ {isOpen && (
+
+ 수정하기
+ 삭제하기
+
+ )}
+
+ );
+}
+
+const DropdownWrapper = styled.div`
+ position: relative;
+`;
+
+const DropdownButton = styled.button``;
+
+const DropdownListWrapper = styled.div`
+ position: absolute;
+ top: 30px;
+ right: 0;
+ background: white;
+ border-radius: 12px;
+ border: 1px solid ${theme.colors.Gray200};
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
+ width: 130px;
+ z-index: 10;
+`;
+
+const DropdownItem = styled.div`
+ padding: 12px 16px;
+ font: ${theme.fonts.H5Regular};
+ cursor: pointer;
+ text-align: center;
+
+ &:hover {
+ background-color: ${theme.colors.Gray100};
+ }
+`;
diff --git a/src/pages/ItemsDetailPage/ItemsDetailPage.jsx b/src/pages/ItemsDetailPage/ItemsDetailPage.jsx
new file mode 100644
index 00000000..b8e93ef0
--- /dev/null
+++ b/src/pages/ItemsDetailPage/ItemsDetailPage.jsx
@@ -0,0 +1,61 @@
+// itemsDetailPage.jsx
+import styled from "styled-components";
+import ItemDetail from "../../components/ItemDetail";
+import theme from "../../styles/theme";
+import { useNavigate, useParams } from "react-router-dom";
+import backToListBtn from "../../assets/images/icons/backToListBtn.svg";
+import ProductInquiry from "../../components/ProductInquiry";
+
+export default function ItemsPage() {
+ const navigate = useNavigate();
+ const { productId } = useParams();
+
+ return (
+
+ -
+
+
+
+ navigate("/items")}>
+
+
+
+ );
+}
+
+export const ItemContainer = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ padding: 24px;
+ margin: 24px 0 60px;
+ gap: 62px;
+`;
+
+export const Item = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 40px;
+`;
+
+export const BackToList = styled.div`
+ width: 240px;
+ height: 48px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ border-radius: 40px;
+ font: ${theme.fonts.H4Bold};
+ color: ${theme.colors.Gray100};
+ cursor: pointer;
+ img {
+ width: 240px;
+ height: 48px;
+ }
+`;
diff --git a/src/pages/LoginPage/LoginPage.css b/src/pages/LoginPage/LoginPage.css
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/pages/LoginPage/LoginPage.jsx b/src/pages/LoginPage/LoginPage.jsx
index 15f72376..d0497b63 100644
--- a/src/pages/LoginPage/LoginPage.jsx
+++ b/src/pages/LoginPage/LoginPage.jsx
@@ -1,7 +1,14 @@
-import React from "react";
+import logo from "../../assets/images/logo/logo.svg";
-function LoginPage() {
- return
LoginPage
;
+export default function LoginPage() {
+ return (
+
+
판다마켓
+
이메일
+
+
비밀번호
+
+
+
+ );
}
-
-export default LoginPage;
diff --git a/src/styles/theme.jsx b/src/styles/theme.jsx
index d322feee..edba4941 100644
--- a/src/styles/theme.jsx
+++ b/src/styles/theme.jsx
@@ -2,6 +2,7 @@
const theme = {
fonts: {
//weight , size , height , fontfamily
+ H0: "400 40px/42px 'Pretendard', sans-serif",
H1: "700 28px/42px 'Pretendard', sans-serif",
H2Bold: "700 24px/36px 'Pretendard', sans-serif",
H2Regular: "400 24px/36px 'Pretendard', sans-serif",