diff --git a/package-lock.json b/package-lock.json index a730a528..c79f83ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@testing-library/user-event": "^13.5.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-router-dom": "^7.9.5", + "react-router-dom": "^6.30.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" } @@ -3073,6 +3073,15 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -13878,50 +13887,35 @@ } }, "node_modules/react-router": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", - "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" + "@remix-run/router": "1.23.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } + "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz", - "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "license": "MIT", "dependencies": { - "react-router": "7.9.5" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=14.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" + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-scripts": { @@ -14818,12 +14812,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index c799bed6..cb5106ac 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@testing-library/user-event": "^13.5.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-router-dom": "^7.9.5", + "react-router-dom": "^6.30.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/src/App.js b/src/App.js index 12193c73..73514c01 100644 --- a/src/App.js +++ b/src/App.js @@ -1,17 +1,28 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; import React from "react"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; import Main from "./pages/Main"; -import Login from "./pages/Login"; -import SignUp from "./pages/SignUp"; -import Market from "./pages/Market" +import MarketPage from "./pages/MarketPage/MarketPage"; +import Login from "./pages/login/Login"; +import SignUp from "./pages/signup/SignUp"; +import Item from "./pages/item/Item"; +import Privacy from "./pages/privacy/Privacy"; +import Faq from "./pages/faq/Faq"; +import RegistrationPage from "./pages/registration/RegistrationPage"; +import ProductDetailPage from "./pages/product/ProductDetailPage"; + function App() { return ( - } /> - } /> - {/* } /> */} - {/* } /> */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> ); diff --git a/src/api/itemApi.js b/src/api/itemApi.js new file mode 100644 index 00000000..5e4c5af7 --- /dev/null +++ b/src/api/itemApi.js @@ -0,0 +1,40 @@ +export async function getProducts(params = {}) { + const query = new URLSearchParams(params).toString(); + + try { + const response = await fetch( + ` https://panda-market-api.vercel.app/docs/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; + } +} + +export async function createProduct(productData) { + try { + const response = await fetch( + `https://panda-market-api.vercel.app/docs/products`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(productData), + } + ); + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + const body = await response.json(); + return body; + } catch (error) { + console.error("Failed to create product:", error); + throw error; + } +} diff --git a/src/assets/Matket/defaultImg.svg b/src/assets/Matket/defaultImg.svg new file mode 100644 index 00000000..1f155779 --- /dev/null +++ b/src/assets/Matket/defaultImg.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/images/icons/arrow_left.svg b/src/assets/images/icons/arrow_left.svg new file mode 100644 index 00000000..2a9de23a --- /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 00000000..daa483c3 --- /dev/null +++ b/src/assets/images/icons/arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_arrow_down.svg b/src/assets/images/icons/ic_arrow_down.svg new file mode 100644 index 00000000..8308690f --- /dev/null +++ b/src/assets/images/icons/ic_arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_heart.svg b/src/assets/images/icons/ic_heart.svg new file mode 100644 index 00000000..cad016c1 --- /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 00000000..52241e6d --- /dev/null +++ b/src/assets/images/icons/ic_search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/ic_sort_mobile.svg b/src/assets/images/icons/ic_sort_mobile.svg new file mode 100644 index 00000000..657b44f9 --- /dev/null +++ b/src/assets/images/icons/ic_sort_mobile.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 00000000..d497acbf --- /dev/null +++ b/src/assets/images/logo/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/components/Constants.js b/src/components/Constants.js index c0b6f608..f8b31cf4 100644 --- a/src/components/Constants.js +++ b/src/components/Constants.js @@ -6,3 +6,5 @@ const USER_DATA = [ { email: "codeit5@codeit.com", password: "codeit505!" }, { email: "codeit6@codeit.com", password: "codeit606!" }, ]; + +export default USER_DATA; diff --git a/src/components/Errormodals.js b/src/components/Errormodals.js deleted file mode 100644 index 8e29acb6..00000000 --- a/src/components/Errormodals.js +++ /dev/null @@ -1,25 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - const errorModal = document.getElementById("error-modal"); - const modalMessage = document.getElementById("modal-message"); - const confirmButton = document.getElementById("confirm-button"); - - - function showModal(message, url = null) { - console.log(url); - modalMessage.textContent = message; - errorModal.style.display = "block"; - redirectUrl = url; - } - - function closeModal() { - errorModal.style.display = "none"; - if (redirectUrl) { - window.location.href = redirectUrl; - } - } - - confirmButton.addEventListener("click", closeModal); - - window.showModal = showModal; - console.log(USER_DATA); -}); diff --git a/src/components/Layout/Header.css b/src/components/Layout/Header.css new file mode 100644 index 00000000..1d0c90d7 --- /dev/null +++ b/src/components/Layout/Header.css @@ -0,0 +1,49 @@ +.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:hover { + color: var(--blue); +} + +.globalHeader nav ul li.active a { + color: #3692ff; +} + +.loginLink { + font-size: 16px; + font-weight: 600; + border-radius: 8px; + padding: 11.5px 23px; +} + +@media (min-width: 744px) { + .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 00000000..7c105fdf --- /dev/null +++ b/src/components/Layout/Header.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import Logo from "../../assets/images/logo/logo.svg"; +import "./Header.css"; +import { Link, useLocation } from "react-router-dom"; + +function Header() { + const location = useLocation(); + const isItemsPage = location.pathname === "/items"; + + return ( +
+
+ + 판다마켓 로고 + + + +
+ +
+ ); +} + +export default Header; diff --git a/src/components/UI/DropdownList.css b/src/components/UI/DropdownList.css new file mode 100644 index 00000000..4d25cf91 --- /dev/null +++ b/src/components/UI/DropdownList.css @@ -0,0 +1,29 @@ +.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 { + width: 100%; + padding: 12px 44px; + border-bottom: 1px solid #e5e7eb; + font-size: 16px; + color: #1f2937; + cursor: pointer; + background-color: transparent; + text-align: left; +} + +.dropdownItem:last-child { + border-bottom: none; +} + +.dropdownItem:hover { + background-color: #f3f4f6; +} diff --git a/src/components/UI/DropdownList.jsx b/src/components/UI/DropdownList.jsx new file mode 100644 index 00000000..b2985613 --- /dev/null +++ b/src/components/UI/DropdownList.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import "./DropdownList.css"; + +function DropdownList({ onSortSelection }) { + return ( +
+ + +
+ ); +} + +export default DropdownList; diff --git a/src/components/UI/PaginationBar.css b/src/components/UI/PaginationBar.css new file mode 100644 index 00000000..8c5975cb --- /dev/null +++ b/src/components/UI/PaginationBar.css @@ -0,0 +1,30 @@ +.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; + background-color: #fff; +} + +.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 00000000..3bcffdaf --- /dev/null +++ b/src/components/UI/PaginationBar.jsx @@ -0,0 +1,59 @@ +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"; + +function PaginationBar({ totalPageNum = 1, activePageNum = 1, 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) }, + (_, index) => startPage + index + ); + + return ( +
+ + {pages.map((page) => ( + + ))} + +
+ ); +} + +export default PaginationBar; diff --git a/src/styles/modal.css b/src/components/modal/modal.css similarity index 100% rename from src/styles/modal.css rename to src/components/modal/modal.css diff --git a/src/index.js b/src/index.js index 895d8184..b391783d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,11 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import "./styles/global.css" +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( ); - diff --git a/src/pages/Item.js b/src/pages/Item.js deleted file mode 100644 index a6a6beec..00000000 --- a/src/pages/Item.js +++ /dev/null @@ -1,6 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import '../styles/item.css' - -export default function Item() { - const -} \ No newline at end of file diff --git a/src/pages/Login.js b/src/pages/Login.js deleted file mode 100644 index 75e4f501..00000000 --- a/src/pages/Login.js +++ /dev/null @@ -1,236 +0,0 @@ -import React, { useEffect, useMemo, useState } from "react"; -import "../styles/auth.css" -import "../styles/modal.css" -import USER_DATA from "../components/Constants" -//로고 -import logo from "../assets/logo/logo.svg"; -import googleLogo from "../assets/social/google-logo.png"; -import kakaoLogo from "../assets/social/kakao-logo.png"; -import eyeInvisible from "../assets/icons/eye-invisible.svg"; -import eyeVisible from "../assets/icons/eye-visible.svg"; -import { Link } from "react-router-dom"; - -const errors = { - passwordMismatch: "이메일 또는 비밀번호가 일치하지 않습니다.", - emailExists: "이미 가입된 이메일입니다." -}; - -export default function Login() { - const [email, setEamil] = useState(""); - const [password, setPassword] = useState(""); - - const [isEmailValid, setIsEmailValid] = useState(false); - const [isPasswordValid, setIsPasswordInvalid] = useState(false); - - const [showEmailEmpty, setShowEmailEmpty] = useState(false); - const [showEmailInvalid, setShowEmailInvalid] = useState(false); - const [showPasswordEmpty, setShowPasswordEmpty] = useState(false); - const [showPasswordShort, setShowPasswordShort] = useState(false); - - const [showPassword, setShowPassword] = useState(false); - - const [modalOpen, setModalOpen] = useState(false); - const [modalMessage, setModalMessage] = useState(""); - const [modalConfirmHref, setModalConfirmHref] = useState(""); - - const isFormValid = useMemo( - () => isEmailValid && isPasswordValid, - [isEmailValid, isPasswordValid] - ); - - const validateEmailString = (value) => { - const emailRegex = /^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/; - return emailRegex.test(value); - }; - - const checkEmailValidity = () => { - const v = email.trim(); - setIsEmailValid(false); - setShowEmailEmpty(false); - setShowEmailInvalid(false); - - if (!v) { - setShowEmailEmpty(true); - } else if (!validateEmailString(v)) { - setShowEmailInvalid(true); - } else { - setIsEmailValid(true); - } - }; - - const checkPasswordValidity = () => { - const v = password.trim(); - setIsPasswordInvalid(false); - setShowPasswordEmpty(false); - setShowPasswordShort(false); - - if (!v) { - setShowPasswordEmpty(true); - } else if (v.length < 8) { - setShowPasswordShort(true); - } else { - setIsPasswordInvalid(true); - } - }; - - useEffect(() => { - checkEmailValidity(); - checkPasswordValidity(); - }, []); - - const showModal = (message, confirmHref = "") => { - setModalMessage(message); - setModalConfirmHref(confirmHref); - setModalOpen(true); - }; - - const closeModal = () => { - setModalOpen(false); - setModalMessage(""); - setModalConfirmHref(""); - }; - - const handleModalConfirm = () => { - if (modalConfirmHref) { - window.location.href = modalConfirmHref; - } else { - closeModal(); - } - } - - const onSubmit = (e) => { - e.preventDefault(); - checkEmailValidity(); - checkPasswordValidity(); - if (!isFormValid) return; - - const user = USER_DATA.find((u) => u.email === email.trim()); - if (!user || user.password !== password.trim()) { - showModal(errors.passwordMismatch, "/src/pages/SignUp.js"); - } else { - window.location.href = "./Items.js"; - } - }; - return ( - <> -
- - 판다마켓 로고 - - -
-
- - setEamil(e.target.value)} - onBlur={checkEmailValidity} - aria-invalid={showEmailEmpty || showEmailInvalid} - /> - {showEmailEmpty && ( - - 이메일을 입력해주세요 - - )} - {showEmailInvalid && ( - - 잘못된 이메일 형식입니다 - - )} -
- -
- -
- setPassword(e.target.value)} - onInput={checkPasswordValidity} - aria-invalid={showPasswordEmpty || showPasswordShort} - /> - -
- {showPasswordEmpty && ( - - 비밀번호를 입력해 주세요 - - )} - {showPasswordShort && ( - - 비밀번호를 8자 이상 입력해주세요 - - )} -
- - -
- -
-

