diff --git a/react-app/package-lock.json b/react-app/package-lock.json index 22e46cc0..3b3eb878 100644 --- a/react-app/package-lock.json +++ b/react-app/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "axios": "^1.18.0", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router-dom": "^7.18.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -1124,6 +1125,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2451,6 +2465,44 @@ "react": "^19.2.7" } }, + "node_modules/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "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.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/rolldown": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", @@ -2501,6 +2553,12 @@ "semver": "bin/semver.js" } }, + "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/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/react-app/package.json b/react-app/package.json index eb08da50..cf7ea733 100644 --- a/react-app/package.json +++ b/react-app/package.json @@ -12,7 +12,8 @@ "dependencies": { "axios": "^1.18.0", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-router-dom": "^7.18.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/react-app/src/App.jsx b/react-app/src/App.jsx index aed31227..995d4c5b 100644 --- a/react-app/src/App.jsx +++ b/react-app/src/App.jsx @@ -1,7 +1,20 @@ -import MarketPage from './pages/MarketPage' +import { BrowserRouter, Navigate, Routes, Route } from 'react-router-dom' +import MarketPage from './pages/MarketPage.jsx' +import LandingPage from './pages/LandingPage.jsx' +import RegistrationPage from './pages/RegistrationPage.jsx' +import ProductDetailPage from './pages/ProductDetailPage.jsx' function App() { - return + return ( + + + } /> + } /> + } /> + } /> + + + ) } export default App diff --git a/react-app/src/api/products.js b/react-app/src/api/products.js new file mode 100644 index 00000000..d5ca05f1 --- /dev/null +++ b/react-app/src/api/products.js @@ -0,0 +1,40 @@ +const BASE_URL = 'http://localhost:3000' + + +export async function getProducts({ + offset = 0, + limit = 10, + keyword = '', + orderBy = 'recent', +}) { + const query = new URLSearchParams({ + offset, + limit, + keyword, + orderBy, + }) + + const response = await fetch(`${BASE_URL}/products?${query}`) + + if (!response.ok) { + throw new Error('상품 목록 조회 실패') + } + + return response.json() +} + +export async function createProduct(productData) { + const response = await fetch(`${BASE_URL}/products`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(productData), + }) + + if (!response.ok) { + throw new Error('상품 등록 실패') + } + + return response.json() +} \ No newline at end of file diff --git a/react-app/src/assets/Img_home_01.png b/react-app/src/assets/Img_home_01.png new file mode 100644 index 00000000..9305e5b3 Binary files /dev/null and b/react-app/src/assets/Img_home_01.png differ diff --git a/react-app/src/assets/Img_home_02.png b/react-app/src/assets/Img_home_02.png new file mode 100644 index 00000000..db7813fa Binary files /dev/null and b/react-app/src/assets/Img_home_02.png differ diff --git a/react-app/src/assets/Img_home_03.png b/react-app/src/assets/Img_home_03.png new file mode 100644 index 00000000..b8ad4d82 Binary files /dev/null and b/react-app/src/assets/Img_home_03.png differ diff --git a/react-app/src/assets/Img_home_bottom.png b/react-app/src/assets/Img_home_bottom.png new file mode 100644 index 00000000..d71932e5 Binary files /dev/null and b/react-app/src/assets/Img_home_bottom.png differ diff --git a/react-app/src/assets/Img_home_top.png b/react-app/src/assets/Img_home_top.png new file mode 100644 index 00000000..157b1a94 Binary files /dev/null and b/react-app/src/assets/Img_home_top.png differ diff --git a/react-app/src/assets/img_default.png b/react-app/src/assets/img_default.png new file mode 100644 index 00000000..625d2fe5 Binary files /dev/null and b/react-app/src/assets/img_default.png differ diff --git a/react-app/src/components/Footer.module.css b/react-app/src/components/Footer.module.css index 0a2402c2..c51203b4 100644 --- a/react-app/src/components/Footer.module.css +++ b/react-app/src/components/Footer.module.css @@ -1,6 +1,6 @@ .footer { background: var(--Secondary-900, #111827); - margin-top: 120px; + margin-top: 0px; } .inner { diff --git a/react-app/src/components/Header.jsx b/react-app/src/components/Header.jsx index deb7dabe..d2ab8117 100644 --- a/react-app/src/components/Header.jsx +++ b/react-app/src/components/Header.jsx @@ -1,3 +1,4 @@ +import { NavLink } from 'react-router-dom' import styles from './Header.module.css' import logoImage from '../assets/logo.png' import logoText from '../assets/logo-text.png' @@ -7,28 +8,38 @@ function Header() {
- + 판다마켓 판다마켓 - +
- + 로그인 - +
) diff --git a/react-app/src/components/Header.module.css b/react-app/src/components/Header.module.css index b5d7ca71..32fce6c3 100644 --- a/react-app/src/components/Header.module.css +++ b/react-app/src/components/Header.module.css @@ -103,6 +103,11 @@ white-space: nowrap; } +.active { + color: #3692ff; +} + + /* Tablet */ @media (max-width: 1199px) { .inner { diff --git a/react-app/src/components/Pagination.jsx b/react-app/src/components/Pagination.jsx index fa7ad602..0fd15e3a 100644 --- a/react-app/src/components/Pagination.jsx +++ b/react-app/src/components/Pagination.jsx @@ -14,12 +14,12 @@ function Pagination({ page, totalPages, onPageChange }) { //시작 페이지 계산 / 현재 페이지를 기준으로 왼쪽으로 2칸 // ex) page = 5, startPage = 3, 화면 : 3 4 5 6 7 // -1 방지를 위해 startPage가 무조건 1보다 큰 수일 수 있도록 max 사용 - const startPage = Math.max(page - 2, 1) + let startPage = Math.max(page - 2, 1) // 끝 페이지 계산 // startPage부터 총 5개의 페이지 버튼을 보여주기 위해 + (5 - 1) // ex) startPage = 3이면 endPage = 7 → 화면 : 3 4 5 6 7 // 마지막 페이지(totalPages)를 넘지 않도록 min 사용 - const endPage = Math.min(startPage + maxVisiblePages - 1, totalPages) + let endPage = Math.min(startPage + maxVisiblePages - 1, totalPages) // 현재 화면에 보이는 페이지 버튼 개수 계산 // 공식: 끝 페이지 - 시작 페이지 + 1 // ex) startPage = 8, endPage = 10 → 10 - 8 + 1 = 3개 (8 9 10) diff --git a/react-app/src/components/ProductCard.jsx b/react-app/src/components/ProductCard.jsx index 88d88c78..425c00c2 100644 --- a/react-app/src/components/ProductCard.jsx +++ b/react-app/src/components/ProductCard.jsx @@ -1,4 +1,5 @@ import styles from './ProductCard.module.css' +import defaultImg from '../assets/img_default.png' //상품 카드를 하나씩 띄워주는 역할 @@ -16,9 +17,13 @@ function ProductCard({ product, variant = 'default' }) { {imageUrl ? ( {product.name} ) : ( -
이미지 없음
+ {product.name} + // 디폴트 이미지 출력 )} - {/* 이미지가 있으면 이미지를 출력하고 : 없으면 이미지 없음 출력 */}
@@ -41,7 +46,7 @@ function ProductCard({ product, variant = 'default' }) { /> - {product.favoriteCount} + {product.favoriteCount ?? 0}
diff --git a/react-app/src/hooks/useProductFormValidation.js b/react-app/src/hooks/useProductFormValidation.js new file mode 100644 index 00000000..28721edd --- /dev/null +++ b/react-app/src/hooks/useProductFormValidation.js @@ -0,0 +1,49 @@ +function useProductFormValidation({ name, description, price, tags, tagInput }) { + const errors = {} + + if (!name.trim()) { + errors.name = '상품명을 입력해주세요.' + } else if (name.trim().length > 10) { + errors.name = '상품명은 10자 이내로 입력해주세요.' + } + + if (!description.trim()) { + errors.description = '상품 소개를 입력해주세요.' + } else if (description.trim().length < 10) { + errors.description = '상품 소개는 10자 이상 입력해주세요.' + } else if (description.trim().length > 100) { + errors.description = '상품 소개는 100자 이내로 입력해주세요.' + } + + if (!price.trim()) { + errors.price = '판매 가격을 입력해주세요.' + } else if (!/^\d+$/.test(price)) { + errors.price = '판매 가격은 숫자로 입력해주세요.' + } + + const hasInvalidTag = tags.some((tag) => tag.length > 5) + + if (tagInput.trim().length > 5) { + errors.tag = '태그는 5글자 이내로 입력해주세요.' + } else if (hasInvalidTag) { + errors.tag = '태그는 5글자 이내로 입력해주세요.' + } + + const isValid = + !errors.name && + !errors.description && + !errors.price && + !errors.tag && + tags.length > 0 + + if (tags.length === 0) { + errors.tag = errors.tag || '태그를 1개 이상 입력해주세요.' + } + + return { + errors, + isValid: isValid && tags.length > 0, + } +} + +export default useProductFormValidation \ No newline at end of file diff --git a/react-app/src/hooks/useProducts.js b/react-app/src/hooks/useProducts.js index b23d8c18..b20d3c58 100644 --- a/react-app/src/hooks/useProducts.js +++ b/react-app/src/hooks/useProducts.js @@ -14,13 +14,19 @@ function useProducts({ orderBy, keyword, page, pageSize }) { setError(null); //API를 요청 시도하기 // /products에 GET 요청을 보낸다 + + //추가한부분// + const offset = (page - 1) * pageSize + try { const response = await axios.get('/products', { params: { orderBy, keyword, - page, - pageSize, + // page, + // pageSize, + offset, + limit: pageSize, }, }); diff --git a/react-app/src/pages/LandingPage.css b/react-app/src/pages/LandingPage.css new file mode 100644 index 00000000..4eb8ca7a --- /dev/null +++ b/react-app/src/pages/LandingPage.css @@ -0,0 +1,508 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: #374151; +} + +a { + text-decoration: none; +} + +.gnb { + position: sticky; + top: 0; + z-index: 100; + height: 70px; + background: #ffffff; + border-bottom: 1px solid #DFDFDF; +} + +.gnb-inner { + /* max-width: 1120px; */ + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 200px; + +} + +.logo img { + width: 153px; + cursor: pointer; +} + +.login-button { + width: 128px; + height: 48px; + padding: 12px 23px; + border-radius: 8px; + background: var(--Primary-100, #3692FF); + color: var(--Cool-Gray-100, #F3F4F6); + + /* pretendard/lg-16px-semibold */ + font-family: Pretendard-SemiBold; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 26px; + /* 162.5% */ + + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; +} + +.content-wrapper { + max-width: 1120px; + margin: 0 auto; +} + +.home-top { + background: #cfe5ff; + height: 540px; + overflow: hidden; +} + +.home-top-inner { + height: 100%; + display: flex; + align-items: flex-end; + justify-content: space-between; +} + +.home-top-text { + margin-bottom: 160px; +} + +.home-top-text h1 { + + color: var(--Secondary-700, #374151); + font-family: Pretendard; + font-size: 40px; + font-style: normal; + font-weight: 700; + line-height: 140%; + /* 56px */ +} + +.primary-button { + width: 357px; + height: 56px; + border-radius: 40px; + background: var(--brand-blue, #3692FF); + color: #ffffff; + + display: flex; + align-items: center; + justify-content: center; + + color: var(--Secondary-50, #F9FAFB); + text-align: center; + + /* pretendard/xl-20px-semibold */ + font-family: Pretendard-SemiBold; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 32px; + /* 160% */ +} + +.home-top-image { + width: 746px; +} + +.info-section, +.second-info-section, +.third-info-section { + padding: 138px 0 0; +} + +.info-card, +.second-info-card, +.third-info-card { + max-width: 988px; + height: 444px; + margin: 0 auto; + border-radius: 12px; + background: #ffffff; + display: flex; + align-items: center; + overflow: hidden; + + gap: 64px; +} + +.info-card img, +.second-info-card img, +.third-info-card img { + width: 588px; +} + +.info-text, +.second-info-text, +.third-info-text { + flex: 1; +} + +.info-text span, +.second-info-text span, +.third-info-text span { + color: var(--Primary-100, #3692FF); + + /* pretendard/2lg-18px-bold */ + font-family: Pretendard; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 26px; + /* 144.444% */ +} + +.info-text h2, +.second-info-text h2, +.third-info-text h2 { + margin: 12px 0 24px; + + color: var(--Secondary-700, #374151); + font-family: Pretendard; + font-size: 40px; + font-style: normal; + font-weight: 700; + line-height: 140%; + /* 56px */ + letter-spacing: 0.8px; +} + +.info-text p, +.second-info-text p, +.third-info-text p { + margin: 0; + font-size: 24px; + line-height: 1.5; + font-weight: 500; + color: #374151; +} + +.second-info-text { + text-align: right; + padding-left: 64px; +} + +.third-info-section { + padding-bottom: 138px; +} + +.bottom-banner { + padding-top: 80px; + background: #cfe5ff; + overflow: hidden; +} + +.banner-inner { + height: 100%; + display: flex; + align-items: flex-end; + justify-content: space-between; +} + +.banner-text { + margin-bottom: 172px; + font-size: 40px; + line-height: 1.4; + font-weight: 700; + color: #374151; +} + +.banner-inner img { + width: 746px; +} + +/* tablet */ +@media (max-width: 1199px) { + + .gnb-inner, + .content-wrapper { + max-width: 100%; + } + + .home-top { + height: 771px; + } + + .home-top-inner { + flex-direction: column; + align-items: center; + justify-content: flex-end; + text-align: center; + } + + .home-top-text { + margin: 84px 0 80px; + } + + .home-top-image { + width: 100%; + max-width: 744px; + } + + .info-section, + .second-info-section, + .third-info-section { + padding: 24px 24px 0; + } + + .info-card, + .second-info-card, + .third-info-card { + max-width: 696px; + height: auto; + flex-direction: column; + align-items: stretch; + background: transparent; + border-radius: 12px; + } + + .info-card img, + .second-info-card img, + .third-info-card img { + width: 100%; + background: #f3f4f6; + border-radius: 12px; + } + + .second-info-card { + flex-direction: column-reverse; + } + + .info-text, + .second-info-text, + .third-info-text { + padding: 24px 0 48px; + } + + .second-info-text { + text-align: right; + padding-left: 0; + } + + .third-info-section { + padding-bottom: 56px; + } + + .bottom-banner { + height: 540px; + padding-top: 0; + } + + .banner-inner { + height: 100%; + flex-direction: column; + align-items: center; + justify-content: flex-end; + text-align: center; + } + + .banner-text { + margin: 0 0 220px; + } + + .banner-inner img { + width: 100%; + max-width: 744px; + } +} +.desktop-break { + display: block; +} + +@media (max-width: 1199px) { + .desktop-break { + display: none; + } +} +/* mobile */ +@media (max-width: 767px) { + + .gnb { + height: 51px; + } + + .gnb-inner { + padding: 0 16px; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; + } + + .logo img { + width: 103px; + } + + .login-button { + width: 88px; + height: 42px; + font-size: 14px; + padding: 0; + } + + /* Hero Section */ + .home-top { + height: 540px; + position: relative; + overflow: hidden; + } + + .home-top-inner { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + position: relative; + padding-top: 48px; + } + + .home-top-text { + margin: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + z-index: 2; + } + + .home-top-text h1 { + margin: 0; + font-size: 32px; + line-height: 140%; + text-align: center; + } + + .primary-button { + width: 240px; + height: 48px; + padding: 0; + font-size: 16px; + line-height: 26px; + white-space: nowrap; + margin: 0 auto; + } + + .home-top-image { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 448px; + max-width: unset; + min-width: unset; + } + + /* Info Sections */ + .info-section, + .second-info-section, + .third-info-section { + padding: 40px 16px 0; + } + + .info-card, + .second-info-card, + .third-info-card { + max-width: 100%; + height: auto; + flex-direction: column; + align-items: stretch; + background: transparent; + border-radius: 12px; + gap: 0; + } + + .second-info-card { + flex-direction: column-reverse; + } + + .info-card img, + .second-info-card img, + .third-info-card img { + width: 100%; + border-radius: 12px; + background: #f3f4f6; + } + + .info-text, + .second-info-text, + .third-info-text { + padding: 24px 0 48px; + } + + .second-info-text { + text-align: right; + padding-left: 0; + } + + .info-text span, + .second-info-text span, + .third-info-text span { + font-size: 16px; + } + + .info-text h2, + .second-info-text h2, + .third-info-text h2 { + font-size: 24px; + margin: 8px 0 16px; + } + + .info-text p, + .second-info-text p, + .third-info-text p { + font-size: 16px; + line-height: 1.5; + } + + .third-info-section { + padding-bottom: 40px; + } + + /* Bottom Banner */ + .bottom-banner { + height: 540px; + position: relative; + overflow: hidden; + } + + .banner-inner { + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + position: relative; + padding-top: 80px; + } + + .banner-text { + margin: 0; + font-size: 32px; + text-align: center; + line-height: 140%; + z-index: 2; + } + + .banner-inner img { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 448px; + } +} \ No newline at end of file diff --git a/react-app/src/pages/LandingPage.jsx b/react-app/src/pages/LandingPage.jsx new file mode 100644 index 00000000..9c3bfe7c --- /dev/null +++ b/react-app/src/pages/LandingPage.jsx @@ -0,0 +1,116 @@ +import './LandingPage.css' +import Footer from '../components/Footer.jsx' + +import logo from '../assets/logo.png' +import homeTop from '../assets/Img_home_top.png' +import home01 from '../assets/Img_home_01.png' +import home02 from '../assets/Img_home_02.png' +import home03 from '../assets/Img_home_03.png' +import homeBottom from '../assets/Img_home_bottom.png' + +function LandingPage() { + return ( + <> +
+
+ + 판다마켓 로고 + + + + 로그인 + +
+
+ +
+
+
+
+

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

+ + + 구경하러 가기 + +
+ + 판다 캐릭터 +
+
+ +
+
+ 인기 상품 +
+ Hot item +

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

+

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

+
+
+
+ +
+
+
+ Search +

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

+

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

+
+ 검색 이미지 +
+
+ +
+
+ 상품 등록 +
+ Register +

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

+

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

+
+
+
+ +
+
+
+ 믿을 수 있는 +
+ 판다마켓 중고 거래 +
+ + 판다 이미지 +
+
+
+