diff --git a/.gitignore b/.gitignore
index 4d29575d..0340c1db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+.eslint.config.js
+.prettierrc.js
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index a1e590ee..4aab955e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,8 +13,12 @@
"@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"
+ },
+ "devDependencies": {
+ "eslint-config-prettier": "^10.1.5"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -7333,6 +7337,22 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.5",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
+ "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
"node_modules/eslint-config-react-app": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
@@ -14671,6 +14691,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 +15547,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..cacba980 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"
},
@@ -34,5 +35,8 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "eslint-config-prettier": "^10.1.5"
}
}
diff --git a/src/App.js b/src/App.js
index 37845757..9aad1263 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,24 +1,14 @@
-import logo from './logo.svg';
-import './App.css';
+import { Outlet } from "react-router-dom";
+import Nav from "./components/Nav";
function App() {
return (
-
+ <>
+
+
+
+
+ >
);
}
diff --git a/src/Main.js b/src/Main.js
new file mode 100644
index 00000000..338311b2
--- /dev/null
+++ b/src/Main.js
@@ -0,0 +1,24 @@
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import App from "./App";
+import "./css/base/reset.css";
+import "./css/base/variables.css";
+import "./css/base/common.css";
+import ItemsPage from "./pages/ItemsPage";
+import ItemRegisterPage from "./pages/ItemRegisterPage";
+
+function Main() {
+ return (
+
+
+ }>
+
+ }>
+ }>
+
+
+
+
+ );
+}
+
+export default Main;
diff --git a/src/api/Items.js b/src/api/Items.js
new file mode 100644
index 00000000..65fa4c99
--- /dev/null
+++ b/src/api/Items.js
@@ -0,0 +1,43 @@
+const BASEURL = "https://panda-market-api.vercel.app";
+
+export async function getFavoriteItems({
+ page = 1,
+ pageSize = 4,
+ orderBy = "favorite",
+}) {
+ const params = {
+ page: page,
+ pageSize: pageSize,
+ orderBy: orderBy,
+ };
+
+ const query = new URLSearchParams(params).toString();
+ const response = await fetch(`${BASEURL}/products?${query}`);
+ if (!response.ok) {
+ throw new Error("상품 목록을 불러오는데 오류가 발생했습니다");
+ }
+ const body = await response.json();
+ return body;
+}
+
+export async function getAllItems({
+ page = 1,
+ pageSize = 10,
+ orderBy = "recent",
+ keyword = "",
+}) {
+ const params = {
+ page: page,
+ pageSize: pageSize,
+ orderBy: orderBy,
+ keyword: keyword,
+ };
+
+ const query = new URLSearchParams(params).toString();
+ const response = await fetch(`${BASEURL}/products?${query}`);
+ if (!response.ok) {
+ throw new Error("상품 목록을 불러오는데 오류가 발생했습니다");
+ }
+ const body = await response.json();
+ return body;
+}
diff --git a/src/components/Button.js b/src/components/Button.js
new file mode 100644
index 00000000..9a3d19aa
--- /dev/null
+++ b/src/components/Button.js
@@ -0,0 +1,23 @@
+import "../css/components/Button.css";
+
+const Button = ({ className, type, children, onClick }) => {
+ const btnStyleClass = {
+ register: "btn-register",
+ small: "btn-small",
+ large: "btn-large",
+ };
+
+ const btnClassName = `btn ${btnStyleClass[type] || ""} ${className}`;
+
+ const onClickButton = () => {
+ onClick();
+ };
+
+ return (
+
+ );
+};
+
+export default Button;
diff --git a/src/components/Card.js b/src/components/Card.js
new file mode 100644
index 00000000..6c0730f1
--- /dev/null
+++ b/src/components/Card.js
@@ -0,0 +1,33 @@
+import "../css/components/Card.css";
+import HeartIcon from "./HeartIcon.js";
+import defaultImg from "../img/img_default.svg";
+
+const Card = ({ data }) => {
+ const { images, name, price, favoriteCount } = data;
+ return (
+
+

{
+ e.target.onError = null;
+ e.target.src = defaultImg;
+ }}
+ />
+
+
{name}
+
+ {price.toLocaleString("ko-KR")}원
+
+
+
+
+ {favoriteCount}
+
+
+
+ );
+};
+
+export default Card;
diff --git a/src/components/Dropdown.js b/src/components/Dropdown.js
new file mode 100644
index 00000000..c150e64a
--- /dev/null
+++ b/src/components/Dropdown.js
@@ -0,0 +1,43 @@
+import "../css/components/Dropdown.css";
+import sortIcon from "../img/sort.svg";
+import arrowDownIcon from "../img/arrow_down.svg";
+
+const Dropdown = ({
+ className,
+ onClickDropdown,
+ onClickDropdownItem,
+ dropdownList,
+ showDropdown,
+ value,
+}) => {
+ return (
+
+
+
{value.name}
+

+

+
+ {showDropdown && (
+
+ {dropdownList?.map((item, index) => (
+ - onClickDropdownItem(item)}
+ >
+ {item.name}
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default Dropdown;
diff --git a/src/components/HeartIcon.js b/src/components/HeartIcon.js
new file mode 100644
index 00000000..e6686044
--- /dev/null
+++ b/src/components/HeartIcon.js
@@ -0,0 +1,20 @@
+const HeartIcon = ({ isLiked = false }) => {
+ return (
+
+ );
+};
+
+export default HeartIcon;
diff --git a/src/components/Nav.js b/src/components/Nav.js
new file mode 100644
index 00000000..ae87cab7
--- /dev/null
+++ b/src/components/Nav.js
@@ -0,0 +1,59 @@
+import { Link, NavLink } from "react-router-dom";
+import textLogoIcon from "../img/logo_text.jpg";
+import logoIcon from "../img/logo.svg";
+import userIcon from "../img/user.svg";
+import "../css/components/Nav.css";
+
+const Nav = () => {
+ return (
+
+
+
+
+

+
+
+ -
+
+ isActive ? "header__content__link--active" : ""
+ }
+ >
+ 자유게시판
+
+
+
+ -
+
+ isActive ? "header__content__link--active" : ""
+ }
+ >
+ 중고마켓
+
+
+
+
+
+
+

+
+
+
+
+ );
+};
+
+export default Nav;
diff --git a/src/components/Pagination.js b/src/components/Pagination.js
new file mode 100644
index 00000000..45ebfeb8
--- /dev/null
+++ b/src/components/Pagination.js
@@ -0,0 +1,63 @@
+import "../css/components/Pagination.css";
+import arrowLeftActive from "../img/arrow_left_active.svg";
+import arrowLeftInactive from "../img/arrow_left_inactive.svg";
+import arrowRightActive from "../img/arrow_right_active.svg";
+import arrowRightInactive from "../img/arrow_right_inactive.svg";
+
+const Pagination = ({
+ totalCount,
+ pageSize,
+ currentPage,
+ onClickNext,
+ onClickPrev,
+ onClickPage,
+}) => {
+ const pageTotal = Math.ceil(totalCount / pageSize);
+ const totalPageList = Array(pageTotal)
+ .fill()
+ .map((e, i) => i + 1);
+
+ let startPage = Math.floor((currentPage - 1) / 5) * 5 + 1;
+ let endPage = Math.min(startPage + 5 - 1, pageTotal);
+
+ const visiblePageList = totalPageList.slice(startPage - 1, endPage);
+
+ return (
+
+
+ {visiblePageList?.map((number, index) => (
+
+ ))}
+
+
+ );
+};
+
+export default Pagination;
diff --git a/src/components/SearchInput.js b/src/components/SearchInput.js
new file mode 100644
index 00000000..2ddded3a
--- /dev/null
+++ b/src/components/SearchInput.js
@@ -0,0 +1,24 @@
+import searchIcon from "../img/search.svg";
+import "../css/components/SearchInput.css";
+
+const SearchInput = ({ value, onInput, onKeyDown, onClick, className }) => {
+ return (
+
+
+

+
+ );
+};
+
+export default SearchInput;
diff --git a/src/css/base/common.css b/src/css/base/common.css
new file mode 100644
index 00000000..5d0efa1d
--- /dev/null
+++ b/src/css/base/common.css
@@ -0,0 +1,53 @@
+body {
+ font-family: "Pretendard Variable";
+}
+
+a {
+ cursor: pointer;
+}
+
+input {
+ font-family: "Pretendard Variable";
+ font-size: var(--font-size-xs);
+ color: var(--color-secondary-800);
+ background-color: var(--color-secondary-100);
+ padding: 16px 20px;
+ border-radius: 12px;
+ height: 56px;
+ border: none;
+}
+
+input::placeholder {
+ color: var(--color-secondary-400);
+ font-size: var(--font-size-xs);
+}
+
+input.success {
+ outline: 1px solid var(--color-primary-100);
+}
+
+input.error {
+ outline: 1px solid var(--color-error);
+}
+
+input {
+ outline: none;
+}
+
+label {
+ font-size: var(--font-size-sm);
+ font-weight: 700;
+ color: var(--color-secondary-800);
+}
+
+.msg {
+ font-size: var(--font-size-xxs);
+ font-weight: 600;
+ padding-left: 16px;
+ display: none;
+ color: var(--color-error);
+}
+
+.msg.error {
+ display: block;
+}
diff --git a/src/css/base/reset.css b/src/css/base/reset.css
new file mode 100644
index 00000000..a5d386bb
--- /dev/null
+++ b/src/css/base/reset.css
@@ -0,0 +1,18 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+button {
+ border: none;
+}
+
+ul {
+ list-style: none;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
diff --git a/src/css/base/variables.css b/src/css/base/variables.css
new file mode 100644
index 00000000..72ef33ca
--- /dev/null
+++ b/src/css/base/variables.css
@@ -0,0 +1,35 @@
+:root {
+ /* Colors */
+ --color-background-white: #ffffff;
+ --color-background-blue: #cfe5ff;
+ --color-background-light-blue: #e6f2ff;
+ --color-background-bright-blue: #2f80ed;
+ --color-primary-100: #3692ff;
+ --color-primary-200: #1967d6;
+ --color-primary-300: #1251aa;
+ --color-secondary-50: #f9fafb;
+ --color-secondary-100: #f3f4f6;
+ --color-secondary-200: #e5e7eb;
+ --color-secondary-400: #9ca3af;
+ --color-secondary-500: #6b7280;
+ --color-secondary-600: #4b5563;
+ --color-secondary-700: #374151;
+ --color-secondary-800: #1f2937;
+ --color-secondary-900: #111827;
+ --color-error: #f74747;
+
+ /* Font Sizes */
+ --font-size-xxxs: 12px;
+ --font-size-xxs: 14px;
+ --font-size-xs: 16px;
+ --font-size-sm: 18px;
+ --font-size-lg: 20px;
+ --font-size-xl: 24px;
+ --font-size-2xl: 32px;
+ --font-size-3xl: 40px;
+
+ /* Border Radius */
+ --border-radius-sm: 8px;
+ --border-radius-md: 12px;
+ --border-radius-lg: 40px;
+}
diff --git a/src/css/components/Button.css b/src/css/components/Button.css
new file mode 100644
index 00000000..86b423a0
--- /dev/null
+++ b/src/css/components/Button.css
@@ -0,0 +1,40 @@
+.btn {
+ background-color: var(--color-primary-100);
+ text-decoration: none;
+ color: var(--color-secondary-100);
+ padding: 12px 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: 600;
+}
+
+.btn-register {
+ border-radius: var(--border-radius-sm);
+ min-width: 133px;
+ height: 42px;
+ font-size: var(--font-size-xs);
+}
+
+.btn-small {
+ border-radius: var(--border-radius-sm);
+ min-width: 128px;
+ height: 48px;
+ font-size: var(--font-size-xs);
+}
+
+.btn-large {
+ border-radius: var(--border-radius-lg);
+ width: 240px;
+ height: 48px;
+ font-size: var(--font-size-sm);
+}
+
+.btn a {
+ text-decoration: none;
+ color: var(--color-secondary-100);
+}
+
+.btn:disabled {
+ background-color: var(--color-secondary-400);
+}
diff --git a/src/css/components/Card.css b/src/css/components/Card.css
new file mode 100644
index 00000000..c9d28992
--- /dev/null
+++ b/src/css/components/Card.css
@@ -0,0 +1,41 @@
+.card__image {
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ border-radius: 16px;
+ margin-bottom: 16px;
+}
+
+.card__info {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.card__info__title {
+ font-size: var(--font-size-xxs);
+ font-weight: 500;
+ color: var(--color-secondary-800);
+}
+
+.card__info__price {
+ font-size: var(--font-size-xs);
+ font-weight: 700;
+ color: var(--color-secondary-800);
+}
+
+.card__icon__group {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.heart__icon {
+ width: 16px;
+ height: 16px;
+}
+
+.heart__icon__count {
+ font-size: var(--font-size-xxxs);
+ font-weight: 500;
+ color: var(--color-secondary-600);
+}
diff --git a/src/css/components/Dropdown.css b/src/css/components/Dropdown.css
new file mode 100644
index 00000000..358d73dc
--- /dev/null
+++ b/src/css/components/Dropdown.css
@@ -0,0 +1,73 @@
+.dropdown__container {
+ position: relative;
+}
+
+.dropdown__button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px 20px;
+ width: 42px;
+ height: 42px;
+ border: 1px solid var(--color-secondary-200);
+ border-radius: 12px;
+ background-color: #ffffff;
+ font-size: var(--font-size-xs);
+ font-weight: 400;
+ color: var(--color-secondary-800);
+}
+
+@media screen and (min-width: 744px) {
+ .dropdown__button {
+ width: 130px;
+ justify-content: space-between;
+ }
+}
+
+.dropdown__button__icon {
+ display: none;
+}
+
+@media screen and (min-width: 744px) {
+ .dropdown__button__icon {
+ display: block;
+ }
+}
+
+.dropdown__text {
+ display: none;
+}
+
+@media screen and (min-width: 744px) {
+ .dropdown__text {
+ display: block;
+ }
+}
+
+@media screen and (min-width: 744px) {
+ .dropdown__sort__icon {
+ display: none;
+ }
+}
+
+.dropdown__list {
+ position: absolute;
+ background-color: var(--color-background-white);
+ width: 130px;
+ height: 84px;
+ right: 0;
+ border: 1px solid var(--color-secondary-200);
+ border-radius: 12px;
+ margin-top: 4px;
+}
+
+.dropdown__list > li {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 42px;
+}
+
+.dropdown__list > li:nth-child(2n-1) {
+ border-bottom: 1px solid var(--color-secondary-200);
+}
diff --git a/src/css/components/Nav.css b/src/css/components/Nav.css
new file mode 100644
index 00000000..e9e45597
--- /dev/null
+++ b/src/css/components/Nav.css
@@ -0,0 +1,84 @@
+header {
+ padding: 15px 0;
+ background-color: var(--color-background-white);
+ position: sticky;
+ top: 0;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.15);
+ z-index: 1;
+}
+
+.header__content {
+ padding: 0 16px;
+ max-width: 1920px;
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-sizing: border-box;
+}
+
+@media screen and (min-width: 744px) {
+ .header__content {
+ padding: 0 24px;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .header__content {
+ padding: 0 200px;
+ }
+}
+
+.header__content__logo {
+ width: 103px;
+ vertical-align: middle;
+}
+
+@media screen and (min-width: 768px) {
+ .header__content__logo {
+ width: 153px;
+ }
+}
+
+.header__content__navigation__group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+@media screen and (min-width: 744px) {
+ .header__content__navigation__group {
+ gap: 35px;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .header__content__navigation__group {
+ gap: 47px;
+ }
+}
+
+.header__content__navigation__group ul {
+ display: flex;
+ gap: 8px;
+ font-size: var(--font-size-xs);
+ font-weight: 700;
+ color: var(--color-secondary-600);
+ align-items: center;
+}
+
+@media screen and (min-width: 744px) {
+ .header__content__navigation__group ul {
+ gap: 30px;
+ font-size: var(--font-size-sm);
+ }
+}
+
+.header__content__link--active {
+ color: var(--color-primary-100);
+}
+
+.header__content__user__icon {
+ width: 40px;
+ vertical-align: middle;
+}
diff --git a/src/css/components/Pagination.css b/src/css/components/Pagination.css
new file mode 100644
index 00000000..0650f2e0
--- /dev/null
+++ b/src/css/components/Pagination.css
@@ -0,0 +1,39 @@
+.pagination__container {
+ display: flex;
+ justify-content: center;
+ gap: 4px;
+ margin-top: 40px;
+}
+
+.pagination__button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ background-color: var(--color-background-white);
+ border: 1px solid var(--color-secondary-200);
+ border-radius: 40px;
+ color: var(--color-secondary-500);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+}
+
+.pagination__button[aria-current="page"] {
+ background-color: var(--color-background-bright-blue);
+ color: var(--color-secondary-50);
+}
+
+.items__container__pagination__button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ border: 1px solid #e5e7eb;
+ border-radius: 40px;
+ color: var(--color-secondary-500);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ background-color: var(--color-background-white);
+}
diff --git a/src/css/components/SearchInput.css b/src/css/components/SearchInput.css
new file mode 100644
index 00000000..d3e9d307
--- /dev/null
+++ b/src/css/components/SearchInput.css
@@ -0,0 +1,22 @@
+.search__input__container {
+ position: relative;
+}
+
+.search__input {
+ height: 42px;
+ width: 288px;
+ padding: 16px 16px 16px 44px;
+}
+
+@media screen and (min-width: 744px) {
+ .search__input {
+ width: 242px;
+ }
+}
+
+.search__icon {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ left: 16px;
+}
diff --git a/src/css/pages/ItemListPage.css b/src/css/pages/ItemListPage.css
new file mode 100644
index 00000000..9dbba046
--- /dev/null
+++ b/src/css/pages/ItemListPage.css
@@ -0,0 +1,142 @@
+.items__container {
+ padding: 17px 16px 35px 16px;
+}
+
+@media screen and (min-width: 744px) {
+ .items__container {
+ padding: 24px 24px 72px 24px;
+ }
+}
+
+.items__container__inner {
+ max-width: 344px;
+ gap: 24px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 0 auto;
+}
+
+@media screen and (min-width: 744px) {
+ .items__container__inner {
+ max-width: 696px;
+ gap: 40px;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .items__container__inner {
+ max-width: 1200px;
+ gap: 40px;
+ }
+}
+
+.items__container__title {
+ font-size: var(--font-size-lg);
+ font-weight: 700;
+ color: var(--color-secondary-900);
+ flex-grow: 1;
+}
+
+.items__container__registerBtn {
+ order: 1;
+}
+
+.items__container__search__input {
+ order: 2;
+}
+
+@media screen and (min-width: 744px) {
+ .items__container__search__input {
+ order: 1;
+ }
+}
+
+.items__container__dropdown {
+ order: 3;
+}
+
+.items__container__best__list {
+ display: grid;
+ gap: 24px;
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr;
+}
+
+@media screen and (min-width: 744px) {
+ .items__container__best__list {
+ gap: 10px;
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: 1fr;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .items__container__best__list {
+ gap: 10px;
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-rows: 1fr;
+ }
+}
+
+.items__container__best__header {
+ min-width: 344px;
+ margin-bottom: 16px;
+}
+
+@media screen and (min-width: 744px) {
+ .items__container__best__header {
+ min-width: 696px;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .items__container__best__header {
+ min-width: 1152px;
+ }
+}
+
+.items__container__all__header {
+ min-width: 344px;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+@media screen and (min-width: 744px) {
+ .items__container__all__header {
+ min-width: 696px;
+ gap: 12px;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .items__container__all__header {
+ min-width: 1152px;
+ }
+}
+
+.items__container__all__list {
+ display: grid;
+ gap: 24px;
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+}
+
+@media screen and (min-width: 744px) {
+ .items__container__all__list {
+ gap: 16px;
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .items__container__all__list {
+ grid-template-columns: repeat(5, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+ }
+}
diff --git a/src/css/pages/ItemRegisterPage.css b/src/css/pages/ItemRegisterPage.css
new file mode 100644
index 00000000..e69de29b
diff --git a/src/hooks/usePagination.js b/src/hooks/usePagination.js
new file mode 100644
index 00000000..bb35238b
--- /dev/null
+++ b/src/hooks/usePagination.js
@@ -0,0 +1,27 @@
+import { useState } from "react";
+
+const usePagination = () => {
+ const [paginationCurrentPage, setPaginationCurrentPage] = useState(1);
+
+ const onClickNextPage = () => {
+ setPaginationCurrentPage((prev) => prev + 1);
+ };
+
+ const onClickPrevPage = () => {
+ setPaginationCurrentPage((prev) => prev - 1);
+ };
+
+ const onClickPage = (page) => {
+ setPaginationCurrentPage(page);
+ };
+
+ return {
+ paginationCurrentPage,
+ setPaginationCurrentPage,
+ onClickNextPage,
+ onClickPrevPage,
+ onClickPage,
+ };
+};
+
+export default usePagination;
diff --git a/src/img/arrow_down.svg b/src/img/arrow_down.svg
new file mode 100644
index 00000000..8308690f
--- /dev/null
+++ b/src/img/arrow_down.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/img/arrow_left_active.svg b/src/img/arrow_left_active.svg
new file mode 100644
index 00000000..4b110c20
--- /dev/null
+++ b/src/img/arrow_left_active.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/img/arrow_left_inactive.svg b/src/img/arrow_left_inactive.svg
new file mode 100644
index 00000000..1daeca5c
--- /dev/null
+++ b/src/img/arrow_left_inactive.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/img/arrow_right_active.svg b/src/img/arrow_right_active.svg
new file mode 100644
index 00000000..0ad718ef
--- /dev/null
+++ b/src/img/arrow_right_active.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/img/arrow_right_inactive.svg b/src/img/arrow_right_inactive.svg
new file mode 100644
index 00000000..764302b6
--- /dev/null
+++ b/src/img/arrow_right_inactive.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/img/img_default.svg b/src/img/img_default.svg
new file mode 100644
index 00000000..19452bde
--- /dev/null
+++ b/src/img/img_default.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/img/logo.svg b/src/img/logo.svg
new file mode 100644
index 00000000..3799efba
--- /dev/null
+++ b/src/img/logo.svg
@@ -0,0 +1,15 @@
+
diff --git a/src/img/logo_text.jpg b/src/img/logo_text.jpg
new file mode 100644
index 00000000..38d9619f
Binary files /dev/null and b/src/img/logo_text.jpg differ
diff --git a/src/img/search.svg b/src/img/search.svg
new file mode 100644
index 00000000..52241e6d
--- /dev/null
+++ b/src/img/search.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/img/sort.svg b/src/img/sort.svg
new file mode 100644
index 00000000..657b44f9
--- /dev/null
+++ b/src/img/sort.svg
@@ -0,0 +1,6 @@
+
diff --git a/src/img/sort_button.svg b/src/img/sort_button.svg
new file mode 100644
index 00000000..41751e7b
--- /dev/null
+++ b/src/img/sort_button.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/img/user.svg b/src/img/user.svg
new file mode 100644
index 00000000..0480454d
--- /dev/null
+++ b/src/img/user.svg
@@ -0,0 +1,24 @@
+
diff --git a/src/index.js b/src/index.js
index d563c0fb..cf10cb97 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,15 +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 "./index.css";
+import Main from "./Main";
+import reportWebVitals from "./reportWebVitals";
-const root = ReactDOM.createRoot(document.getElementById('root'));
-root.render(
-
-
-
-);
+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))
diff --git a/src/pages/ItemRegisterPage.js b/src/pages/ItemRegisterPage.js
new file mode 100644
index 00000000..0218c7f2
--- /dev/null
+++ b/src/pages/ItemRegisterPage.js
@@ -0,0 +1,6 @@
+import "../css/pages/ItemRegisterPage.css";
+function ItemRegisterPage() {
+ return 상품 등록 페이지
;
+}
+
+export default ItemRegisterPage;
diff --git a/src/pages/ItemsPage.js b/src/pages/ItemsPage.js
new file mode 100644
index 00000000..ca6a9956
--- /dev/null
+++ b/src/pages/ItemsPage.js
@@ -0,0 +1,239 @@
+import "../css/pages/ItemListPage.css";
+import { useNavigate } from "react-router-dom";
+import { useState, useEffect } from "react";
+import { getFavoriteItems, getAllItems } from "../api/Items.js";
+import Card from "../components/Card";
+import SearchInput from "../components/SearchInput.js";
+import Button from "../components/Button.js";
+import Dropdown from "../components/Dropdown.js";
+import Pagination from "../components/Pagination.js";
+import usePagination from "../hooks/usePagination.js";
+
+function ItemListPage() {
+ const navigate = useNavigate();
+
+ const [orderBy, setOrderBy] = useState({
+ name: "최신순",
+ value: "recent",
+ });
+
+ const [searchValue, setSearchValue] = useState("");
+ const [keyword, setKeyword] = useState("");
+
+ const [totalCount, setTotalCount] = useState(0);
+
+ /* usePagination: 페이지네이션 훅 */
+ const {
+ onClickNextPage,
+ onClickPrevPage,
+ onClickPage,
+ paginationCurrentPage,
+ setPaginationCurrentPage,
+ } = usePagination();
+
+ const [bestItems, setBestItems] = useState([
+ {
+ favoriteCount: 0,
+ images: [],
+ price: 0,
+ description: "",
+ name: "",
+ id: 0,
+ },
+ ]);
+ const [allItems, setAllItems] = useState([
+ {
+ favoriteCount: 0,
+ images: [],
+ price: 0,
+ description: "",
+ name: "",
+ id: 0,
+ },
+ ]);
+
+ const [showDropdown, setShowDropdown] = useState(false);
+ const dropdownList = [
+ { name: "최신순", value: "recent" },
+ { name: "좋아요순", value: "favorite" },
+ ];
+
+ const fetchFavoriteItems = async (queryParams) => {
+ try {
+ const { list } = await getFavoriteItems(queryParams);
+ setBestItems(list);
+ } catch (error) {
+ } finally {
+ }
+ };
+
+ const fetchAllItems = async (queryParams) => {
+ try {
+ const { list, totalCount } = await getAllItems(queryParams);
+ setAllItems(list);
+ setTotalCount(totalCount);
+ } catch (error) {
+ } finally {
+ }
+ };
+
+ const onClickDropdown = () => {
+ setShowDropdown(!showDropdown);
+ };
+
+ const onClickDropdownItem = (order) => {
+ if (order.value !== orderBy.value) {
+ setOrderBy(order);
+ setPaginationCurrentPage(1);
+ }
+ };
+
+ const onKeywordChange = (e) => {
+ setSearchValue(e.target.value);
+ };
+
+ const onKeyDown = (e) => {
+ if (e.key === "Enter") {
+ onClickSearch();
+ }
+ };
+
+ const onClickSearch = async () => {
+ setPaginationCurrentPage(1);
+ setKeyword(searchValue);
+ };
+
+ const calcBreakPoint = (width) => {
+ if (width >= 1200) return "pc";
+ if (width >= 744) return "tablet";
+ return "mobile";
+ };
+
+ const [deviceType, setDeviceType] = useState(
+ calcBreakPoint(window.innerWidth)
+ );
+
+ const devicePageSize = {
+ mobile: {
+ best: 1,
+ all: 4,
+ },
+ tablet: {
+ best: 2,
+ all: 6,
+ },
+ pc: {
+ best: 4,
+ all: 10,
+ },
+ };
+
+ const bestPageSize = devicePageSize[deviceType]["best"];
+
+ const allPageSize = devicePageSize[deviceType]["all"];
+
+ const handleOnClickRegister = () => {
+ navigate("addItem");
+ };
+
+ useEffect(() => {
+ fetchFavoriteItems({
+ page: 1,
+ pageSize: bestPageSize,
+ orderBy: "favorite",
+ });
+ }, [deviceType, bestPageSize]);
+
+ useEffect(() => {
+ fetchAllItems({
+ page: paginationCurrentPage,
+ pageSize: allPageSize,
+ orderBy: orderBy.value,
+ keyword,
+ });
+ }, [deviceType, orderBy.value, paginationCurrentPage, allPageSize, keyword]);
+
+ useEffect(() => {
+ const handleResize = () => {
+ const newDeviceType = calcBreakPoint(window.innerWidth);
+ setDeviceType(newDeviceType);
+ };
+
+ window.addEventListener("resize", handleResize);
+
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ };
+ }, []);
+
+ return (
+
+ {/* 메인 영역*/}
+
+
+ {/* 베스트 상품 영역 */}
+
+
+
+ {bestItems?.map((item, index) => (
+ -
+
+
+ ))}
+
+
+
+ {/* 전체 상품 영역 */}
+
+
+
전체 상품
+
+
+
+
+
+ {allItems?.map((item, index) => (
+ -
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
+
+export default ItemListPage;