diff --git a/package-lock.json b/package-lock.json
index 8b6c3e46f..772e4d846 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^1.7.2",
"classnames": "^2.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -5308,6 +5309,29 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+ "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -8322,9 +8346,9 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
},
"node_modules/follow-redirects": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
- "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -14403,6 +14427,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
diff --git a/package.json b/package.json
index ea7b18a19..7a3786a45 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^1.7.2",
"classnames": "^2.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/src/Main.js b/src/Main.js
index 056dfd213..5a5c100f1 100644
--- a/src/Main.js
+++ b/src/Main.js
@@ -1,13 +1,19 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
-import App from "./components/App";
+import App from "./components/ui/App";
import UsedMarketPage from "./pages/UsedMarketPage";
+import ProductRagistrationPage from "./pages/ProductRagistrationPage";
+import FreeBoardPage from "./pages/FreeBoardPage";
+import ProductDetailedPage from "./pages/ProductDetailedPage";
function Main() {
return (
+ } />
} />
+ } />
+ } />
diff --git a/src/api.js b/src/api.js
index 0d279986a..647090d8e 100644
--- a/src/api.js
+++ b/src/api.js
@@ -1,8 +1,23 @@
-export async function getProducts() {
- const response = await fetch("https://panda-market-api.vercel.app/products/");
- if (!response.ok) {
- throw new Error("데이터를 불러오는데 실패했습니다");
- }
- const body = await response.json();
- return body;
+import axios from "axios";
+
+const instance = axios.create({
+ baseURL: "https://panda-market-api.vercel.app",
+ timeout: 3000,
+});
+
+export async function getProducts(params = {}) {
+ const query = new URLSearchParams(params).toString();
+ const res = await instance.get(`/products?${query}`);
+ return res.data;
+}
+
+export async function getProduct(productId) {
+ const res = await instance.get(`/products/${productId}`);
+ return res.data;
+}
+
+export async function getComment({ productId, params }) {
+ const query = new URLSearchParams(params).toString();
+ const res = await instance.get(`/products/${productId}/comments?${query}`);
+ return res.data;
}
diff --git a/src/assets/arrow-down.png b/src/assets/arrow-down.png
index 771f146a2..0e312dd6d 100644
Binary files a/src/assets/arrow-down.png and b/src/assets/arrow-down.png differ
diff --git a/src/assets/arrow-left.png b/src/assets/arrow-left.png
new file mode 100644
index 000000000..5f3584021
Binary files /dev/null and b/src/assets/arrow-left.png differ
diff --git a/src/assets/arrow-right.png b/src/assets/arrow-right.png
new file mode 100644
index 000000000..c8fe6f123
Binary files /dev/null and b/src/assets/arrow-right.png differ
diff --git a/src/assets/back-icon.png b/src/assets/back-icon.png
new file mode 100644
index 000000000..c80f4478e
Binary files /dev/null and b/src/assets/back-icon.png differ
diff --git a/src/assets/heart.png b/src/assets/heart.png
new file mode 100644
index 000000000..89b6bbe36
Binary files /dev/null and b/src/assets/heart.png differ
diff --git a/src/assets/userIcon.png b/src/assets/userIcon.png
new file mode 100644
index 000000000..634ea26db
Binary files /dev/null and b/src/assets/userIcon.png differ
diff --git a/src/components/Container.js b/src/components/container/Container.jsx
similarity index 100%
rename from src/components/Container.js
rename to src/components/container/Container.jsx
diff --git a/src/components/Container.module.css b/src/components/container/Container.module.css
similarity index 100%
rename from src/components/Container.module.css
rename to src/components/container/Container.module.css
diff --git a/src/components/container/ListContainer.jsx b/src/components/container/ListContainer.jsx
new file mode 100644
index 000000000..3599a1862
--- /dev/null
+++ b/src/components/container/ListContainer.jsx
@@ -0,0 +1,57 @@
+import ProductListContainer from "../ui/ProductListContainer";
+import { useState, useEffect } from "react";
+import { getProducts } from "../../api";
+
+function ListContainer() {
+ const [products, setProducts] = useState([]);
+ const [order, setOrder] = useState("createdAt");
+ const [favoriteProducts, setFavoriteProducts] = useState([]);
+ const [searchText, setSearchText] = useState("");
+
+ const sortedProducts = products.sort((a, b) => b[order] - a[order]);
+
+ const loadList = async () => {
+ try {
+ const { list } = await getProducts();
+ setProducts(list);
+ } catch (e) {
+ if (e.response) {
+ console.log(e.response.status);
+ console.log(e.response.data);
+ } else {
+ console.log("리퀘스트가 실패했습니다.");
+ }
+ }
+ };
+
+ useEffect(() => {
+ loadList();
+ }, [order]);
+
+ useEffect(() => {
+ const sortedFavoriteProducts = [...products].sort(
+ (a, b) => b.favoriteCount - a.favoriteCount
+ );
+ setFavoriteProducts(sortedFavoriteProducts);
+ }, [products]);
+
+ const handleToggleClick = (option) => {
+ if (option === "newest") {
+ setOrder("createdAt");
+ } else if (option === "likes") {
+ setOrder("favoriteCount");
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default ListContainer;
diff --git a/src/components/container/PaginationContainer.jsx b/src/components/container/PaginationContainer.jsx
new file mode 100644
index 000000000..ad43a6d15
--- /dev/null
+++ b/src/components/container/PaginationContainer.jsx
@@ -0,0 +1,8 @@
+// import { getProducts } from "../../api";
+// import PaginationBar from "../ui/PaginationBar";
+
+function PaginationContainer() {
+ return;
+}
+
+export default PaginationContainer;
diff --git a/src/components/container/ProductDeatilsContainer.jsx b/src/components/container/ProductDeatilsContainer.jsx
new file mode 100644
index 000000000..6c7ca9f56
--- /dev/null
+++ b/src/components/container/ProductDeatilsContainer.jsx
@@ -0,0 +1,57 @@
+import ProductDetails from "../ui/ProductDetails";
+import { useParams } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { getProduct, getComment } from "../../api";
+import CommentDetails from "../ui/CommentDetails";
+
+function ProductDetailsContainer() {
+ const { productId } = useParams();
+ const [product, setProduct] = useState(null);
+ const [comments, setComments] = useState([]);
+
+ useEffect(() => {
+ const detailedProduct = async () => {
+ try {
+ const productData = await getProduct(productId);
+ setProduct(productData);
+ } catch (e) {
+ if (e.response) {
+ console.log(e.response.status);
+ console.log(e.response.data);
+ } else {
+ console.log("리퀘스트가 실패했습니다.");
+ }
+ }
+ };
+
+ const detailedComment = async () => {
+ const params = { limit: 3 };
+ try {
+ const commentData = await getComment({ productId, params });
+ setComments(commentData.list);
+ } catch (e) {
+ if (e.response) {
+ console.log(e.response.status);
+ console.log(e.response.data);
+ } else {
+ console.log("리퀘스트가 실패했습니다.");
+ }
+ }
+ };
+ detailedComment();
+ detailedProduct();
+ }, [productId]);
+
+ if (!product) {
+ return
로딩중
;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default ProductDetailsContainer;
diff --git a/src/components/container/RagistrationContainer.jsx b/src/components/container/RagistrationContainer.jsx
new file mode 100644
index 000000000..6454c1beb
--- /dev/null
+++ b/src/components/container/RagistrationContainer.jsx
@@ -0,0 +1,46 @@
+import RagistrationForm from "../ui/RagistrationForm";
+import { useState } from "react";
+
+function RagistrationContainer() {
+ const [values, setValues] = useState({
+ imageFile: null,
+ productName: "",
+ productDescription: "",
+ productPrice: "",
+ productTag: "",
+ });
+
+ const handleChange = (name, value) => {
+ setValues((prevValues) => ({
+ ...prevValues,
+ [name]: value,
+ }));
+ };
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ handleChange(name, value);
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+
+ setValues({
+ imageFile: null,
+ productName: "",
+ productDescription: "",
+ productPrice: "",
+ productTag: "",
+ });
+ };
+ return (
+
+ );
+}
+
+export default RagistrationContainer;
diff --git a/src/components/container/ToggleMenuContainer.jsx b/src/components/container/ToggleMenuContainer.jsx
new file mode 100644
index 000000000..82285ef5a
--- /dev/null
+++ b/src/components/container/ToggleMenuContainer.jsx
@@ -0,0 +1,27 @@
+import { useCallback, useEffect, useState } from "react";
+import ToggleMenu from "../ui/ToggleMenu";
+
+function ToggleMenuContainer({ onClick }) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const onToggleMenu = useCallback((e) => {
+ e.stopPropagation();
+ setIsOpen((nextIsOpen) => !nextIsOpen);
+ }, []);
+
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const handleClickOutside = () => setIsOpen(false);
+ window.addEventListener("click", handleClickOutside);
+
+ return () => {
+ window.removeEventListener("click", handleClickOutside);
+ };
+ }, [isOpen]);
+ return (
+
+ );
+}
+
+export default ToggleMenuContainer;
diff --git a/src/components/App.js b/src/components/ui/App.jsx
similarity index 100%
rename from src/components/App.js
rename to src/components/ui/App.jsx
diff --git a/src/components/App.module.css b/src/components/ui/App.module.css
similarity index 54%
rename from src/components/App.module.css
rename to src/components/ui/App.module.css
index 65964649f..401c79b8f 100644
--- a/src/components/App.module.css
+++ b/src/components/ui/App.module.css
@@ -1,14 +1,17 @@
-@import-normalize; /* bring in normalize.css styles */
+@import-normalize;
* {
box-sizing: border-box;
word-break: keep-all;
font-family: "Pretendard";
+ font-size: 16px;
+ font-weight: 400;
+ color: #1f2937;
}
html {
overflow-y: scroll;
- background-color: #f9f9f9;
+ background-color: #fcfcfc;
}
body {
@@ -16,8 +19,8 @@ body {
}
a {
- color: #494949;
text-decoration: none;
+ color: #4b5563;
}
:global(#root) {
@@ -33,3 +36,20 @@ a {
width: 100%;
margin: 0 auto;
}
+
+button {
+ border: none;
+ cursor: pointer;
+}
+
+@media (max-width: 1199px) {
+ .body {
+ padding: 0 24px;
+ }
+}
+
+@media (max-width: 767px) {
+ .body {
+ padding: 0 16px;
+ }
+}
diff --git a/src/components/ui/BestProduct.jsx b/src/components/ui/BestProduct.jsx
new file mode 100644
index 000000000..970d2c1f9
--- /dev/null
+++ b/src/components/ui/BestProduct.jsx
@@ -0,0 +1,27 @@
+import styles from "./BestProduct.module.css";
+import favoriteImg from "../../assets/favorite.png";
+import { Link } from "react-router-dom";
+
+function BestProduct({ product }) {
+ return (
+
+
+
+
+
+
{product.name}
+
{product.price}원
+
+
+
{product.favoriteCount}
+
+
+
+ );
+}
+
+export default BestProduct;
diff --git a/src/components/ui/BestProduct.module.css b/src/components/ui/BestProduct.module.css
new file mode 100644
index 000000000..2d0276e7d
--- /dev/null
+++ b/src/components/ui/BestProduct.module.css
@@ -0,0 +1,34 @@
+.bestItem__img {
+ max-width: 100%;
+ width: 282px;
+ height: 282px;
+ border-radius: 16px;
+ margin-bottom: 16px;
+}
+
+.best_name {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 16.71px;
+ margin: 0;
+ margin-top: 16px;
+}
+
+.price {
+ font-weight: 700;
+ line-height: 19.09px;
+ margin-top: 6px;
+ margin-bottom: 6px;
+}
+
+.best_name,
+.price {
+ color: #1f2937;
+}
+
+.favorite {
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 14.32px;
+ color: #4b5563;
+}
diff --git a/src/components/ui/BestProductsList.jsx b/src/components/ui/BestProductsList.jsx
new file mode 100644
index 000000000..cb697d771
--- /dev/null
+++ b/src/components/ui/BestProductsList.jsx
@@ -0,0 +1,20 @@
+import styles from "./BestProductsList.module.css";
+import "./global.css";
+import BestProduct from "./BestProduct";
+
+function BestProductsList({ products }) {
+ const limitedFavoriteProducts = products.slice(0, 4);
+ return (
+
+ {limitedFavoriteProducts.map((product) => {
+ return (
+ -
+
+
+ );
+ })}
+
+ );
+}
+
+export default BestProductsList;
diff --git a/src/components/ui/BestProductsList.module.css b/src/components/ui/BestProductsList.module.css
new file mode 100644
index 000000000..70d099416
--- /dev/null
+++ b/src/components/ui/BestProductsList.module.css
@@ -0,0 +1,5 @@
+.bestProductsList {
+ list-style: none;
+ display: flex;
+ padding: 0;
+}
diff --git a/src/components/ui/Button.jsx b/src/components/ui/Button.jsx
new file mode 100644
index 000000000..eb01c8b95
--- /dev/null
+++ b/src/components/ui/Button.jsx
@@ -0,0 +1,15 @@
+import styles from "./Button.module.css";
+
+function Button({ onClick, children, variant, type }) {
+ const buttonClassName = `${styles.button} ${styles[variant] || ""}`;
+
+ return (
+ <>
+
+ >
+ );
+}
+
+export default Button;
diff --git a/src/components/ui/Button.module.css b/src/components/ui/Button.module.css
new file mode 100644
index 000000000..51b31d2ce
--- /dev/null
+++ b/src/components/ui/Button.module.css
@@ -0,0 +1,25 @@
+.button {
+ height: 42px;
+ width: 88px;
+ border-radius: 8px;
+ padding: 12px 23px 12px 23px;
+ gap: 10px;
+ background-color: #3692ff;
+ color: #ffffff;
+ border: none;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 19.09px;
+ cursor: pointer;
+}
+
+.login {
+ margin-top: 14px;
+ margin-bottom: 14px;
+}
+
+.productRagistration {
+ width: 133px;
+ margin-left: 12px;
+ margin-right: 12px;
+}
diff --git a/src/components/ui/CommentDetails.jsx b/src/components/ui/CommentDetails.jsx
new file mode 100644
index 000000000..dd94af0fa
--- /dev/null
+++ b/src/components/ui/CommentDetails.jsx
@@ -0,0 +1,50 @@
+import styles from "../ui/CommentDetails.module.css";
+import backIcon from "../../assets/back-icon.png";
+import { Link } from "react-router-dom";
+import { elapsedTime } from "../../utils";
+
+function CommentDetails({ comments }) {
+ return (
+
+
+
+ {comments.map((comment) => (
+
+
{comment.content}
+
+
+
+
{comment.writer.nickname}
+
+ {elapsedTime(comment.updatedAt)}
+
+
+
+
+ ))}
+
+
+
목록으로 돌아가기
+
+
+
+ );
+}
+
+export default CommentDetails;
diff --git a/src/components/ui/CommentDetails.module.css b/src/components/ui/CommentDetails.module.css
new file mode 100644
index 000000000..b344375f2
--- /dev/null
+++ b/src/components/ui/CommentDetails.module.css
@@ -0,0 +1,109 @@
+.commentWrapper {
+ border-top: 1px solid #e5e7eb;
+ display: flex;
+ flex-direction: column;
+}
+.commentWrapper h2 {
+ font-weight: 600;
+ line-height: 19.09px;
+ margin-top: 24px;
+ margin-bottom: 16px;
+}
+
+.commentWrapper input {
+ border: none;
+ outline: none;
+ border-radius: 12px;
+ padding: 16px 24px 16px 24px;
+ width: 100%;
+ min-height: 104px;
+ background-color: #f3f4f6;
+}
+
+.buttonWrapper {
+ text-align: right;
+ margin-top: 16px;
+ margin-bottom: 24px;
+}
+
+.disabledButton {
+ background-color: #9ca3af;
+ color: #ffffff;
+ cursor: default;
+ padding: 12px 20px 12px 20px;
+ height: 42px;
+ width: 88px;
+ border-radius: 8px;
+ font-weight: 600;
+ line-height: 19.09px;
+}
+
+form:valid .disabledButton {
+ background-color: #3692ff;
+ cursor: pointer;
+}
+
+.content {
+ line-height: 22.4px;
+ margin-top: 24px;
+ margin-bottom: 24px;
+}
+
+.contentWrapper {
+ display: flex;
+ border-bottom: 1px solid #e5e7eb;
+ margin-bottom: 40px;
+}
+
+.comment img {
+ width: 40px;
+ height: 40px;
+}
+
+.commentContent {
+ margin-left: 8px;
+ margin-bottom: 5px;
+}
+
+.nickname {
+ margin: 0;
+ margin-bottom: 4px;
+ font-size: 14px;
+ line-height: 16.71px;
+ color: #4b5563;
+}
+
+.updatedAt {
+ font-size: 12px;
+ line-height: 14.32px;
+ margin: 0;
+ margin-bottom: 29px;
+ color: #9ca3af;
+}
+
+.goBackButton {
+ width: 240px;
+ height: 48px;
+ border-radius: 40px;
+ padding: 0;
+ background-color: #3692ff;
+ border: none;
+ cursor: pointer;
+ padding: 12px 71px 12px 71px;
+ margin: 0 auto 250px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-wrap: nowrap;
+}
+
+.goBackButton span {
+ color: #ffffff;
+ font-weight: 600;
+ font-size: 18px;
+ line-height: 24px;
+}
+
+.backIcon {
+ margin-left: 10px;
+}
diff --git a/src/components/ui/FileInput.jsx b/src/components/ui/FileInput.jsx
new file mode 100644
index 000000000..e0f8236e6
--- /dev/null
+++ b/src/components/ui/FileInput.jsx
@@ -0,0 +1,67 @@
+import { useRef } from "react";
+import iconImg from "../../assets/plus-icon.png";
+import styles from "./FileInput.module.css";
+
+function FileInput({ name, value, onChange }) {
+ const inputRef = useRef();
+
+ let preview = null;
+
+ const handleChange = (e) => {
+ const nextValue = e.target.files[0];
+ onChange(name, nextValue);
+ };
+
+ const handleClearClick = () => {
+ const inputNode = inputRef.current;
+ if (!inputNode) return;
+
+ inputNode.value = "";
+ onChange(name, null);
+ };
+
+ return (
+
+
+
+
+
+ {value && (
+
+ )}
+ {preview && (
+
+ )}
+ {value && (
+
+ )}
+
+ );
+}
+
+export default FileInput;
diff --git a/src/components/ui/FileInput.module.css b/src/components/ui/FileInput.module.css
new file mode 100644
index 000000000..3ffe34949
--- /dev/null
+++ b/src/components/ui/FileInput.module.css
@@ -0,0 +1,59 @@
+.image_upload {
+ display: flex;
+}
+
+.image_upload_frame {
+ width: 282px;
+ height: 315px;
+ margin-bottom: 24px;
+ margin-right: 24px;
+ border-radius: 12px;
+ background-color: #f3f4f6;
+ cursor: pointer;
+}
+
+.image_upload_frame_wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 99px auto;
+}
+
+.image_upload_frame_wrapper img {
+ width: 48px;
+ height: 48px;
+}
+
+.image_upload_frame_wrapper p {
+ color: #9ca3af;
+ font-weight: 600;
+}
+
+img.image_upload__image {
+ width: 282px;
+ height: 315px;
+ border-radius: 12px;
+ border: 1px;
+}
+
+.image_delete_button {
+ background-color: #3692ff;
+ color: #ffffff;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ margin-left: -30px;
+ margin-top: 10px;
+}
+
+.file_input {
+ display: none;
+}
+
+input {
+ background-color: #f3f4f6;
+}
+
+.image_upload__p {
+ color: #9ca3af;
+}
diff --git a/src/components/ui/Logo.jsx b/src/components/ui/Logo.jsx
new file mode 100644
index 000000000..6c257cd8e
--- /dev/null
+++ b/src/components/ui/Logo.jsx
@@ -0,0 +1,15 @@
+import { Link } from "react-router-dom";
+import styles from "./Logo.module.css";
+import logoImg from "../../assets/logo.png";
+
+function Logo() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+export default Logo;
diff --git a/src/components/ui/Logo.module.css b/src/components/ui/Logo.module.css
new file mode 100644
index 000000000..4321dd15c
--- /dev/null
+++ b/src/components/ui/Logo.module.css
@@ -0,0 +1,11 @@
+/* Tablet size*/
+@media screen and (max-width: 1199px) {
+ .logo {
+ }
+}
+/*Mobile size*/
+@media screen and (max-width: 767px) {
+ .logo {
+ height: 40px;
+ }
+}
diff --git a/src/components/ui/Nav.jsx b/src/components/ui/Nav.jsx
new file mode 100644
index 000000000..75711e4bf
--- /dev/null
+++ b/src/components/ui/Nav.jsx
@@ -0,0 +1,55 @@
+import { NavLink, useLocation } from "react-router-dom";
+import Container from "../container/Container";
+import styles from "./Nav.module.css";
+import Logo from "./Logo";
+import Button from "./Button";
+import userIcon from "../../assets/userIcon.png";
+
+function Nav() {
+ const location = useLocation();
+
+ return (
+
+
+
+
+ -
+ {
+ return {
+ color:
+ isActive && location.pathname === "/free" ? "#3692ff" : "",
+ };
+ }}
+ >
+ 자유게시판
+
+
+ -
+ {
+ return {
+ color:
+ isActive || location.pathname === "/additem"
+ ? "#3692ff"
+ : "",
+ };
+ }}
+ >
+ 중고마켓
+
+
+
+ {location.pathname === "/additem" ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+export default Nav;
diff --git a/src/components/Nav.module.css b/src/components/ui/Nav.module.css
similarity index 82%
rename from src/components/Nav.module.css
rename to src/components/ui/Nav.module.css
index 92276566e..c806e496b 100644
--- a/src/components/Nav.module.css
+++ b/src/components/ui/Nav.module.css
@@ -1,8 +1,7 @@
.nav {
position: relative;
z-index: 1;
- padding: 15px 0;
- background-color: #fff;
+ background-color: #ffffff;
border-bottom: 1px solid #dfdfdf;
width: 100%;
}
@@ -13,7 +12,7 @@
justify-content: space-between;
}
-.login__button {
+.loginBtn {
padding: 12px 23px 12px 23px;
border-radius: 8px;
background-color: #3692ff;
@@ -24,10 +23,6 @@
line-height: 19.09px;
}
-.visit__link:focus {
- color: #3692ff;
-}
-
.item {
font-weight: 700;
line-height: 21.48px;
@@ -46,3 +41,10 @@ ul.menu {
ul.menu > li:not(:last-child) {
margin-right: 30px;
}
+
+@media (max-width: 767px) {
+ .item {
+ font-size: 16px;
+ line-height: 19.09px;
+ }
+}
diff --git a/src/components/ui/PaginationBar.jsx b/src/components/ui/PaginationBar.jsx
new file mode 100644
index 000000000..6c49a2cd9
--- /dev/null
+++ b/src/components/ui/PaginationBar.jsx
@@ -0,0 +1,23 @@
+import styles from "./PaginationBar.module.css";
+import arrowLeft from "../../assets/arrow-left.png";
+import arrowRight from "../../assets/arrow-right.png";
+
+function PaginationBar() {
+ const pages = [1, 2, 3, 4, 5];
+
+ return (
+
+
+ {pages.map((page) => (
+
+ ))}
+
+
+ );
+}
+
+export default PaginationBar;
diff --git a/src/components/ui/PaginationBar.module.css b/src/components/ui/PaginationBar.module.css
new file mode 100644
index 000000000..0cb73ae47
--- /dev/null
+++ b/src/components/ui/PaginationBar.module.css
@@ -0,0 +1,24 @@
+.paginationBar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ margin-top: 40px;
+ margin-bottom: 113px;
+}
+
+.paginationButton,
+.currentPageButton {
+ width: 40px;
+ height: 40px;
+ border: 1px solid #e5e7eb;
+ color: #6b7280;
+ padding: 12.5px;
+ border-radius: 40px;
+ font-weight: 600;
+ line-height: 19.09px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12.5px;
+}
diff --git a/src/components/ui/Product.jsx b/src/components/ui/Product.jsx
new file mode 100644
index 000000000..5c60e35c0
--- /dev/null
+++ b/src/components/ui/Product.jsx
@@ -0,0 +1,27 @@
+import styles from "./Product.module.css";
+import favoriteImg from "../../assets/favorite.png";
+import { Link } from "react-router-dom";
+
+function Product({ product }) {
+ return (
+
+
+
+
+
+
{product.name}
+
{product.price}원
+
+
+
{product.favoriteCount}
+
+
+
+ );
+}
+
+export default Product;
diff --git a/src/components/ui/Product.module.css b/src/components/ui/Product.module.css
new file mode 100644
index 000000000..0c948038a
--- /dev/null
+++ b/src/components/ui/Product.module.css
@@ -0,0 +1,34 @@
+.productImg {
+ max-width: 100%;
+ width: 282px;
+ height: 282px;
+ border-radius: 16px;
+ margin-bottom: 16px;
+}
+
+.name {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 16.71px;
+ margin: 0;
+ margin-top: 16px;
+}
+
+.price {
+ font-weight: 700;
+ line-height: 19.09px;
+ margin-top: 6px;
+ margin-bottom: 6px;
+}
+
+.name,
+.price {
+ color: #1f2937;
+}
+
+.favorite {
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 14.32px;
+ color: #4b5563;
+}
diff --git a/src/components/ui/ProductDetails.jsx b/src/components/ui/ProductDetails.jsx
new file mode 100644
index 000000000..1c23f6bd5
--- /dev/null
+++ b/src/components/ui/ProductDetails.jsx
@@ -0,0 +1,28 @@
+import styles from "../ui/ProductDetails.module.css";
+import heartIcon from "../../assets/heart.png";
+
+function ProductDetails({ product }) {
+ return (
+
+
+
+
{product.name}
+
{product.price}원
+
상품 소개
+
{product.description}
+
상품 태그
+
#{product.tags}
+
+
+
+ );
+}
+
+export default ProductDetails;
diff --git a/src/components/ui/ProductDetails.module.css b/src/components/ui/ProductDetails.module.css
new file mode 100644
index 000000000..f3030e477
--- /dev/null
+++ b/src/components/ui/ProductDetails.module.css
@@ -0,0 +1,127 @@
+.productImg {
+ width: 486px;
+ height: 486px;
+ margin-right: 24px;
+ border-radius: 20px;
+}
+
+.productWrapper {
+ max-width: 996px;
+ display: flex;
+ justify-content: space-between;
+ padding-top: 24px;
+ padding-bottom: 32px;
+ margin: 0 auto;
+}
+
+.productContent {
+ width: 486px;
+ height: 486px;
+}
+
+.productContent h2 {
+ font-weight: 600;
+ font-size: 24px;
+ line-height: 28.64px;
+ color: #1f2937;
+ margin: 0;
+}
+
+.productContent h1 {
+ font-weight: 600;
+ font-size: 40px;
+ line-height: 47.73px;
+ color: #1f2937;
+ margin: 0;
+ margin-top: 16px;
+ padding-bottom: 16px;
+}
+
+.productContent p {
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 16.71px;
+ color: #4b5563;
+ margin-top: 16px;
+ margin-bottom: 8px;
+}
+
+.productContent h3 {
+ line-height: 22.4px;
+ color: #1f2937;
+ margin: 0;
+}
+
+.productContent h3 {
+ margin-bottom: 24px;
+}
+
+.favoriteTag {
+ width: 87px;
+ height: 40px;
+ border: 1px solid #e5e7eb;
+ border-radius: 35px;
+ padding: 4px 12px 4px 12px;
+ margin-top: 124px;
+ gap: 10px;
+ display: flex;
+ align-items: center;
+ color: #6b7280;
+ font-weight: 500;
+ line-height: 19.09px;
+}
+
+/* Tablet size*/
+@media screen and (max-width: 1199px) {
+ .productContent h2 {
+ font-size: 20px;
+ line-height: 23.87px;
+ }
+
+ .productContent h1 {
+ font-size: 32px;
+ line-height: 38.19px;
+ }
+
+ .productContent p {
+ line-height: 19.6px;
+ }
+
+ .productImg {
+ width: 340px;
+ height: 340px;
+ }
+
+ .favoriteTag {
+ margin-top: 25px;
+ }
+}
+/*Mobile size*/
+@media screen and (max-width: 767px) {
+ .productContent h2 {
+ font-size: 16px;
+ line-height: 19.09px;
+ }
+
+ .productContent h1 {
+ font-size: 24px;
+ line-height: 28.64px;
+ }
+
+ .productContent p {
+ line-height: 16.71px;
+ }
+
+ .favoriteTag img {
+ width: 24px;
+ }
+
+ .productImg {
+ width: 343px;
+ height: 343px;
+ }
+
+ .favoriteTag {
+ margin-top: 25px;
+ }
+}
diff --git a/src/components/ui/ProductListContainer.jsx b/src/components/ui/ProductListContainer.jsx
new file mode 100644
index 000000000..587848096
--- /dev/null
+++ b/src/components/ui/ProductListContainer.jsx
@@ -0,0 +1,50 @@
+import ProductsList from "../ui/ProductsList";
+import BestProductsList from "../ui/BestProductsList";
+import styles from "./ProductListContainer.module.css";
+import Button from "../ui/Button";
+import { Link } from "react-router-dom";
+import ToggleMenuContainer from "../container/ToggleMenuContainer";
+import PaginationBar from "./PaginationBar";
+
+function ProductListContainer({
+ favoriteProducts,
+ sortedProducts,
+ searchText,
+ handleToggleClick,
+ setSearchText,
+}) {
+ return (
+
+ );
+}
+
+export default ProductListContainer;
diff --git a/src/components/ui/ProductListContainer.module.css b/src/components/ui/ProductListContainer.module.css
new file mode 100644
index 000000000..5496c685c
--- /dev/null
+++ b/src/components/ui/ProductListContainer.module.css
@@ -0,0 +1,57 @@
+.search_container h2 {
+ font-size: 20px;
+ line-height: 28px;
+ margin-top: 6px;
+ margin-bottom: 6px;
+ color: #1f2937;
+}
+
+.search_container h2,
+.best_product {
+ font-weight: 700;
+ font-size: 20px;
+ line-height: 28px;
+}
+
+.best_product {
+ color: #111827;
+ margin-top: 24px;
+}
+
+.search_bar {
+ display: flex;
+ align-items: center;
+}
+
+.search_container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 40px;
+ margin-bottom: 24px;
+}
+
+.search_bar__form {
+ display: flex;
+}
+
+.search_bar__form input {
+ background-image: url("../../assets/search.png");
+ background-repeat: no-repeat;
+ border-radius: 12px;
+ padding: 9px 20px 9px 44px;
+ background-position: 16px 50%;
+ gap: 10px;
+ border: none;
+ background-color: #f3f4f6;
+}
+
+.search_bar__form input:focus {
+ outline: none;
+}
+
+.search_bar_wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
diff --git a/src/components/ui/ProductsList.jsx b/src/components/ui/ProductsList.jsx
new file mode 100644
index 000000000..cd55c3227
--- /dev/null
+++ b/src/components/ui/ProductsList.jsx
@@ -0,0 +1,19 @@
+import styles from "./ProductsList.module.css";
+import "./global.css";
+import Product from "./Product";
+
+function ProductsList({ products }) {
+ return (
+
+ {products.map((product) => {
+ return (
+ -
+
+
+ );
+ })}
+
+ );
+}
+
+export default ProductsList;
diff --git a/src/components/ui/ProductsList.module.css b/src/components/ui/ProductsList.module.css
new file mode 100644
index 000000000..e623dd52f
--- /dev/null
+++ b/src/components/ui/ProductsList.module.css
@@ -0,0 +1,8 @@
+.ProductsList {
+ list-style: none;
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ grid-auto-rows: auto;
+ gap: 16px;
+ padding: 0;
+}
diff --git a/src/components/ui/RagistrationForm.jsx b/src/components/ui/RagistrationForm.jsx
new file mode 100644
index 000000000..597efb63a
--- /dev/null
+++ b/src/components/ui/RagistrationForm.jsx
@@ -0,0 +1,78 @@
+import FileInput from "./FileInput";
+import styles from "./RagistrationForm.module.css";
+
+function ProductRagistrationForm({
+ values,
+ handleChange,
+ handleInputChange,
+ handleSubmit,
+}) {
+ return (
+
+ );
+}
+
+export default ProductRagistrationForm;
diff --git a/src/components/ui/RagistrationForm.module.css b/src/components/ui/RagistrationForm.module.css
new file mode 100644
index 000000000..6f08acac1
--- /dev/null
+++ b/src/components/ui/RagistrationForm.module.css
@@ -0,0 +1,96 @@
+form.product_ragistration_form {
+ display: flex;
+ flex-direction: column;
+ margin: 24px auto 0px;
+}
+
+form.product_ragistration_form h1 {
+ font-size: 28px;
+ font-weight: 700;
+ line-height: 33.41px;
+}
+
+textarea {
+ width: 1200px;
+ height: 200px;
+ border: none;
+ padding: 16px 0 16px 24px;
+}
+
+input {
+ background-color: #f3f4f6;
+}
+
+form.product_ragistration_form label {
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 21.48px;
+ margin-bottom: 12px;
+ cursor: pointer;
+}
+
+form.product_ragistration_form input {
+ gap: 10px;
+ border: none;
+ height: 56px;
+}
+
+.product_ragistration_form input:focus,
+textarea:focus {
+ outline: none;
+}
+
+form.product_ragistration_form input,
+textarea {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 24px;
+ border-radius: 12px;
+ background-color: #f3f4f6;
+ margin-bottom: 24px;
+}
+
+.product_ragistration_form_wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+form.product_ragistration_form:valid .disabledButton {
+ background-color: #3692ff;
+ cursor: pointer;
+}
+
+.disabledButton {
+ background-color: #9ca3af;
+ color: #ffffff;
+ cursor: default;
+ padding: 12px 20px 12px 20px;
+ height: 42px;
+ width: 88px;
+ border-radius: 8px;
+ font-weight: 600;
+ line-height: 19.09px;
+}
+
+.product_ragistration_form input {
+ padding: 16px 0 16px 24px;
+}
+
+.tag {
+ width: 100px;
+ height: 48px;
+ background-color: #f9fafb;
+ border-radius: 26px;
+ padding: 12px 12px 12px 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.delete_button {
+ background-color: #9ca3af;
+ border-radius: 50%;
+ color: #ffffff;
+ margin-left: 9px;
+}
diff --git a/src/components/ui/ToggleMenu.jsx b/src/components/ui/ToggleMenu.jsx
new file mode 100644
index 000000000..a918cbbe4
--- /dev/null
+++ b/src/components/ui/ToggleMenu.jsx
@@ -0,0 +1,38 @@
+import styles from "./ToggleMenu.module.css";
+import arrowDownImg from "../../assets/arrow-down.png";
+
+const OPTIONS = {
+ NEWEST: { label: "최신순", value: "newest" },
+ MOST_LIKED: { label: "좋아요순", value: "likes" },
+};
+
+function ToggleMenu({ onClick, onToggleMenu, isOpen }) {
+ return (
+
+
+ {isOpen && (
+
+ )}
+
+ );
+}
+
+export default ToggleMenu;
diff --git a/src/components/ToggleMenu.module.css b/src/components/ui/ToggleMenu.module.css
similarity index 94%
rename from src/components/ToggleMenu.module.css
rename to src/components/ui/ToggleMenu.module.css
index b05ea05df..c66e57d1f 100644
--- a/src/components/ToggleMenu.module.css
+++ b/src/components/ui/ToggleMenu.module.css
@@ -15,6 +15,10 @@
align-items: center;
}
+.iconText {
+ font-size: 16px;
+}
+
.toggleMenu .iconText {
position: relative;
left: -15px;
@@ -44,8 +48,7 @@ ul.popup li.disabled {
background-color: #ffffff;
color: #c4c4c4;
user-select: none;
- cursor: default;
- border: 1px 1px 0px 1px;
+ cursor: pointer;
height: 42px;
}
diff --git a/src/components/ui/global.css b/src/components/ui/global.css
new file mode 100644
index 000000000..c262ed07e
--- /dev/null
+++ b/src/components/ui/global.css
@@ -0,0 +1,16 @@
+.favorite-contents {
+ display: flex;
+ align-items: center;
+}
+
+.favorite-contents img {
+ height: 16px;
+}
+
+.favorite-contents h3 {
+ margin-left: 4px;
+}
+
+.favorite-contents h3 {
+ color: #4b5563;
+}
diff --git a/src/pages/FreeBoardPage.js b/src/pages/FreeBoardPage.js
new file mode 100644
index 000000000..b51c453e5
--- /dev/null
+++ b/src/pages/FreeBoardPage.js
@@ -0,0 +1,9 @@
+function FreeBoardPage() {
+ return (
+ <>
+ 자유게시판
+ >
+ );
+}
+
+export default FreeBoardPage;
diff --git a/src/pages/ProductDetailedPage.js b/src/pages/ProductDetailedPage.js
new file mode 100644
index 000000000..9da6d98e4
--- /dev/null
+++ b/src/pages/ProductDetailedPage.js
@@ -0,0 +1,7 @@
+import ProductDetailsContainer from "../components/container/ProductDeatilsContainer";
+
+function ProductDetailedPage() {
+ return ;
+}
+
+export default ProductDetailedPage;
diff --git a/src/pages/ProductRagistrationPage.js b/src/pages/ProductRagistrationPage.js
index ea00b9c8d..c2cf4acb2 100644
--- a/src/pages/ProductRagistrationPage.js
+++ b/src/pages/ProductRagistrationPage.js
@@ -1,7 +1,7 @@
-import ProductRagistrationForm from "../components/ProductRagistrationForm";
+import RagistrationContainer from "../components/container/RagistrationContainer";
function ProductRagistrationPage() {
- return ;
+ return ;
}
export default ProductRagistrationPage;
diff --git a/src/pages/UsedMarketPage.js b/src/pages/UsedMarketPage.js
index e24328602..69309d656 100644
--- a/src/pages/UsedMarketPage.js
+++ b/src/pages/UsedMarketPage.js
@@ -1,83 +1,7 @@
-import { Link } from "react-router-dom";
-import { useState, useEffect } from "react";
-import { getProducts } from "../api";
-import ProductsList from "../components/ProductsList";
-import BestProductsList from "../components/BestProductsList";
-import "./UsedMarketPage.css";
-import searchImg from "../assets/search.png";
-import ToggleMenu from "../components/ToggleMenu";
+import ListContainer from "../components/container/ListContainer";
function UsedMarketPage() {
- const [products, setProducts] = useState([]);
- const [order, setOrder] = useState("createdAt");
- const [favoriteProducts, setFavoriteProducts] = useState([]);
-
- const [searchText, setSearchText] = useState("");
-
- const sortedProducts = products.sort((a, b) => b[order] - a[order]);
-
- const handleLoad = async () => {
- const { list } = await getProducts();
- setProducts(list);
- };
-
- useEffect(() => {
- handleLoad();
- }, [order]);
-
- useEffect(() => {
- const sortedFavoriteProducts = [...products].sort(
- (a, b) => b.favoriteCount - a.favoriteCount
- );
- setFavoriteProducts(sortedFavoriteProducts);
- }, [products]);
-
- const handleToggleClick = (option) => {
- if (option === "newest") {
- setOrder("createdAt");
- } else if (option === "likes") {
- setOrder("favoriteCount");
- }
- };
-
- return (
-
-
-
베스트 상품
-
-
-
-
전체 상품
-
-
-
-
-
-
-
- );
+ return ;
}
export default UsedMarketPage;
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 000000000..025d0ce8b
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,18 @@
+export const elapsedTime = (date) => {
+ const start = new Date(date);
+ const end = new Date();
+
+ const seconds = Math.floor((end.getTime() - start.getTime()) / 1000);
+ if (seconds < 60) return "방금 전";
+
+ const minutes = seconds / 60;
+ if (minutes < 60) return `${Math.floor(minutes)}분 전`;
+
+ const hours = minutes / 60;
+ if (hours < 24) return `${Math.floor(hours)}시간 전`;
+
+ const days = hours / 24;
+ if (days < 7) return `${Math.floor(days)}일 전`;
+
+ return start.toLocaleDateString();
+};