diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..ec2b712d --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/index.html b/index.html new file mode 100644 index 00000000..9353f978 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + 판다마켓 + + +
+ + + diff --git a/package-DESKTOP-HJ6G99B.json b/package-DESKTOP-HJ6G99B.json new file mode 100644 index 00000000..7ff0d6b5 --- /dev/null +++ b/package-DESKTOP-HJ6G99B.json @@ -0,0 +1,38 @@ +{ + "name": "1-weekly-mission", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/package.json b/package.json index 7ff0d6b5..0bf07bc3 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,29 @@ { - "name": "1-weekly-mission", - "version": "0.1.0", + "name": "16-sprint-mission", "private": true, - "dependencies": { - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" - }, + "version": "0.0.0", + "type": "module", "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] + "dependencies": { + "axios": "^1.9.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^6.30.1" }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "vite": "^6.3.5" } } diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 00000000..50a46335 --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 00000000..a8cfc7a8 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,22 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom' +import "./App.css"; +import Navbar from "./components/Navbar"; +import HomePage from './pages/HomePage'; +import ItemsPage from './pages/ItemsPage'; +import AddItemPage from './pages/AddItemPage'; + + +function App() { + return ( + + + + }/> + }/> + }/> + + + ); +} + +export default App; diff --git a/src/api/itemApi.jsx b/src/api/itemApi.jsx new file mode 100644 index 00000000..ef30bf07 --- /dev/null +++ b/src/api/itemApi.jsx @@ -0,0 +1,10 @@ +import axios from "axios"; + +const BASE_URL = 'https://panda-market-api.vercel.app'; + +async function fetchItems(params) { + const res = await axios.get(`${BASE_URL}/products`, {params}); + return res.data; +} + +export default fetchItems; diff --git a/src/assets/arrow_next.svg b/src/assets/arrow_next.svg new file mode 100644 index 00000000..368742c9 --- /dev/null +++ b/src/assets/arrow_next.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/arrow_prev.svg b/src/assets/arrow_prev.svg new file mode 100644 index 00000000..040e81c2 --- /dev/null +++ b/src/assets/arrow_prev.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/favorite_icon.svg b/src/assets/favorite_icon.svg new file mode 100644 index 00000000..576d3a17 --- /dev/null +++ b/src/assets/favorite_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/ic_arrow_down.svg b/src/assets/ic_arrow_down.svg new file mode 100644 index 00000000..b423610b --- /dev/null +++ b/src/assets/ic_arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/ic_search.svg b/src/assets/ic_search.svg new file mode 100644 index 00000000..52241e6d --- /dev/null +++ b/src/assets/ic_search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/ic_sort.svg b/src/assets/ic_sort.svg new file mode 100644 index 00000000..657b44f9 --- /dev/null +++ b/src/assets/ic_sort.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 00000000..ceaa7bf0 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/mb_logo.svg b/src/assets/mb_logo.svg new file mode 100644 index 00000000..ccdc9bcf --- /dev/null +++ b/src/assets/mb_logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/tb_logo.svg b/src/assets/tb_logo.svg new file mode 100644 index 00000000..e6848692 --- /dev/null +++ b/src/assets/tb_logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/user_icon.svg b/src/assets/user_icon.svg new file mode 100644 index 00000000..241e031b --- /dev/null +++ b/src/assets/user_icon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/AllItemsSection.jsx b/src/components/AllItemsSection.jsx new file mode 100644 index 00000000..86b44f43 --- /dev/null +++ b/src/components/AllItemsSection.jsx @@ -0,0 +1,59 @@ +import { useState, useEffect } from "react"; +import useItem from "./Hooks/useItem"; +import ItemCard from "./ItemCard"; +import ItemControls from "./ItemControls"; +import styles from "../pages/ItemsPage.module.css"; +import Pagination from "./Pagination"; + +function AllItemsSectoin({ bp }) { + // 정렬 모드 & 페이지 + const [orderBy, setOrderBy] = useState("recent"); + const [page, setPage] = useState(1); + + // 브레이크포인트에 따라 한 번에 불러올 개수 + const pageSizeMap = { mobile: 4, tablet: 6, desktop: 10 }; + const pageSize = pageSizeMap[bp]; + + // pageSize가 바뀌면 페이지 번호 초기화 + useEffect(() => { + setPage(1); + }, [pageSize]); + + //데이터 패칭 + const { items: allItems = [], totalCount = 0 } = useItem({ + page, + pageSize, + orderBy, + }); + + return ( +
+
+