간편 로그인하기

-
- - 구글 로그인 - - - 카카오톡 로그인 - -
-
- -
- 판다마켓이 처음이신가요? 회원가입 -
-
- - {modalOpen && ( - - )} - - ); -} \ No newline at end of file diff --git a/src/pages/Main.js b/src/pages/Main.js index 13e8e100..7ecfbbe9 100644 --- a/src/pages/Main.js +++ b/src/pages/Main.js @@ -1,7 +1,7 @@ import React from "react"; -import "../styles/home.css" -// 이미지 -import logo from "../assets/logo/logo.svg" +import "../styles/home.css"; +import { Link } from "react-router-dom"; +import logo from "../assets/logo/logo.svg"; import feature1 from "../assets/home/feature1-image.png"; import feature2 from "../assets/home/feature2-image.png"; import feature3 from "../assets/home/feature3-image.png"; @@ -9,96 +9,96 @@ import facebookIcon from "../assets/social/facebook-logo.svg"; import twitterIcon from "../assets/social/twitter-logo.svg"; import youtubeIcon from "../assets/social/youtube-logo.svg"; import instagramIcon from "../assets/social/instagram-logo.svg"; -import { Link } from "react-router-dom"; -import Login from "./Login"; -export default function Main () { +export default function Main() { return ( <> -
- - 판다마켓 로고 - - 로그인 -
- -
-
-
-

