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 (
+
+
+

{
+ 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 && (
+
+ {OPTIONS.map((opt) => (
+ -
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+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()],
+})