diff --git a/public/index.html b/public/index.html index 21b8800f6..d8b2efff5 100644 --- a/public/index.html +++ b/public/index.html @@ -1,10 +1,30 @@ - - - - 판다 마켓 + + + 판다마켓 + + + + +
diff --git a/src/Api.js b/src/Api.js deleted file mode 100644 index e6979ea58..000000000 --- a/src/Api.js +++ /dev/null @@ -1,13 +0,0 @@ -export async function productList({ orderBy = "recent", pageSize = 10 }) { - const query = `page=1&pageSize=${pageSize}&orderBy=${orderBy}`; - const response = await fetch( - `https://panda-market-api.vercel.app/products?${query}` - ); - if (!response.ok) { - throw new Error("제품목록을 가지고 오는데 실패했습니다."); - } - const date = await response.json(); - return date; -} - -export default productList; diff --git a/src/App.js b/src/App.js new file mode 100644 index 000000000..74736b325 --- /dev/null +++ b/src/App.js @@ -0,0 +1,29 @@ +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import HomePage from "./pages/HomePage/HomePage"; +import LoginPage from "./pages/LoginPage/LoginPage"; +import MarketPage from "./pages/MarketPage/MarketPage"; +import AddItemPage from "./pages/AddItemPage/AddItemPage"; +import CommunityFeedPage from "./pages/CommunityFeedPage/CommunityFeedPage"; +import Header from "./components/Layout/Header"; + +function App() { + return ( + + {/* Global Navigation Bar */} +
+ +
+ + {/* React Router v6부터는 path="/" 대신 간단하게 `index`라고 표기하면 돼요 */} + } /> + } /> + } /> + } /> + } /> + +
+ + ); +} + +export default App; diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 51f561d13..000000000 --- a/src/App.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Route, Routes, BrowserRouter, Navigate } from "react-router-dom"; -import Header from "./components/Header/Header.jsx"; -import Home from "./pages/Home/Home.jsx"; -import AddItem from "./pages/AddItem/AddItem.jsx"; -import Items from "./pages/Items/Items.jsx"; -import "./css/Reset.css"; -import "./css/Global.css"; - -function App() { - return ( -
- -
- - } - > - }> - }> - -
-
- ); -} - -export default App; diff --git a/src/api/itemApi.js b/src/api/itemApi.js new file mode 100644 index 000000000..fd6ab6a90 --- /dev/null +++ b/src/api/itemApi.js @@ -0,0 +1,18 @@ +export async function getProducts(params = {}) { + // URLSearchParams을 이용하면 파라미터 값을 자동으로 쉽게 인코딩할 수 있어요. + const query = new URLSearchParams(params).toString(); + + try { + const response = await fetch( + `https://panda-market-api.vercel.app/products?${query}` + ); + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + const body = await response.json(); + return body; + } catch (error) { + console.error("Failed to fetch products:", error); + throw error; + } +} diff --git a/src/assets/images/icons/arrow_left.svg b/src/assets/images/icons/arrow_left.svg new file mode 100644 index 000000000..2a9de23a6 --- /dev/null +++ b/src/assets/images/icons/arrow_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/arrow_right.svg b/src/assets/images/icons/arrow_right.svg new file mode 100644 index 000000000..daa483c3e --- /dev/null +++ b/src/assets/images/icons/arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_X.png b/src/assets/images/icons/ic_X.png new file mode 100644 index 000000000..4acc054fe Binary files /dev/null and b/src/assets/images/icons/ic_X.png differ diff --git a/src/assets/images/icons/ic_heart.svg b/src/assets/images/icons/ic_heart.svg new file mode 100644 index 000000000..cad016c13 --- /dev/null +++ b/src/assets/images/icons/ic_heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_search.svg b/src/assets/images/icons/ic_search.svg new file mode 100644 index 000000000..52241e6d8 --- /dev/null +++ b/src/assets/images/icons/ic_search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_sort.svg b/src/assets/images/icons/ic_sort.svg new file mode 100644 index 000000000..657b44f93 --- /dev/null +++ b/src/assets/images/icons/ic_sort.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/logo/logo.svg b/src/assets/images/logo/logo.svg new file mode 100644 index 000000000..d497acbfe --- /dev/null +++ b/src/assets/images/logo/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css deleted file mode 100644 index 3b4f7c965..000000000 --- a/src/components/Header/Header.css +++ /dev/null @@ -1,43 +0,0 @@ -.header { - height: 70px; - border-bottom: 1px solid #dfdfdf; -} - -.header-nav__container { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 200px; -} - -.header-nav { - display: flex; - justify-content: center; - align-items: center; - gap: 32px; -} - -.logo__container { - display: flex; - justify-content: space-between; - align-items: center; - width: 153px; -} - -.logo__text { - font-size: 26px; - font-weight: 700; - line-height: 34px; - color: #3692ff; -} - -.header-menu__container { - display: flex; -} - -.header-menu__text { - padding: 15px 21px; - font-size: 18px; - line-height: 26px; - font-weight: 700; -} diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx deleted file mode 100644 index 80535e8c8..000000000 --- a/src/components/Header/Header.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import PandaImg from "../../img/pandaLogo__header.png"; -import UserImg from "../../img/userImg.png"; -import "./Header.css"; - -export default function Header() { - return ( -
-
-
-
- 판다 로고 이미지 -

판다마켓

-
-
-

자유게시판

-

중고마켓

-
-
- 사용자 이미지 -
-
- ); -} diff --git a/src/components/ItemList/ItemList.css b/src/components/ItemList/ItemList.css deleted file mode 100644 index e68f5dd86..000000000 --- a/src/components/ItemList/ItemList.css +++ /dev/null @@ -1,14 +0,0 @@ -.ItemList-ul.flex { - display: flex; - justify-content: center; - align-items: center; - gap: 24px; -} - -.ItemList-ul.grid { - display: grid; - grid-template-columns: repeat(5, 1fr); - justify-content: center; - align-items: center; - gap: 24px; -} diff --git a/src/components/ItemList/ItemList.jsx b/src/components/ItemList/ItemList.jsx deleted file mode 100644 index f02e6b8fa..000000000 --- a/src/components/ItemList/ItemList.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import Products from "../Products/Products.jsx"; -import "./ItemList.css"; - -export default function ItemList({ items, layoutType }) { - return ( - - ); -} diff --git a/src/components/Layout/Header.css b/src/components/Layout/Header.css new file mode 100644 index 000000000..1bc2b02c5 --- /dev/null +++ b/src/components/Layout/Header.css @@ -0,0 +1,45 @@ +.headerLeft { + display: flex; + align-items: center; +} + +.headerLogo { + margin-right: 16px; +} + +.globalHeader nav ul { + display: flex; + list-style: none; + gap: 8px; + font-weight: bold; + font-size: 16px; + color: #4b5563; +} + +.globalHeader nav ul li a:hover { + color: var(--blue); +} + +.loginLink { + font-size: 16px; + font-weight: 600; + border-radius: 8px; + padding: 11.5px 23px; +} + +@media (min-width: 768px) { + .globalHeader nav ul { + gap: 36px; + font-size: 18px; + } + + .headerLogo { + margin-right: 35px; + } +} + +@media (min-width: 1280px) { + .headerLogo { + margin-right: 47px; + } +} diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx new file mode 100644 index 000000000..9bab51e9b --- /dev/null +++ b/src/components/Layout/Header.jsx @@ -0,0 +1,42 @@ +import React from "react"; +import Logo from "../../assets/images/logo/logo.svg"; +import { Link, NavLink } from "react-router-dom"; +import "./Header.css"; + +// react-router-dom의 NavLink를 이용하면 활성화된 네비게이션 항목을 하이라이트해줄 수 있어요! +function getLinkStyle({ isActive }) { + return { color: isActive ? "var(--blue)" : undefined }; +} + +function Header() { + return ( +
+
+ + 판다마켓 로고 + + + +
+ + + 로그인 + +
+ ); +} + +export default Header; diff --git a/src/components/Products/Product.css b/src/components/Products/Product.css deleted file mode 100644 index 90ba25a8b..000000000 --- a/src/components/Products/Product.css +++ /dev/null @@ -1,44 +0,0 @@ -.product { - display: flex; - flex-direction: column; - gap: 16px; -} - -.product-img { - border-radius: 16px; - object-fit: cover; -} - -.product-img.flex { - width: 282px; - height: 282px; -} - -.product-img.grid { - width: 221px; - height: 221px; -} - -.product-name { - font-size: 14px; - font-weight: 500; - line-height: 24px; -} - -.favorite-count__container { - display: flex; - gap: 4px; -} - -.product-price { - font-size: 16px; - font-weight: 700; - line-height: 26px; -} - -.product-favoriteCount { - color: #4b5563; - font-size: 12px; - font-weight: 500; - line-height: 18px; -} diff --git a/src/components/Products/Products.jsx b/src/components/Products/Products.jsx deleted file mode 100644 index 99667b855..000000000 --- a/src/components/Products/Products.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import "./Product.css"; -import heartImg from "../../img/heartIcon.png"; - -export default function Products({ item, layoutType }) { - return ( -
- 제품 이미지 -

{item.name}

-

{item.price}

-
- 하트이미지 -

{item.favoriteCount}

-
-
- ); -} diff --git a/src/components/UI/DropdownList.css b/src/components/UI/DropdownList.css new file mode 100644 index 000000000..74f59823a --- /dev/null +++ b/src/components/UI/DropdownList.css @@ -0,0 +1,22 @@ +.dropdownList { + position: absolute; + top: 110%; + right: 0; + background: #fff; + border-radius: 8px; + border: 1px solid #e5e7eb; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: 99; +} + +.dropdownItem { + padding: 12px 44px; + border-bottom: 1px solid #e5e7eb; + font-size: 16px; + color: #1f2937; + cursor: pointer; +} + +.dropdownItem:last-child { + border-bottom: none; +} diff --git a/src/components/UI/DropdownList.jsx b/src/components/UI/DropdownList.jsx new file mode 100644 index 000000000..a9adae989 --- /dev/null +++ b/src/components/UI/DropdownList.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import "./DropdownList.css"; + +function DropdownList({ onSortSelection }) { + return ( +
+
onSortSelection("recent")}> + 최신순 +
+
onSortSelection("favorite")}> + 인기순 +
+
+ ); +} +export default DropdownList; diff --git a/src/components/UI/PaginationBar.css b/src/components/UI/PaginationBar.css new file mode 100644 index 000000000..1841297a9 --- /dev/null +++ b/src/components/UI/PaginationBar.css @@ -0,0 +1,29 @@ +.paginationBar { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.paginationButton { + border: 1px solid #e5e7eb; + border-radius: 50%; + width: 40px; + height: 40px; + color: #6b7280; + font-weight: 600; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.paginationButton:disabled { + cursor: default; + opacity: 0.5; +} + +.paginationButton.active { + background-color: var(--blue); + color: #fff; +} diff --git a/src/components/UI/PaginationBar.jsx b/src/components/UI/PaginationBar.jsx new file mode 100644 index 000000000..4116ebb40 --- /dev/null +++ b/src/components/UI/PaginationBar.jsx @@ -0,0 +1,53 @@ +import React from "react"; +import "./PaginationBar.css"; +import { ReactComponent as LeftArrow } from "../../assets/images/icons/arrow_left.svg"; +import { ReactComponent as RightArrow } from "../../assets/images/icons/arrow_right.svg"; + +const PaginationBar = ({ totalPageNum, activePageNum, onPageChange }) => { + const maxVisiblePages = 5; + let startPage; + + if (totalPageNum <= maxVisiblePages) { + startPage = 1; + } else { + startPage = Math.max(activePageNum - Math.floor(maxVisiblePages / 2), 1); + startPage = Math.min(startPage, totalPageNum - maxVisiblePages + 1); + } + + const pages = Array.from( + { length: Math.min(maxVisiblePages, totalPageNum - startPage + 1) }, + (_, i) => startPage + i + ); + + return ( +
+ + {pages.map((page) => ( + + ))} + +
+ ); +}; + +export default PaginationBar; diff --git a/src/css/Global.css b/src/css/Global.css deleted file mode 100644 index e2b2e1679..000000000 --- a/src/css/Global.css +++ /dev/null @@ -1,20 +0,0 @@ -.App { - background-color: #fcfcfc; - font-family: "Pretendard-Regular"; -} - -* { - box-sizing: border-box; -} - -@font-face { - font-family: "Pretendard-Regular"; - src: url("https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff") - format("woff"); - font-weight: 400; - font-style: normal; -} - -p { - color: #1f2937; -} diff --git a/src/css/Reset.css b/src/css/Reset.css deleted file mode 100644 index 45a05ecf8..000000000 --- a/src/css/Reset.css +++ /dev/null @@ -1,129 +0,0 @@ -/* http://meyerweb.com/eric/tools/css/reset/ - v2.0 | 20110126 - License: none (public domain) -*/ - -html, -body, -div, -span, -applet, -object, -iframe, -h1, -h2, -h3, -h4, -h5, -h6, -p, -blockquote, -pre, -a, -abbr, -acronym, -address, -big, -cite, -code, -del, -dfn, -em, -img, -ins, -kbd, -q, -s, -samp, -small, -strike, -strong, -sub, -sup, -tt, -var, -b, -u, -i, -center, -dl, -dt, -dd, -ol, -ul, -li, -fieldset, -form, -label, -legend, -table, -caption, -tbody, -tfoot, -thead, -tr, -th, -td, -article, -aside, -canvas, -details, -embed, -figure, -figcaption, -footer, -header, -hgroup, -menu, -nav, -output, -ruby, -section, -summary, -time, -mark, -audio, -video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font: inherit; - vertical-align: baseline; -} -/* HTML5 display-role reset for older browsers */ -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section { - display: block; -} -body { - line-height: 1; -} -ol, -ul { - list-style: none; -} -blockquote, -q { - quotes: none; -} -blockquote:before, -blockquote:after, -q:before, -q:after { - content: ""; - content: none; -} -table { - border-collapse: collapse; - border-spacing: 0; -} diff --git a/src/img/heartIcon.png b/src/img/heartIcon.png deleted file mode 100644 index bd7e864de..000000000 Binary files a/src/img/heartIcon.png and /dev/null differ diff --git a/src/img/pandaLogo__header.png b/src/img/pandaLogo__header.png deleted file mode 100644 index 0a1a4f202..000000000 Binary files a/src/img/pandaLogo__header.png and /dev/null differ diff --git a/src/img/userImg.png b/src/img/userImg.png deleted file mode 100644 index 0844dd1db..000000000 Binary files a/src/img/userImg.png and /dev/null differ diff --git a/src/index.js b/src/index.js index db7b51ca3..77ca5404e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,11 @@ +import React from "react"; import ReactDOM from "react-dom/client"; - import App from "./App"; +import "./styles/global.css"; // index.js에서 global stylesheet을 import하면 전역적으로 스타일이 적용돼요 const root = ReactDOM.createRoot(document.getElementById("root")); -root.render(); +root.render( + + + +); diff --git a/src/pages/AddItem/AddItem.jsx b/src/pages/AddItem/AddItem.jsx deleted file mode 100644 index 2d23dfadf..000000000 --- a/src/pages/AddItem/AddItem.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export default function AddItem() { - return
; -} diff --git a/src/pages/AddItemPage/AddItemPage.css b/src/pages/AddItemPage/AddItemPage.css new file mode 100644 index 000000000..2f671d5a5 --- /dev/null +++ b/src/pages/AddItemPage/AddItemPage.css @@ -0,0 +1,80 @@ +.addItems__container { + width: 1200px; + max-width: 1200px; + margin: 0 auto; + padding-top: 24px; + padding-bottom: 69px; +} + +.addItems-title__container { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 24px; +} + +.addItems-title { + font-size: 20px; + font-weight: 700; + line-height: 32px; +} + +.addItems-button { + background-color: #9ca3af; + height: 42px; + border-radius: 8px; + padding: 0 23px; + font-size: 16px; + font-weight: 600; + line-height: 26px; +} + +.input-form__container { + display: flex; + flex-direction: column; + gap: 32px; +} + +.addItems-img__container, +.addItems-name__container, +.addItems-explain__container, +.addItems-price__container, +.addItems-tag__container { + display: flex; + flex-direction: column; + gap: 16px; +} + +.addItems-img__title, +.addItems-name__title, +.addItems-explain__title, +.addItems-price__title, +.addItems-tag__title { + font-size: 18px; + font-weight: 700; + line-height: 26px; +} + +.addItems-name__input, +.addItems-price__input, +.addItems-tag__input { + height: 56px; + border: none; + background-color: #f3f4f6; + border-radius: 12px; + padding: 24px 15px; +} + +.addItems-explain__input { + height: 282px; + border: none; + background-color: #f3f4f6; + border-radius: 12px; + padding: 24px 15px; +} + +.addItems-tag__flex-container { + display: flex; + flex-direction: column; + gap: 14px; +} diff --git a/src/pages/AddItemPage/AddItemPage.jsx b/src/pages/AddItemPage/AddItemPage.jsx new file mode 100644 index 000000000..90f4a591b --- /dev/null +++ b/src/pages/AddItemPage/AddItemPage.jsx @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import "./AddItemPage.css"; +import Tag from "./components/Tag"; +function AddItemPage() { + const [tags, setTags] = useState([]); + const [inputValue, setInputValue] = useState(""); + + const handleKeyDown = (event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (inputValue.trim()) { + setTags((prevTags) => [...prevTags, inputValue.trim()]); + setInputValue(""); + } + } + }; + const handleInputChange = (event) => { + setInputValue(event.target.value); + }; + + const handleDeleteClick = (tags) => { + setTags((prevTags) => prevTags.filter((tag) => tag != tags)); + }; + return ( +
+
+

상품 등록하기

+ +
+
+
+

상품 이미지

+ +
+
+

상품명

+ +
+
+

상품 소개

+ +
+
+

판매가격

+ +
+
+

태그

+
+ + +
+
+
+
+ ); +} + +export default AddItemPage; diff --git a/src/pages/AddItemPage/components/Tag.css b/src/pages/AddItemPage/components/Tag.css new file mode 100644 index 000000000..2aa8f66bc --- /dev/null +++ b/src/pages/AddItemPage/components/Tag.css @@ -0,0 +1,14 @@ +.tags__container { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.tag { + display: inline-flex; + align-items: center; + gap: 8px; + border-radius: 26px; + background-color: #f3f4f6; + flex-shrink: 0; +} diff --git a/src/pages/AddItemPage/components/Tag.jsx b/src/pages/AddItemPage/components/Tag.jsx new file mode 100644 index 000000000..2690215b7 --- /dev/null +++ b/src/pages/AddItemPage/components/Tag.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import "./Tag.css"; +import iconX from "../../../assets/images/icons/ic_X.png"; + +export default function Tag({ tags, onDelete }) { + return ( +
+ {tags.map((tag) => { + return ( +
+

{tag}

+ 삭제아이콘 onDelete(tag)} + /> +
+ ); + })} +
+ ); +} diff --git a/src/pages/AddItem/AddItem.css b/src/pages/CommunityFeedPage/CommunityFeedPage.css similarity index 100% rename from src/pages/AddItem/AddItem.css rename to src/pages/CommunityFeedPage/CommunityFeedPage.css diff --git a/src/pages/CommunityFeedPage/CommunityFeedPage.jsx b/src/pages/CommunityFeedPage/CommunityFeedPage.jsx new file mode 100644 index 000000000..60f3d3be2 --- /dev/null +++ b/src/pages/CommunityFeedPage/CommunityFeedPage.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +function CommunityFeedPage() { + return
CommunityFeedPage
; +} + +export default CommunityFeedPage; diff --git a/src/pages/Home/Home.css b/src/pages/Home/Home.css deleted file mode 100644 index 5a084e16a..000000000 --- a/src/pages/Home/Home.css +++ /dev/null @@ -1,76 +0,0 @@ -.products-section { - width: 1200px; - max-width: 1200px; - height: auto; - margin: 0 auto; - padding-top: 24px; -} - -.favorite-products__container, -.all-products__container { - display: flex; - flex-direction: column; - justify-content: center; - gap: 16px; -} - -.favorite-products__container { - padding-bottom: 40px; -} - -.all-products__container { - padding-bottom: 43px; -} - -.products-section__title--best, -.products-section__title--all { - font-size: 20px; - font-weight: 700; - line-height: 32px; - color: #111827; -} - -.all-products__header { - display: flex; - justify-content: space-between; -} - -.all-products__search-container { - display: flex; - gap: 12px; - align-items: center; -} - -.all-products__add-button { - text-decoration: none; - padding: 8px 23px; - height: 42px; - border: none; - border-radius: 8px; - font-size: 16px; - font-weight: 500; - line-height: 26px; - background-color: #3692ff; - color: #f3f4f6; - text-align: center; -} - -.all-products__input { - width: 325px; - height: 42px; - border-radius: 12px; - border: none; - background-color: #f3f4f6; - padding-left: 16px; -} - -.dropDown-products { - width: 130px; - height: 42px; - border: 1px solid #e5e7eb; - border-radius: 12px; -} - -.dropDown-option__recent { - border-top-left-radius: 8px; -} diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx deleted file mode 100644 index 44e8a199d..000000000 --- a/src/pages/Home/Home.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useEffect, useState } from "react"; -import productList from "../../Api.js"; -import ItemList from "../../components/ItemList/ItemList.jsx"; -import "./Home.css"; -import { Link } from "react-router-dom"; - -function Home() { - const [allItems, setAllItems] = useState([]); - const [bestItems, setBestItems] = useState([]); - const [selectedOption, setSelectedOption] = useState("최신순"); - const loadAllItems = async () => { - try { - const { list } = await productList({ orderBy: "recent" }); - setAllItems(list); - } catch (error) { - console.error("전체 상품 데이터를 불러오는 중 오류 발생:", error.message); - } - }; - - const loadBestItems = async () => { - try { - const { list } = await productList({ orderBy: "favorite", pageSize: 4 }); - setBestItems(list); - } catch (error) { - console.error( - "베스트 상품 데이터를 불러오는 중 오류 발생:", - error.message - ); - } - }; - - useEffect(() => { - loadAllItems(); - loadBestItems(); - }, []); - - const handleOptionClick = (e) => { - setSelectedOption(e.target.value); - }; - - return ( -
-
-

베스트 상품

- -
-
-
-

전체 상품

-
- - - 상품 등록하기 - - -
-
- -
-
- ); -} - -export default Home; diff --git a/src/pages/Items/Items.css b/src/pages/HomePage/HomePage.css similarity index 100% rename from src/pages/Items/Items.css rename to src/pages/HomePage/HomePage.css diff --git a/src/pages/HomePage/HomePage.jsx b/src/pages/HomePage/HomePage.jsx new file mode 100644 index 000000000..26705c224 --- /dev/null +++ b/src/pages/HomePage/HomePage.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +function HomePage() { + return
HomePage
; +} + +export default HomePage; diff --git a/src/pages/Items/Items.jsx b/src/pages/Items/Items.jsx deleted file mode 100644 index 2bb95a20b..000000000 --- a/src/pages/Items/Items.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export default function Items() { - return
; -} diff --git a/src/pages/LoginPage/LoginPage.css b/src/pages/LoginPage/LoginPage.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/pages/LoginPage/LoginPage.jsx b/src/pages/LoginPage/LoginPage.jsx new file mode 100644 index 000000000..15f723760 --- /dev/null +++ b/src/pages/LoginPage/LoginPage.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +function LoginPage() { + return
LoginPage
; +} + +export default LoginPage; diff --git a/src/pages/MarketPage/MarketPage.css b/src/pages/MarketPage/MarketPage.css new file mode 100644 index 000000000..f304e4a4f --- /dev/null +++ b/src/pages/MarketPage/MarketPage.css @@ -0,0 +1,145 @@ +.sectionTitle { + color: #111827; + font-weight: bold; + font-size: 20px; + line-height: normal; +} + +.itemCard { + color: #1f2937; + overflow: hidden; + cursor: pointer; +} + +.itemCardThumbnail { + width: 100%; + height: auto; + object-fit: cover; + border-radius: 16px; + overflow: hidden; + aspect-ratio: 1; /* 정사각형으로 만들어 줌 */ + margin-bottom: 16px; +} + +.itemSummary { + display: flex; + flex-direction: column; + gap: 10px; + flex-grow: 1; +} + +.itemName { + font-size: 16px; + font-weight: 400; + /* 모든 상품 카드가 동일한 크기일 수 있도록 상품명을 한 줄로 제한 */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.itemPrice { + font-size: 16px; + font-weight: bold; +} + +.favoriteCount { + display: flex; + align-items: center; + gap: 4px; + color: #4b5563; + font-size: 12px; +} + +.bestItemsContainer { + padding-top: 17px; + padding-bottom: 24px; +} + +.allItemsCardSection { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 32px 8px; +} + +.allItemsSectionHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.allItemsSectionHeader:first-child { + padding-bottom: 8px; +} + +.allItemsSectionHeader:nth-child(2) { + padding-bottom: 16px; +} + +.sortButtonWrapper { + position: relative; +} + +.sortDropdownTriggerButton { + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 9px; + margin-left: 8px; +} + +.searchBarWrapper { + display: flex; + background-color: #f3f4f6; + border-radius: 12px; + padding: 9px 16px; + flex: 1; + align-items: center; +} + +.searchBarInput { + border: none; + flex: 1; + background-color: inherit; + margin-left: 4px; +} + +.searchBarInput::placeholder { + color: #9ca3af; + font-size: 16px; +} + +.searchBarInput:focus { + outline: none; +} + +.paginationBarWrapper { + padding-top: 40px; + padding-bottom: 80px; +} + +@media (min-width: 768px) { + .bestItemsContainer { + margin-bottom: 40px; + } + + .bestItemsCardSection { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + } + + .allItemsCardSection { + grid-template-columns: repeat(3, 1fr); + gap: 40px 16px; + } +} + +@media (min-width: 1280px) { + .bestItemsCardSection { + grid-template-columns: repeat(4, 1fr); + } + + .allItemsCardSection { + grid-template-columns: repeat(5, 1fr); + gap: 40px 24px; + } +} diff --git a/src/pages/MarketPage/MarketPage.jsx b/src/pages/MarketPage/MarketPage.jsx new file mode 100644 index 000000000..dd78d9065 --- /dev/null +++ b/src/pages/MarketPage/MarketPage.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import BestItemsSection from "./components/BestItemsSection"; +import AllItemsSection from "./components/AllItemsSection"; +import "./MarketPage.css"; + +function MarketPage() { + return ( +
+ + +
+ ); +} + +export default MarketPage; diff --git a/src/pages/MarketPage/components/AllItemsSection.jsx b/src/pages/MarketPage/components/AllItemsSection.jsx new file mode 100644 index 000000000..a0876a483 --- /dev/null +++ b/src/pages/MarketPage/components/AllItemsSection.jsx @@ -0,0 +1,113 @@ +import React, { useEffect, useState } from "react"; +import { getProducts } from "../../../api/itemApi"; +import ItemCard from "./ItemCard"; +import { ReactComponent as SortIcon } from "../../../assets/images/icons/ic_sort.svg"; +import { ReactComponent as SearchIcon } from "../../../assets/images/icons/ic_search.svg"; +import { Link } from "react-router-dom"; +import DropdownList from "../../../components/UI/DropdownList"; +import PaginationBar from "../../../components/UI/PaginationBar"; + +const getPageSize = () => { + const width = window.innerWidth; + if (width < 768) { + // Mobile viewport + return 4; + } else if (width < 1280) { + // Tablet viewport + return 6; + } else { + // Desktop viewport + return 10; + } +}; + +function AllItemsSection() { + const [orderBy, setOrderBy] = useState("recent"); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(getPageSize()); + const [itemList, setItemList] = useState([]); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const [totalPageNum, setTotalPageNum] = useState(); + + const fetchSortedData = async ({ orderBy, page, pageSize }) => { + const products = await getProducts({ orderBy, page, pageSize }); + setItemList(products.list); + setTotalPageNum(Math.ceil(products.totalCount / pageSize)); + }; + + const handleSortSelection = (sortOption) => { + setOrderBy(sortOption); + setIsDropdownVisible(false); + }; + + useEffect(() => { + const handleResize = () => { + setPageSize(getPageSize()); + }; + + // 화면 크기 변경할 때마다 pageSize를 다시 계산해 넣음 + window.addEventListener("resize", handleResize); + fetchSortedData({ orderBy, page, pageSize }); + + // Cleanup function + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [orderBy, page, pageSize]); + + const toggleDropdown = () => { + setIsDropdownVisible(!isDropdownVisible); + }; + + const onPageChange = (pageNumber) => { + setPage(pageNumber); + }; + + return ( +
+
+

판매 중인 상품

+ + 상품 등록하기 + +
+ +
+
+ + +
+
+ + {isDropdownVisible && ( + + )} +
+
+ +
+ {itemList?.map((item) => ( + + ))} +
+ +
+ +
+
+ ); +} + +export default AllItemsSection; diff --git a/src/pages/MarketPage/components/BestItemsSection.jsx b/src/pages/MarketPage/components/BestItemsSection.jsx new file mode 100644 index 000000000..a2072fbb9 --- /dev/null +++ b/src/pages/MarketPage/components/BestItemsSection.jsx @@ -0,0 +1,56 @@ +import React, { useEffect, useState } from "react"; +import ItemCard from "./ItemCard"; +import { getProducts } from "../../../api/itemApi"; + +const getPageSize = () => { + const width = window.innerWidth; + if (width < 768) { + // Mobile viewport + return 1; + } else if (width < 1280) { + // Tablet viewport + return 2; + } else { + // Desktop viewport + return 4; + } +}; + +function BestItemsSection() { + const [itemList, setItemList] = useState([]); + const [pageSize, setPageSize] = useState(getPageSize()); + + const fetchSortedData = async ({ orderBy, pageSize }) => { + const products = await getProducts({ orderBy, pageSize }); + setItemList(products.list); + }; + + useEffect(() => { + const handleResize = () => { + setPageSize(getPageSize()); + }; + + // 화면 크기 변경할 때마다 pageSize를 다시 계산해 넣음 + window.addEventListener("resize", handleResize); + fetchSortedData({ orderBy: "favorite", pageSize }); + + // Cleanup function + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [pageSize]); + + return ( +
+

베스트 상품

+ +
+ {itemList?.map((item) => ( + + ))} +
+
+ ); +} + +export default BestItemsSection; diff --git a/src/pages/MarketPage/components/ItemCard.jsx b/src/pages/MarketPage/components/ItemCard.jsx new file mode 100644 index 000000000..226c06baa --- /dev/null +++ b/src/pages/MarketPage/components/ItemCard.jsx @@ -0,0 +1,20 @@ +import React from "react"; +import { ReactComponent as HeartIcon } from "../../../assets/images/icons/ic_heart.svg"; + +function ItemCard({ item }) { + return ( +
+ {item.name} +
+

{item.name}

+

{item.price.toLocaleString()}원

+
+ + {item.favoriteCount} +
+
+
+ ); +} + +export default ItemCard; diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 000000000..851341a11 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,206 @@ +/* Mobile styles */ + +:root { + /* Gray scale */ + --gray-900: #1b1d1f; + --gray-800: #26282b; + --gray-600: #454c53; + --gray-500: #72787f; + --gray-400: #9ea4a8; + --gray-200: #e5e7eb; + --gray-100: #e8ebed; + --gray-50: #f7f7f8; + + /* Primary color */ + --blue: #3692ff; + + /* Layout dimensions */ + --header-height: 70px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +a { + text-decoration: none; + color: inherit; +} + +button, +input, +textarea, +select { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +button { + background: none; + border: none; + outline: none; + box-shadow: none; + cursor: pointer; +} + +img, +svg { + vertical-align: bottom; +} + +body { + color: #374151; + word-break: keep-all; + font-family: "Pretendard", sans-serif; +} + +header { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: var(--header-height); + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 16px; + background-color: #ffffff; + border-bottom: 1px solid #dfdfdf; + z-index: 999; +} + +.withHeader { + margin-top: var(--header-height); +} + +footer { + background-color: #111827; + color: #9ca3af; + font-size: 16px; + padding: 32px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 60px; +} + +#copyright { + order: 3; + flex-basis: 100%; +} + +#footerMenu { + display: flex; + gap: 30px; + color: var(--gray-200); +} + +#socialMedia { + display: flex; + gap: 12px; +} + +.wrapper { + width: 100%; + padding: 0 16px; +} + +h1 { + font-size: 40px; + font-weight: 700; + line-height: 56px; + letter-spacing: 0.02em; +} + +.button { + background-color: var(--blue); + color: #ffffff; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.button:hover { + background-color: #1967d6; +} + +.button:focus { + background-color: #1251aa; +} + +.button:disabled { + background-color: #9ca3af; + cursor: default; + pointer-events: none; +} + +.pill-button { + font-size: 16px; + font-weight: 600; + border-radius: 999px; + padding: 14.5px 33.5px; +} + +.full-width { + width: 100%; +} + +.break-on-desktop { + display: none; +} + +/* Tablet styles */ + +@media (min-width: 768px) { + header { + padding: 0 24px; + } + + .wrapper { + padding: 0 24px; + } + + .pill-button { + font-size: 20px; + font-weight: 700; + padding: 16px 126px; + } + + footer { + padding: 32px 104px 108px 104px; + } + + #copyright { + flex-basis: auto; + order: 0; + } +} + +/* Desktop styles */ + +@media (min-width: 1280px) { + header { + padding: 0 200px; + } + + .wrapper { + max-width: 1200px; + margin: 0 auto; + } + + .break-on-desktop { + display: inline; + } + + footer { + padding: 32px 200px 108px 200px; + } +}