전체 상품

+ +
+
+ {allItems.map((item) => { + return ( + + ); + })} +
+ setPage(newPage)} + /> +
+ ); +} + +export default AllItemsSectoin; diff --git a/src/components/BestItemsSection.jsx b/src/components/BestItemsSection.jsx new file mode 100644 index 00000000..0585d081 --- /dev/null +++ b/src/components/BestItemsSection.jsx @@ -0,0 +1,33 @@ +import useItem from "./Hooks/useItem"; +import ItemCard from "./ItemCard"; +import styles from "../pages/ItemsPage.module.css"; + +function BestItemsSection({ bp }) { + const bestCountMap = { mobile: 1, tablet: 2, desktop: 4 }; + const bestCount = bestCountMap[bp]; + const { items: bestItems = [] } = useItem({ + page: 1, + pageSize: bestCount, + orderBy: "favorite", + }); + + return ( +
+

베스트 상품

+
+ {bestItems.map((item) => ( + + ))} +
+
+ ); +} + +export default BestItemsSection; diff --git a/src/components/Hooks/useBreakpoint.jsx b/src/components/Hooks/useBreakpoint.jsx new file mode 100644 index 00000000..eba0520e --- /dev/null +++ b/src/components/Hooks/useBreakpoint.jsx @@ -0,0 +1,33 @@ +import { useEffect, useState } from "react"; + +//Hydration 불일치 오류 방지 초기값 설정 +const DEFAULT_BP = "mobile"; + +function getBreakpoint() { + const w = window.innerWidth; + if (w >= 1200) return "desktop"; + if (w >= 768) return "tablet"; + return "mobile"; +} + +function useBreakpoint() { + const [bp, setBp] = useState(DEFAULT_BP); + + useEffect(() => { + const updateBp = () => { + setBp(getBreakpoint()); + }; + + window.addEventListener("resize", updateBp); //resize 이벤트 감지 시 실행 + updateBp(); //마운트 시점 동기화 + + return () => { + //클린업 이벤트 제거 + window.removeEventListener("resize", updateBp); + }; + }, []); + + return bp; +} + +export default useBreakpoint; diff --git a/src/components/Hooks/useItem.jsx b/src/components/Hooks/useItem.jsx new file mode 100644 index 00000000..74d454fd --- /dev/null +++ b/src/components/Hooks/useItem.jsx @@ -0,0 +1,36 @@ +import { useState, useEffect } from "react"; +import fetchItems from "../../api/itemApi"; + +function useItem({ + page = 1, + pageSize = 10, + orderBy = "recent", + name = "", +} = {}) { + const [items, setItems] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const loadItems = async () => { + setLoading(true); + setError(null); + try { + const data = await fetchItems({ page, pageSize, orderBy, name }); + setItems(data.list); + setTotalCount(data.totalCount); + } catch (err) { + console.error("상품 데이터 불러오기 실패:", err); + setError(err); + } finally { + setLoading(false); + } + }; + loadItems(); + }, [page, pageSize, orderBy, name]); + + return { items, totalCount, loading, error }; +} + +export default useItem; diff --git a/src/components/ItemCard.jsx b/src/components/ItemCard.jsx new file mode 100644 index 00000000..d49c4140 --- /dev/null +++ b/src/components/ItemCard.jsx @@ -0,0 +1,27 @@ +import styles from "./ItemCard.module.css"; +import fvIcon from "../assets/favorite_icon.svg"; + +function ItemCard({ image, name, price, favoriteCount, imageClassName = "" }) { + const localePrice = price.toLocaleString(); + return ( +
+
+ {name} { + e.target.src = "https://placehold.co/600x400?text=Image+Not+Found"; + }} + /> +
+
{name}
+
{localePrice}원
+
+ 좋아요 + {favoriteCount} +
+
+ ); +} + +export default ItemCard; diff --git a/src/components/ItemCard.module.css b/src/components/ItemCard.module.css new file mode 100644 index 00000000..5a9586a8 --- /dev/null +++ b/src/components/ItemCard.module.css @@ -0,0 +1,51 @@ +.card { + display: flex; + flex-direction: column; + gap: 16px; + justify-content: space-between; + cursor: pointer; +} + +.imageWrapper { + width: 100%; + height: 164px; + border-radius: 12px; + overflow: hidden; +} + +.imageWrapper img { + width: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; +} + +.name { + font-size: 14px; + font-weight: 500; + color: var(--secondary-color-gray800); +} + +.price { + font-size: 16px; + font-weight: 700; + color: var(--secondary-color-gray800); +} + +.favorite { + font-size: 12px; + color: var(--secondary-color-gray600); + display: flex; + align-items: center; + gap: 4px; +} + +.favorite img { + width: 16px; + height: 16px; +} + +@media (min-width: 768px) { + .allItems .imageWrapper { + height: 221px; + } +} diff --git a/src/components/ItemControls.jsx b/src/components/ItemControls.jsx new file mode 100644 index 00000000..29819db5 --- /dev/null +++ b/src/components/ItemControls.jsx @@ -0,0 +1,31 @@ +import { Link } from "react-router-dom"; +import styles from "./ItemControls.module.css"; +import SortDropdown from "./SortDropdown"; + +/** + * ItemControls 컴포넌트 + * @param {Object} props + * @param {string} props.orderBy - 현재 선택된 정렬 기준 + * @param {(value: string) => void} props.onSortChange - 정렬 기준 변경 핸들러 + * @param {(keyword: string) => void} props.onSearch - 검색 실행 핸들러 + */ + +function ItemControls({ orderBy, onSortChange }) { + return ( +
+
+ +
+ + 상품 등록하기 + + +
+ ); +} + +export default ItemControls; diff --git a/src/components/ItemControls.module.css b/src/components/ItemControls.module.css new file mode 100644 index 00000000..f767b7b0 --- /dev/null +++ b/src/components/ItemControls.module.css @@ -0,0 +1,58 @@ +.controlsWrapper { + width: 100%; + height: 42px; + display: flex; + justify-content: space-between; + position: relative; +} + +.addItemLink { + width: 133px; + height: 42px; + border-radius: 8px; + background-color: var(--blue); + text-align: center; + line-height: 42px; + color: #fff; + position: absolute; + right: 0; + top: -6px; + transform: translateY(-100%); +} + +.searchInput { + width: 288px; + height: 100%; + border-radius: 12px; + padding: 9px 20px 9px 36px; + box-sizing: border-box; + background-color: var(--secondary-color-gray100); + background-image: url(../assets/ic_search.svg); + background-repeat: no-repeat; + background-size: 24px 24px; + background-position: 8px 50%; + color: var(--secondary-color-gray400); + font-size: 16px; + font-weight: 400; +} + +.searchInput:focus { + outline: 2px solid var(--blue); +} + +@media (min-width: 768px) { + .controlsWrapper { + width: auto; + justify-content: flex-end; + gap: 12px; + } + + .searchInput { + width: 242px; + } + + .addItemLink { + position: static; + transform: translateY(0); + } +} diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx new file mode 100644 index 00000000..445ad110 --- /dev/null +++ b/src/components/Navbar.jsx @@ -0,0 +1,43 @@ +import { Link, NavLink } from "react-router-dom"; +import mbLogo from "../assets/mb_logo.svg"; +import tbLogo from "../assets/tb_logo.svg"; +import userIcon from "../assets/user_icon.svg"; +import styles from "./Navbar.module.css"; + +function getLinkStyle({ isActive}) { + return{ + color: isActive ? 'var(--blue)' : 'var(--secondary-color-gray600)' + } +} + + + +function Navbar() { + return ( +
+
+ +

+ + 로고 + 로고 + +

+
    +
  • + 자유게시판 +
  • +
  • + 중고마켓 +
  • +
+
+ +
+
+ ); +} + +export default Navbar; diff --git a/src/components/Navbar.module.css b/src/components/Navbar.module.css new file mode 100644 index 00000000..33d3e1de --- /dev/null +++ b/src/components/Navbar.module.css @@ -0,0 +1,97 @@ +.navbar { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 70px; + background-color: #ffffff; + border-bottom: 1px solid var(--border-color); + z-index: 999; +} + +.tbLogo { + display: none; +} + +.navbar div { + max-width: 344px; + height: 100%; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.navbar span { + display: flex; + align-items: center; + gap: 12px; +} + +.navbar h1 { + height: 27px; +} + +.navbar ul { + display: flex; + gap: 12px; + list-style: none; + color: var(--secondary-color-gray600); + font-size: 16px; + font-weight: 700; +} + +.userButton { + background: none; + border: none; + cursor: pointer; +} + +.userButton img { + height: 40px; + width: 40px; + border-radius: 50%; +} + +@media (min-width: 768px) { + .navbar div { + max-width: 696px; + } + + .navbar h1 { + height: 40px; + } + + .logo { + display: flex; + align-items: center; + gap: 4px; + } + + .tbLogo { + width: 40px; + height: 40px; + display: block; + } + + .navbar span, + .navbar ul { + gap: 24px; + } + + .navbar ul { + font-size: 18px; + } +} + +@media (min-width: 1200px) { + .navbar div { + max-width: none; + padding: 0 200px; + } +} +@media (min-width: 1920px) { + .navbar div { + padding: 0 400px; + } +} diff --git a/src/components/Pagination.jsx b/src/components/Pagination.jsx new file mode 100644 index 00000000..712c7f73 --- /dev/null +++ b/src/components/Pagination.jsx @@ -0,0 +1,48 @@ +import styles from "./Pagination.module.css"; + +function Pagination({ page, pageSize, totalCount, onPageChange }) { + const blockSize = 5; + const totalPages = Math.ceil(totalCount / pageSize); + const currentBlock = Math.ceil(page / blockSize); + const startPage = (currentBlock - 1) * blockSize + 1; + const endPage = Math.min(startPage + blockSize - 1, totalPages); + + const handlePrevBlock = () => { + const prevBlockPage = Math.max(startPage - blockSize, 1); + onPageChange(prevBlockPage); + }; + + const handleNextBlock = () => { + const nextBlockPage = Math.min(startPage + blockSize, totalPages); + onPageChange(nextBlockPage); + }; + + return ( +
+ + {[...Array(endPage - startPage + 1)].map((_, idx) => { + const pageNum = startPage + idx; + return ( + + ); + })} + +
+ ); +} + +export default Pagination; diff --git a/src/components/Pagination.module.css b/src/components/Pagination.module.css new file mode 100644 index 00000000..908a8d4b --- /dev/null +++ b/src/components/Pagination.module.css @@ -0,0 +1,39 @@ +.paginationWrapper { + margin: 40px auto 0; + width: 304px; + min-height: 40px; + display: flex; + align-items: center; + gap: 4px; +} + +.paginationWrapper button { + width: 40px; + height: 40px; + border-radius: 40px; + background-color: #fff; + border: 1px solid var(--secondary-color-gray200); + + font-size: 16px; + font-weight: 600; + color: var(--secondary-color-gray500); +} + +.paginationWrapper .active { + background-color: #2f80ed; + color: var(--secondary-color-gray200); +} + +.prevBtn, +.nextBtn { + background-size: 16px 16px; + background-repeat: no-repeat; + background-position: center; +} + +.prevBtn { + background-image: url(../assets/arrow_prev.svg); +} +.nextBtn { + background-image: url(../assets/arrow_next.svg); +} diff --git a/src/components/SearchInput.jsx b/src/components/SearchInput.jsx new file mode 100644 index 00000000..e6f5bf29 --- /dev/null +++ b/src/components/SearchInput.jsx @@ -0,0 +1,20 @@ +import React from "react"; +import styles from "./SearchInput.module.css"; + +export default function SearchInput({ value, onChange, onSearch }) { + return ( +
+ onChange(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && onSearch()} + /> + +
+ ); +} diff --git a/src/components/SortDropdown.jsx b/src/components/SortDropdown.jsx new file mode 100644 index 00000000..eb4ab51f --- /dev/null +++ b/src/components/SortDropdown.jsx @@ -0,0 +1,49 @@ +import { useState } from "react"; +import styles from "./SortDropdown.module.css"; + +const OPTIONS = [ + { value: "recent", label: "최신순" }, + { value: "favorite", label: "좋아요순" }, +]; + +function SortDropdown({ value, onChange }) { + const [open, setOpen] = useState(false); + + // 메뉴 토글 + const handleToggle = () => setOpen((prev) => !prev); + + // 옵션 선택 후: value 전달 + 메뉴 닫기 + const handleSelect = (selectedValue) => { + onChange(selectedValue); + setOpen(false); + }; + + // 버튼에 보여줄 레이블 + const selectedLabel = OPTIONS.find((o) => o.value === value)?.label; + + return ( +
+ + {open && ( + + )} +
+ ); +} + +export default SortDropdown; diff --git a/src/components/SortDropdown.module.css b/src/components/SortDropdown.module.css new file mode 100644 index 00000000..ae85925d --- /dev/null +++ b/src/components/SortDropdown.module.css @@ -0,0 +1,72 @@ +.selectWrapper { + position: relative; + display: inline-block; +} + +.button { + width: 42px; + height: 42px; + background-color: #fff; + border-radius: 12px; + border: 1px solid var(--secondary-color-gray200); +} + +.icon { + width: 100%; + height: 100%; + display: block; + background-image: url(../assets/ic_sort.svg); + background-position: center; + background-repeat: no-repeat; + background-size: 24px 24px; +} + +.label { + display: none; +} + +.menu { + position: absolute; + right: 0; + bottom: -4px; + transform: translateY(100%); + border-radius: 12px; + background-color: #fff; + border: 1px solid var(--secondary-color-gray200); + overflow: hidden; +} + +.option { + width: 130px; + height: 42px; + font-size: 16px; + font-weight: 400; + color: var(--secondary-color-gray800); +} + +.menu > li:nth-child(2) .option { + border-top: 1px solid var(--secondary-color-gray200); +} + +.option:hover { + background-color: var(--secondary-color-gray100); +} + +@media (min-width: 768px) { + .icon { + display: none; + } + + .label { + display: inline; + } + + .button { + width: 130px; + text-align: left; + padding: 0 20px; + background-image: url(../assets/ic_arrow_down.svg); + background-repeat: no-repeat; + background-position: 90px 50%; + } +} diff --git a/src/global.css b/src/global.css new file mode 100644 index 00000000..5b1b2115 --- /dev/null +++ b/src/global.css @@ -0,0 +1,119 @@ +/* Mobile styles */ +@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; + font-display: swap; +} + +:root { + --secondary-color-gray900: #111827; + --secondary-color-gray800: #1f2937; + --secondary-color-gray600: #4b5563; + --secondary-color-gray500: #6b7280; + --secondary-color-gray400: #9ca3af; + --secondary-color-gray200: #e5e7eb; + --secondary-color-gray100: #f3f4f6; + --secondary-color-gray50: #f9fafb; + --background-color: #cfe5ff; + --border-color: #dfdfdf; + + /* Primary color */ + --blue: #3692ff; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +a { + text-decoration: none; + color: inherit; + display: block; +} + +ul { + list-style: none; +} + +button, +input, +textarea, +select { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: none; +} + +button { + background: none; + outline: none; + box-shadow: none; + cursor: pointer; +} + +img { + vertical-align: top; +} + +body { + margin-top: 70px; + color: #374151; + word-break: keep-all; + font-family: "Pretendard", sans-serif; +} + +/* #copyright { + order: 3; + flex-basis: 100%; +} + +#footerMenu { + display: flex; + gap: 30px; + color: var(--gray-200); +} + +#socialMedia { + display: flex; + gap: 12px; +} */ + +.container { + width: 100%; + max-width: 344px; + margin: 0 auto; +} + +/* Tablet styles */ + +@media (min-width: 768px) { + .container { + max-width: 696px; + } +} + +/* Desktop styles */ + +@media (min-width: 1200px) { + header { + padding: 0 200px; + } + + .container { + max-width: 1200px; + } + + .navbar .container { + max-width: 1920px; + } +} diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 00000000..21fdd1d7 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,8 @@ +import { createRoot } from 'react-dom/client' +import App from './App.jsx' +import './global.css' + + +createRoot(document.getElementById('root')).render( + +) diff --git a/src/pages/AddItemPage.jsx b/src/pages/AddItemPage.jsx new file mode 100644 index 00000000..90e76930 --- /dev/null +++ b/src/pages/AddItemPage.jsx @@ -0,0 +1,5 @@ +function AddItemPage() { + return
상품 추가 페이지입니다.
+} + +export default AddItemPage; \ No newline at end of file diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx new file mode 100644 index 00000000..f46e9b97 --- /dev/null +++ b/src/pages/HomePage.jsx @@ -0,0 +1,5 @@ +function HomePage() { + return
홈페이지입니다.
+} + +export default HomePage; \ No newline at end of file diff --git a/src/pages/ItemsPage.jsx b/src/pages/ItemsPage.jsx new file mode 100644 index 00000000..8f76bb1f --- /dev/null +++ b/src/pages/ItemsPage.jsx @@ -0,0 +1,17 @@ +import useBreakpoint from "../components/Hooks/useBreakpoint"; +import BestItemsSection from "../components/BestItemsSection"; +import AllItemsSection from "../components/AllItemsSection"; +import styles from "./ItemsPage.module.css"; + +function ItemsPage() { + const bp = useBreakpoint(); + + return ( +
+ + +
+ ); +} + +export default ItemsPage; diff --git a/src/pages/ItemsPage.module.css b/src/pages/ItemsPage.module.css new file mode 100644 index 00000000..4913f3c8 --- /dev/null +++ b/src/pages/ItemsPage.module.css @@ -0,0 +1,72 @@ +.wrap { + display: flex; + flex-direction: column; + gap: 24px; + padding: 17px 0; +} + +.wrap section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.wrap h2 { + font-size: 20px; + font-weight: 700; + color: var(--secondary-color-gray900); +} + +.allGrid { + display: grid; + gap: 16px; + grid-template-columns: repeat(2, 1fr); + min-height: 558px; +} + +.bestImgWrap { + height: 343px; +} + +.sectionHeader { + display: flex; + flex-direction: column; + gap: 14px; +} + +@media (min-width: 768px) { + .allGrid { + grid-template-columns: repeat(3, 1fr); + } + + .bestGrid { + display: grid; + gap: 16px; + grid-template-columns: repeat(2, 1fr); + } + + .allImgWrap { + height: 221px; + } + + .sectionHeader { + flex-direction: row; + justify-content: space-between; + align-items: center; + } +} + +@media (min-width: 1200px) { + .allGrid { + grid-template-columns: repeat(5, 1fr); + } + + .bestGrid { + gap: 24px; + grid-template-columns: repeat(4, 1fr); + } + + .bestImgWrap { + height: 282px; + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 00000000..8b0f57b9 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})