From 574770927b8bda232e75f1e15e582580eda975a2 Mon Sep 17 00:00:00 2001 From: joojjang Date: Tue, 5 Nov 2024 16:45:57 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat(SearchBar):=20=EC=84=9C=EC=B9=98=20?= =?UTF-8?q?=EB=B0=94=20=ED=8F=AC=EC=A7=80=EC=85=98=20fixed=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD,=20z-index=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layouts/SearchBar/index.tsx | 4 +++- src/pages/Discover/index.tsx | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/layouts/SearchBar/index.tsx b/src/components/layouts/SearchBar/index.tsx index eb453ac..c869442 100644 --- a/src/components/layouts/SearchBar/index.tsx +++ b/src/components/layouts/SearchBar/index.tsx @@ -4,6 +4,7 @@ import CancelIcon from '@/assets/icons/cancel-filled-gray.svg?react'; import SearchIcon from '@/assets/icons/search.svg?react'; import IconButton from '@/components/common/IconButton'; import { SEARCH_ARRAY_KEY } from '@/components/common/SearchModal/RecentSearch'; +import Z_INDEX from '@/styles/z_index'; import { useForm } from 'react-hook-form'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { HEADER_HEIGHT } from '../Header'; @@ -88,7 +89,8 @@ export default SearchBar; const SEARCHBAR_HEIGHT = HEADER_HEIGHT; const SearchBarWrapper = styled.div` - position: sticky; + position: fixed; + z-index: ${Z_INDEX.Header}; top: 0; width: 100%; height: ${SEARCHBAR_HEIGHT}; diff --git a/src/pages/Discover/index.tsx b/src/pages/Discover/index.tsx index 84adaa0..f6767b2 100644 --- a/src/pages/Discover/index.tsx +++ b/src/pages/Discover/index.tsx @@ -1,5 +1,22 @@ +import styled from '@emotion/styled'; + +import SearchBar from '@/components/layouts/SearchBar'; +import { HEADER_HEIGHT } from '@/components/layouts/Header'; +import { TABBAR_HEIGHT } from '@/components/layouts/TabBar'; + const Discover = () => { - return <>Discover; + return ( + + + + ); }; export default Discover; + +const Wrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; + margin: ${HEADER_HEIGHT} 0 ${TABBAR_HEIGHT} 0; +`; From a89baac2b1e4f56b4f97cb1f32d13fc9d972f75c Mon Sep 17 00:00:00 2001 From: joojjang Date: Tue, 5 Nov 2024 19:15:43 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat(Discover):=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EB=8D=B0=EC=9D=B4=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/products/useGetFeed.ts | 0 src/pages/Discover/index.tsx | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/apis/products/useGetFeed.ts diff --git a/src/apis/products/useGetFeed.ts b/src/apis/products/useGetFeed.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/Discover/index.tsx b/src/pages/Discover/index.tsx index f6767b2..f2de55c 100644 --- a/src/pages/Discover/index.tsx +++ b/src/pages/Discover/index.tsx @@ -1,13 +1,51 @@ import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; import SearchBar from '@/components/layouts/SearchBar'; import { HEADER_HEIGHT } from '@/components/layouts/Header'; import { TABBAR_HEIGHT } from '@/components/layouts/TabBar'; const Discover = () => { + const [imageList, setImageList] = useState([]); + const [page, setPage] = useState(1); + + // 아무 이미지 fetch + const fetchImages = async () => { + const newImages = Array.from( + { length: 10 }, + (_, i) => `https://picsum.photos/300/300?random=${page * 10 + i}`, + ); + setImageList((prevImages) => [...prevImages, ...newImages]); + }; + + useEffect(() => { + fetchImages(); + }, [page]); + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = document.documentElement; + if (scrollTop + clientHeight >= scrollHeight - 100) { + setPage((prevPage) => prevPage + 1); + } + }; + + useEffect(() => { + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + return ( + + + {imageList.map((src, index) => ( + + + + ))} + + ); }; @@ -20,3 +58,26 @@ const Wrapper = styled.div` flex-direction: column; margin: ${HEADER_HEIGHT} 0 ${TABBAR_HEIGHT} 0; `; + +const ContentWrapper = styled.div` + padding: 4px; + overflow-y: auto; + flex: 1; +`; + +const ImageGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 8px; +`; + +const ImageItem = styled.div` + border-radius: var(--border-radius); + overflow: hidden; + + img { + width: 100%; + height: auto; + display: block; + } +`; From beebd09100912c3cfd4e087cd4cace4b0ccbf3d4 Mon Sep 17 00:00:00 2001 From: joojjang Date: Tue, 5 Nov 2024 19:16:39 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat(SearchBar):=20=EB=B0=B1=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EC=9A=B4=EB=93=9C=20=EC=BB=AC=EB=9F=AC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layouts/SearchBar/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/layouts/SearchBar/index.tsx b/src/components/layouts/SearchBar/index.tsx index c869442..c4362e6 100644 --- a/src/components/layouts/SearchBar/index.tsx +++ b/src/components/layouts/SearchBar/index.tsx @@ -99,6 +99,7 @@ const SearchBarWrapper = styled.div` justify-content: space-between; align-items: center; gap: 10px; + background-color: var(--color-white); `; const InputBox = styled.form` From 99f9ebf47e24c1e444be2a87cd258dcb41cbcca1 Mon Sep 17 00:00:00 2001 From: joojjang Date: Tue, 5 Nov 2024 19:51:22 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat(useGetFeed):=20=EB=91=98=EB=9F=AC?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/products/useGetFeed.ts | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/apis/products/useGetFeed.ts b/src/apis/products/useGetFeed.ts index e69de29..2d77b8c 100644 --- a/src/apis/products/useGetFeed.ts +++ b/src/apis/products/useGetFeed.ts @@ -0,0 +1,53 @@ +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; + +import { fetchInstance } from '../instance'; + +type GetFeedProps = { + size?: number; +}; + +type Products = { + id: number; + name: string; + artist: string; + price: number; + thumnailUrl: string; +}; + +type GetFeedResponse = { + hasNext: boolean; + products: Products[]; +}; + +async function getFeed({ size }: GetFeedProps): Promise { + try { + const response = await fetchInstance().get(`/products/feed?size=${size}`); + // console.log('getFeed response: ', response); + + return response.data; + } catch (error) { + if (isAxiosError(error)) { + if (error.response) { + throw new Error(error.response.data.message || '피드 가져오기 실패'); + } else { + throw new Error('네트워크 오류 또는 서버에 연결할 수 없습니다.'); + } + } else { + throw new Error('알 수 없는 오류입니다.'); + } + } +} + +const useGetFeed = (size: number) => { + return useSuspenseInfiniteQuery({ + queryKey: ['feed'], + queryFn: () => getFeed({ size }), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + return lastPage.hasNext ? lastPage.products.length / size + 1 : undefined; + }, + }); +}; + +export default useGetFeed; From c0f222f4966d046583da2b3193d4203a6a15c2d5 Mon Sep 17 00:00:00 2001 From: joojjang Date: Sat, 9 Nov 2024 16:14:50 +0900 Subject: [PATCH 5/6] =?UTF-8?q?fix(useGetFeed):=20API=20=EC=97=B0=EB=8F=99?= =?UTF-8?q?=ED=95=98=EB=A0=A4=EA=B3=A0=20=EC=8B=9C=EB=8F=84=20...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/products/useGetFeed.ts | 30 ++++++++++---------- src/pages/Discover/index.tsx | 49 ++++++++++++++------------------- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/apis/products/useGetFeed.ts b/src/apis/products/useGetFeed.ts index 2d77b8c..21bdc16 100644 --- a/src/apis/products/useGetFeed.ts +++ b/src/apis/products/useGetFeed.ts @@ -3,26 +3,24 @@ import { isAxiosError } from 'axios'; import { fetchInstance } from '../instance'; -type GetFeedProps = { - size?: number; -}; - -type Products = { +export type Product = { id: number; name: string; artist: string; price: number; - thumnailUrl: string; + thumbnailUrl: string; }; type GetFeedResponse = { - hasNext: boolean; - products: Products[]; + pages: { + hasNext: boolean; + products: Product[]; + }; }; -async function getFeed({ size }: GetFeedProps): Promise { +async function getFeed(size: number, pageParam: number): Promise { try { - const response = await fetchInstance().get(`/products/feed?size=${size}`); + const response = await fetchInstance().get(`/products/feed?size=${size}&page=${pageParam}`); // console.log('getFeed response: ', response); return response.data; @@ -39,13 +37,15 @@ async function getFeed({ size }: GetFeedProps): Promise { } } -const useGetFeed = (size: number) => { - return useSuspenseInfiniteQuery({ - queryKey: ['feed'], - queryFn: () => getFeed({ size }), +const useGetFeed = () => { + const size = 20; + + return useSuspenseInfiniteQuery({ + queryKey: ['feed', size], + queryFn: ({ pageParam = 0 }) => getFeed(size, pageParam as number), initialPageParam: 0, getNextPageParam: (lastPage) => { - return lastPage.hasNext ? lastPage.products.length / size + 1 : undefined; + return lastPage.pages.hasNext ? lastPage.pages.products.length / size + 1 : undefined; }, }); }; diff --git a/src/pages/Discover/index.tsx b/src/pages/Discover/index.tsx index f2de55c..65e4dac 100644 --- a/src/pages/Discover/index.tsx +++ b/src/pages/Discover/index.tsx @@ -1,50 +1,43 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import { Suspense, useEffect } from 'react'; -import SearchBar from '@/components/layouts/SearchBar'; +import useGetFeed, { type Product } from '@/apis/products/useGetFeed'; import { HEADER_HEIGHT } from '@/components/layouts/Header'; +import SearchBar from '@/components/layouts/SearchBar'; import { TABBAR_HEIGHT } from '@/components/layouts/TabBar'; const Discover = () => { - const [imageList, setImageList] = useState([]); - const [page, setPage] = useState(1); - - // 아무 이미지 fetch - const fetchImages = async () => { - const newImages = Array.from( - { length: 10 }, - (_, i) => `https://picsum.photos/300/300?random=${page * 10 + i}`, - ); - setImageList((prevImages) => [...prevImages, ...newImages]); - }; - - useEffect(() => { - fetchImages(); - }, [page]); + const { data, fetchNextPage, hasNextPage } = useGetFeed(); + // 스크롤 내려감에 따라 다음 페이지 데이터 페칭 const handleScroll = () => { const { scrollTop, scrollHeight, clientHeight } = document.documentElement; - if (scrollTop + clientHeight >= scrollHeight - 100) { - setPage((prevPage) => prevPage + 1); + if (scrollTop + clientHeight >= scrollHeight - 100 && hasNextPage) { + fetchNextPage(); } }; useEffect(() => { window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, []); + + return () => window.removeEventListener('scroll', handleScroll); // 언마운트될 때 이벤트 리스너 해제 + }, [fetchNextPage, hasNextPage]); return ( - - {imageList.map((src, index) => ( - - - - ))} - + Loading...}> + + {data?.pages.map((page) => + page.products.map((product: Product) => ( + + {product.name} + + )), + )} + + ); From 58320aab047b3d37596e1d4e1dc361a041e42489 Mon Sep 17 00:00:00 2001 From: joojjang Date: Sat, 9 Nov 2024 16:27:42 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix(useGetFeed):=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=8B=A4=EC=8B=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/products/useGetFeed.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/apis/products/useGetFeed.ts b/src/apis/products/useGetFeed.ts index 21bdc16..efca4b5 100644 --- a/src/apis/products/useGetFeed.ts +++ b/src/apis/products/useGetFeed.ts @@ -12,10 +12,8 @@ export type Product = { }; type GetFeedResponse = { - pages: { - hasNext: boolean; - products: Product[]; - }; + hasNext: boolean; + products: Product[]; }; async function getFeed(size: number, pageParam: number): Promise { @@ -45,7 +43,7 @@ const useGetFeed = () => { queryFn: ({ pageParam = 0 }) => getFeed(size, pageParam as number), initialPageParam: 0, getNextPageParam: (lastPage) => { - return lastPage.pages.hasNext ? lastPage.pages.products.length / size + 1 : undefined; + return lastPage.hasNext ? lastPage.products.length / size + 1 : undefined; }, }); };