diff --git a/package-lock.json b/package-lock.json
index a1e590ee..d9442d2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^7.6.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
@@ -14671,6 +14672,53 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
+ "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
+ "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-router/node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -15480,6 +15528,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
diff --git a/package.json b/package.json
index 7ff0d6b5..cc88c710 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^7.6.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index a11777cc..00000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/index.html b/public/index.html
index aa069f27..0ca3abd0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,5 +1,5 @@
-
+
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
- React App
+ 판다마켓
diff --git a/public/logo192.png b/public/logo192.png
deleted file mode 100644
index fc44b0a3..00000000
Binary files a/public/logo192.png and /dev/null differ
diff --git a/public/logo512.png b/public/logo512.png
deleted file mode 100644
index a4e47a65..00000000
Binary files a/public/logo512.png and /dev/null differ
diff --git a/public/robots.txt b/public/robots.txt
deleted file mode 100644
index e9e57dc4..00000000
--- a/public/robots.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# https://www.robotstxt.org/robotstxt.html
-User-agent: *
-Disallow:
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e053..00000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/src/App.js b/src/App.js
index 37845757..d9fe58ce 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,24 +1,23 @@
-import logo from './logo.svg';
-import './App.css';
+import { BrowserRouter, Route, Routes } from "react-router-dom";
+import Header from "./components/Header";
+import HomePage from "./pages/Homepage/HomePage";
+import CommunityPage from "./pages/CommunityPage/CommunityPage";
+import ProductsPage from "./pages/ProductsPage/ProductsPage";
+import LoginPage from "./pages/LoginPage/LoginPage";
function App() {
return (
-
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+
+
+
);
}
diff --git a/src/App.test.js b/src/App.test.js
deleted file mode 100644
index 1f03afee..00000000
--- a/src/App.test.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/api.js b/src/api.js
new file mode 100644
index 00000000..1d81755a
--- /dev/null
+++ b/src/api.js
@@ -0,0 +1,12 @@
+const BASE_URL = "https://panda-market-api.vercel.app";
+
+export async function getProducts(params = {}) {
+ const query = new URLSearchParams(params).toString();
+
+ const response = await fetch(`${BASE_URL}/products?${query}`);
+ if (!response.ok) {
+ throw new Error("상품을 불러오는데 실패했습니다.");
+ }
+ const body = await response.json();
+ return body;
+}
diff --git a/src/assets/heart.svg b/src/assets/heart.svg
new file mode 100644
index 00000000..cad016c1
--- /dev/null
+++ b/src/assets/heart.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/leftBtn.svg b/src/assets/leftBtn.svg
new file mode 100644
index 00000000..2a9de23a
--- /dev/null
+++ b/src/assets/leftBtn.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/logo.png b/src/assets/logo.png
new file mode 100644
index 00000000..f663a9a8
Binary files /dev/null and b/src/assets/logo.png differ
diff --git a/src/assets/rightBtn.svg b/src/assets/rightBtn.svg
new file mode 100644
index 00000000..daa483c3
--- /dev/null
+++ b/src/assets/rightBtn.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/searchicon.svg b/src/assets/searchicon.svg
new file mode 100644
index 00000000..52241e6d
--- /dev/null
+++ b/src/assets/searchicon.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/assets/sorticon.svg b/src/assets/sorticon.svg
new file mode 100644
index 00000000..657b44f9
--- /dev/null
+++ b/src/assets/sorticon.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/assets/userMenu.png b/src/assets/userMenu.png
new file mode 100644
index 00000000..3e28565b
Binary files /dev/null and b/src/assets/userMenu.png differ
diff --git a/src/components/Header.css b/src/components/Header.css
new file mode 100644
index 00000000..33a07c69
--- /dev/null
+++ b/src/components/Header.css
@@ -0,0 +1,54 @@
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.headerLeft {
+ display: flex;
+ align-items: center;
+}
+
+.logo {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
+.logo a {
+ color: var(--primary-color);
+ font-size: var(--text-xl);
+ font-weight: 700;
+ line-height: 100%;
+ cursor: pointer;
+}
+
+.headerNav {
+ display: flex;
+ list-style: none;
+ gap: 10px;
+ margin-left: 16px;
+}
+
+.headerNav a {
+ color: var(--gray);
+ font-size: var(--text-base);
+ line-height: 26px;
+ font-weight: 600;
+}
+
+.headerUser {
+ width: 40px;
+ height: 40px;
+}
+
+@media (min-width: 768px) {
+ .logo a {
+ font-size: 25.63px;
+ }
+ .headerNav a {
+ font-size: var(--text-lg);
+ }
+}
+
+@media (min-width: 1280px) {
+}
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
new file mode 100644
index 00000000..7b8d872f
--- /dev/null
+++ b/src/components/Header.jsx
@@ -0,0 +1,35 @@
+import { Link, NavLink } from "react-router-dom";
+import "./Header.css";
+import usermenu from "../assets/userMenu.png";
+import logo from "../assets/logo.png";
+
+function Header() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default Header;
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index ec2585e8..00000000
--- a/src/index.css
+++ /dev/null
@@ -1,13 +0,0 @@
-body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
diff --git a/src/index.js b/src/index.js
index d563c0fb..b391783d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,17 +1,11 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./styles/global.css";
-const root = ReactDOM.createRoot(document.getElementById('root'));
+const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
);
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 9dfc1c05..00000000
--- a/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/pages/CommunityPage/CommunityPage.jsx b/src/pages/CommunityPage/CommunityPage.jsx
new file mode 100644
index 00000000..30216eb9
--- /dev/null
+++ b/src/pages/CommunityPage/CommunityPage.jsx
@@ -0,0 +1,5 @@
+function CommunityPage() {
+ return CommunityPage
;
+}
+
+export default CommunityPage;
diff --git a/src/pages/Homepage/HomePage.jsx b/src/pages/Homepage/HomePage.jsx
new file mode 100644
index 00000000..f7c6ae05
--- /dev/null
+++ b/src/pages/Homepage/HomePage.jsx
@@ -0,0 +1,5 @@
+function HomaPage() {
+ return HomePage
;
+}
+
+export default HomaPage;
diff --git a/src/pages/LoginPage/LoginPage.jsx b/src/pages/LoginPage/LoginPage.jsx
new file mode 100644
index 00000000..4f27ccc3
--- /dev/null
+++ b/src/pages/LoginPage/LoginPage.jsx
@@ -0,0 +1,5 @@
+function LoginPage() {
+ return LoginPage
;
+}
+
+export default LoginPage;
diff --git a/src/pages/ProductsPage/AllItemsSection.jsx b/src/pages/ProductsPage/AllItemsSection.jsx
new file mode 100644
index 00000000..0a710885
--- /dev/null
+++ b/src/pages/ProductsPage/AllItemsSection.jsx
@@ -0,0 +1,114 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { getProducts } from "../../api";
+import Item from "./Item";
+import SortMethod from "./SortMethod";
+import searchicon from "../../assets/searchicon.svg";
+import sorticon from "../../assets/sorticon.svg";
+import PageBar from "./PageBar";
+
+const getPageSize = () => {
+ const width = window.innerWidth;
+ if (width < 768) {
+ //mobile
+ return 4;
+ } else if (width < 1280) {
+ //tablet
+ return 6;
+ } else {
+ //desktop
+ return 10;
+ }
+};
+
+function AllItemsSection() {
+ const [orderBy, setOrderBy] = useState("recent");
+ const [itemList, setItemList] = useState([]);
+ const [page, setPage] = useState(1);
+ const [pageSize, setPageSize] = useState(getPageSize());
+ const [totalPage, setTotalPage] = useState();
+ const [isToggleVisible, setIsToggleVisible] = useState(false);
+
+ const fetchData = async ({ orderBy, page, pageSize }) => {
+ const products = await getProducts({ orderBy, page, pageSize });
+ setItemList(products.list);
+ setTotalPage(Math.ceil(products.totalCount / pageSize));
+ };
+
+ const handleSortSelection = (sortOption) => {
+ setOrderBy(sortOption);
+ setIsToggleVisible(false);
+ };
+
+ useEffect(() => {
+ const handleSize = () => {
+ setPageSize(getPageSize());
+ };
+
+ window.addEventListener("resize", handleSize);
+ fetchData({ orderBy, page, pageSize });
+
+ return () => {
+ window.removeEventListener("resize", handleSize);
+ };
+ }, [orderBy, page, pageSize]);
+
+ const toggleDown = () => {
+ setIsToggleVisible(!isToggleVisible);
+ };
+
+ const onPageChange = (pageNum) => {
+ setPage(pageNum);
+ };
+
+ return (
+
+
+
전체 상품
+
+ 상품 등록하기
+
+
+
+

+
+
+
+
+
+ {isToggleVisible && (
+
+ )}
+
+
+ {isToggleVisible && (
+
+ )}
+
+
+
+
+ {itemList?.map((item) => (
+
+ ))}
+
+
+
+
+ );
+}
+
+export default AllItemsSection;
diff --git a/src/pages/ProductsPage/BestItemsSection.jsx b/src/pages/ProductsPage/BestItemsSection.jsx
new file mode 100644
index 00000000..19678dda
--- /dev/null
+++ b/src/pages/ProductsPage/BestItemsSection.jsx
@@ -0,0 +1,53 @@
+import { useEffect, useState } from "react";
+import { getProducts } from "../../api";
+import Item from "./Item";
+
+const getPageSize = () => {
+ const width = window.innerWidth;
+ if (width < 768) {
+ //mobile
+ return 1;
+ } else if (width < 1280) {
+ //tablet
+ return 2;
+ } else {
+ //desktop
+ return 4;
+ }
+};
+
+function BestItemsSection() {
+ const [itemList, setItemList] = useState([]);
+ const [pageSize, setPageSize] = useState(getPageSize());
+
+ const fetchData = async ({ orderBy, pageSize }) => {
+ const products = await getProducts({ orderBy, pageSize });
+ setItemList(products.list);
+ };
+
+ useEffect(() => {
+ const handleSize = () => {
+ setPageSize(getPageSize);
+ };
+
+ window.addEventListener("resize", handleSize);
+ fetchData({ orderBy: "favorite", pageSize });
+
+ return () => {
+ window.removeEventListener("resize", handleSize);
+ };
+ }, [pageSize]);
+
+ return (
+
+
베스트 상품
+
+ {itemList?.map((item) => (
+
+ ))}
+
+
+ );
+}
+
+export default BestItemsSection;
diff --git a/src/pages/ProductsPage/Item.jsx b/src/pages/ProductsPage/Item.jsx
new file mode 100644
index 00000000..441be281
--- /dev/null
+++ b/src/pages/ProductsPage/Item.jsx
@@ -0,0 +1,19 @@
+import heart from "../../assets/heart.svg";
+
+function Item({ item }) {
+ return (
+
+

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

+ {item.favoriteCount}
+
+
+
+ );
+}
+
+export default Item;
diff --git a/src/pages/ProductsPage/PageBar.css b/src/pages/ProductsPage/PageBar.css
new file mode 100644
index 00000000..ae0954a7
--- /dev/null
+++ b/src/pages/ProductsPage/PageBar.css
@@ -0,0 +1,19 @@
+.pageBar {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+.pageButton {
+ width: 40px;
+ height: 40px;
+ border-radius: 1px solid #e5e7eb;
+ border-radius: 50%;
+ border: none;
+ background-color: var(--white);
+ color: var(--gray-500);
+}
+
+.pageButton.active {
+ background-color: var(--primary-color);
+ color: var(--white);
+}
diff --git a/src/pages/ProductsPage/PageBar.jsx b/src/pages/ProductsPage/PageBar.jsx
new file mode 100644
index 00000000..51e6e936
--- /dev/null
+++ b/src/pages/ProductsPage/PageBar.jsx
@@ -0,0 +1,50 @@
+import { useState } from "react";
+import leftBtn from "../../assets/leftBtn.svg";
+import rightBtn from "../../assets/rightBtn.svg";
+import "./PageBar.css";
+
+const PageBar = ({ totalPage, activePage, onPageChange }) => {
+ const maxVisiblePages = 5;
+ const [currentGroup, setCurrentGroup] = useState(0);
+ const startPage = currentGroup * maxVisiblePages + 1;
+
+ const pages = Array.from(
+ {
+ length: Math.min(maxVisiblePages, totalPage - startPage + 1),
+ },
+ (_, i) => startPage + i
+ );
+
+ const maxGroup = Math.floor((totalPage - 1) / maxVisiblePages);
+
+ return (
+
+
+ {pages.map((page) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default PageBar;
diff --git a/src/pages/ProductsPage/Products.css b/src/pages/ProductsPage/Products.css
new file mode 100644
index 00000000..836a3eca
--- /dev/null
+++ b/src/pages/ProductsPage/Products.css
@@ -0,0 +1,173 @@
+.itemCard {
+ display: flex;
+ flex-direction: column;
+ color: var(--dark-800);
+ cursor: pointer;
+ gap: 10px;
+}
+
+.itemImage {
+ width: 100%;
+ height: auto;
+ aspect-ratio: 1;
+ border-radius: 16px;
+}
+
+.itemDescription {
+ display: flex;
+ flex-direction: column;
+ color: var(--dark-800);
+ gap: 6px;
+}
+
+.itemTitle {
+ font-size: var(--text-sm);
+}
+.itemPrice {
+ font-size: var(--text-base);
+}
+
+.favoriteHeart {
+ color: var(--gray);
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+}
+
+.bestItemContainer {
+ display: flex;
+ flex-direction: column;
+ padding-top: 17px;
+ padding-bottom: 24px;
+ gap: 16px;
+}
+
+.bestItemSection {
+ color: #fcfcfc;
+ display: grid;
+ grid-template-columns: repeat(1, 1fr);
+ gap: 32px 8px;
+}
+
+.allItemHeaderWrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.registerButton {
+ background-color: var(--primary-color);
+ font-size: var(--text-base);
+ color: #f3f4f6;
+ padding: 12px 32px;
+ gap: 10px;
+ border-radius: 8px;
+ border: none;
+}
+
+.searchBarContainer {
+ width: 288px;
+ display: flex;
+ align-items: center;
+ background-color: #f3f4f6;
+ padding: 9px 20px 9px 16px;
+ border-radius: 12px;
+ gap: 10px;
+}
+
+.searchInput {
+ width: 174px;
+ background-color: inherit;
+ border: none;
+ line-height: 26px;
+}
+
+.searchInput::placeholder {
+ color: #9ca3af;
+ font-size: 16px;
+}
+
+.searchInput:focus {
+ outline: none;
+}
+
+.sortButtonContainer {
+ position: relative;
+}
+
+.sortIconButton {
+ background-color: var(--white);
+ border: 1px solid #e5e7eb;
+ border-radius: 12px;
+ padding: 9px;
+ gap: 10px;
+ align-items: center;
+}
+.sortTextButton {
+ display: none;
+}
+
+.allItemSection {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 40px;
+}
+
+.pageNavigationBar {
+ padding: 40px 0;
+}
+
+@media (min-width: 768px) {
+ .bestItemSection {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .allItemSection {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .allItemTitle {
+ order: 1;
+ margin-right: auto;
+ }
+ .searchBarContainer {
+ order: 2;
+ }
+ .registerButton {
+ order: 3;
+ }
+ .sortButtonContainer {
+ order: 4;
+ }
+
+ .sortIconButton {
+ display: none;
+ }
+
+ .sortTextButton {
+ width: 130px;
+ height: 42px;
+ display: inline-block;
+ background-color: var(--white);
+ font-size: var(--text-base);
+ border: 1px solid #e5e7eb;
+ border-radius: 12px;
+ padding: 12px 20px;
+ gap: 10px;
+ align-items: center;
+ }
+}
+
+@media (min-width: 1280px) {
+ .bestItemSection {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .allItemSection {
+ grid-template-columns: repeat(5, 1fr);
+ }
+}
diff --git a/src/pages/ProductsPage/ProductsPage.jsx b/src/pages/ProductsPage/ProductsPage.jsx
new file mode 100644
index 00000000..d8c4cd8f
--- /dev/null
+++ b/src/pages/ProductsPage/ProductsPage.jsx
@@ -0,0 +1,17 @@
+import "./Products.css";
+import BestItemsSection from "./BestItemsSection";
+import AllItemsSection from "./AllItemsSection";
+
+function ProductPage() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default ProductPage;
diff --git a/src/pages/ProductsPage/SortMethod.css b/src/pages/ProductsPage/SortMethod.css
new file mode 100644
index 00000000..0350e1cf
--- /dev/null
+++ b/src/pages/ProductsPage/SortMethod.css
@@ -0,0 +1,19 @@
+.toggleList {
+ width: 130px;
+ top: 110%;
+ right: 0;
+ position: absolute;
+ background-color: var(--white);
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
+}
+
+.toggleItem {
+ display: flex;
+ justify-content: center;
+ padding: 12px;
+ font-size: var(--text-base);
+ color: var(--dark-800);
+ cursor: pointer;
+ border-bottom: 1px solid #e5e7eb;
+}
diff --git a/src/pages/ProductsPage/SortMethod.jsx b/src/pages/ProductsPage/SortMethod.jsx
new file mode 100644
index 00000000..b224418b
--- /dev/null
+++ b/src/pages/ProductsPage/SortMethod.jsx
@@ -0,0 +1,16 @@
+import "./SortMethod.css";
+
+function SortMethod({ onSortSelection }) {
+ return (
+
+
onSortSelection("recent")}>
+ 최신순
+
+
onSortSelection("favorite")}>
+ 좋아요순
+
+
+ );
+}
+
+export default SortMethod;
diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js
deleted file mode 100644
index 5253d3ad..00000000
--- a/src/reportWebVitals.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const reportWebVitals = onPerfEntry => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/src/setupTests.js b/src/setupTests.js
deleted file mode 100644
index 8f2609b7..00000000
--- a/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
diff --git a/src/styles/global.css b/src/styles/global.css
new file mode 100644
index 00000000..5b3b33aa
--- /dev/null
+++ b/src/styles/global.css
@@ -0,0 +1,78 @@
+:root {
+ --primary-color: #3692ff;
+ --gray: #4b5563;
+ --gray-500: #6b7280;
+ --dark: #111827;
+ --dark-800: #1f2937;
+ --white: #ffffff;
+
+ /* Font Sizes */
+ --text-sm: 14px;
+ --text-base: 16px;
+ --text-lg: 18px;
+ --text-xl: 20px;
+ --text-2xl: 24px;
+ --text-3xl: 40px;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+header {
+ background-color: var(--white);
+ border-bottom: 1px solid #dfdfdf;
+ width: 100%;
+ height: 70px;
+ padding: 0 16px;
+ position: fixed;
+ top: 0;
+ z-index: 1;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+a {
+ text-decoration: none;
+}
+
+h1 {
+ font-size: var(--text-xl);
+}
+
+button {
+ cursor: pointer;
+}
+
+.container {
+ margin-top: 70px;
+}
+
+.productsWrapper {
+ width: 100%;
+ padding: 0 16px;
+}
+
+@media (min-width: 768px) {
+ header {
+ padding: 0 24px;
+ }
+
+ .productsWrapper {
+ padding: 0 24px;
+ }
+}
+
+@media (min-width: 1280px) {
+ header {
+ padding: 0 200px;
+ }
+
+ .productsWrapper {
+ max-width: 1200px;
+ margin: 0 auto;
+ }
+}