-
+
handleItemClick(item.id)}
+ />
-
{item.name}
+
handleItemClick(item.id)}
+ >
+ {item.name}
+
{item.price}원
diff --git a/src/components/items/ItemInfo/InquiryComments.jsx b/src/components/items/ItemInfo/InquiryComments.jsx
new file mode 100644
index 000000000..aed526131
--- /dev/null
+++ b/src/components/items/ItemInfo/InquiryComments.jsx
@@ -0,0 +1,42 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { getProductComments } from "../../../core/api";
+import { INITIAL_COMMENTS } from "../../../constants";
+import useFetch from "../../../lib/hooks/useFetch";
+import { countTime } from "../../../lib/utils/countTime";
+
+function InquiryComments({ productId, limit }) {
+ const { data: commentsData } = useFetch(
+ getProductComments,
+ { productId, limit },
+ { list: [INITIAL_COMMENTS] }
+ );
+ const navigate = useNavigate();
+
+ const comments = commentsData.list;
+
+ return (
+
+
+ {comments.map((comment) => (
+ -
+
{comment.content}
+
+
+
+ {comment.writer.nickname}
+
+
{countTime(comment)}
+
+
+
+ ))}
+
+
+
+ );
+}
+export default InquiryComments;
diff --git a/src/components/items/ItemInfo/InquiryInput.jsx b/src/components/items/ItemInfo/InquiryInput.jsx
new file mode 100644
index 000000000..890a96955
--- /dev/null
+++ b/src/components/items/ItemInfo/InquiryInput.jsx
@@ -0,0 +1,48 @@
+import React, { useState } from "react";
+import InquiryComments from "./InquiryComments";
+
+function InquiryInput({ productId }) {
+ const [isFormValid, setIsFormValid] = useState(false);
+ const [inquiryInput, setInquiryInput] = useState("");
+
+ const handleInputChange = (e) => {
+ const value = e.target.value;
+ setInquiryInput(value);
+ setIsFormValid(value.trim().length > 0);
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default InquiryInput;
diff --git a/src/components/items/PageNavBar.jsx b/src/components/items/PageNavBar.jsx
deleted file mode 100644
index 31f9802fc..000000000
--- a/src/components/items/PageNavBar.jsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from "react";
-import "../../pages/Items.css";
-
-function PageNavBar() {
- return (
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default PageNavBar;
diff --git a/src/components/items/Pagination.jsx b/src/components/items/Pagination.jsx
new file mode 100644
index 000000000..a881b1e26
--- /dev/null
+++ b/src/components/items/Pagination.jsx
@@ -0,0 +1,51 @@
+import React from "react";
+
+function Pagination({ currentPage, onPageChange, totalPages }) {
+ const handlePageChange = (page) => {
+ if (page >= 1 && page <= totalPages) {
+ onPageChange(page);
+ }
+ };
+
+ //reflect totalPage, make pageButtons
+ const renderPageButtons = (e) => {
+ const buttons = [];
+ for (let i = 0; i < totalPages; i++) {
+ const pageNumber = i + 1;
+ buttons.push(
+
+ );
+ }
+ return buttons;
+ };
+
+ return (
+
+
+ {renderPageButtons()}
+
+
+ );
+}
+
+export default Pagination;
diff --git a/src/constants.jsx b/src/constants.jsx
new file mode 100644
index 000000000..d1e329956
--- /dev/null
+++ b/src/constants.jsx
@@ -0,0 +1,22 @@
+export const INITIAL_PRODUCTID = {
+ createdAt: "",
+ favoriteCount: 0,
+ ownerId: 0,
+ images: [],
+ tags: [],
+ price: 0,
+ description: "",
+ name: "",
+ id: 0,
+ isFavorite: false,
+};
+
+export const INITIAL_COMMENTS = {
+ id: 0,
+ content: "",
+ createdAt: "",
+ updatedAt: "",
+ writer: { id: 0, image: "", nickname: "" },
+};
+
+export const defaultImageUrl = "https://example.com/...";
diff --git a/src/core/api.jsx b/src/core/api.jsx
index be1974cf3..b79473a0c 100644
--- a/src/core/api.jsx
+++ b/src/core/api.jsx
@@ -1,13 +1,40 @@
-export async function getItems({ page, pageSize, orderBy }) {
+const BASE_URL = "https://panda-market-api.vercel.app";
+
+export async function getProducts({ page, pageSize, orderBy }) {
const query = new URLSearchParams({ page, pageSize, orderBy }).toString();
- const BASE_URL = "https://panda-market-api.vercel.app";
try {
const response = await fetch(`${BASE_URL}/products?${query}`);
const itemData = await response.json();
return itemData;
} catch (error) {
- console.error("리퀘스트 실패:", error);
+ console.error("getProducts 리퀘스트 실패:", error);
+ throw error;
+ }
+}
+
+export async function getProductId({ productId }) {
+ try {
+ const response = await fetch(`${BASE_URL}/products/${productId}`);
+ const itemData = await response.json();
+ return itemData;
+ } catch (error) {
+ console.error("getProductId 리퀘스트 실패:", error);
+ throw error;
+ }
+}
+
+export async function getProductComments({ productId, limit = 3 }) {
+ const query = new URLSearchParams({ limit }).toString();
+
+ try {
+ const response = await fetch(
+ `${BASE_URL}/products/${productId}/comments?${query}`
+ );
+ const itemData = await response.json();
+ return itemData;
+ } catch (error) {
+ console.error("getProductComments 리퀘스트 실패:", error);
throw error;
}
}
diff --git a/src/index.jsx b/src/index.jsx
index 3a300dd1b..dd78cb95a 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,12 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
-import App from "./App";
+import Router from "./Router";
import "./assets/styles/global.css";
-import "./assets/styles/mediaquery.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
-
+
);
diff --git a/src/lib/hooks/useFetch.jsx b/src/lib/hooks/useFetch.jsx
new file mode 100644
index 000000000..e1a1a4939
--- /dev/null
+++ b/src/lib/hooks/useFetch.jsx
@@ -0,0 +1,25 @@
+import { useState, useEffect } from "react";
+
+function useFetch(apiCall, requestParams, initialValue) {
+ const [data, setData] = useState(initialValue);
+ //prevent object's reference
+ const paramsString = JSON.stringify(requestParams);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const parsedParams = JSON.parse(paramsString);
+ const result = await apiCall(parsedParams);
+ setData(result);
+ } catch (error) {
+ console.error("데이터 패칭 실패:", error);
+ }
+ };
+
+ fetchData();
+ }, [apiCall, paramsString]);
+
+ return { data };
+}
+
+export default useFetch;
diff --git a/src/lib/utils/countPageItems.jsx b/src/lib/utils/countPageItems.jsx
new file mode 100644
index 000000000..497502cf8
--- /dev/null
+++ b/src/lib/utils/countPageItems.jsx
@@ -0,0 +1,12 @@
+// count items according to pageSize
+const countPageItems = (mobile, tablet, pc) => {
+ if (window.innerWidth < 768) {
+ return mobile;
+ } else if (window.innerWidth < 1200) {
+ return tablet;
+ } else {
+ return pc;
+ }
+};
+
+export default countPageItems;
diff --git a/src/lib/utils/countTime.jsx b/src/lib/utils/countTime.jsx
new file mode 100644
index 000000000..064e0c755
--- /dev/null
+++ b/src/lib/utils/countTime.jsx
@@ -0,0 +1,14 @@
+export function countTime(params) {
+ const now = new Date();
+ const createdAt = new Date(params.createdAt);
+ const diff = (now - createdAt) / 60000;
+
+ if (diff < 60) {
+ return `${Math.floor(diff)}분 전`;
+ } else if (diff < 1440) {
+ // 1440분 = 24시간
+ return `${Math.floor(diff / 60)}시간 전`;
+ } else {
+ return `${Math.floor(diff / 1440)}일 전`;
+ }
+}
diff --git a/src/pages/AddItem.css b/src/pages/AddItem.css
index dc2c4f696..46224229d 100644
--- a/src/pages/AddItem.css
+++ b/src/pages/AddItem.css
@@ -140,3 +140,30 @@
width: 22px;
height: 24px;
}
+/* mobile */
+@media (max-width: 768px) {
+ /* Additem page */
+ div.upload-container,
+ img.img-preview {
+ max-width: 168px;
+ max-height: 168px;
+ }
+
+ div.upload-content {
+ height: 168px;
+ }
+}
+
+/* tablet */
+@media (min-width: 769px) and (max-width: 1199px) {
+ /* AddItem page */
+ div.upload-container,
+ img.img-preview {
+ max-width: 162px;
+ max-height: 162px;
+ }
+
+ div.upload-content {
+ height: 162px;
+ }
+}
diff --git a/src/pages/AddItem.jsx b/src/pages/AddItem.jsx
index 449020ef7..e66c4ee49 100644
--- a/src/pages/AddItem.jsx
+++ b/src/pages/AddItem.jsx
@@ -1,7 +1,8 @@
import React, { useState, useEffect } from "react";
import "./AddItem.css";
-import ItemImageUpload from "../components/AddItem/ItemImageUpload";
-import ItemDetails from "../components/AddItem/ItemDetails";
+import AddItemImage from "../components/AddItem/AddItemImage";
+import AddItemDetails from "../components/AddItem/AddItemDetails";
+import AddItemTags from "../components/AddItem/AddItemTags";
function AddItem() {
const [uploadedImage, setUploadedImage] = useState(null);
@@ -17,21 +18,6 @@ function AddItem() {
setUploadedImage(file);
};
- const handleDetailsChange = (e) => {
- const { name, value } = e.target;
- setItemDetails((prevDetails) => ({
- ...prevDetails,
- [name]: value,
- }));
- };
-
- const handleTagsChange = (newTags) => {
- setItemDetails((prevDetails) => ({
- ...prevDetails,
- itemTags: newTags,
- }));
- };
-
const handleSubmit = (e) => {
e.preventDefault();
console.log("제출된 상품 정보:", itemDetails);
@@ -54,15 +40,9 @@ function AddItem() {
>
등록
-
-
+
+
+
>
);
diff --git a/src/pages/ItemInfo.css b/src/pages/ItemInfo.css
new file mode 100644
index 000000000..96f977712
--- /dev/null
+++ b/src/pages/ItemInfo.css
@@ -0,0 +1,212 @@
+.item-info-container {
+ display: flex;
+ gap: 24px;
+}
+
+.item-info-img {
+ border-radius: 16px;
+ width: 486px;
+ height: 486px;
+}
+
+.item-info {
+ color: var(--coolgray-800);
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ flex-grow: 1;
+}
+
+.item-info-title {
+ font-weight: 600;
+ line-height: 50px;
+}
+
+.item-info-title > h1 {
+ font-size: 24px;
+}
+
+.item-info-title > h2 {
+ font-size: 40px;
+}
+
+.line {
+ border-bottom: 1px solid var(--coolgray-200);
+}
+
+.item-info-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 22px;
+}
+
+.item-info-content > h3 {
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.item-info-favorite {
+ display: flex;
+ padding: 4px 12px;
+ align-items: center;
+ border-radius: 35px;
+ border: 1px solid var(--coolgray-200);
+ background-color: var(--fff);
+ position: absolute;
+ bottom: 10px;
+ gap: 5px;
+}
+
+.ic-heart {
+ background: url(/src/assets/images/ic_heart.png) no-repeat;
+ background-size: contain;
+ width: 26px;
+ height: 23px;
+}
+
+.item-info-favorite-count {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--coolgray-500);
+}
+
+.tag-info {
+ border-radius: 26px;
+ background-color: var(--coolgray-100);
+ color: var(--coolgray-800);
+ padding: 6px 16px;
+}
+
+/* InquiryInput*/
+.inquiry-form {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.inquiry-form > .inquiry-input {
+ height: 104px;
+ padding: 16px 24px;
+ margin-bottom: 60px;
+}
+
+.inquiry-submit-btn {
+ position: absolute;
+ background-color: var(--coolgray-400);
+ text-align: center;
+ color: var(--fff);
+ bottom: 10px;
+ font-weight: 600;
+ padding: 12px 23px;
+ border-radius: 8px;
+ right: 0;
+ bottom: 0;
+}
+
+/* InquiryComments */
+.inquiry-comments {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 40px;
+}
+
+.comments-ul {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ flex-grow: 1;
+ gap: 24px;
+}
+
+.comments-ul > li {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ color: var(--coolgray-800);
+ gap: 30px;
+}
+
+.comment-user-info {
+ display: grid;
+ grid-template-columns: 1fr 100%;
+ grid-template-areas:
+ "comment-user-img comment-user-name"
+ "comment-user-img comment-user-times";
+ align-items: center;
+ gap: 4px;
+}
+
+.comment-user-img {
+ grid-area: comment-user-img;
+ width: 40px;
+ height: 40px;
+ margin-right: 8px;
+}
+
+.comment-user-name {
+ grid-area: comment-user-name;
+ font-size: 14px;
+ color: var(--coolgray-600);
+}
+
+.comment-user-times {
+ grid-area: comment-user-times;
+ font-size: 12px;
+ color: var(--coolgray-400);
+}
+
+/* back button */
+.back-btn {
+ width: 248px;
+ background-color: var(--brand);
+ border-radius: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--fff);
+ gap: 10px;
+}
+
+.ic-back {
+ background: url(../assets/images/ic_back.png) no-repeat;
+ width: 24px;
+ height: 24px;
+}
+
+/* mediaquery */
+/* mobile */
+@media (max-width: 768px) {
+ .item-info-container {
+ flex-direction: column;
+ }
+
+ .item-info-img {
+ width: 100%;
+ height: 100%;
+ }
+
+ .item-info-favorite {
+ bottom: 0;
+ }
+}
+/* tablet */
+@media (min-width: 769px) and (max-width: 1199px) {
+ .item-info-favorite {
+ bottom: 0;
+ }
+
+ .item-info-content > .tags {
+ display: flex;
+ flex-wrap: wrap;
+ }
+}
diff --git a/src/pages/ItemInfo.jsx b/src/pages/ItemInfo.jsx
new file mode 100644
index 000000000..8040d0ed8
--- /dev/null
+++ b/src/pages/ItemInfo.jsx
@@ -0,0 +1,64 @@
+import React from "react";
+import { useParams } from "react-router-dom";
+import useFetch from "../lib/hooks/useFetch";
+import { getProductId } from "../core/api";
+import { INITIAL_PRODUCTID, defaultImageUrl } from "../constants";
+import InquiryInput from "../components/Items/ItemInfo/InquiryInput";
+import "./ItemInfo.css";
+
+function ItemInfo() {
+ const { productId } = useParams();
+ const { data: productData = INITIAL_PRODUCTID } = useFetch(
+ getProductId,
+ {
+ productId,
+ },
+ INITIAL_PRODUCTID
+ );
+
+ return (
+ <>
+
+
+
+
+
{productData.name}
+ {productData.price}원
+
+
+
+
상품 소개
+
{productData.description}
+
+
+
상품 태그
+
+ {productData.tags.map((tag, index) => (
+ -
+ #{tag}
+
+ ))}
+
+
+
+
+
+ {productData.favoriteCount}
+
+
+
+
+
+ >
+ );
+}
+
+export default ItemInfo;
diff --git a/src/pages/Items.css b/src/pages/Items.css
index 74669b9c7..3309947e4 100644
--- a/src/pages/Items.css
+++ b/src/pages/Items.css
@@ -1,3 +1,7 @@
+html {
+ background-color: #fcfcfc;
+}
+
.main {
max-width: 1200px;
margin: 94px auto;
@@ -7,6 +11,12 @@
}
/* section's title area */
+.best-items-container {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
.all-items-header {
display: flex;
justify-content: flex-start;
@@ -50,7 +60,6 @@
.dropdown-ul {
position: relative;
color: var(--coolgray-800);
- background-color: var(---fff);
border: 1px solid var(--coolgray-200);
border-radius: 12px;
width: 130px;
@@ -66,6 +75,8 @@
.dropdown-ul {
position: absolute;
+ border-radius: 12px;
+ background-color: var(--fff);
margin-top: 5px;
text-align: center;
}
@@ -76,6 +87,7 @@
.dropdown-li {
padding: 12px;
+ border-radius: 12px;
}
.ic-arrow-down {
@@ -109,6 +121,7 @@
gap: 24px;
}
+.item-default-img,
.item-img {
width: 100%;
aspect-ratio: 1 / 1;
@@ -116,6 +129,11 @@
border-radius: 19px;
}
+.item-default-img {
+ background: url("/src/assets/images/img_item_default.png");
+ background-size: cover;
+}
+
.item-container {
display: flex;
flex-direction: column;
@@ -144,8 +162,9 @@
font-size: 0.8rem;
}
-.favorite-count-btn {
- background-image: url("/src/assets/images/ic_heart.png");
+.btn-heart {
+ background: url("/src/assets/images/ic_heart.png") no-repeat;
+ background-size: contain;
width: 16px;
height: 16px;
margin-right: 4px;
@@ -168,3 +187,109 @@
font-weight: 600;
font-size: 1rem;
}
+
+/* mobile */
+@media (max-width: 768px) {
+ /* common */
+ .header {
+ padding: 0 16px;
+ }
+
+ main.main {
+ margin: 94px 16px;
+ }
+
+ .logo-link {
+ font-size: 0;
+ }
+
+ .logo {
+ content: url("/src/assets/images/logo_pandamarket_name.png");
+ width: 81px;
+ height: 27px;
+ margin: 0;
+ }
+
+ /* Items page */
+ .best-items-container > .best-items-list {
+ grid-template-columns: repeat(1, 1fr);
+ }
+
+ .all-items-container > .all-items-header {
+ display: grid;
+ grid-template-columns: 1fr 91px 42px;
+ grid-template-rows: 42px;
+ grid-template-areas:
+ "section-title login-btn login-btn"
+ "search search dropdown-orderby";
+ gap: 8px;
+ height: auto;
+ }
+
+ h1.section-title {
+ grid-area: section-title;
+ }
+
+ .all-items-header > a.login-btn {
+ grid-area: login-btn;
+ margin-right: 0;
+ }
+
+ input.search {
+ grid-area: search;
+ margin-right: 0;
+ width: 100%;
+ }
+
+ div.ic-search {
+ bottom: 37px;
+ left: 19px;
+ }
+
+ .dropdown-ul {
+ right: 16px;
+ }
+
+ div.dropdown-orderby {
+ grid-area: dropdown-orderby;
+ }
+
+ .dropdown-orderby > .dropdown-btn {
+ width: 42px;
+ height: 42px;
+ font-size: 0;
+ background: url("/src/assets/images/ic_sort.png") no-repeat 50%;
+ }
+
+ div.ic-arrow-down {
+ display: none;
+ }
+
+ .all-items-container > .all-items-list {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 8px;
+ }
+}
+
+/* tablet */
+@media (min-width: 769px) and (max-width: 1199px) {
+ /* common */
+ .header {
+ padding: 0 24px;
+ }
+
+ main.main {
+ margin: 94px 24px;
+ }
+
+ /* Items page */
+ .best-items-container > .best-items-list {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 24px;
+ }
+
+ .all-items-container > .all-items-list {
+ grid-template-columns: repeat(3, 1fr);
+ gap: 40px;
+ }
+}
diff --git a/src/pages/Items.jsx b/src/pages/Items.jsx
index c6a9fad4b..a0139a8d3 100644
--- a/src/pages/Items.jsx
+++ b/src/pages/Items.jsx
@@ -1,7 +1,6 @@
import React from "react";
-import BestItemsContainer from "../components/items/BestItemsContainer";
-import AllItemsContainer from "../components/items/AllItemsContainer";
-import PageNavBar from "../components/items/PageNavBar";
+import BestItemsContainer from "../components/Items/BestItemsContainer";
+import AllItemsContainer from "../components/Items/AllItemsContainer";
import "./Items.css";
function Items() {
@@ -9,7 +8,6 @@ function Items() {
<>
-
>
);
}
diff --git a/src/pages/NotFound.jsx b/src/pages/NotFound.jsx
new file mode 100644
index 000000000..e3470b17f
--- /dev/null
+++ b/src/pages/NotFound.jsx
@@ -0,0 +1,11 @@
+import React from "react";
+
+function NotFound() {
+ return (
+ <>
+
X
+ >
+ );
+}
+
+export default NotFound;