From dcca953d60c1fa26a3b35a5ad6e01d449438716a Mon Sep 17 00:00:00 2001 From: sumin Date: Thu, 8 May 2025 20:20:49 +0900 Subject: [PATCH 01/17] =?UTF-8?q?fix:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 21 +++------------------ src/routes/AppRoutes.jsx | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 src/routes/AppRoutes.jsx diff --git a/src/App.jsx b/src/App.jsx index 2556b37d..b80ce7d3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,15 +2,8 @@ import './App.css'; import './styles/reset.css'; import './styles/layout.css'; import './styles/common.css'; -import { useState, useReducer, useRef, createContext } from 'react'; -import { Routes, Route } from 'react-router-dom'; -import Login from './pages/Login'; -import Signup from './pages/Signup'; -import Items from './pages/Items'; -import Board from './pages/Board'; -import Notfound from './pages/Notfound'; -import AddItem from './pages/AddItem'; -import Home from './pages/Home'; +import { createContext } from 'react'; +import AppRoutes from './routes/AppRoutes'; const AuthStateContext = createContext(); const AuthDispatchContext = createContext(); @@ -19,15 +12,7 @@ export { AuthStateContext, AuthDispatchContext }; function App() { return ( <> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + ); } diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx new file mode 100644 index 00000000..05471c9b --- /dev/null +++ b/src/routes/AppRoutes.jsx @@ -0,0 +1,24 @@ +import { Routes, Route } from 'react-router-dom'; +import Login from '../pages/Login'; +import Signup from '../pages/Signup'; +import Items from '../pages/Items'; +import Board from '../pages/Board'; +import Notfound from '../pages/Notfound'; +import AddItem from '../pages/AddItem'; +import Home from '../pages/Home'; + +const AppRoutes = () => { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ); +}; + +export default AppRoutes; \ No newline at end of file From d0a2f04e854fa6dbd39f4f2788be0afe685e5744 Mon Sep 17 00:00:00 2001 From: sumin Date: Thu, 8 May 2025 20:24:42 +0900 Subject: [PATCH 02/17] =?UTF-8?q?fix:=20=EC=A0=84=EC=97=AD=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20import=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=ED=95=98=EC=97=AC=20=ED=95=9C=20=EB=B2=88?= =?UTF-8?q?=EC=97=90=20=EB=AA=A8=EC=95=84=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 5 +---- src/pages/Login/styles/index.module.css | 0 src/styles/index.css | 4 ++++ 3 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 src/pages/Login/styles/index.module.css create mode 100644 src/styles/index.css diff --git a/src/App.jsx b/src/App.jsx index b80ce7d3..9dd7542f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,4 @@ -import './App.css'; -import './styles/reset.css'; -import './styles/layout.css'; -import './styles/common.css'; +import './styles/index.css'; import { createContext } from 'react'; import AppRoutes from './routes/AppRoutes'; diff --git a/src/pages/Login/styles/index.module.css b/src/pages/Login/styles/index.module.css deleted file mode 100644 index e69de29b..00000000 diff --git a/src/styles/index.css b/src/styles/index.css new file mode 100644 index 00000000..5612149a --- /dev/null +++ b/src/styles/index.css @@ -0,0 +1,4 @@ +@import './reset.css'; +@import './layout.css'; +@import './common.css'; +@import '../App.css'; \ No newline at end of file From 3572fbc92cf4243bf567f914709a16593821daec Mon Sep 17 00:00:00 2001 From: sumin Date: Thu, 8 May 2025 20:42:56 +0900 Subject: [PATCH 03/17] =?UTF-8?q?fix:=20Pagination=20arrow=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20disabled=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 4 ---- src/components/Product/Paging.jsx | 12 ++++++++++-- src/components/Product/styles/Paging.module.css | 6 +++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 9dd7542f..f83c0668 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,10 +2,6 @@ import './styles/index.css'; import { createContext } from 'react'; import AppRoutes from './routes/AppRoutes'; -const AuthStateContext = createContext(); -const AuthDispatchContext = createContext(); -export { AuthStateContext, AuthDispatchContext }; - function App() { return ( <> diff --git a/src/components/Product/Paging.jsx b/src/components/Product/Paging.jsx index 7bd4ab58..e565ddf5 100644 --- a/src/components/Product/Paging.jsx +++ b/src/components/Product/Paging.jsx @@ -7,7 +7,11 @@ export default function Paging({ totalPage, currentPage, setCurrentPage }) { return (
- {currentPages.map((page) => ( @@ -15,7 +19,11 @@ export default function Paging({ totalPage, currentPage, setCurrentPage }) { {page} ))} -
diff --git a/src/components/Product/styles/Paging.module.css b/src/components/Product/styles/Paging.module.css index 464db2f2..6b675a79 100644 --- a/src/components/Product/styles/Paging.module.css +++ b/src/components/Product/styles/Paging.module.css @@ -37,13 +37,17 @@ transition: all 0.3s ease; } } - &:hover { + &:not(.disabled):hover { svg { path { stroke: var(--gray600); } } } + &.disabled { + opacity: 0.5; + cursor: default; + } } } } From 13e30e059e13cd066f1b6141bedb3db8b4f4caec Mon Sep 17 00:00:00 2001 From: sumin Date: Thu, 8 May 2025 20:56:17 +0900 Subject: [PATCH 04/17] =?UTF-8?q?fix:=20=EA=B8=B0=EB=8A=A5=EB=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Product/SearchInput.jsx | 3 - src/hooks/useBestProducts.js | 21 +++++++ src/hooks/useMediaQuery.js | 8 +++ src/hooks/usePageSize.js | 26 ++++++++ src/hooks/useProductList.js | 41 +++++++++++++ src/hooks/useProducts.js | 84 +++++--------------------- 6 files changed, 112 insertions(+), 71 deletions(-) create mode 100644 src/hooks/useBestProducts.js create mode 100644 src/hooks/useMediaQuery.js create mode 100644 src/hooks/usePageSize.js create mode 100644 src/hooks/useProductList.js diff --git a/src/components/Product/SearchInput.jsx b/src/components/Product/SearchInput.jsx index 8e9a598b..46505c3a 100644 --- a/src/components/Product/SearchInput.jsx +++ b/src/components/Product/SearchInput.jsx @@ -6,9 +6,6 @@ const SearchInput = ({placeholder, setOrderBy, setSearchKeyword}) => { const [isOpen, toggleOpen] = useReducer((state) => !state, false); const [selectedSort, setSelectedSort] = useState('최신순'); const [search, setSearch] = useState(''); - - - const handleOptionClick = (option, apiValue) => { setSelectedSort(option); setOrderBy(apiValue); diff --git a/src/hooks/useBestProducts.js b/src/hooks/useBestProducts.js new file mode 100644 index 00000000..3363b13f --- /dev/null +++ b/src/hooks/useBestProducts.js @@ -0,0 +1,21 @@ +import { useState, useCallback, useEffect } from 'react'; +import { productAPI } from '../api/productAPI'; + +export const useBestProducts = (bestPageSize) => { + const [bestProducts, setBestProducts] = useState([]); + + const fetchBestProducts = useCallback(async () => { + try { + const response = await productAPI.getProducts(1, bestPageSize, 'favorite'); + setBestProducts(response.list); + } catch (error) { + alert('베스트 상품을 불러오는데 실패했습니다. 잠시 후 다시 시도해주세요.'); + } + }, [bestPageSize]); + + useEffect(() => { + fetchBestProducts(); + }, [fetchBestProducts]); + + return { bestProducts }; +}; diff --git a/src/hooks/useMediaQuery.js b/src/hooks/useMediaQuery.js new file mode 100644 index 00000000..23ffd3d5 --- /dev/null +++ b/src/hooks/useMediaQuery.js @@ -0,0 +1,8 @@ +import { useMediaQuery as useResponsive } from 'react-responsive'; + +export const useMediaQuery = () => { + const isMobile = useResponsive({ query: '(max-width: 425px)' }); + const isTablet = useResponsive({ query: '(min-width: 426px) and (max-width: 768px)' }); + + return { isMobile, isTablet }; +}; diff --git a/src/hooks/usePageSize.js b/src/hooks/usePageSize.js new file mode 100644 index 00000000..e7a620f8 --- /dev/null +++ b/src/hooks/usePageSize.js @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; +import { useMediaQuery } from './useMediaQuery'; + +export const usePageSize = () => { + const { isMobile, isTablet } = useMediaQuery(); + const [pageSize, setPageSize] = useState(10); + const [bestPageSize, setBestPageSize] = useState(4); + + useEffect(() => { + let newPageSize = 10; + let newBestPageSize = 4; + + if (isMobile) { + newPageSize = 4; + newBestPageSize = 1; + } else if (isTablet) { + newPageSize = 6; + newBestPageSize = 2; + } + + setPageSize(newPageSize); + setBestPageSize(newBestPageSize); + }, [isMobile, isTablet]); + + return { pageSize, bestPageSize, setPageSize }; +}; diff --git a/src/hooks/useProductList.js b/src/hooks/useProductList.js new file mode 100644 index 00000000..bd869fa9 --- /dev/null +++ b/src/hooks/useProductList.js @@ -0,0 +1,41 @@ +import { useState, useCallback, useEffect } from 'react'; +import { productAPI } from '../api/productAPI'; + +export const useProductList = (pageSize) => { + const [currentPage, setCurrentPage] = useState(1); + const [totalCount, setTotalCount] = useState(0); + const [totalProducts, setTotalProducts] = useState([]); + const [orderBy, setOrderBy] = useState('recent'); + const [searchKeyword, setSearchKeyword] = useState(''); + + const fetchProducts = useCallback(async () => { + try { + const response = await productAPI.getProducts(currentPage, pageSize, orderBy, searchKeyword); + setTotalProducts(response.list); + setTotalCount(response.totalCount); + } catch (error) { + alert('상품을 불러오는데 실패했습니다. 잠시 후 다시 시도해주세요.'); + } + }, [currentPage, pageSize, orderBy, searchKeyword]); + + useEffect(() => { + fetchProducts(); + }, [fetchProducts]); + + useEffect(() => { + setCurrentPage(1); + fetchProducts(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageSize]); + + return { + currentPage, + totalCount, + totalProducts, + orderBy, + searchKeyword, + setCurrentPage, + setOrderBy, + setSearchKeyword, + }; +}; diff --git a/src/hooks/useProducts.js b/src/hooks/useProducts.js index bbdabfba..e7875f5e 100644 --- a/src/hooks/useProducts.js +++ b/src/hooks/useProducts.js @@ -1,73 +1,21 @@ // src/hooks/useProducts.js - -import { useState, useEffect, useCallback } from 'react'; -import { productAPI } from '../api/productAPI'; -import { useMediaQuery } from 'react-responsive'; - +import { usePageSize } from './usePageSize'; +import { useBestProducts } from './useBestProducts'; +import { useProductList } from './useProductList'; export const useProducts = () => { - const isMobile = useMediaQuery({ query: '(max-width: 425px)' }); - const isTablet = useMediaQuery({ query: '(min-width: 426px) and (max-width: 768px)' }); - const [currentPage, setCurrentPage] = useState(1); - const [totalCount, setTotalCount] = useState(0); - const [bestProducts, setBestProducts] = useState([]); - const [totalProducts, setTotalProducts] = useState([]); - const [orderBy, setOrderBy] = useState('recent'); - const [searchKeyword, setSearchKeyword] = useState(''); - const [pageSize, setPageSize] = useState(10); - const [bestPageSize, setBestPageSize] = useState(4); - - // 미디어 쿼리에 따라 페이지 사이즈 설정 - useEffect(() => { - let newPageSize = 10; - let newBestPageSize = 4; - - if (isMobile) { - newPageSize = 4; - newBestPageSize = 1; - } else if (isTablet) { - newPageSize = 6; - newBestPageSize = 2; - } - - setPageSize(newPageSize); - setBestPageSize(newBestPageSize); - }, [isMobile, isTablet]); - - const fetchBestProducts = useCallback(async () => { - try { - const response = await productAPI.getProducts(1, bestPageSize, 'favorite'); - setBestProducts(response.list); - } catch (error) { - alert('베스트 상품을 불러오는데 실패했습니다. 잠시 후 다시 시도해주세요.'); - } - }, [bestPageSize]); - - const fetchProducts = useCallback(async () => { - try { - const response = await productAPI.getProducts(currentPage, pageSize, orderBy, searchKeyword); - setTotalProducts(response.list); - setTotalCount(response.totalCount); - } catch (error) { - alert('상품을 불러오는데 실패했습니다. 잠시 후 다시 시도해주세요.'); - } - }, [currentPage, pageSize, orderBy, searchKeyword]); - - useEffect(() => { - fetchBestProducts(); - }, [fetchBestProducts]); - - useEffect(() => { - fetchProducts(); - }, [fetchProducts]); - - // 페이지 사이즈 변경 시 상품 리스트 새로고침 - useEffect(() => { - // 페이지 사이즈가 변경되면 페이지를 1로 리셋하고 상품 다시 로드 - setCurrentPage(1); - fetchProducts(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageSize]); + const { pageSize, bestPageSize, setPageSize } = usePageSize(); + const { bestProducts } = useBestProducts(bestPageSize); + const { + currentPage, + totalCount, + totalProducts, + orderBy, + searchKeyword, + setCurrentPage, + setOrderBy, + setSearchKeyword, + } = useProductList(pageSize); return { currentPage, @@ -80,4 +28,4 @@ export const useProducts = () => { setSearchKeyword, setPageSize, }; -}; \ No newline at end of file +}; From f611aad0d0529002a91ed8d69e851bb020b241da Mon Sep 17 00:00:00 2001 From: sumin Date: Thu, 8 May 2025 22:15:59 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8C=85=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20useProductDetail?= =?UTF-8?q?=20=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 1 - src/components/Product/ProductListItem.jsx | 2 +- src/hooks/useProductDetail.js | 29 ++++++++++++++++++++++ src/pages/ItemDetail/index.jsx | 24 ++++++++++++++++++ src/pages/Notfound.jsx | 5 ++-- src/routes/AppRoutes.jsx | 8 +++--- 6 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useProductDetail.js create mode 100644 src/pages/ItemDetail/index.jsx diff --git a/src/App.jsx b/src/App.jsx index f83c0668..8e36ad5e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,4 @@ import './styles/index.css'; -import { createContext } from 'react'; import AppRoutes from './routes/AppRoutes'; function App() { diff --git a/src/components/Product/ProductListItem.jsx b/src/components/Product/ProductListItem.jsx index 5561fcef..18e1da07 100644 --- a/src/components/Product/ProductListItem.jsx +++ b/src/components/Product/ProductListItem.jsx @@ -9,7 +9,7 @@ const ProductListItem = ({ id, title, image, price, favorite }) => { return (
  • - +
    { + const [product, setProduct] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchProductDetail = async () => { + try { + setLoading(true); + const response = await productAPI.getProductById(productId); + setProduct(response); + setError(null); + } catch (error) { + setError('상품 정보를 불러오는데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + if (productId) { + fetchProductDetail(); + } + }, [productId]); + + return { product, loading, error }; +}; diff --git a/src/pages/ItemDetail/index.jsx b/src/pages/ItemDetail/index.jsx new file mode 100644 index 00000000..c826f3ba --- /dev/null +++ b/src/pages/ItemDetail/index.jsx @@ -0,0 +1,24 @@ +import { useParams } from 'react-router-dom'; +import { useProductDetail } from '@/hooks/useProductDetail'; + +const ItemDetail = () => { + const { productId } = useParams(); + const { product, loading, error } = useProductDetail(productId); + + if (loading) return
    로딩 중...
    ; + if (error) return
    {error}
    ; + if (!product) return
    상품을 찾을 수 없습니다.
    ; + + return ( +
    +

    {product.name}

    +

    {product.price}

    +

    {product.favoriteCount}

    + +

    {product.tags}

    +

    {product.description}

    +
    + ); +}; + +export default ItemDetail; diff --git a/src/pages/Notfound.jsx b/src/pages/Notfound.jsx index 00beafb7..c7a06f3a 100644 --- a/src/pages/Notfound.jsx +++ b/src/pages/Notfound.jsx @@ -1,5 +1,6 @@ -const Notfound = () => { +const NotFound = () => { return
    잘못된 페이지입니다.
    ; }; -export default Notfound; +export default NotFound; + \ No newline at end of file diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx index 05471c9b..7906cf0c 100644 --- a/src/routes/AppRoutes.jsx +++ b/src/routes/AppRoutes.jsx @@ -2,8 +2,9 @@ import { Routes, Route } from 'react-router-dom'; import Login from '../pages/Login'; import Signup from '../pages/Signup'; import Items from '../pages/Items'; +import ItemDetail from '../pages/ItemDetail'; import Board from '../pages/Board'; -import Notfound from '../pages/Notfound'; +import NotFound from '../pages/NotFound'; import AddItem from '../pages/AddItem'; import Home from '../pages/Home'; @@ -14,11 +15,12 @@ const AppRoutes = () => { } /> } /> } /> + } /> } /> } /> - } /> + } /> ); }; -export default AppRoutes; \ No newline at end of file +export default AppRoutes; From 42a2331cb58757d7de808191136fe21fbf28516c Mon Sep 17 00:00:00 2001 From: sumin Date: Thu, 8 May 2025 22:50:11 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=ED=99=94=EB=A9=B4=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ItemDetail/index.jsx | 59 +++++++++++++++++--- src/pages/ItemDetail/styles/index.module.css | 26 +++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 src/pages/ItemDetail/styles/index.module.css diff --git a/src/pages/ItemDetail/index.jsx b/src/pages/ItemDetail/index.jsx index c826f3ba..8d13ade5 100644 --- a/src/pages/ItemDetail/index.jsx +++ b/src/pages/ItemDetail/index.jsx @@ -1,5 +1,8 @@ import { useParams } from 'react-router-dom'; import { useProductDetail } from '@/hooks/useProductDetail'; +import styles from './styles/index.module.css'; +import Footer from '@/components/Footer/Footer'; +import Header from '@/components/Header/Header'; const ItemDetail = () => { const { productId } = useParams(); @@ -8,16 +11,58 @@ const ItemDetail = () => { if (loading) return
    로딩 중...
    ; if (error) return
    {error}
    ; if (!product) return
    상품을 찾을 수 없습니다.
    ; + console.log(product) return ( -
    -

    {product.name}

    -

    {product.price}

    -

    {product.favoriteCount}

    - + <>

    {product.tags}

    -

    {product.description}

    -
    +
    +
    +
    +
    +
    + 상품 상세 이미지 +
    +
    +
      +
    • +

      {product.name}

      + {product.price.toLocaleString()}원 +
    • +
    • +

      상품 소개

      +

      {product.description}

      +
    • +
    • +

      상품 태그

      +
        + {product.tags.map((tag, index) => ( +
      • {tag}
      • + ))} +
      +
    • +
    +
    +
    +
    + 프로필 이미지 +
    +
    +

    {product.ownerNickname}

    +

    {product.updatedAt.split('T')[0].replaceAll('-', '.')}

    +
    +
    + +
    +
    +
    +
    +
    +