From 186c7a9cb7cf217460b40ad94f0b05e3e8613b6e Mon Sep 17 00:00:00 2001 From: loveydev Date: Tue, 9 Jul 2024 15:50:06 +0900 Subject: [PATCH 01/30] =?UTF-8?q?Docs:=20Step=20=EB=A7=88=EB=8B=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=A0=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index e69de29b..3991360c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,35 @@ +

카카오 테크 캠퍼스 - 프론트엔드 카카오 선물하기 편

+ +

1️⃣ Step 1 체크리스트

+ +- `oas.yaml` 파일과 `mock API URL` 을 사용해서 API 구현하기 + +- Main Page → + + - [ ] Theme 카테고리 섹션: `/api/v1/themes` API를 사용하여 Section 구현 + + - [ ] 실시간 급상승 선물랭킹 섹션: `api/v1/ranking/products` API를 사용하여 Section 구현 + +- Theme Page → + + - [ ] Header : url의 pathParams와 `/api/v1/themes` API를 사용하여 Section을 구현 + + - [ ] 상품 목록 섹션 : `/api/v1/themes/{themeKey}/products` API를 사용하여 상품 목록을 구현. API 요청 시 한번에 20개의 상품 목록이 내려오도록 한다. + +
+ +

2️⃣ Step 2 체크리스트

+ +- [ ] 각 API에서 Loading 상태에 대한 UI 대응 + +- [ ] 데이터가 없는 경우에 대한 UI 대응 + +- [ ] HTTP Status에 따라 Error를 다르게 처리 + +
+ +

3️⃣ Step 3 체크리스트