- 일상의 모든 물건을 -
- 거래해 보세요 -

- - 구경하러가기 - -
-
+
+ + 판다마켓 로고 + + + 로그인 + +
-
-
- 인기 상품 -
-

Hot item

+
+
+

- 인기 상품을
- 확인해보세요 -

-

- 가장 HOT한 중고거래 물품을 + 일상의 모든 물건을
- 판다마켓에서 확인해 보세요 -

+ 거래해 보세요 + + + 구경하러가기 +
-
+
-
- 검색 가능 -
-

Search

-

- 구매를 원하는
- 상품을 검색하세요 -

-

- 구매하고 싶은 물품은 검색해서 -
- 쉽게 찾아보세요 -

+
+
+ 인기 상품 +
+

Hot item

+

+ 인기 상품을
+ 확인해보세요 +

+

+ 가장 HOT한 중고거래 물품을 +
+ 판다마켓에서 확인해 보세요 +

+
-
-
- 판매 상품 등록 -
-

Register

+
+ 검색 가능 +
+

Search

+

+ 구매를 원하는
+ 상품을 검색하세요 +

+

+ 구매하고 싶은 물품은 검색해서 +
+ 쉽게 찾아보세요 +

+
+
+ +
+ 판매 상품 등록 +
+

Register

+

+ 판매를 원하는
+ 상품을 등록하세요 +

+

+ 어떤 물건이든 판매하고 싶은 상품을 +
+ 쉽게 등록하세요 +

+
+
+ + +
+

- 판매를 원하는
- 상품을 등록하세요 -

-

- 어떤 물건이든 판매하고 싶은 상품을 + 믿을 수 있는
- 쉽게 등록하세요 -

+ 판다마켓 중고거래 +
-
- + +
-
-
-

- 믿을 수 있는 -
- 판다마켓 중고거래 -

-
-
- - -