diff --git a/.gitignore b/.gitignore index 4d29575d..8692cf66 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # misc .DS_Store +.env .env.local .env.development.local .env.test.local diff --git a/src/App.js b/src/App.js index ea806be3..afabad3f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,6 @@ import { Routes, Route } from "react-router-dom"; import Home from "./pages/Home"; -import Items from "./pages/Items"; +import Items from "./pages/Items/Items.js"; import DefaultLayout from "./layouts/DefaultLayout"; import "pretendard/dist/web/static/pretendard.css"; import "./styles/global.css"; diff --git a/src/api/config.js b/src/api/config.js index 5b310e87..d8badfe3 100644 --- a/src/api/config.js +++ b/src/api/config.js @@ -1,2 +1 @@ -export const BASE_URL = "https://panda-market-api.vercel.app"; - +export const BASE_API_URL = process.env.REACT_APP_BASE_API_URL; \ No newline at end of file diff --git a/src/api/products.js b/src/api/products.js index bc329362..0a1ed187 100644 --- a/src/api/products.js +++ b/src/api/products.js @@ -1,7 +1,7 @@ -import { BASE_URL } from "./config"; +import { BASE_API_URL } from "./config"; export async function fetchProducts() { - const res = await fetch(`${BASE_URL}/products`); + const res = await fetch(`${BASE_API_URL}/products`); if (!res.ok) { throw new Error("상품 데이터를 불러오는데 실패했어요"); } @@ -11,7 +11,7 @@ export async function fetchProducts() { export async function fetchPaginatedProducts({ page = 1, pageSize = 10 } = {}) { const res = await fetch( - `${BASE_URL}/products?page=${page}&pageSize=${pageSize}` + `${BASE_API_URL}/products?page=${page}&pageSize=${pageSize}` ); if (!res.ok) throw new Error("상품 데이터를 불러오는데 실패했습니다."); const json = await res.json(); diff --git a/src/components/AllProducts/AllProducts.js b/src/components/AllProducts/AllProducts.js deleted file mode 100644 index 1b5524b9..00000000 --- a/src/components/AllProducts/AllProducts.js +++ /dev/null @@ -1,43 +0,0 @@ -import styles from "./AllProducts.module.css"; -import useResponsiveLimit from "../../hooks/useResponsiveLimit"; -import usePaginationState from "../../hooks/usePaginationState"; -import usePaginatedProducts from "../../hooks/usePaginatedProducts"; -import ProductSection from "../ProductSection/ProductSection"; -import Pagination from "../Pagination/Pagination"; -import { useState, useCallback } from "react"; - -function AllProducts({ title, itemsPerDevice }) { - const limit = useResponsiveLimit(itemsPerDevice); - const [page, changePage] = usePaginationState(limit); - const [sort, setSort] = useState("latest"); - - const { products, totalPages } = usePaginatedProducts({ page, limit, sort }); - - const handleSortChange = useCallback((newSort) => { - setSort(newSort); - changePage(1); - }, [changePage]); - - return ( - <> - -
- -
- - ); -} - - -export default AllProducts; diff --git a/src/components/BestProducts/BestProducts.js b/src/components/BestProducts/BestProducts.js deleted file mode 100644 index 397c1fa2..00000000 --- a/src/components/BestProducts/BestProducts.js +++ /dev/null @@ -1,11 +0,0 @@ -import useBestProducts from "../../hooks/useBestProducts"; -import ProductSection from "../ProductSection/ProductSection"; - -function BestProducts({ title, itemsPerDevice}) { - - const bestProducts = useBestProducts(itemsPerDevice); - -return ; -} - -export default BestProducts; diff --git a/src/hooks/usePaginatedProducts.js b/src/hooks/usePaginatedProducts.js deleted file mode 100644 index 51f748b3..00000000 --- a/src/hooks/usePaginatedProducts.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from "react"; -import { fetchPaginatedProducts } from "../api/products"; - -export default function usePaginatedProducts({ page, limit, sort }) { - const [products, setProducts] = useState([]); - const [totalPages, setTotalPages] = useState(1); - - useEffect(() => { - const load = async () => { - const res = await fetchPaginatedProducts({ page, pageSize: limit }); - let data = res.list || []; - - if (sort === "likes") { - data.sort((a, b) => b.favoriteCount - a.favoriteCount); - } else { - data.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); - } - - setProducts(data); - setTotalPages(Math.ceil(res.totalCount / limit)); - }; - - if (page && limit) load(); - }, [page, limit, sort]); - - return { products, totalPages }; -} diff --git a/src/hooks/useResponsiveLimit.js b/src/hooks/useResponsiveLimit.js deleted file mode 100644 index 61169c64..00000000 --- a/src/hooks/useResponsiveLimit.js +++ /dev/null @@ -1,41 +0,0 @@ -import { useEffect, useState } from "react"; -import { getLimitFromWindowWidth } from "../utils/getLimitFromWindowWidth"; - -export default function useResponsiveLimit(itemsPerDevice) { - const [limit, setLimit] = useState(() => - getLimitFromWindowWidth( - itemsPerDevice.desktop, - itemsPerDevice.tablet, - itemsPerDevice.mobile - ) - ); - - useEffect(() => { - let timeoutId; - - const update = () => { - // 디바운싱 추가 - clearTimeout(timeoutId); - timeoutId = setTimeout(() => { - const newLimit = getLimitFromWindowWidth( - itemsPerDevice.desktop, - itemsPerDevice.tablet, - itemsPerDevice.mobile - ); - setLimit(prev => { - if (prev !== newLimit) return newLimit; - return prev; - }); - }, 120); // 리사이즈 멈춘 뒤 120ms 후에만 실행 - }; - - update(); - window.addEventListener("resize", update); - return () => { - clearTimeout(timeoutId); - window.removeEventListener("resize", update); - }; - }, [itemsPerDevice]); - - return limit; -} diff --git a/src/pages/Items.js b/src/pages/Items/Items.js similarity index 71% rename from src/pages/Items.js rename to src/pages/Items/Items.js index 0a17c4ee..449a1f1c 100644 --- a/src/pages/Items.js +++ b/src/pages/Items/Items.js @@ -1,8 +1,8 @@ import style from "./Items.module.css" -import BestProducts from "../components/BestProducts/BestProducts"; -import AllProducts from "../components/AllProducts/AllProducts"; -import { BEST_PRODUCTS_PER_DEVICE,ALL_PRODUCTS_PER_DEVICE } from "../constants/products"; -import { TITLE_ALL_PRODUCTS_COMP,TITLE_BEST_PRODUCTS_COMP } from "../constants/titles"; +import BestProducts from "./components/BestProducts/BestProducts"; +import AllProducts from "./components/AllProducts/AllProducts"; +import { BEST_PRODUCTS_PER_DEVICE,ALL_PRODUCTS_PER_DEVICE } from "../../constants/products"; +import { TITLE_ALL_PRODUCTS_COMP,TITLE_BEST_PRODUCTS_COMP } from "../../constants/titles"; function Items() { return (
diff --git a/src/pages/Items.module.css b/src/pages/Items/Items.module.css similarity index 100% rename from src/pages/Items.module.css rename to src/pages/Items/Items.module.css diff --git a/src/pages/Items/components/AllProducts/AllProducts.js b/src/pages/Items/components/AllProducts/AllProducts.js new file mode 100644 index 00000000..475cb504 --- /dev/null +++ b/src/pages/Items/components/AllProducts/AllProducts.js @@ -0,0 +1,31 @@ +import styles from "./AllProducts.module.css"; +import useProductsPagination from "../../hooks/useProductsPagination"; +import ProductSection from "../ProductSection/ProductSection"; +import Pagination from "../Pagination/Pagination"; + +function AllProducts({ title, itemsPerDevice }) { + const { products, totalPages, page, changePage, sort, handleSortChange, } = + useProductsPagination(itemsPerDevice); + + return ( + <> + +
+ +
+ + ); +} + +export default AllProducts; diff --git a/src/components/AllProducts/AllProducts.module.css b/src/pages/Items/components/AllProducts/AllProducts.module.css similarity index 100% rename from src/components/AllProducts/AllProducts.module.css rename to src/pages/Items/components/AllProducts/AllProducts.module.css diff --git a/src/hooks/useBestProducts.js b/src/pages/Items/components/BestProducts/BestProducts.js similarity index 66% rename from src/hooks/useBestProducts.js rename to src/pages/Items/components/BestProducts/BestProducts.js index c606dc5c..1a7de7c3 100644 --- a/src/hooks/useBestProducts.js +++ b/src/pages/Items/components/BestProducts/BestProducts.js @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; -import { fetchProducts } from "../api/products"; -import { getLimitFromWindowWidth } from "../utils/getLimitFromWindowWidth"; +import { fetchProducts } from "../../../../api/products"; +import { getLimitFromWindowWidth } from "../../../../utils/getLimitFromWindowWidth"; +import ProductSection from "../ProductSection/ProductSection"; -export default function useBestProducts(itemsPerDevice) { +function BestProducts({ title, itemsPerDevice }) { const [bestProducts, setBestProducts] = useState([]); useEffect(() => { @@ -23,5 +24,8 @@ export default function useBestProducts(itemsPerDevice) { return () => window.removeEventListener("resize", resizeHandler); }, [itemsPerDevice]); - return bestProducts; + + return ; } + +export default BestProducts; diff --git a/src/components/Pagination/Pagination.js b/src/pages/Items/components/Pagination/Pagination.js similarity index 92% rename from src/components/Pagination/Pagination.js rename to src/pages/Items/components/Pagination/Pagination.js index 74a21be6..b7b8c4f9 100644 --- a/src/components/Pagination/Pagination.js +++ b/src/pages/Items/components/Pagination/Pagination.js @@ -1,10 +1,12 @@ import styles from "./Pagination.module.css"; +const DEFAULT_MAX_PAGE_BUTTONS = 5; + function Pagination({ currentPage, totalPages, onPageChange, - maxPageButtons = 5, + maxPageButtons = DEFAULT_MAX_PAGE_BUTTONS, }) { const groupStart = Math.floor((currentPage - 1) / maxPageButtons) * maxPageButtons + 1; @@ -58,4 +60,4 @@ function Pagination({ ); } -export default Pagination; \ No newline at end of file +export default Pagination; diff --git a/src/components/Pagination/Pagination.module.css b/src/pages/Items/components/Pagination/Pagination.module.css similarity index 100% rename from src/components/Pagination/Pagination.module.css rename to src/pages/Items/components/Pagination/Pagination.module.css diff --git a/src/components/ImageWithFallback.js b/src/pages/Items/components/ProductCard/ImageWithFallback.js similarity index 100% rename from src/components/ImageWithFallback.js rename to src/pages/Items/components/ProductCard/ImageWithFallback.js diff --git a/src/components/ProductCard/ProductCard.js b/src/pages/Items/components/ProductCard/ProductCard.js similarity index 83% rename from src/components/ProductCard/ProductCard.js rename to src/pages/Items/components/ProductCard/ProductCard.js index 771aa8ad..41b6a437 100644 --- a/src/components/ProductCard/ProductCard.js +++ b/src/pages/Items/components/ProductCard/ProductCard.js @@ -1,11 +1,10 @@ import styles from "./ProductCard.module.css"; -import ImageWithFallback from "../ImageWithFallback"; -import replaceImg from "../../assets/images/no-image-icon.png"; +import ImageWithFallback from "./ImageWithFallback"; +import replaceImg from "../../../../assets/images/no-image-icon.png"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faHeart as farHeart } from "@fortawesome/free-regular-svg-icons"; -function ProductCard({ product}) { - +function ProductCard({ product }) { return (
{ + setSort(newSort); + changePage(1); + }, + [changePage] + ); + + useEffect(() => { + const load = async () => { + const res = await fetchPaginatedProducts({ page, pageSize: limit }); + let data = res.list || []; + + if (sort === "likes") { + data.sort((a, b) => b.favoriteCount - a.favoriteCount); + } else { + data.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + } + + setProducts(data); + setTotalPages(Math.ceil(res.totalCount / limit)); + }; + + if (page && limit) load(); + }, [page, limit, sort]); + + return { + products, + totalPages, + page, + changePage, + sort, + handleSortChange, + }; +} \ No newline at end of file diff --git a/src/pages/Items/hooks/useResponsiveLimit.js b/src/pages/Items/hooks/useResponsiveLimit.js new file mode 100644 index 00000000..8023abf5 --- /dev/null +++ b/src/pages/Items/hooks/useResponsiveLimit.js @@ -0,0 +1,44 @@ +import { useEffect, useState } from "react"; +import { getLimitFromWindowWidth } from "../../../utils/getLimitFromWindowWidth"; + +export default function useResponsiveLimit(itemsPerDevice) { + const DEFAULT_RESPONSIVE_LIMIT = itemsPerDevice?.tablet || 6; + // 아주 최소 연산으로만 초기limit값 처리 + const [limit, setLimit] = useState(() => { + // 아주 최소 연산만 처리 + // SSR 대응 + return typeof window === "undefined" + ? DEFAULT_RESPONSIVE_LIMIT // 아주 보수적인 default + : null; + }); + + useEffect(() => { + //window 접근 못 할 경우 return; + if (typeof window === "undefined") return; + + let timeoutId; + + const update = () => { + // 디바운싱 추가 + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + const newLimit = getLimitFromWindowWidth( + itemsPerDevice.desktop, + itemsPerDevice.tablet, + itemsPerDevice.mobile + ); + setLimit((prev) => (prev !== newLimit ? newLimit : prev)); + }, 120); // 리사이즈 멈춘 뒤 120ms 후에만 실행 + }; + + update(); // mount 시 1회 실행 + window.addEventListener("resize", update); + + return () => { + clearTimeout(timeoutId); + window.removeEventListener("resize", update); + }; + }, [itemsPerDevice]); + + return limit; +} diff --git a/src/styles/variables.css b/src/styles/variables.css index 20f011c8..794d6d48 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -140,6 +140,28 @@ --spacing-xl: 32px; --spacing-2xl: 40px; + /* ===================== */ + /* z-index */ + /* ===================== */ + /* Base layers */ + --z-base: 0; + --z-above-base: 1; + + /* UI Components */ + --z-dropdown: 1000; + --z-modal: 2000; + --z-toast: 3000; + --z-tooltip: 4000; + + /* Fixed elements */ + --z-header: 100; + --z-footer: 100; + --z-sidebar: 200; + + /* Overlay elements */ + --z-overlay: 1500; + --z-overlay-above: 2500; + /* ======================= */ /* (반응형) 모바일 환경 변수 설정 */ /* ======================= */