+ +- [ ] 스크롤을 내리면 추가로 데이터를 요청하여 보여지게 함 + +- [ ] 1단계에서 구현한 API를 `react-query` 를 사용해서 구현 \ No newline at end of file From a92fcde95742a497e531d8e3e10d2ac6768bf1d8 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 12:38:13 +0900 Subject: [PATCH 02/30] =?UTF-8?q?Chore:=20axios=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 31 ++++++++++++++++++------------- package.json | 1 + 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 89581c64..fd598900 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,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", + "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 +13509,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 +15491,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,16 +18414,16 @@ } }, "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", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -18614,7 +18623,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 +24752,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 +24760,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 +27582,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 33d477337948302f017e2dcf6eeaf8bbff932bb4 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:01:14 +0900 Subject: [PATCH 03/30] =?UTF-8?q?Feat:=20API=20=EB=A1=9C=EB=B6=80=ED=84=B0?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EA=B0=80=EC=A0=B8?= =?UTF-8?q?=EC=98=AC=20=ED=95=A8=EC=88=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/index.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/api/index.tsx diff --git a/src/api/index.tsx b/src/api/index.tsx new file mode 100644 index 00000000..17f2fc60 --- /dev/null +++ b/src/api/index.tsx @@ -0,0 +1,16 @@ +import axios from "axios" + +const BASE_URL = 'https://react-gift-mock-api-hyunaeri.vercel.app/' + +const fetchData = async (target: string) => { + try { + const response = await axios.get(`${BASE_URL}${target}`) + return response.data + } + catch (error) { + console.error(`Error fetching data from ${target}`, error) + throw error + } +} + +export default fetchData \ No newline at end of file From 360b91ee801d741c417502feaf060e6f3bf27391 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:06:06 +0900 Subject: [PATCH 04/30] =?UTF-8?q?Feat:=20API=20=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20Theme=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=84=B9=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/ThemeCategorySection/index.tsx | 120 ++++++------------ 1 file changed, 41 insertions(+), 79 deletions(-) diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index d82e3afe..a15bd29b 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -1,14 +1,42 @@ -import styled from '@emotion/styled'; -import { Link } from 'react-router-dom'; +import styled from '@emotion/styled' +import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' -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 fetchData 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 { ThemeCategoryItem } from './ThemeCategoryItem'; +import { ThemeCategoryItem } from './ThemeCategoryItem' + +interface Theme { + id: number + key: string + label: string + imageURL: string + title: string + description?: string +} export const ThemeCategorySection = () => { + const [themeFromAPI, setThemeFromAPI] = useState([]) + + // 최초 렌더링 시 한 번만 실행 + useEffect(() => { + const fetchThemeData = async () => { + try { + const data = await fetchData('api/v1/themes') + setThemeFromAPI(data.themes) + console.log('Fetch Theme Data Success: ', data.themes) + } + catch (error) { + console.error('Fetch Theme Data Fail: ', error) + } + } + fetchThemeData() + }, []) + return ( @@ -18,78 +46,12 @@ export const ThemeCategorySection = () => { md: 6, }} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {themeFromAPI.map((theme) => ( + // 각 Theme Detail Page로 이동할 링크 + + + + ))} From de7339ebdf415a7036de21fa3bd650e7a269a810 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:09:38 +0900 Subject: [PATCH 05/30] =?UTF-8?q?Docs:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20Theme=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=84=B9=EC=85=98=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3991360c..737678e6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - Main Page → - - [ ] Theme 카테고리 섹션: `/api/v1/themes` API를 사용하여 Section 구현 + - [x] Theme 카테고리 섹션: `/api/v1/themes` API를 사용하여 Section 구현 - [ ] 실시간 급상승 선물랭킹 섹션: `api/v1/ranking/products` API를 사용하여 Section 구현 From 321592cdbc29c4c8a33af688a0b2f376476f9e76 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:10:28 +0900 Subject: [PATCH 06/30] =?UTF-8?q?Feat:=20API=20=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=9E=AD=ED=82=B9=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=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 | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 9464d67c..fd4e1c5e 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -1,10 +1,10 @@ import styled from '@emotion/styled'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import fetchData 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'; @@ -15,14 +15,31 @@ export const GoodsRankingSection = () => { rankType: 'MANY_WISH', }); - // GoodsMockData를 21번 반복 생성 + const [rankingProducts, setRankingProducts] = useState([]) + + // filterOption 에 변화가 생길 때 마다 실행 + useEffect(() => { + const fetchRankingProductData = async () => { + try { + const { targetType, rankType } = filterOption + const data = await fetchData( + `api/v1/ranking/products?targetType=${targetType}&rankType=${rankType}`, + ) + setRankingProducts(data.products) + } + catch (error) { + console.error('Fetch Goods Ranking Data Fail: ', error) + } + } + fetchRankingProductData() + }, [filterOption]); return ( 실시간 급상승 선물랭킹 - + ); From 9164d64b5ab31c2e3a9f263a4416a2dbbc433f4a Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:11:04 +0900 Subject: [PATCH 07/30] =?UTF-8?q?Docs:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=9E=AD=ED=82=B9=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 737678e6..e2f68333 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - [x] Theme 카테고리 섹션: `/api/v1/themes` API를 사용하여 Section 구현 - - [ ] 실시간 급상승 선물랭킹 섹션: `api/v1/ranking/products` API를 사용하여 Section 구현 + - [x] 실시간 급상승 선물랭킹 섹션: `api/v1/ranking/products` API를 사용하여 Section 구현 - Theme Page → From 957919504989663b78b2011771cf360781fb4661 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:41:32 +0900 Subject: [PATCH 08/30] =?UTF-8?q?Feat:=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=B4=20console.log=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/features/Home/ThemeCategorySection/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index a15bd29b..91a03b1a 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -28,10 +28,10 @@ export const ThemeCategorySection = () => { try { const data = await fetchData('api/v1/themes') setThemeFromAPI(data.themes) - console.log('Fetch Theme Data Success: ', data.themes) + console.log('[ThemeCategorySection] Fetch Theme Data Success: ', data.themes) } catch (error) { - console.error('Fetch Theme Data Fail: ', error) + console.error('[ThemeCategorySection] Fetch Theme Data Fail: ', error) } } fetchThemeData() From 33d94dc4d400e60aa7fb54d8c0ffe8d147d5d8f0 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:42:08 +0900 Subject: [PATCH 09/30] =?UTF-8?q?Feat:=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=B4=20console.log=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/features/Home/GoodsRankingSection/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index fd4e1c5e..2f0462c9 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -26,9 +26,10 @@ export const GoodsRankingSection = () => { `api/v1/ranking/products?targetType=${targetType}&rankType=${rankType}`, ) setRankingProducts(data.products) + console.log('[GoodsRankingSection] Fetch Goods Ranking Data Success: ', data.products) } catch (error) { - console.error('Fetch Goods Ranking Data Fail: ', error) + console.error('[GoodsRankingSection] Fetch Goods Ranking Data Fail: ', error) } } fetchRankingProductData() From 40d9cd16adb924792a65ddaad061d2d2dd0c0389 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:44:09 +0900 Subject: [PATCH 10/30] =?UTF-8?q?Feat:=20API=20=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20Theme=20Page=20=EC=9D=98=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=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 | 46 +++++++++++++------ src/pages/Theme/index.tsx | 12 ++--- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 36cfc038..dc1ef402 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,29 +1,45 @@ -import styled from '@emotion/styled'; +import styled from '@emotion/styled' +import { useEffect, useState } from 'react' -import { Container } from '@/components/common/layouts/Container'; -import { breakpoints } from '@/styles/variants'; -import type { ThemeData } from '@/types'; -import { ThemeMockList } from '@/types/mock'; +import fetchData from '@/api' +import { Container } from '@/components/common/layouts/Container' +import { breakpoints } from '@/styles/variants' +import type { ThemeData } from '@/types' type Props = { - themeKey: string; -}; + themeKey: string +} export const ThemeHeroSection = ({ themeKey }: Props) => { - const currentTheme = getCurrentTheme(themeKey, ThemeMockList); + const [currentTheme, setCurrentTheme] = useState() + + // themeKey 가 변할 때 마다 실행 + useEffect(() => { + const fetchThemeData = async () => { + try { + const data = await fetchData(`api/v1/themes`) + const theme = getCurrentTheme(themeKey, data.themes) + setCurrentTheme(theme) + console.log('[ThemeHeroSection] Fetch Theme Data Success: ', data.themes) + } + catch (error) { + console.error('[ThemeHeroSection] Fetch Theme Data Fail: ', error) + } + } + fetchThemeData() + }, [themeKey]) + if (!currentTheme) { - return null; + return null } - const { backgroundColor, label, title, description } = currentTheme; - return ( - + - - {title} - {description && {description}} + + {currentTheme.title} + {currentTheme.description && {currentTheme.description}} ); diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx index 4d02e6c1..5413e26d 100644 --- a/src/pages/Theme/index.tsx +++ b/src/pages/Theme/index.tsx @@ -1,17 +1,11 @@ -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'; + export const ThemePage = () => { const { themeKey = '' } = useParams<{ themeKey: string }>(); - const currentTheme = getCurrentTheme(themeKey, ThemeMockList); - - if (!currentTheme) { - return ; - } return ( <> From 5f6d9f6f82d9325e722a2ba21c8d8ef3bedb37b8 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 14:52:50 +0900 Subject: [PATCH 11/30] =?UTF-8?q?Docs:=20Theme=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=9D=98=20=ED=97=A4=EB=8D=94=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2f68333..21611228 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - Theme Page → - - [ ] Header : url의 pathParams와 `/api/v1/themes` API를 사용하여 Section을 구현 + - [x] Header : url의 pathParams와 `/api/v1/themes` API를 사용하여 Section을 구현 - [ ] 상품 목록 섹션 : `/api/v1/themes/{themeKey}/products` API를 사용하여 상품 목록을 구현. API 요청 시 한번에 20개의 상품 목록이 내려오도록 한다. From 1979c504a172bd1753155dd252dce3abe67e7bfd Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 15:07:45 +0900 Subject: [PATCH 12/30] =?UTF-8?q?Feat:=20API=20=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20Theme=20Page=20=EC=9D=98=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=AA=A9=EB=A1=9D=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Theme/ThemeGoodsSection/index.tsx | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index 8edbf70e..18fdfa4e 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 styled from '@emotion/styled' +import { useEffect, useState } from 'react' -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 fetchData 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; + themeKey: string }; -export const ThemeGoodsSection = ({}: Props) => { +export const ThemeGoodsSection = ({ themeKey }: Props) => { + const [currentGoods, setCurrentGoods] = useState([]) + + // themeKey 가 변할 때마다 실행 + useEffect(() => { + const fetchThemeData = async () => { + try { + const MaxItems = 20 + const queryParams = `?maxItems=${MaxItems}` + const data = await fetchData(`api/v1/themes/${themeKey}/products${queryParams}`) + setCurrentGoods(data.products) + console.log('[ThemeGoodsSection] Fetch Theme Goods Data Success: ', data.products) + } + catch (error) { + console.error('[ThemeGoodsSection] Fetch Theme Goods Data Fail: ', error) + } + } + fetchThemeData() + }, [themeKey]) + + return ( @@ -21,13 +43,13 @@ export const ThemeGoodsSection = ({}: Props) => { }} gap={16} > - {GoodsMockList.map(({ id, imageURL, name, price, brandInfo }) => ( + {currentGoods.map((goods) => ( ))} From bfad8345c5bbf3e61c5bd0593076ba4242819ec1 Mon Sep 17 00:00:00 2001 From: loveydev Date: Wed, 10 Jul 2024 15:08:36 +0900 Subject: [PATCH 13/30] =?UTF-8?q?Docs:=20Theme=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=9D=98=20=EC=83=81=ED=92=88=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21611228..68b7fd01 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ - [x] Header : url의 pathParams와 `/api/v1/themes` API를 사용하여 Section을 구현 - - [ ] 상품 목록 섹션 : `/api/v1/themes/{themeKey}/products` API를 사용하여 상품 목록을 구현. API 요청 시 한번에 20개의 상품 목록이 내려오도록 한다. + - [x] 상품 목록 섹션 : `/api/v1/themes/{themeKey}/products` API를 사용하여 상품 목록을 구현. API 요청 시 한번에 20개의 상품 목록이 내려오도록 한다.
From f889e32b292ca353fa8e02a47f4a982edcd95ff7 Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 14:00:34 +0900 Subject: [PATCH 14/30] =?UTF-8?q?Feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20Theme=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=84=B9=EC=85=98=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/ThemeCategorySection/index.tsx | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index 91a03b1a..678f34f6 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -21,22 +21,38 @@ interface Theme { export const ThemeCategorySection = () => { const [themeFromAPI, setThemeFromAPI] = useState([]) + const [loading, setLoading] = useState(true) // 최초 렌더링 시 한 번만 실행 useEffect(() => { const fetchThemeData = async () => { try { const data = await fetchData('api/v1/themes') - setThemeFromAPI(data.themes) - console.log('[ThemeCategorySection] Fetch Theme Data Success: ', data.themes) + + // 의도적으로 지연 시간을 추가 + setTimeout(() => { + setThemeFromAPI(data.themes) + setLoading(false) + console.log('[ThemeCategorySection] Fetch Theme Data Success: ', data.themes) + }, 2000) } catch (error) { console.error('[ThemeCategorySection] Fetch Theme Data Fail: ', error) + setLoading(false) } } fetchThemeData() }, []) + if (loading) { + return ( + + + Loading... + + ) + } + return ( @@ -65,3 +81,31 @@ const Wrapper = styled.section` padding: 45px 52px 23px; } `; + +const LoadingWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 500px; +`; + +const Spinner = styled.div` + width: 40px; + height: 40px; + border: 4px solid rgba(0, 0, 0, 0.1); + border-top: 4px solid #000; + border-radius: 50%; + animation: spin 1s linear infinite; + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } +`; + +const LoadingText = styled.div` + margin-top: 10px; + font-size: 1.2rem; + color: #555; +`; From deaebe7219ecd56125ddc031df602ff8a5257f8e Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 14:09:17 +0900 Subject: [PATCH 15/30] =?UTF-8?q?Feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=9D=98=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=9E=AD=ED=82=B9=20=EC=84=B9=EC=85=98?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/GoodsRankingSection/index.tsx | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 2f0462c9..cb5b18fa 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -16,6 +16,7 @@ export const GoodsRankingSection = () => { }); const [rankingProducts, setRankingProducts] = useState([]) + const [loading, setLoading] = useState(true) // filterOption 에 변화가 생길 때 마다 실행 useEffect(() => { @@ -25,16 +26,32 @@ export const GoodsRankingSection = () => { const data = await fetchData( `api/v1/ranking/products?targetType=${targetType}&rankType=${rankType}`, ) - setRankingProducts(data.products) - console.log('[GoodsRankingSection] Fetch Goods Ranking Data Success: ', data.products) + + // 의도적으로 지연 시간을 추가 + setTimeout(() => { + setRankingProducts(data.products) + setLoading(false) + console.log('[GoodsRankingSection] Fetch Goods Ranking Data Success: ', data.products) + }, 2000) + } catch (error) { console.error('[GoodsRankingSection] Fetch Goods Ranking Data Fail: ', error) + setLoading(false) } } fetchRankingProductData() }, [filterOption]); + if (loading) { + return ( + + + Loading... + + ) + } + return ( @@ -68,3 +85,31 @@ const Title = styled.h2` line-height: 50px; } `; + +const LoadingWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 500px; +`; + +const Spinner = styled.div` + width: 40px; + height: 40px; + border: 4px solid rgba(0, 0, 0, 0.1); + border-top: 4px solid #000; + border-radius: 50%; + animation: spin 1s linear infinite; + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } +`; + +const LoadingText = styled.div` + margin-top: 10px; + font-size: 1.2rem; + color: #555; +`; From 2252f6f5beb4e12a732d64a62a18921ca3c82b4d Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 14:13:47 +0900 Subject: [PATCH 16/30] =?UTF-8?q?Feat:=20Theme=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=9D=98=20=EC=83=81=ED=92=88=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=A1=9C=EB=94=A9=20=EC=83=81=ED=83=9C=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Theme/ThemeGoodsSection/index.tsx | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index 18fdfa4e..06f2e698 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -14,6 +14,7 @@ type Props = { export const ThemeGoodsSection = ({ themeKey }: Props) => { const [currentGoods, setCurrentGoods] = useState([]) + const [loading, setLoading] = useState(true) // themeKey 가 변할 때마다 실행 useEffect(() => { @@ -22,17 +23,31 @@ export const ThemeGoodsSection = ({ themeKey }: Props) => { const MaxItems = 20 const queryParams = `?maxItems=${MaxItems}` const data = await fetchData(`api/v1/themes/${themeKey}/products${queryParams}`) - setCurrentGoods(data.products) - console.log('[ThemeGoodsSection] Fetch Theme Goods Data Success: ', data.products) + + // 의도적으로 지연 시간을 추가 + setTimeout(() => { + setCurrentGoods(data.products) + setLoading(false) + console.log('[ThemeGoodsSection] Fetch Theme Goods Data Success: ', data.products) + }, 2000) } catch (error) { console.error('[ThemeGoodsSection] Fetch Theme Goods Data Fail: ', error) + setLoading(false) } } fetchThemeData() }, [themeKey]) - + if (loading) { + return ( + + + Loading... + + ) + } + return ( @@ -66,3 +81,31 @@ const Wrapper = styled.section` padding: 40px 16px 360px; } `; + +const LoadingWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 500px; +`; + +const Spinner = styled.div` + width: 40px; + height: 40px; + border: 4px solid rgba(0, 0, 0, 0.1); + border-top: 4px solid #000; + border-radius: 50%; + animation: spin 1s linear infinite; + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } +`; + +const LoadingText = styled.div` + margin-top: 10px; + font-size: 1.2rem; + color: #555; +`; From cde6af23f8535bfe42bd6e54821d13b0364d1861 Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 14:18:48 +0900 Subject: [PATCH 17/30] =?UTF-8?q?Feat:=20Theme=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=9D=98=20=ED=97=A4=EB=8D=94=20=EC=84=B9=EC=85=98?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/Theme/ThemeHeroSection/index.tsx | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index dc1ef402..2b43ac3e 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -10,8 +10,13 @@ type Props = { themeKey: string } +export const getCurrentTheme = (themeKey: string, themeList: ThemeData[]) => { + return themeList.find((theme) => theme.key === themeKey); +}; + export const ThemeHeroSection = ({ themeKey }: Props) => { const [currentTheme, setCurrentTheme] = useState() + const [loading, setLoading] = useState(true) // themeKey 가 변할 때 마다 실행 useEffect(() => { @@ -19,16 +24,30 @@ export const ThemeHeroSection = ({ themeKey }: Props) => { try { const data = await fetchData(`api/v1/themes`) const theme = getCurrentTheme(themeKey, data.themes) - setCurrentTheme(theme) - console.log('[ThemeHeroSection] Fetch Theme Data Success: ', data.themes) + + // 의도적으로 지연 시간을 추가 + setTimeout(() => { + setCurrentTheme(theme) + setLoading(false) + console.log('[ThemeHeroSection] Fetch Theme Data Success: ', data.themes) + }, 2000) } catch (error) { console.error('[ThemeHeroSection] Fetch Theme Data Fail: ', error) + setLoading(false) } } fetchThemeData() }, [themeKey]) + if (loading) { + return ( + + + Loading... + + ) + } if (!currentTheme) { return null @@ -99,6 +118,30 @@ const Description = styled.p` } `; -export const getCurrentTheme = (themeKey: string, themeList: ThemeData[]) => { - return themeList.find((theme) => theme.key === themeKey); -}; +const LoadingWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100px; +`; + +const Spinner = styled.div` + width: 40px; + height: 40px; + border: 4px solid rgba(0, 0, 0, 0.1); + border-top: 4px solid #000; + border-radius: 50%; + animation: spin 1s linear infinite; + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } +`; + +const LoadingText = styled.div` + margin-top: 10px; + font-size: 1.2rem; + color: #555; +`; From d3ce4febadcd5ee5ab94d8bf5d8232a47e3d2ecc Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 14:19:49 +0900 Subject: [PATCH 18/30] =?UTF-8?q?Docs:=20=EA=B0=81=20API=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Data=20Loading=20=EC=83=81=ED=83=9C=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20UI=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68b7fd01..e0ec8048 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@

2️⃣ Step 2 체크리스트

-- [ ] 각 API에서 Loading 상태에 대한 UI 대응 +- [x] 각 API에서 Loading 상태에 대한 UI 대응 - [ ] 데이터가 없는 경우에 대한 UI 대응 From 07bb158430bb1c858a754d552a688432d734c071 Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 14:24:39 +0900 Subject: [PATCH 19/30] =?UTF-8?q?Feat:=20Footer=20(=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=ED=86=A1=20=EC=84=A0=EB=AC=BC=ED=95=98=EA=B8=B0)=20im?= =?UTF-8?q?age=20=EC=82=BD=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/features/Layout/Footer.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/features/Layout/Footer.tsx b/src/components/features/Layout/Footer.tsx index c5a28861..8a071c6d 100644 --- a/src/components/features/Layout/Footer.tsx +++ b/src/components/features/Layout/Footer.tsx @@ -7,7 +7,7 @@ export const Footer = () => { return ( -

카카오톡 선물하기

+
); @@ -23,3 +23,7 @@ export const Wrapper = styled.footer` padding: 40px 16px 120px; } `; + +const FooterLogo = styled.img` + height: 20px; +`; From cd938a06b38d33a8c68a07ce8b00e3ad327eb4b5 Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 14:46:11 +0900 Subject: [PATCH 20/30] =?UTF-8?q?Feat:=20API=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EA=B0=80=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91=ED=95=A0=20UI=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 | 47 ++++++++++++------- .../Home/ThemeCategorySection/index.tsx | 9 ++-- .../Theme/ThemeGoodsSection/index.tsx | 29 +++++++++--- .../features/Theme/ThemeHeroSection/index.tsx | 9 ++-- 4 files changed, 59 insertions(+), 35 deletions(-) diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index cb5b18fa..d5ceeaa7 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -27,13 +27,9 @@ export const GoodsRankingSection = () => { `api/v1/ranking/products?targetType=${targetType}&rankType=${rankType}`, ) - // 의도적으로 지연 시간을 추가 - setTimeout(() => { - setRankingProducts(data.products) - setLoading(false) - console.log('[GoodsRankingSection] Fetch Goods Ranking Data Success: ', data.products) - }, 2000) - + setRankingProducts(data.products) + setLoading(false) + console.log('[GoodsRankingSection] Fetch Goods Ranking Data Success: ', data.products) } catch (error) { console.error('[GoodsRankingSection] Fetch Goods Ranking Data Fail: ', error) @@ -43,21 +39,23 @@ export const GoodsRankingSection = () => { fetchRankingProductData() }, [filterOption]); - if (loading) { - return ( - - - Loading... - - ) - } - return ( 실시간 급상승 선물랭킹 - + {loading ? ( + + + Loading... + + ) : rankingProducts.length === 0 ? ( + + No ranking products available + + ) : ( + + )} ); @@ -92,6 +90,7 @@ const LoadingWrapper = styled.div` justify-content: center; align-items: center; height: 500px; + width: 100%; `; const Spinner = styled.div` @@ -113,3 +112,17 @@ const LoadingText = styled.div` font-size: 1.2rem; color: #555; `; + +const NoDataWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 500px; + width: 100%; +`; + +const NoDataText = styled.div` + font-size: 1.5rem; + color: #999; + text-align: center; +`; diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index 678f34f6..0d1bfba9 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -29,12 +29,9 @@ export const ThemeCategorySection = () => { try { const data = await fetchData('api/v1/themes') - // 의도적으로 지연 시간을 추가 - setTimeout(() => { - setThemeFromAPI(data.themes) - setLoading(false) - console.log('[ThemeCategorySection] Fetch Theme Data Success: ', data.themes) - }, 2000) + setThemeFromAPI(data.themes) + setLoading(false) + console.log('[ThemeCategorySection] Fetch Theme Data Success: ', data.themes) } catch (error) { console.error('[ThemeCategorySection] Fetch Theme Data Fail: ', error) diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index 06f2e698..49ae2696 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -24,12 +24,9 @@ export const ThemeGoodsSection = ({ themeKey }: Props) => { const queryParams = `?maxItems=${MaxItems}` const data = await fetchData(`api/v1/themes/${themeKey}/products${queryParams}`) - // 의도적으로 지연 시간을 추가 - setTimeout(() => { - setCurrentGoods(data.products) - setLoading(false) - console.log('[ThemeGoodsSection] Fetch Theme Goods Data Success: ', data.products) - }, 2000) + setCurrentGoods(data.products) + setLoading(false) + console.log('[ThemeGoodsSection] Fetch Theme Goods Data Success: ', data.products) } catch (error) { console.error('[ThemeGoodsSection] Fetch Theme Goods Data Fail: ', error) @@ -47,6 +44,14 @@ export const ThemeGoodsSection = ({ themeKey }: Props) => { ) } + + if (currentGoods.length === 0) { + return ( + + No data available + + ) + } return ( @@ -109,3 +114,15 @@ const LoadingText = styled.div` font-size: 1.2rem; color: #555; `; + +const NoDataWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 500px; +`; + +const NoDataText = styled.div` + font-size: 1.5rem; + color: #999; +`; diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 2b43ac3e..164d6489 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -25,12 +25,9 @@ export const ThemeHeroSection = ({ themeKey }: Props) => { const data = await fetchData(`api/v1/themes`) const theme = getCurrentTheme(themeKey, data.themes) - // 의도적으로 지연 시간을 추가 - setTimeout(() => { - setCurrentTheme(theme) - setLoading(false) - console.log('[ThemeHeroSection] Fetch Theme Data Success: ', data.themes) - }, 2000) + setCurrentTheme(theme) + setLoading(false) + console.log('[ThemeHeroSection] Fetch Theme Data Success: ', data.themes) } catch (error) { console.error('[ThemeHeroSection] Fetch Theme Data Fail: ', error) From 9e5aa5fbda5a9c186f9ebf13bd8c7f95f84413ed Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 14:47:41 +0900 Subject: [PATCH 21/30] =?UTF-8?q?Docs:=20=EA=B0=81=20API=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B6=88=EB=9F=AC=EC=98=A8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EA=B0=80=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=EC=9D=91=ED=95=A0=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0ec8048..efea8977 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ - [x] 각 API에서 Loading 상태에 대한 UI 대응 -- [ ] 데이터가 없는 경우에 대한 UI 대응 +- [x] 데이터가 없는 경우에 대한 UI 대응 - [ ] HTTP Status에 따라 Error를 다르게 처리 From 0099caad747ad1716240887cc3e4ef264f17ca4b Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 15:08:28 +0900 Subject: [PATCH 22/30] =?UTF-8?q?Feat:=20lint=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EB=AA=85=EB=A0=B9=EC=96=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index f1f65cfa..d9c69975 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "start": "craco start", "build": "craco build", "test": "craco test", + "lint": "eslint --fix src/", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, From 91e8af77bda3e78bb53944e7803bc08538e33b68 Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 15:08:55 +0900 Subject: [PATCH 23/30] =?UTF-8?q?Feat:=20'@typescript-eslint/no-explicit-a?= =?UTF-8?q?ny'=20=EA=B7=9C=EC=B9=99=20=EB=B9=84=20=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index bebdb7ce..1d18e2b5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,6 +33,7 @@ module.exports = { 'react/react-in-jsx-scope': 'off', 'simple-import-sort/imports': 'error', 'simple-import-sort/exports': 'error', + "@typescript-eslint/no-explicit-any": "off", '@typescript-eslint/consistent-type-imports': 'warn', '@typescript-eslint/no-unused-vars': [ 'error', From c71e0e5d8e3ce0ee0bd8e51e121301a8a2464e76 Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 15:09:42 +0900 Subject: [PATCH 24/30] =?UTF-8?q?Feat:=20HTTP=20status=20=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20Error=20=EB=A5=BC=20=EB=8B=A4=EB=A5=B4?= =?UTF-8?q?=EA=B2=8C=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=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 | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index d5ceeaa7..3aa28815 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -1,4 +1,6 @@ import styled from '@emotion/styled'; +import type { AxiosError } from 'axios'; +import axios from 'axios'; // AxiosError를 import import { useEffect, useState } from 'react'; import fetchData from '@/api'; @@ -17,6 +19,7 @@ export const GoodsRankingSection = () => { const [rankingProducts, setRankingProducts] = useState([]) const [loading, setLoading] = useState(true) + const [fetchError, setFetchError] = useState(null); // filterOption 에 변화가 생길 때 마다 실행 useEffect(() => { @@ -29,11 +32,43 @@ export const GoodsRankingSection = () => { setRankingProducts(data.products) setLoading(false) + setFetchError(null); console.log('[GoodsRankingSection] Fetch Goods Ranking Data Success: ', data.products) } - catch (error) { + catch (error: any) { console.error('[GoodsRankingSection] Fetch Goods Ranking Data Fail: ', error) setLoading(false) + + if (axios.isAxiosError(error)) { + // AxiosError에서 response 속성을 통해 HTTP 상태 코드를 확인할 수 있음 + const axiosError = error as AxiosError; + if (axiosError.response) { + const status = axiosError.response.status; + switch (status) { + case 400: + setFetchError('Bad Request: The server could not understand the request.'); + break; + case 404: + setFetchError('Not Found: The requested resource could not be found.'); + break; + case 500: + setFetchError('Internal Server Error: Something went wrong on the server.'); + break; + default: + setFetchError(`Unexpected Error: ${status}`); + break; + } + } else if (axiosError.request) { + // 요청이 만들어졌지만 응답을 받지 못한 경우 + setFetchError('No response from the server. Please try again later.'); + } else { + // 다른 종류의 에러 + setFetchError(`Error: ${error.message}`); + } + } else { + // AxiosError가 아닌 다른 종류의 에러 + setFetchError(`Error: ${error.message}`); + } } } fetchRankingProductData() @@ -49,6 +84,10 @@ export const GoodsRankingSection = () => { Loading... + ) : fetchError ? ( + + {fetchError} + ) : rankingProducts.length === 0 ? ( No ranking products available @@ -126,3 +165,17 @@ const NoDataText = styled.div` color: #999; text-align: center; `; + +const ErrorWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 500px; + width: 100%; +`; + +const ErrorText = styled.div` + font-size: 1.5rem; + color: #e74c3c; + text-align: center; +`; From 4dae04c47cb62a0a390dae20b368da1d5f2c1f92 Mon Sep 17 00:00:00 2001 From: loveydev Date: Thu, 11 Jul 2024 15:10:22 +0900 Subject: [PATCH 25/30] =?UTF-8?q?Docs:=20HTTP=20status=20=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20Error=20=EC=B2=98=EB=A6=AC=20=EB=B0=A9?= =?UTF-8?q?=EB=B2=95=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index efea8977..b936ae01 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - [x] 데이터가 없는 경우에 대한 UI 대응 -- [ ] HTTP Status에 따라 Error를 다르게 처리 +- [x] HTTP Status에 따라 Error를 다르게 처리
From dfa623287507ed479f73a07145ec8ddcfda76f36 Mon Sep 17 00:00:00 2001 From: loveydev Date: Fri, 12 Jul 2024 16:36:03 +0900 Subject: [PATCH 26/30] =?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=EA=B5=AC=ED=98=84=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20react-query=20=EB=B0=8F=20react-intersection-observ?= =?UTF-8?q?er=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 138 +++++++++++++++++++++++++++++++++++++++------- package.json | 2 + 2 files changed, 120 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd598900..424bb09e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "axios": "^1.7.2", "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": { @@ -12677,8 +12679,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", @@ -12738,7 +12739,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" } @@ -12881,6 +12881,22 @@ "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==", + "license": "MIT", + "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", @@ -13609,8 +13625,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", @@ -15544,8 +15559,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", @@ -18722,8 +18736,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", @@ -19748,7 +19761,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" @@ -19757,8 +19769,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", @@ -23858,6 +23869,12 @@ "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==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -24634,6 +24651,16 @@ "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==", + "license": "MIT", + "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", @@ -24736,6 +24763,12 @@ "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==", + "license": "MIT" + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -24976,6 +25009,15 @@ "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==", + "license": "ISC", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -25520,6 +25562,12 @@ "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==", + "license": "MIT" + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -25557,7 +25605,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" } @@ -25841,7 +25888,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" } @@ -28271,11 +28317,52 @@ "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==", + "license": "MIT", + "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", "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==", + "license": "MIT", + "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", @@ -28946,6 +29033,12 @@ "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==", + "license": "MIT" + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -29140,7 +29233,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" }, @@ -29155,7 +29247,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" @@ -29165,7 +29256,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", @@ -29185,7 +29275,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" }, @@ -31847,6 +31936,16 @@ "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==", + "license": "Apache-2.0", + "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", @@ -33318,8 +33417,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 d9c69975..9f3ba557 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "axios": "^1.7.2", "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": { From 30b5ddeb8fd6a003ef4a1fce783dbb3189ff5a1b Mon Sep 17 00:00:00 2001 From: loveydev Date: Fri, 12 Jul 2024 20:59:47 +0900 Subject: [PATCH 27/30] =?UTF-8?q?Feat:=20=EC=8A=A4=ED=81=AC=EB=A1=A4?= =?UTF-8?q?=EC=9D=84=20=EB=82=B4=EB=A6=AC=EB=A9=B4=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=A1=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=ED=95=98=EC=97=AC=20=EB=B3=B4=EC=97=AC=EC=A3=BC?= =?UTF-8?q?=EA=B2=8C=20=ED=95=98=EB=8A=94=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 2 + package-lock.json | 27 ++++++ package.json | 1 + src/App.tsx | 12 ++- .../Theme/ThemeGoodsSection/index.tsx | 92 +++++++++++-------- src/constants/index.ts | 1 + src/types/index.ts | 1 + src/types/mock.ts | 2 + 8 files changed, 98 insertions(+), 40 deletions(-) create mode 100644 src/constants/index.ts diff --git a/.eslintrc.js b/.eslintrc.js index 1d18e2b5..5cd67bf5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,10 +31,12 @@ module.exports = { ], rules: { 'react/react-in-jsx-scope': 'off', + 'react-hooks/exhaustive-deps': 'off', 'simple-import-sort/imports': 'error', 'simple-import-sort/exports': 'error', "@typescript-eslint/no-explicit-any": "off", '@typescript-eslint/consistent-type-imports': 'warn', + '@typescript-eslint/no-shadow': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { diff --git a/package-lock.json b/package-lock.json index 424bb09e..8572be17 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", @@ -9884,6 +9885,32 @@ "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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 9f3ba557..202eb083 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,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", diff --git a/src/App.tsx b/src/App.tsx index 26d8766c..e16a1545 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,17 @@ +import { QueryClient, QueryClientProvider } from 'react-query'; + import { AuthProvider } from './provider/Auth'; import { Routes } from './routes'; +const queryClient = new QueryClient(); + const App = () => { return ( - - - + + + + + ); }; diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index 49ae2696..b8579d48 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,58 +1,72 @@ import styled from '@emotion/styled' -import { useEffect, useState } from 'react' +import axios from 'axios'; +import { useEffect } from 'react' +import { useInView } from 'react-intersection-observer' +import { useInfiniteQuery } from 'react-query'; -import fetchData from '@/api' import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default' import { Container } from '@/components/common/layouts/Container' import { Grid } from '@/components/common/layouts/Grid' +import { BASE_URL } from '@/constants'; import { breakpoints } from '@/styles/variants' import type { GoodsData } from '@/types' -type Props = { +type FetchProps = { + pageParam?: number themeKey: string }; -export const ThemeGoodsSection = ({ themeKey }: Props) => { - const [currentGoods, setCurrentGoods] = useState([]) - const [loading, setLoading] = useState(true) +const fetchGoodsList = async ({ pageParam, themeKey }: FetchProps) => { + const maxResults = 20 + const params: { maxResults: number; pageToken?: number } = { maxResults } + if (pageParam) { + params.pageToken = pageParam + } + const response = await axios.get(`${BASE_URL}api/v1/themes/${themeKey}/products`, { params }) + return response.data +} + +export const ThemeGoodsSection = ({ themeKey }: { themeKey: string }) => { + const { ref, inView } = useInView() + + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteQuery( + ['goodsList', themeKey], + ({ pageParam = 0 }) => + fetchGoodsList({ pageParam: pageParam === 0 ? undefined : pageParam, themeKey }), + { + getNextPageParam: (lastPage, pages) => { + if (lastPage.products.length < 20) { + return undefined; + } + return pages.length; + }, + }, + ); - // themeKey 가 변할 때마다 실행 useEffect(() => { - const fetchThemeData = async () => { - try { - const MaxItems = 20 - const queryParams = `?maxItems=${MaxItems}` - const data = await fetchData(`api/v1/themes/${themeKey}/products${queryParams}`) - - setCurrentGoods(data.products) - setLoading(false) - console.log('[ThemeGoodsSection] Fetch Theme Goods Data Success: ', data.products) - } - catch (error) { - console.error('[ThemeGoodsSection] Fetch Theme Goods Data Fail: ', error) - setLoading(false) - } + if (inView && hasNextPage) { + fetchNextPage(); } - fetchThemeData() - }, [themeKey]) + }, [inView, hasNextPage, fetchNextPage]); - if (loading) { + if (isLoading && !isFetchingNextPage) { return ( Loading... ) - } - if (currentGoods.length === 0) { + } + if (!data || data.pages[0].products.length === 0) { return ( No data available ) } - + return ( @@ -63,16 +77,19 @@ export const ThemeGoodsSection = ({ themeKey }: Props) => { }} gap={16} > - {currentGoods.map((goods) => ( - - ))} + {data?.pages.map((page) => + page.products.map((goods: GoodsData) => ( + + )), + )} +
{isFetchingNextPage ? '상품 추가로 불러오는 중...' : ''}
); @@ -109,6 +126,7 @@ const Spinner = styled.div` } `; + const LoadingText = styled.div` margin-top: 10px; font-size: 1.2rem; @@ -125,4 +143,4 @@ const NoDataWrapper = styled.div` const NoDataText = styled.div` font-size: 1.5rem; color: #999; -`; +`; \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 00000000..3cd27c8e --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1 @@ +export const BASE_URL = 'https://react-gift-mock-api-hyunaeri.vercel.app/' \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 9d76b97b..ce86d11d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,6 +3,7 @@ export type ThemeData = { key: string; label: string; title: string; + imageURL: string; description?: string; backgroundColor: string; }; diff --git a/src/types/mock.ts b/src/types/mock.ts index cdd90cf7..f2434977 100644 --- a/src/types/mock.ts +++ b/src/types/mock.ts @@ -5,6 +5,8 @@ export const ThemeMockData: ThemeData = { key: 'life_small_gift', label: '가벼운 선물', title: '예산은 가볍게, 감동은 무겁게❤️', + imageURL: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292120240213_MPJIS.png', description: '당신의 센스를 뽐내줄 부담 없는 선물', backgroundColor: '#4b4d50', }; From 5e97a6adc0c78a5ad33f9f8bcb6f06fc9e25ce38 Mon Sep 17 00:00:00 2001 From: loveydev Date: Fri, 12 Jul 2024 21:14:07 +0900 Subject: [PATCH 28/30] =?UTF-8?q?Feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=20=EB=8F=84?= =?UTF-8?q?=EC=A4=91=20=EC=98=A4=EB=A5=98=EA=B0=80=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=96=88=EC=9D=84=20=EB=95=8C=20UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/ThemeCategorySection/index.tsx | 96 ++++++++++++------- .../Theme/ThemeGoodsSection/index.tsx | 23 ++++- 2 files changed, 81 insertions(+), 38 deletions(-) diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index 0d1bfba9..f1a2ae0d 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -1,55 +1,63 @@ -import styled from '@emotion/styled' -import { useEffect, useState } from 'react' -import { Link } from 'react-router-dom' +import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; -import fetchData 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 fetchData 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 { ThemeCategoryItem } from './ThemeCategoryItem' +import { ThemeCategoryItem } from './ThemeCategoryItem'; interface Theme { - id: number - key: string - label: string - imageURL: string - title: string - description?: string + id: number; + key: string; + label: string; + imageURL: string; + title: string; + description?: string; } export const ThemeCategorySection = () => { - const [themeFromAPI, setThemeFromAPI] = useState([]) - const [loading, setLoading] = useState(true) + const [themeFromAPI, setThemeFromAPI] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); // 최초 렌더링 시 한 번만 실행 useEffect(() => { const fetchThemeData = async () => { try { - const data = await fetchData('api/v1/themes') - - setThemeFromAPI(data.themes) - setLoading(false) - console.log('[ThemeCategorySection] Fetch Theme Data Success: ', data.themes) - } - catch (error) { - console.error('[ThemeCategorySection] Fetch Theme Data Fail: ', error) - setLoading(false) + const data = await fetchData('api/v1/themes'); + setThemeFromAPI(data.themes); + setLoading(false); + console.log('[ThemeCategorySection] Fetch Theme Data Success: ', data.themes); + } catch (error) { + console.error('[ThemeCategorySection] Fetch Theme Data Fail: ', error); + setError('Failed to fetch themes. Please try again later.'); + setLoading(false); } - } - fetchThemeData() - }, []) - + }; + fetchThemeData(); + }, []); + if (loading) { return ( Loading... - ) + ); } - + + if (error) { + return ( + + {error} + + ); + } + return ( @@ -61,9 +69,9 @@ export const ThemeCategorySection = () => { > {themeFromAPI.map((theme) => ( // 각 Theme Detail Page로 이동할 링크 - + - + ))} @@ -87,6 +95,13 @@ const LoadingWrapper = styled.div` height: 500px; `; +const ErrorWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 500px; +`; + const Spinner = styled.div` width: 40px; height: 40px; @@ -96,8 +111,12 @@ const Spinner = styled.div` animation: spin 1s linear infinite; @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } `; @@ -106,3 +125,8 @@ const LoadingText = styled.div` font-size: 1.2rem; color: #555; `; + +const ErrorText = styled.div` + font-size: 1.5rem; + color: #ff6347; +`; diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index b8579d48..aad3592c 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -29,7 +29,7 @@ const fetchGoodsList = async ({ pageParam, themeKey }: FetchProps) => { export const ThemeGoodsSection = ({ themeKey }: { themeKey: string }) => { const { ref, inView } = useInView() - const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + const { data, isError, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery( ['goodsList', themeKey], ({ pageParam = 0 }) => @@ -50,6 +50,14 @@ export const ThemeGoodsSection = ({ themeKey }: { themeKey: string }) => { } }, [inView, hasNextPage, fetchNextPage]); + if (isError) { + return ( + + 데이터를 불러오는 중 오류가 발생하였습니다. + + ); + } + if (isLoading && !isFetchingNextPage) { return ( @@ -126,7 +134,6 @@ const Spinner = styled.div` } `; - const LoadingText = styled.div` margin-top: 10px; font-size: 1.2rem; @@ -143,4 +150,16 @@ const NoDataWrapper = styled.div` const NoDataText = styled.div` font-size: 1.5rem; color: #999; +`; + +const ErrorWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 500px; +`; + +const ErrorText = styled.div` + font-size: 1.5rem; + color: #ff6347; `; \ No newline at end of file From 90a89333429e7b60c72c8ed4ac9229df881824b4 Mon Sep 17 00:00:00 2001 From: loveydev Date: Fri, 12 Jul 2024 22:30:45 +0900 Subject: [PATCH 29/30] =?UTF-8?q?Feat:=20Step1=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=9C=20API=20=EB=93=A4=EC=9D=84=20react-?= =?UTF-8?q?query=20=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EC=9E=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.tsx | 16 --- .../Home/GoodsRankingSection/List.tsx | 6 +- .../Home/GoodsRankingSection/index.tsx | 119 +++++++----------- .../Home/ThemeCategorySection/index.tsx | 82 ++++++------ .../features/Theme/ThemeHeroSection/index.tsx | 69 ++++++---- 5 files changed, 129 insertions(+), 163 deletions(-) delete mode 100644 src/api/index.tsx diff --git a/src/api/index.tsx b/src/api/index.tsx deleted file mode 100644 index 17f2fc60..00000000 --- a/src/api/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import axios from "axios" - -const BASE_URL = 'https://react-gift-mock-api-hyunaeri.vercel.app/' - -const fetchData = async (target: string) => { - try { - const response = await axios.get(`${BASE_URL}${target}`) - return response.data - } - catch (error) { - console.error(`Error fetching data from ${target}`, error) - throw error - } -} - -export default fetchData \ No newline at end of file diff --git a/src/components/features/Home/GoodsRankingSection/List.tsx b/src/components/features/Home/GoodsRankingSection/List.tsx index 27bd332c..6c08b505 100644 --- a/src/components/features/Home/GoodsRankingSection/List.tsx +++ b/src/components/features/Home/GoodsRankingSection/List.tsx @@ -8,13 +8,13 @@ import { breakpoints } from '@/styles/variants'; import type { GoodsData } from '@/types'; type Props = { - goodsList: GoodsData[]; + goodsList: GoodsData[] | undefined; }; export const GoodsRankingList = ({ goodsList }: Props) => { const [hasMore, setHasMore] = useState(false); - const currentGoodsList = hasMore ? goodsList : goodsList.slice(0, 6); + const currentGoodsList = hasMore ? goodsList : goodsList?.slice(0, 6); return ( @@ -26,7 +26,7 @@ export const GoodsRankingList = ({ goodsList }: Props) => { }} gap={16} > - {currentGoodsList.map(({ id, imageURL, name, price, brandInfo }, index) => ( + {currentGoodsList?.map(({ id, imageURL, name, price, brandInfo }, index) => ( { + const params = { + targetType: filterOption.targetType, + rankType: filterOption.rankType + } + const response = await axios.get(`${BASE_URL}api/v1/ranking/products`, { params }) + return response.data.products +} + export const GoodsRankingSection = () => { const [filterOption, setFilterOption] = useState({ targetType: 'ALL', rankType: 'MANY_WISH', }); - const [rankingProducts, setRankingProducts] = useState([]) - const [loading, setLoading] = useState(true) - const [fetchError, setFetchError] = useState(null); - - // filterOption 에 변화가 생길 때 마다 실행 - useEffect(() => { - const fetchRankingProductData = async () => { - try { - const { targetType, rankType } = filterOption - const data = await fetchData( - `api/v1/ranking/products?targetType=${targetType}&rankType=${rankType}`, - ) - - setRankingProducts(data.products) - setLoading(false) - setFetchError(null); - console.log('[GoodsRankingSection] Fetch Goods Ranking Data Success: ', data.products) - } - catch (error: any) { - console.error('[GoodsRankingSection] Fetch Goods Ranking Data Fail: ', error) - setLoading(false) - - if (axios.isAxiosError(error)) { - // AxiosError에서 response 속성을 통해 HTTP 상태 코드를 확인할 수 있음 - const axiosError = error as AxiosError; - if (axiosError.response) { - const status = axiosError.response.status; - switch (status) { - case 400: - setFetchError('Bad Request: The server could not understand the request.'); - break; - case 404: - setFetchError('Not Found: The requested resource could not be found.'); - break; - case 500: - setFetchError('Internal Server Error: Something went wrong on the server.'); - break; - default: - setFetchError(`Unexpected Error: ${status}`); - break; - } - } else if (axiosError.request) { - // 요청이 만들어졌지만 응답을 받지 못한 경우 - setFetchError('No response from the server. Please try again later.'); - } else { - // 다른 종류의 에러 - setFetchError(`Error: ${error.message}`); - } - } else { - // AxiosError가 아닌 다른 종류의 에러 - setFetchError(`Error: ${error.message}`); - } - } + const { data, isLoading, isError } = useQuery(['rankingList', filterOption], () => + fetchRankingList(filterOption) + ) + + const renderingFunc = () => { + if (isError) { + return ( + + 데이터를 불러오는 중 오류가 발생하였습니다. + + ); + } + + if (isLoading) { + return ( + + + Loading... + + ); } - fetchRankingProductData() - }, [filterOption]); + + if (data?.length === 0) { + return ( + + No data available + + ) + } + return + } return ( 실시간 급상승 선물랭킹 - {loading ? ( - - - Loading... - - ) : fetchError ? ( - - {fetchError} - - ) : rankingProducts.length === 0 ? ( - - No ranking products available - - ) : ( - - )} + {renderingFunc()} ); diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index f1a2ae0d..ae72fe2e 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -1,60 +1,52 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import axios from 'axios'; +import { useQuery } from 'react-query'; import { Link } from 'react-router-dom'; -import fetchData from '@/api'; import { Container } from '@/components/common/layouts/Container'; import { Grid } from '@/components/common/layouts/Grid'; +import { BASE_URL } from '@/constants'; import { getDynamicPath } from '@/routes/path'; import { breakpoints } from '@/styles/variants'; +import type { ThemeData } from '@/types'; import { ThemeCategoryItem } from './ThemeCategoryItem'; -interface Theme { - id: number; - key: string; - label: string; - imageURL: string; - title: string; - description?: string; +export const fetchThemeCategory = async () => { + const response = await axios.get(`${BASE_URL}api/v1/themes`) + return response.data.themes } +/* + useQuery hooks 는 다음과 같은 객체 반환 + { + data?: TData, // 쿼리 데이터 + error?: Error, // 에러 객체 (에러가 없으면 undefined) + isError: boolean, // 에러 여부를 나타내는 불리언 값 + isLoading: boolean, // 데이터 로딩 중 여부를 나타내는 불리언 값 + isSuccess: boolean, // 데이터 요청 성공 여부를 나타내는 불리언 값 + refetch: (options?) => void, // 쿼리 재요청 함수 + remove: () => void, // 쿼리 제거 함수 + } +*/ + export const ThemeCategorySection = () => { - const [themeFromAPI, setThemeFromAPI] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // 최초 렌더링 시 한 번만 실행 - useEffect(() => { - const fetchThemeData = async () => { - try { - const data = await fetchData('api/v1/themes'); - setThemeFromAPI(data.themes); - setLoading(false); - console.log('[ThemeCategorySection] Fetch Theme Data Success: ', data.themes); - } catch (error) { - console.error('[ThemeCategorySection] Fetch Theme Data Fail: ', error); - setError('Failed to fetch themes. Please try again later.'); - setLoading(false); - } - }; - fetchThemeData(); - }, []); - - if (loading) { + const { data, isError, isLoading } = useQuery(['ThemeData'], fetchThemeCategory) + + if (isError) { return ( - - - Loading... - + + 데이터를 불러오는 중 오류가 발생하였습니다. + ); } - if (error) { + if (isLoading) { return ( - - {error} - + + + Loading... + ); } @@ -67,7 +59,7 @@ export const ThemeCategorySection = () => { md: 6, }} > - {themeFromAPI.map((theme) => ( + {data?.map((theme) => ( // 각 Theme Detail Page로 이동할 링크 @@ -102,6 +94,11 @@ const ErrorWrapper = styled.div` height: 500px; `; +const ErrorText = styled.div` + font-size: 1.5rem; + color: #ff6347; +`; + const Spinner = styled.div` width: 40px; height: 40px; @@ -126,7 +123,4 @@ const LoadingText = styled.div` color: #555; `; -const ErrorText = styled.div` - font-size: 1.5rem; - color: #ff6347; -`; + diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 164d6489..6266d63d 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,8 +1,10 @@ import styled from '@emotion/styled' +import axios from 'axios' import { useEffect, useState } from 'react' +import { useQuery } from 'react-query' -import fetchData from '@/api' import { Container } from '@/components/common/layouts/Container' +import { BASE_URL } from '@/constants' import { breakpoints } from '@/styles/variants' import type { ThemeData } from '@/types' @@ -10,40 +12,41 @@ type Props = { themeKey: string } -export const getCurrentTheme = (themeKey: string, themeList: ThemeData[]) => { +const fetchThemeHero = async (): Promise => { + const response = await axios.get(`${BASE_URL}api/v1/themes`) + return response.data.themes +} + +const getCurrentTheme = (themeKey: string, themeList: ThemeData[]) => { return themeList.find((theme) => theme.key === themeKey); }; export const ThemeHeroSection = ({ themeKey }: Props) => { - const [currentTheme, setCurrentTheme] = useState() - const [loading, setLoading] = useState(true) + const [currentTheme, setCurrentTheme] = useState() + const { data, isLoading, isError } = useQuery(['ThemeData', themeKey], fetchThemeHero) - // themeKey 가 변할 때 마다 실행 useEffect(() => { - const fetchThemeData = async () => { - try { - const data = await fetchData(`api/v1/themes`) - const theme = getCurrentTheme(themeKey, data.themes) - - setCurrentTheme(theme) - setLoading(false) - console.log('[ThemeHeroSection] Fetch Theme Data Success: ', data.themes) - } - catch (error) { - console.error('[ThemeHeroSection] Fetch Theme Data Fail: ', error) - setLoading(false) - } + if (data) { + const theme = getCurrentTheme(themeKey, data) + setCurrentTheme(theme) } - fetchThemeData() - }, [themeKey]) + }, [data, themeKey]) - if (loading) { + if (isError) { + return ( + + 데이터를 불러오는 중 오류가 발생하였습니다. + + ); + } + + if (isLoading) { return ( Loading... - ) + ); } if (!currentTheme) { @@ -120,7 +123,19 @@ const LoadingWrapper = styled.div` flex-direction: column; justify-content: center; align-items: center; - height: 100px; + height: 500px; +`; + +const ErrorWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 500px; +`; + +const ErrorText = styled.div` + font-size: 1.5rem; + color: #ff6347; `; const Spinner = styled.div` @@ -132,8 +147,12 @@ const Spinner = styled.div` animation: spin 1s linear infinite; @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } `; From 71ca24de7d1b35251834b00071622e1b4114a045 Mon Sep 17 00:00:00 2001 From: loveydev Date: Fri, 12 Jul 2024 22:31:34 +0900 Subject: [PATCH 30/30] =?UTF-8?q?Docs:=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4,=20Step1=20=EC=97=90=EC=84=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=95=9C=20API=20=EB=93=A4=20react-query=20=EB=A1=9C?= =?UTF-8?q?=20=EC=9E=AC=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b936ae01..0ee0447e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,6 @@

3️⃣ Step 3 체크리스트

-- [ ] 스크롤을 내리면 추가로 데이터를 요청하여 보여지게 함 +- [x] 스크롤을 내리면 추가로 데이터를 요청하여 보여지게 함 -- [ ] 1단계에서 구현한 API를 `react-query` 를 사용해서 구현 \ No newline at end of file +- [x] 1단계에서 구현한 API를 `react-query` 를 사용해서 구현 \ No newline at end of file