From 8c4232ff2b1fe85075a7d0d8477708af3b68f804 Mon Sep 17 00:00:00 2001 From: eunjin Date: Tue, 9 Jul 2024 21:33:01 +0900 Subject: [PATCH 01/15] =?UTF-8?q?docs=20:=20Readme.md=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index e69de29b..a4e93369 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,16 @@ +# 3주차 상품리스트 구현 - api +## step1 📝구현사항 ++ 첨부된 oas.yaml 파일을 토대로 Request, Response Type을 정의하기 ++ axios 라이브러리 설치 +* 메인페이지 - theme + * /api/v1/themes API를 사용하여 Section을 구현 + * API는 Axios또는 React Query 등을 모두 활용해서 구현 가능 +* 메인 페이지 - 실시간 급상승 선물랭킹 + * /api/v1/ranking/products API를 사용하여 Section을 구현 + * 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 해야함 +* Theme 페이지 - header + * url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 + * themeKey가 잘못 된 경우 메인 페이지로 연결 +* Theme 페이지 - 상품 목록 섹션 + * /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 + *API 요청 시 한번에 20개의 상품 목록이 내려오게 구현 From 911266177941a230a9d1ed607e978a0b2131f4d5 Mon Sep 17 00:00:00 2001 From: eunjin Date: Tue, 9 Jul 2024 21:34:24 +0900 Subject: [PATCH 02/15] =?UTF-8?q?Feat=20:=20axios=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 29 ++++++++++++++++------------- package.json | 1 + 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 89581c64..74216d7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", + "axios": "^1.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.1" @@ -11988,8 +11989,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -12061,6 +12061,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -13498,7 +13508,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -15481,7 +15490,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -18405,10 +18413,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", - "dev": true, + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -18614,7 +18621,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -24744,7 +24750,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -24753,7 +24758,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -27576,8 +27580,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/psl": { "version": "1.9.0", diff --git a/package.json b/package.json index 0a6f0b8f..f1f65cfa 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", + "axios": "^1.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.1" From 2d46f943c598bae8867c63632822218e09c132b4 Mon Sep 17 00:00:00 2001 From: eunjin Date: Tue, 9 Jul 2024 23:10:31 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20api/index.ts=20theme=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 16 +++ .../Home/GoodsRankingSection/index.tsx | 2 - .../Home/ThemeCategorySection/index.tsx | 100 +++++------------- src/types/index.ts | 1 + src/types/mock.ts | 2 + 5 files changed, 46 insertions(+), 75 deletions(-) create mode 100644 src/api/index.ts diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..367897af --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; + +import type { ThemeData } from '@/types'; + +const api = axios.create({ + baseURL: 'https://react-gift-mock-api-eunjin.vercel.app/',}); + +export const getTheme = async (): Promise => { + const response = await api.get<{ themes: ThemeData[] }>('/api/v1/themes'); + return response.data.themes; +}; + + + +export default api; + diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 9464d67c..e90631ed 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -1,11 +1,9 @@ import styled from '@emotion/styled'; import { useState } from 'react'; - import { Container } from '@/components/common/layouts/Container'; import { breakpoints } from '@/styles/variants'; import type { RankingFilterOption } from '@/types'; import { GoodsMockList } from '@/types/mock'; - import { GoodsRankingFilter } from './Filter'; import { GoodsRankingList } from './List'; diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index d82e3afe..1cf3710e 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -1,14 +1,32 @@ import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; +import { getTheme } from '@/api'; import { Container } from '@/components/common/layouts/Container'; import { Grid } from '@/components/common/layouts/Grid'; import { getDynamicPath } from '@/routes/path'; import { breakpoints } from '@/styles/variants'; +import type { ThemeData } from '@/types'; import { ThemeCategoryItem } from './ThemeCategoryItem'; -export const ThemeCategorySection = () => { +export const ThemeCategorySection: React.FC = () => { + const [themes, setThemes] = useState([]); + + useEffect(() => { + const fetchThemes = async () => { + try { + const data = await getTheme(); + setThemes(data); + } catch (error) { + console.error('Failed to fetch themes:', error); + } + }; + + fetchThemes(); + }, []); + return ( @@ -18,78 +36,14 @@ export const ThemeCategorySection = () => { md: 6, }} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {themes.map((theme) => ( + + + + ))} diff --git a/src/types/index.ts b/src/types/index.ts index 9d76b97b..fcf55803 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,7 @@ export type ThemeData = { title: string; description?: string; backgroundColor: string; + imageURL: string; }; export type RankingFilterOption = { diff --git a/src/types/mock.ts b/src/types/mock.ts index cdd90cf7..ff0892cc 100644 --- a/src/types/mock.ts +++ b/src/types/mock.ts @@ -7,8 +7,10 @@ export const ThemeMockData: ThemeData = { title: '예산은 가볍게, 감동은 무겁게❤️', description: '당신의 센스를 뽐내줄 부담 없는 선물', backgroundColor: '#4b4d50', + imageURL: 'https://example.com/image.png', }; + export const ThemeMockList = [ThemeMockData]; export const GoodsMockData: GoodsData = { From a15a16b2aa8a80ef5da0db63bf87f9cb9476e338 Mon Sep 17 00:00:00 2001 From: eunjin Date: Wed, 10 Jul 2024 02:06:56 +0900 Subject: [PATCH 04/15] =?UTF-8?q?add=20:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EA=B8=89=EC=83=81=EC=8A=B9=20=EB=9E=AD=ED=82=B9=20api=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 9 ++++- .../Home/GoodsRankingSection/index.tsx | 39 ++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 367897af..e6234bec 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -import type { ThemeData } from '@/types'; +import type { GoodsData, RankingFilterOption,ThemeData } from '@/types'; const api = axios.create({ baseURL: 'https://react-gift-mock-api-eunjin.vercel.app/',}); @@ -10,6 +10,13 @@ export const getTheme = async (): Promise => { return response.data.themes; }; +export const getRankingGoods = async (filterOption: RankingFilterOption): Promise => { + const response = await api.get<{ products: GoodsData[] }>('/api/v1/ranking/products', { + params: filterOption, + }); + return response.data.products; +}; + export default api; diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index e90631ed..f4c61cfa 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -1,26 +1,46 @@ import styled from '@emotion/styled'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; + +import { getRankingGoods } from '@/api'; import { Container } from '@/components/common/layouts/Container'; import { breakpoints } from '@/styles/variants'; -import type { RankingFilterOption } from '@/types'; -import { GoodsMockList } from '@/types/mock'; +import type { GoodsData, RankingFilterOption } from '@/types'; + import { GoodsRankingFilter } from './Filter'; import { GoodsRankingList } from './List'; -export const GoodsRankingSection = () => { +export const GoodsRankingSection: React.FC = () => { const [filterOption, setFilterOption] = useState({ targetType: 'ALL', rankType: 'MANY_WISH', }); + const [goodsList, setGoodsList] = useState([]); + const [errorMessage, setErrorMessage] = useState(null); // 변수 이름 변경 + + useEffect(() => { + const fetchGoods = async () => { + try { + const data = await getRankingGoods(filterOption); + setGoodsList(data); + } catch (err) { + setErrorMessage('데이터를 불러오는데 실패하였습니다.'); + + } + }; - // GoodsMockData를 21번 반복 생성 + fetchGoods(); + }, [filterOption]); return ( 실시간 급상승 선물랭킹 - + {errorMessage ? ( + {errorMessage} + ) : ( + + )} ); @@ -48,3 +68,10 @@ const Title = styled.h2` line-height: 50px; } `; + +const ErrorMessage = styled.div` + color: #ff0000; + text-align: center; + font-size: 16px; + margin-top: 20px; +`; From 024b798a1a38561610cf391216f98942098daabe Mon Sep 17 00:00:00 2001 From: eunjin Date: Wed, 10 Jul 2024 18:04:27 +0900 Subject: [PATCH 05/15] =?UTF-8?q?fix=20:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EA=B0=80=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 27 ++++++++++++++----- .../Home/GoodsRankingSection/index.tsx | 2 +- .../Home/ThemeCategorySection/index.tsx | 11 ++++---- .../features/Theme/ThemeHeroSection/index.tsx | 24 ++++++++++++----- src/pages/Theme/index.tsx | 17 ++++++------ 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index e6234bec..2070dde5 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,15 +1,33 @@ +// src/api/index.ts import axios from 'axios'; -import type { GoodsData, RankingFilterOption,ThemeData } from '@/types'; +import type { GoodsData, RankingFilterOption, ThemeData } from '@/types'; const api = axios.create({ - baseURL: 'https://react-gift-mock-api-eunjin.vercel.app/',}); + baseURL: 'https://kakao-tech-campus-mock-server.vercel.app', +}); -export const getTheme = async (): Promise => { +export const fetchTheme = async (): Promise => { const response = await api.get<{ themes: ThemeData[] }>('/api/v1/themes'); return response.data.themes; }; +export const fetchThemes = async (themeKey: string): Promise => { + console.log('Fetching theme with key:', themeKey); // 콘솔 로그 추가 + const response = await api.get(`/api/v1/themes/${themeKey}`); + console.log('Fetched theme data:', response.data); + return response.data; +}; + +export const getThemeProducts = async (themeKey: string): Promise => { + const response = await api.get<{ products: GoodsData[] }>(`/api/v1/themes/${themeKey}/products`, { + params: { + maxResults: 20, + }, + }); + return response.data.products; +}; + export const getRankingGoods = async (filterOption: RankingFilterOption): Promise => { const response = await api.get<{ products: GoodsData[] }>('/api/v1/ranking/products', { params: filterOption, @@ -17,7 +35,4 @@ export const getRankingGoods = async (filterOption: RankingFilterOption): Promis return response.data.products; }; - - export default api; - diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index f4c61cfa..9be99bd3 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -19,12 +19,12 @@ export const GoodsRankingSection: React.FC = () => { useEffect(() => { const fetchGoods = async () => { + setErrorMessage(null); try { const data = await getRankingGoods(filterOption); setGoodsList(data); } catch (err) { setErrorMessage('데이터를 불러오는데 실패하였습니다.'); - } }; diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index 1cf3710e..e43389ed 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { getTheme } from '@/api'; +import { fetchTheme } from '@/api'; import { Container } from '@/components/common/layouts/Container'; import { Grid } from '@/components/common/layouts/Grid'; import { getDynamicPath } from '@/routes/path'; @@ -15,18 +15,19 @@ export const ThemeCategorySection: React.FC = () => { const [themes, setThemes] = useState([]); useEffect(() => { - const fetchThemes = async () => { + const fetchThemesData = async () => { try { - const data = await getTheme(); + const data = await fetchTheme(); setThemes(data); } catch (error) { console.error('Failed to fetch themes:', error); } }; - fetchThemes(); + fetchThemesData(); }, []); - + + themes.map((theme)=> console.log(theme.key)); return ( diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 36cfc038..ec9e4963 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,16 +1,32 @@ +// src/components/features/Theme/ThemeHeroSection.tsx import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; +import { fetchTheme } from '@/api'; import { Container } from '@/components/common/layouts/Container'; import { breakpoints } from '@/styles/variants'; import type { ThemeData } from '@/types'; -import { ThemeMockList } from '@/types/mock'; type Props = { themeKey: string; }; export const ThemeHeroSection = ({ themeKey }: Props) => { - const currentTheme = getCurrentTheme(themeKey, ThemeMockList); + const [currentTheme, setCurrentTheme] = useState(null); + + useEffect(() => { + const getThemeData = async () => { + try { + const themes = await fetchTheme(); + const foundTheme = themes.find((theme) => theme.key === themeKey); + setCurrentTheme(foundTheme || null); + } catch (error) { + console.error('Failed to fetch themes:', error); + } + }; + + getThemeData(); + }, [themeKey]); if (!currentTheme) { return null; @@ -82,7 +98,3 @@ const Description = styled.p` line-height: 32px; } `; - -export const getCurrentTheme = (themeKey: string, themeList: ThemeData[]) => { - return themeList.find((theme) => theme.key === themeKey); -}; diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx index 4d02e6c1..40ec4d9a 100644 --- a/src/pages/Theme/index.tsx +++ b/src/pages/Theme/index.tsx @@ -1,17 +1,18 @@ -import { Navigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { ThemeGoodsSection } from '@/components/features/Theme/ThemeGoodsSection'; -import { getCurrentTheme, ThemeHeroSection } from '@/components/features/Theme/ThemeHeroSection'; -import { RouterPath } from '@/routes/path'; -import { ThemeMockList } from '@/types/mock'; +import { ThemeHeroSection } from '@/components/features/Theme/ThemeHeroSection'; +// getCurrentTheme, +// import { RouterPath } from '@/routes/path'; +// import { ThemeMockList } from '@/types/mock'; export const ThemePage = () => { const { themeKey = '' } = useParams<{ themeKey: string }>(); - const currentTheme = getCurrentTheme(themeKey, ThemeMockList); + // const currentTheme = getCurrentTheme(themeKey, ThemeMockList); - if (!currentTheme) { - return ; - } + // if (!currentTheme) { + // return ; + // } return ( <> From 854d8658ca3e808e0cc71dca6c6fe55cb57803a4 Mon Sep 17 00:00:00 2001 From: eunjin Date: Wed, 10 Jul 2024 23:23:50 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat=20:=20themeGoodsection=20api=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Theme/ThemeGoodsSection/index.tsx | 28 +++++++++++++++++-- .../features/Theme/ThemeHeroSection/index.tsx | 8 +++++- src/pages/Theme/index.tsx | 10 +------ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index 8edbf70e..f72a87da 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,16 +1,38 @@ import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; +import { getThemeProducts } from '@/api'; import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default'; import { Container } from '@/components/common/layouts/Container'; import { Grid } from '@/components/common/layouts/Grid'; import { breakpoints } from '@/styles/variants'; -import { GoodsMockList } from '@/types/mock'; +import type { GoodsData } from '@/types'; type Props = { themeKey: string; }; -export const ThemeGoodsSection = ({}: Props) => { +export const ThemeGoodsSection = ({ themeKey }: Props) => { + const [goods, setGoods] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchGoods = async () => { + try { + const data = await getThemeProducts(themeKey); + setGoods(data); + } catch (err) { + setError('상품을 가져오는데 실패하였습니다.'); + } + }; + + fetchGoods(); + }, [themeKey]); + + if (error) { + return
{error}
; + } + return ( @@ -21,7 +43,7 @@ export const ThemeGoodsSection = ({}: Props) => { }} gap={16} > - {GoodsMockList.map(({ id, imageURL, name, price, brandInfo }) => ( + {goods.map(({ id, imageURL, name, price, brandInfo }) => ( { const [currentTheme, setCurrentTheme] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); useEffect(() => { const getThemeData = async () => { @@ -21,13 +24,16 @@ export const ThemeHeroSection = ({ themeKey }: Props) => { const foundTheme = themes.find((theme) => theme.key === themeKey); setCurrentTheme(foundTheme || null); } catch (error) { - console.error('Failed to fetch themes:', error); + setErrorMessage('error'); } }; getThemeData(); }, [themeKey]); + if (errorMessage) { + return ; + } if (!currentTheme) { return null; } diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx index 40ec4d9a..16a55a39 100644 --- a/src/pages/Theme/index.tsx +++ b/src/pages/Theme/index.tsx @@ -1,18 +1,10 @@ import { useParams } from 'react-router-dom'; import { ThemeGoodsSection } from '@/components/features/Theme/ThemeGoodsSection'; -import { ThemeHeroSection } from '@/components/features/Theme/ThemeHeroSection'; -// getCurrentTheme, -// import { RouterPath } from '@/routes/path'; -// import { ThemeMockList } from '@/types/mock'; +import {ThemeHeroSection } from '@/components/features/Theme/ThemeHeroSection'; export const ThemePage = () => { const { themeKey = '' } = useParams<{ themeKey: string }>(); - // const currentTheme = getCurrentTheme(themeKey, ThemeMockList); - - // if (!currentTheme) { - // return ; - // } return ( <> From 81c7ece894b25c8bc68e678178c1d7c134bdce93 Mon Sep 17 00:00:00 2001 From: eunjin Date: Thu, 11 Jul 2024 00:20:52 +0900 Subject: [PATCH 07/15] =?UTF-8?q?Feat=20:=20error=20=EB=B0=8F=20loading=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 4 +- .../Theme/ThemeGoodsSection/index.tsx | 51 ++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 2070dde5..dc51caeb 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -4,7 +4,7 @@ import axios from 'axios'; import type { GoodsData, RankingFilterOption, ThemeData } from '@/types'; const api = axios.create({ - baseURL: 'https://kakao-tech-campus-mock-server.vercel.app', + baseURL: 'https://react-gift-mock-api-eunjin.vercel.app', }); export const fetchTheme = async (): Promise => { @@ -13,7 +13,7 @@ export const fetchTheme = async (): Promise => { }; export const fetchThemes = async (themeKey: string): Promise => { - console.log('Fetching theme with key:', themeKey); // 콘솔 로그 추가 + console.log('Fetching theme with key:', themeKey); const response = await api.get(`/api/v1/themes/${themeKey}`); console.log('Fetched theme data:', response.data); return response.data; diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index f72a87da..1a007938 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -15,24 +15,37 @@ type Props = { export const ThemeGoodsSection = ({ themeKey }: Props) => { const [goods, setGoods] = useState([]); const [error, setError] = useState(null); + const [isLoading, setisLoading] = useState(true); + useEffect(() => { const fetchGoods = async () => { try { + setisLoading(true); const data = await getThemeProducts(themeKey); setGoods(data); + setisLoading(false); } catch (err) { + setisLoading(true); setError('상품을 가져오는데 실패하였습니다.'); + setisLoading(false); } }; fetchGoods(); }, [themeKey]); - if (error) { - return
{error}
; + if (isLoading) { + return ( + + + + ); } + if (error) { + return {error}; + } return ( @@ -66,3 +79,37 @@ const Wrapper = styled.section` padding: 40px 16px 360px; } `; + + +const ErrorMessage = styled.div` + color: black; + text-align: center; + font-size: 16px; + margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + height: 100px; +`; + +const SpinnerWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100px; +`; + +const Spinner = styled.div` + border: 4px solid white; + border-left-color: rgba(0,0,0,0.5); + border-radius: 50%; + width: 36px; + height: 36px; + animation: spin 1s linear infinite; + + @keyframes spin { + to { + transform: rotate(360deg); + } + } +`; From b70dcda073e533f6932e58f8365492fabae4b6f0 Mon Sep 17 00:00:00 2001 From: eunjin Date: Fri, 12 Jul 2024 15:46:28 +0900 Subject: [PATCH 08/15] =?UTF-8?q?add=20:=20react=20query=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 25 +++++++++++++++++++++++++ package.json | 1 + 2 files changed, 26 insertions(+) diff --git a/package-lock.json b/package-lock.json index 74216d7d..e7236fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", + "@tanstack/react-query": "^5.51.1", "axios": "^1.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -9882,6 +9883,30 @@ "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", "dev": true }, + "node_modules/@tanstack/query-core": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.1.tgz", + "integrity": "sha512-fJBMQMpo8/KSsWW5ratJR5+IFr7YNJ3K2kfP9l5XObYHsgfVy1w3FJUWU4FT2fj7+JMaEg33zOcNDBo0LMwHnw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.1.tgz", + "integrity": "sha512-s47HKFnQ4HOJAHoIiXcpna/roMMPZJPy6fJ6p4ZNVn8+/onlLBEDd1+xc8OnDuwgvecqkZD7Z2mnSRbcWefrKw==", + "dependencies": { + "@tanstack/query-core": "5.51.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", diff --git a/package.json b/package.json index f1f65cfa..eee7f24f 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", + "@tanstack/react-query": "^5.51.1", "axios": "^1.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", From 73026ca78475b0d7cebebf5e147dda4759b77092 Mon Sep 17 00:00:00 2001 From: eunjin Date: Sat, 13 Jul 2024 13:37:40 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat=20:=20app.js=20queryclient=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 26d8766c..e48f1a9b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,17 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + import { AuthProvider } from './provider/Auth'; import { Routes } from './routes'; +const queryClient = new QueryClient(); + const App = () => { return ( - - - + + + + + ); }; From 6a35edf8a241f7f426dfcffbf89d291a0233a2d4 Mon Sep 17 00:00:00 2001 From: eunjin Date: Sat, 13 Jul 2024 14:07:26 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat=20:=20api=20goodthemeproducts=20page?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.ts | 5 +++-- src/components/features/Home/ThemeCategorySection/index.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index dc51caeb..3cf80e06 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -19,10 +19,11 @@ export const fetchThemes = async (themeKey: string): Promise => { return response.data; }; -export const getThemeProducts = async (themeKey: string): Promise => { +export const getThemeProducts = async (themeKey: string, page: number): Promise => { const response = await api.get<{ products: GoodsData[] }>(`/api/v1/themes/${themeKey}/products`, { params: { - maxResults: 20, + page, + maxResults: 20, }, }); return response.data.products; diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index e43389ed..dfd3ecda 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -20,7 +20,7 @@ export const ThemeCategorySection: React.FC = () => { const data = await fetchTheme(); setThemes(data); } catch (error) { - console.error('Failed to fetch themes:', error); + console.error('themecategory error', error); } }; @@ -40,7 +40,7 @@ export const ThemeCategorySection: React.FC = () => { {themes.map((theme) => ( From cf48d8090c7af25b03b7f986dfe305b7d2bea116 Mon Sep 17 00:00:00 2001 From: eunjin Date: Sat, 13 Jul 2024 14:09:15 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat=20:=20react-intersection-observer=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 15 +++++++++++++++ package.json | 1 + 2 files changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index e7236fdd..52cd1f10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "axios": "^1.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-intersection-observer": "^9.13.0", "react-router-dom": "^6.22.1" }, "devDependencies": { @@ -28294,6 +28295,20 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "node_modules/react-intersection-observer": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz", + "integrity": "sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index eee7f24f..b9357b01 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "axios": "^1.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-intersection-observer": "^9.13.0", "react-router-dom": "^6.22.1" }, "devDependencies": { From a3328e50434ffbed6902d3ee43b5be2ddc3a28a4 Mon Sep 17 00:00:00 2001 From: eunjin Date: Sat, 13 Jul 2024 15:56:44 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat:=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 113 ++++++++++++++---- package.json | 1 + src/App.tsx | 2 +- src/api/index.ts | 4 +- .../Home/GoodsRankingSection/index.tsx | 2 +- .../Theme/ThemeGoodsSection/index.tsx | 65 ++++++---- 6 files changed, 140 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52cd1f10..fb63e4ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.13.0", + "react-query": "^3.39.3", "react-router-dom": "^6.22.1" }, "devDependencies": { @@ -12702,8 +12703,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -12763,7 +12763,6 @@ "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "dev": true, "engines": { "node": ">=0.6" } @@ -12906,6 +12905,21 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/browser-assert": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", @@ -13634,8 +13648,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -15569,8 +15582,7 @@ "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" }, "node_modules/detect-node-es": { "version": "1.1.0", @@ -18746,8 +18758,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -19772,7 +19783,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -19781,8 +19791,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -23882,6 +23891,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -24658,6 +24672,15 @@ "react": ">= 0.14.0" } }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", @@ -24760,6 +24783,11 @@ "node": ">=8.6" } }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -25000,6 +25028,14 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -25544,6 +25580,11 @@ "integrity": "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==", "dev": true }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -25581,7 +25622,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -25865,7 +25905,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -28314,6 +28353,31 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-query": { + "version": "3.39.3", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", + "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -28984,6 +29048,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -29178,7 +29247,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -29193,7 +29261,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -29203,7 +29270,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -29223,7 +29289,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -31885,6 +31950,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -33356,8 +33430,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", diff --git a/package.json b/package.json index b9357b01..5876ccfb 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.13.0", + "react-query": "^3.39.3", "react-router-dom": "^6.22.1" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index e48f1a9b..e16a1545 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClient, QueryClientProvider } from 'react-query'; import { AuthProvider } from './provider/Auth'; import { Routes } from './routes'; diff --git a/src/api/index.ts b/src/api/index.ts index 3cf80e06..fb3b1670 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -19,11 +19,11 @@ export const fetchThemes = async (themeKey: string): Promise => { return response.data; }; -export const getThemeProducts = async (themeKey: string, page: number): Promise => { +export const getThemeProducts = async (themeKey: string , pageParam: number): Promise => { const response = await api.get<{ products: GoodsData[] }>(`/api/v1/themes/${themeKey}/products`, { params: { - page, maxResults: 20, + page: pageParam }, }); return response.data.products; diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 9be99bd3..a52e9343 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -19,8 +19,8 @@ export const GoodsRankingSection: React.FC = () => { useEffect(() => { const fetchGoods = async () => { - setErrorMessage(null); try { + setErrorMessage(null); const data = await getRankingGoods(filterOption); setGoodsList(data); } catch (err) { diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index 1a007938..df3ca158 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,39 +1,48 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { useInfiniteQuery } from 'react-query'; import { getThemeProducts } from '@/api'; import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default'; import { Container } from '@/components/common/layouts/Container'; import { Grid } from '@/components/common/layouts/Grid'; import { breakpoints } from '@/styles/variants'; -import type { GoodsData } from '@/types'; type Props = { themeKey: string; }; export const ThemeGoodsSection = ({ themeKey }: Props) => { - const [goods, setGoods] = useState([]); - const [error, setError] = useState(null); - const [isLoading, setisLoading] = useState(true); + const { ref, inView } = useInView(); + console.log('inView:', inView); + const fetchGoods = async ({ pageParam = 0 }) => { + const data = await getThemeProducts(themeKey, pageParam); + console.log('Fetched data for page:', pageParam, data); + return { data, nextPage: pageParam + 1 }; + }; + + const { + data, + fetchNextPage, + hasNextPage, + isLoading, + error, + isFetching, + isFetchingNextPage, + } = useInfiniteQuery(['themeProducts', themeKey], fetchGoods, { + getNextPageParam: (lastPage) => { + if (lastPage.data.length < 20) return undefined; + return lastPage.nextPage; + }, + }); useEffect(() => { - const fetchGoods = async () => { - try { - setisLoading(true); - const data = await getThemeProducts(themeKey); - setGoods(data); - setisLoading(false); - } catch (err) { - setisLoading(true); - setError('상품을 가져오는데 실패하였습니다.'); - setisLoading(false); - } - }; - - fetchGoods(); - }, [themeKey]); + if (inView && hasNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, fetchNextPage]); if (isLoading) { return ( @@ -44,8 +53,13 @@ export const ThemeGoodsSection = ({ themeKey }: Props) => { } if (error) { - return {error}; + return 상품을 가져오는데 실패하였습니다.; + } + + if (!data) { + return 상품이 없습니다.; } + return ( @@ -56,7 +70,7 @@ export const ThemeGoodsSection = ({ themeKey }: Props) => { }} gap={16} > - {goods.map(({ id, imageURL, name, price, brandInfo }) => ( + {data.pages.flatMap(page => page.data).map(({ id, imageURL, name, price, brandInfo }) => ( { /> ))} +
{/* ref 설정 */} + {(isFetching || isFetchingNextPage) && ( + + + + )} ); @@ -80,7 +100,6 @@ const Wrapper = styled.section` } `; - const ErrorMessage = styled.div` color: black; text-align: center; From ef4aefb839bdfa34c924744dafc38cb9315ac73c Mon Sep 17 00:00:00 2001 From: eunjin Date: Sat, 13 Jul 2024 15:57:46 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat=20:=20Good=20Ranking=20react-query?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/GoodsRankingSection/index.tsx | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index a52e9343..d557b156 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -1,10 +1,11 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import { useState, useEffect } from 'react'; +import { useQuery } from 'react-query'; import { getRankingGoods } from '@/api'; import { Container } from '@/components/common/layouts/Container'; import { breakpoints } from '@/styles/variants'; -import type { GoodsData, RankingFilterOption } from '@/types'; +import type { RankingFilterOption } from '@/types'; import { GoodsRankingFilter } from './Filter'; import { GoodsRankingList } from './List'; @@ -14,32 +15,30 @@ export const GoodsRankingSection: React.FC = () => { targetType: 'ALL', rankType: 'MANY_WISH', }); - const [goodsList, setGoodsList] = useState([]); - const [errorMessage, setErrorMessage] = useState(null); // 변수 이름 변경 - useEffect(() => { - const fetchGoods = async () => { - try { - setErrorMessage(null); - const data = await getRankingGoods(filterOption); - setGoodsList(data); - } catch (err) { - setErrorMessage('데이터를 불러오는데 실패하였습니다.'); - } - }; + const { data: goodsList, error, isLoading, refetch } = useQuery( + ['rankingGoods', filterOption], + () => getRankingGoods(filterOption), + { + keepPreviousData: true, + } + ); - fetchGoods(); - }, [filterOption]); + useEffect(() => { + refetch(); + }, [filterOption, refetch]); return ( 실시간 급상승 선물랭킹 - {errorMessage ? ( - {errorMessage} + {isLoading ? ( + 로딩 중... + ) : error ? ( + 데이터를 불러오는데 실패하였습니다. ) : ( - + )} @@ -75,3 +74,10 @@ const ErrorMessage = styled.div` font-size: 16px; margin-top: 20px; `; + +const LoadingMessage = styled.div` + color: #000; + text-align: center; + font-size: 16px; + margin-top: 20px; +`; From b28197687ab7727e5eb70cb94b7a9c9ce4aec244 Mon Sep 17 00:00:00 2001 From: eunjin Date: Sat, 13 Jul 2024 16:03:23 +0900 Subject: [PATCH 14/15] =?UTF-8?q?feat=20:=20ThemeHeroSection=20react-query?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/Theme/ThemeHeroSection/index.tsx | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 5eb9d0da..141fabab 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,7 +1,6 @@ -// src/components/features/Theme/ThemeHeroSection.tsx import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; -import { Navigate} from 'react-router-dom'; +import { useQuery } from 'react-query'; +import { Navigate } from 'react-router-dom'; import { fetchTheme } from '@/api'; import { Container } from '@/components/common/layouts/Container'; @@ -14,27 +13,15 @@ type Props = { }; export const ThemeHeroSection = ({ themeKey }: Props) => { - const [currentTheme, setCurrentTheme] = useState(null); - const [errorMessage, setErrorMessage] = useState(null); - - useEffect(() => { - const getThemeData = async () => { - try { - const themes = await fetchTheme(); - const foundTheme = themes.find((theme) => theme.key === themeKey); - setCurrentTheme(foundTheme || null); - } catch (error) { - setErrorMessage('error'); - } - }; - - getThemeData(); - }, [themeKey]); - - if (errorMessage) { + const { data: themes, error, isLoading } = useQuery('themes', fetchTheme); + + const currentTheme = themes?.find((theme: ThemeData) => theme.key === themeKey) || null; + + if (error) { return ; } - if (!currentTheme) { + + if (isLoading || !currentTheme) { return null; } From bcaead0770dd35cbd70eeba33309d67320aa4343 Mon Sep 17 00:00:00 2001 From: eunjin Date: Sun, 14 Jul 2024 02:42:39 +0900 Subject: [PATCH 15/15] =?UTF-8?q?Docs:=20step4=20=EB=8B=B5=EB=B3=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a4e93369..1a3f813e 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,57 @@ -# 3주차 상품리스트 구현 - api -## step1 📝구현사항 -+ 첨부된 oas.yaml 파일을 토대로 Request, Response Type을 정의하기 -+ axios 라이브러리 설치 -* 메인페이지 - theme - * /api/v1/themes API를 사용하여 Section을 구현 - * API는 Axios또는 React Query 등을 모두 활용해서 구현 가능 -* 메인 페이지 - 실시간 급상승 선물랭킹 - * /api/v1/ranking/products API를 사용하여 Section을 구현 - * 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 해야함 -* Theme 페이지 - header - * url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 - * themeKey가 잘못 된 경우 메인 페이지로 연결 -* Theme 페이지 - 상품 목록 섹션 - * /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 - *API 요청 시 한번에 20개의 상품 목록이 내려오게 구현 +# 3주차 상품리스트 구현 - API + +## Step1 📝구현사항 + +- 첨부된 `oas.yaml` 파일을 토대로 Request, Response Type을 정의하기 +- `axios` 라이브러리 설치 +- 메인페이지 - theme + - `/api/v1/themes` API를 사용하여 Section을 구현 + - API는 `Axios` 또는 `React Query` 등을 모두 활용해서 구현 가능 +- 메인 페이지 - 실시간 급상승 선물랭킹 + - `/api/v1/ranking/products` API를 사용하여 Section을 구현 + - 필터 조건을 선택하면 해당 조건에 맞게 API를 요청하여 보여지게 해야 함 +- Theme 페이지 - header + - URL의 `pathParams`와 `/api/v1/themes` API를 사용하여 Section을 구현 + - `themeKey`가 잘못된 경우 메인 페이지로 연결 +- Theme 페이지 - 상품 목록 섹션 + - `/api/v1/themes/{themeKey}/products` API를 사용하여 상품 목록을 구현 + - API 요청 시 한 번에 20개의 상품 목록이 내려오게 구현 + +## Step4 질문답변 + +### 🧐 질문 1. CORS 에러는 무엇이고 언제 발생하는지 설명해주세요. 이를 해결할 수 있는 방법에 대해서도 설명해주세요. + +**CORS 에러** +- **정의**: Cross Origin Resource Sharing, 한 도메인 또는 Origin의 웹페이지가 다른 도메인을 가진 리소스에 액세스할 수 있게 하는 보안 매커니즘이다. +- **발생 이유**: 동일 출처 정책(Same-Origin Policy) 때문에 등장하게 되었으며, 이는 동일한 출처의 리소스에만 접근하도록 제한하는 정책이다. +- **해결 방법**: 요청해야 하는 URL 앞에 프록시 서버 URL을 붙여 이용하면 된다. + +### 🧐 질문 2. 비동기 처리 방법인 callback, promise, async await에 대해 각각 장단점과 함께 설명해주세요. + +**비동기 처리 방법** + +1. **Callback** + - **장점**: 콜백 함수는 간단한 작업에서 이해하기 쉽다. + - **단점**: 콜백 지옥에 빠질 수 있고, 이로 인해 가독성이 떨어진다. + +2. **Promise** + - **장점**: 비동기 작업의 순차 처리가 가능하다. 또한, `return` 값을 가지는 객체이기 때문에 동기 코드와 마찬가지로 값을 변수에 할당하거나 다양한 메소드를 사용하는 것과 같이 추가 작업이 가능하다. + - **단점**: 중첩된 프로미스 체인의 사용은 가독성이 떨어진다. + +3. **Async Await** + - **장점**: 변수에 담아 동기적 코드처럼 작성할 수 있는 편리함을 제공한다. 예외 처리를 쉽게 할 수 있다. + - **단점**: 최신 JS 기능으로 구형 브라우저에서는 폴리필이 필요하다. + +### 🧐 질문 3. React Query의 주요 특징에 대해 설명하고, queryKey는 어떤 역할을 하는지 설명해주세요. + +**React Query의 주요 특징** +- **자동 캐싱**: 서버로부터 가져온 데이터를 자동으로 캐싱 +- **자동 갱신**: 데이터를 최신 상태로 유지하기 위해 백그라운드에서 자동으로 데이터 갱신 +- **쿼리 무효화**: 특정 조건이 발생했을 때 쿼리를 무효화 + +**queryKey의 역할** +- **데이터 식별**: 각 쿼리를 고유하게 식별 +- **캐싱 및 갱신**: `queryKey`를 기반으로 데이터를 캐싱하고 필요할 때 데이터를 다시 가져옴 +- **종속성 관리**: 특정 데이터가 변경되면 해당 `queryKey`를 가진 모든 쿼리가 무효화되어 데이터를 다시 가져오도록 함 +- **구조화된 키 사용**: 문자열, 배열, 객체 등 다양한 형태로 사용 가능 +