From e13f76c35920a53a0f056f957048b4c88f5b0a4a Mon Sep 17 00:00:00 2001 From: leedohyun Date: Tue, 9 Jul 2024 14:51:21 +0900 Subject: [PATCH 01/15] =?UTF-8?q?docs:=20README=20=ED=8C=8C=EC=9D=BC=20ste?= =?UTF-8?q?p1=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 3 ++- README.md | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index bebdb7ce..b23679f0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,7 +13,8 @@ module.exports = { ], parser: '@typescript-eslint/parser', parserOptions: { - project: './tsconfig.json', + project: path.resolve(__dirname, './tsconfig.json'), + tsconfigRootDir: path.resolve(__dirname), ecmaFeatures: { jsx: true, }, diff --git a/README.md b/README.md index e69de29b..2c1a237e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,22 @@ +## step1 요구사항 + +1. API 타입 정의 + +- [] OAS 파일을 참조하여 Request 및 Response 타입 정의 + +2. Axios API 구현 + +- [] 각 API 경로별로 함수 구현 + +3. Main Page + +- [] 테마 카테고리 섹션 : /api/v1/themes API 활용 +- [] 실시간 급상승 선물랭킹 섹션 : /api/v1/ranking/products 활용 +- [] 필터 조건을 선택하면 해당 조건에 맞게 api 보임 + +4. Theme Page + +- [] Header : url의 themekey 사용 +- [] url의 pathParams와 /api/vi/themes API를 사용 +- [] themeKey가 잘못된 경우 메인 페이지로 +- [] 요청시 한번에 20개의 상품 목록이 내려오도록 구현 From 9cc14117558db6261121ae1062feaf831239308d Mon Sep 17 00:00:00 2001 From: leedohyun Date: Tue, 9 Jul 2024 16:59:32 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat=20:=20main=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=ED=85=8C=EB=A7=88=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- package-lock.json | 40 ++++++--- package.json | 2 + src/api/api.tsx | 40 +++++++++ .../Home/ThemeCategorySection/index.tsx | 87 ++++--------------- src/types/index.ts | 1 + src/types/mock.ts | 2 + 7 files changed, 92 insertions(+), 84 deletions(-) create mode 100644 src/api/api.tsx diff --git a/README.md b/README.md index 2c1a237e..65ffc1c7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 1. API 타입 정의 -- [] OAS 파일을 참조하여 Request 및 Response 타입 정의 +- [o] OAS 파일을 참조하여 Request 및 Response 타입 정의 2. Axios API 구현 @@ -10,7 +10,7 @@ 3. Main Page -- [] 테마 카테고리 섹션 : /api/v1/themes API 활용 +- [o] 테마 카테고리 섹션 : /api/v1/themes API 활용 - [] 실시간 급상승 선물랭킹 섹션 : /api/v1/ranking/products 활용 - [] 필터 조건을 선택하면 해당 조건에 맞게 api 보임 diff --git a/package-lock.json b/package-lock.json index 89581c64..d0ee9525 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" @@ -29,6 +30,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/axios": "^0.14.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.82", "@types/react": "^18.2.57", @@ -10176,6 +10178,16 @@ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dev": true, + "dependencies": { + "axios": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -11988,8 +12000,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 +12072,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 +13519,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 +15501,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 +18424,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 +18632,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 +24761,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 +24769,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 +27591,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..296e0a25 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" @@ -43,6 +44,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@types/axios": "^0.14.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.82", "@types/react": "^18.2.57", diff --git a/src/api/api.tsx b/src/api/api.tsx new file mode 100644 index 00000000..5755d380 --- /dev/null +++ b/src/api/api.tsx @@ -0,0 +1,40 @@ +import axios from 'axios'; + +import {GoodsData,ThemeData } from '@/types'; + +const API_URL = axios.create({ + baseURL: 'https://react-gift-mock-api-leedyun.vercel.app/', + headers: { + 'Content-Type': 'application/json', + }, + }); + +export const fetchThemes = async (): Promise => { + try { + const response = await API_URL.get<{themes : ThemeData[]}>('/api/v1/themes'); + if (response.status === 200 && response.data && response.data.themes) { + return response.data.themes; + } else { + console.error("No themes data received", response); + return []; + } + } catch (error) { + console.error("Failed to fetch themes", error); + return []; + } + } + + + export const fetchRankingProducts = async(targetType:string, rankType:string) : Promise => { + const response = await axios.get(`${API_URL}/ranking/products`, { + params : {targetType, rankType} + }); + return response.data.products; + } + + export const fetchThemeProducts = async (themeKey: string): Promise => { + const response = await axios.get(`${API_URL}/themes/${themeKey}/products`, { + params: { maxResults: 20 } + }); + return response.data.products; + }; \ No newline at end of file diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index d82e3afe..a3c9c04a 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -1,14 +1,27 @@ import styled from '@emotion/styled'; +import { useEffect,useState } from 'react'; import { Link } from 'react-router-dom'; +import { fetchThemes } from '@/api/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 { ThemeData } from '@/types'; import { ThemeCategoryItem } from './ThemeCategoryItem'; export const ThemeCategorySection = () => { + const [themes, setThemes] = useState([]); + + useEffect(() => { + const loadThemes = async () => { + const themesData = await fetchThemes(); + setThemes(themesData); + console.log(themesData); + }; + loadThemes(); + }, []); return ( @@ -18,78 +31,14 @@ export const ThemeCategorySection = () => { md: 6, }} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {themes.map((theme, index) => ( + + ))} diff --git a/src/types/index.ts b/src/types/index.ts index 9d76b97b..d931c966 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..92ab2bd9 100644 --- a/src/types/mock.ts +++ b/src/types/mock.ts @@ -7,6 +7,8 @@ export const ThemeMockData: ThemeData = { title: '예산은 가볍게, 감동은 무겁게❤️', description: '당신의 센스를 뽐내줄 부담 없는 선물', backgroundColor: '#4b4d50', + imageURL: + 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', }; export const ThemeMockList = [ThemeMockData]; From bae7a50f1623c92eba9d121141479881e4ef04e0 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Tue, 9 Jul 2024 19:23:36 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat=20:=20Theme=20page=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 --- README.md | 8 ++-- src/App.tsx | 2 +- src/api/api.tsx | 28 ++++++++++--- .../Theme/ThemeGoodsSection/index.tsx | 20 +++++++--- .../features/Theme/ThemeHeroSection/index.tsx | 40 ++++++++++++++----- src/pages/Theme/index.tsx | 34 ++++++++++++++-- 6 files changed, 103 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 65ffc1c7..6d23d301 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ 4. Theme Page -- [] Header : url의 themekey 사용 -- [] url의 pathParams와 /api/vi/themes API를 사용 -- [] themeKey가 잘못된 경우 메인 페이지로 -- [] 요청시 한번에 20개의 상품 목록이 내려오도록 구현 +- [o] Header : url의 themekey 사용 +- [o] url의 pathParams와 /api/vi/themes API를 사용 +- [o] themeKey가 잘못된 경우 메인 페이지로 +- [o] 요청시 한번에 20개의 상품 목록이 내려오도록 구현 \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 26d8766c..f8602e3f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,4 +9,4 @@ const App = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/src/api/api.tsx b/src/api/api.tsx index 5755d380..1ada24f2 100644 --- a/src/api/api.tsx +++ b/src/api/api.tsx @@ -11,7 +11,7 @@ const API_URL = axios.create({ export const fetchThemes = async (): Promise => { try { - const response = await API_URL.get<{themes : ThemeData[]}>('/api/v1/themes'); + const response = await API_URL.get<{themes : ThemeData[]}>('api/v1/themes'); if (response.status === 200 && response.data && response.data.themes) { return response.data.themes; } else { @@ -26,15 +26,31 @@ export const fetchThemes = async (): Promise => { export const fetchRankingProducts = async(targetType:string, rankType:string) : Promise => { - const response = await axios.get(`${API_URL}/ranking/products`, { + const response = await API_URL.get('api/v1/ranking/products', { params : {targetType, rankType} }); return response.data.products; } - export const fetchThemeProducts = async (themeKey: string): Promise => { - const response = await axios.get(`${API_URL}/themes/${themeKey}/products`, { - params: { maxResults: 20 } + export const fetchThemeProducts = async (themeKey: string, maxResults: number = 20): Promise => { + const response = await API_URL.get(`api/v1/themes/${themeKey}/products`, { + params: { maxResults } }); return response.data.products; - }; \ No newline at end of file + }; + + export const fetchTheme = async (themeKey: string): Promise => { + try { + const response = await API_URL.get(`/api/v1/themes/${themeKey}`, { + }); + if (response.status === 200 && response.data) { + return response.data; + } else { + throw new Error("No theme data received"); + } + } catch (error) { + console.error("Failed to fetch theme", error); + throw error; + } + }; + \ No newline at end of file diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index 8edbf70e..ba8f768a 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,16 +1,26 @@ import styled from '@emotion/styled'; +import { useEffect,useState } from 'react'; +import { fetchThemeProducts } from '@/api/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 { GoodsData } from '@/types'; -type Props = { + +interface ThemeGoodsSectionProps { themeKey: string; -}; +} + +export const ThemeGoodsSection = ({ themeKey }: ThemeGoodsSectionProps) => { + const [products, setProducts] = useState([]); -export const ThemeGoodsSection = ({}: Props) => { + useEffect(() => { + fetchThemeProducts(themeKey).then(fetchedProducts => { + setProducts(fetchedProducts); + }).catch(console.error); + }, [themeKey]); return ( @@ -21,7 +31,7 @@ export const ThemeGoodsSection = ({}: Props) => { }} gap={16} > - {GoodsMockList.map(({ id, imageURL, name, price, brandInfo }) => ( + {products.map(({ id, imageURL, name, price, brandInfo }) => ( { - const currentTheme = getCurrentTheme(themeKey, ThemeMockList); +export const ThemeHeroSection = ({ themeKey }: ThemeHeroSectionProps) => { + const [theme, setTheme] = useState(null); + const [loading, setLoading] = useState(true); + const [hasError, setHasError] = useState(false); - if (!currentTheme) { - return null; - } + useEffect(() => { + const fetchThemeDetails = async () => { + try { + const themes = await fetchThemes(); + const foundTheme = themes.find(t => t.key === themeKey); + if (!foundTheme) throw new Error('Theme not found'); + setTheme(foundTheme); + } catch (fetchError) { + console.error('Failed to fetch theme details:', fetchError); + setHasError(true); + } finally { + setLoading(false); + } + }; + + fetchThemeDetails(); + }, [themeKey]); + + if (loading) return
Loading...
; + if (hasError) return ; + if (!theme) return
Theme not found
; - const { backgroundColor, label, title, description } = currentTheme; + const { backgroundColor, label, title, description } = theme; return ( diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx index 4d02e6c1..cebd4583 100644 --- a/src/pages/Theme/index.tsx +++ b/src/pages/Theme/index.tsx @@ -1,18 +1,44 @@ +import { useEffect, useState } from 'react'; import { Navigate, useParams } from 'react-router-dom'; +import { fetchThemes } from '@/api/api'; 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 { ThemeData } from '@/types'; export const ThemePage = () => { const { themeKey = '' } = useParams<{ themeKey: string }>(); - const currentTheme = getCurrentTheme(themeKey, ThemeMockList); + const [currentTheme, setCurrentTheme] = useState(null); + const [loading, setLoading] = useState(true); + const [hasError, setHasError] = useState(false); - if (!currentTheme) { - return ; + useEffect(() => { + (async () => { + try { + const themes = await fetchThemes(); + const foundTheme = getCurrentTheme(themeKey, themes); + if (!foundTheme) { + setHasError(true); + } else { + setCurrentTheme(foundTheme); + } + } catch (fetchError) { + console.error('Failed to fetch themes', fetchError); + setHasError(true); + } finally { + setLoading(false); + } + })(); + }, [themeKey]); + + if (loading) { + return
Loading...
; } + if (hasError || !currentTheme) { + return ; + } return ( <> From 5659b495ead4fd083cdc7b6aebf746b1808f1e5f Mon Sep 17 00:00:00 2001 From: leedohyun Date: Tue, 9 Jul 2024 19:31:47 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat=20:=20=EC=84=A0=EB=AC=BC=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=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 ++ README.md | 8 ++++---- .../Home/GoodsRankingSection/index.tsx | 18 +++++++++++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b23679f0..e20ad98a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { env: { browser: true, diff --git a/README.md b/README.md index 6d23d301..ed279cea 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,17 @@ 2. Axios API 구현 -- [] 각 API 경로별로 함수 구현 +- [o] 각 API 경로별로 함수 구현 3. Main Page - [o] 테마 카테고리 섹션 : /api/v1/themes API 활용 -- [] 실시간 급상승 선물랭킹 섹션 : /api/v1/ranking/products 활용 -- [] 필터 조건을 선택하면 해당 조건에 맞게 api 보임 +- [o] 실시간 급상승 선물랭킹 섹션 : /api/v1/ranking/products 활용 +- [o] 필터 조건을 선택하면 해당 조건에 맞게 api 보임 4. Theme Page - [o] Header : url의 themekey 사용 - [o] url의 pathParams와 /api/vi/themes API를 사용 - [o] themeKey가 잘못된 경우 메인 페이지로 -- [o] 요청시 한번에 20개의 상품 목록이 내려오도록 구현 \ No newline at end of file +- [o] 요청시 한번에 20개의 상품 목록이 내려오도록 구현 diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 9464d67c..d75b59a4 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 { fetchRankingProducts } from '@/api/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,22 @@ export const GoodsRankingSection = () => { rankType: 'MANY_WISH', }); - // GoodsMockData를 21번 반복 생성 + const [goodsList, setGoodsList] = useState([]); + useEffect(() => { + const loadRankingProducts = async () => { + const products = await fetchRankingProducts(filterOption.targetType, filterOption.rankType); + setGoodsList(products); + }; + + loadRankingProducts(); + }, [filterOption]); return ( 실시간 급상승 선물랭킹 - + ); From f2ab6c7ad6ae1aef349144be9b303b4ca23642df Mon Sep 17 00:00:00 2001 From: leedohyun Date: Thu, 11 Jul 2024 10:03:38 +0900 Subject: [PATCH 05/15] =?UTF-8?q?fix=20:=20=EC=A0=84=EC=B2=B4=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20=EB=AC=B8=EB=B2=95=20=EC=88=98=EC=A0=95(import?= =?UTF-8?q?=EB=AC=B8,=20=EB=84=A4=EC=9D=B4=EB=B0=8D,=20=EB=9D=84=EC=96=B4?= =?UTF-8?q?=EC=93=B0=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/api.tsx | 16 ++++++++-------- .../features/Home/ThemeCategorySection/index.tsx | 3 +-- .../features/Theme/ThemeHeroSection/index.tsx | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/api/api.tsx b/src/api/api.tsx index 1ada24f2..a89c3ae7 100644 --- a/src/api/api.tsx +++ b/src/api/api.tsx @@ -1,8 +1,8 @@ import axios from 'axios'; -import {GoodsData,ThemeData } from '@/types'; +import type {GoodsData,ThemeData } from '@/types'; -const API_URL = axios.create({ +const axiosInstance = axios.create({ baseURL: 'https://react-gift-mock-api-leedyun.vercel.app/', headers: { 'Content-Type': 'application/json', @@ -11,8 +11,8 @@ const API_URL = axios.create({ export const fetchThemes = async (): Promise => { try { - const response = await API_URL.get<{themes : ThemeData[]}>('api/v1/themes'); - if (response.status === 200 && response.data && response.data.themes) { + const response = await axiosInstance.get<{themes : ThemeData[]}>('api/v1/themes'); + if (response.status === 200 && response.data) { return response.data.themes; } else { console.error("No themes data received", response); @@ -26,14 +26,14 @@ export const fetchThemes = async (): Promise => { export const fetchRankingProducts = async(targetType:string, rankType:string) : Promise => { - const response = await API_URL.get('api/v1/ranking/products', { - params : {targetType, rankType} + const response = await axiosInstance.get('api/v1/ranking/products', { + params : { targetType, rankType } }); return response.data.products; } export const fetchThemeProducts = async (themeKey: string, maxResults: number = 20): Promise => { - const response = await API_URL.get(`api/v1/themes/${themeKey}/products`, { + const response = await axiosInstance.get(`api/v1/themes/${themeKey}/products`, { params: { maxResults } }); return response.data.products; @@ -41,7 +41,7 @@ export const fetchThemes = async (): Promise => { export const fetchTheme = async (themeKey: string): Promise => { try { - const response = await API_URL.get(`/api/v1/themes/${themeKey}`, { + const response = await axiosInstance.get(`/api/v1/themes/${themeKey}`, { }); if (response.status === 200 && response.data) { return response.data; diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index a3c9c04a..c0dc9ba0 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -7,7 +7,7 @@ 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 { ThemeData } from '@/types'; +import type { ThemeData } from '@/types'; import { ThemeCategoryItem } from './ThemeCategoryItem'; @@ -18,7 +18,6 @@ export const ThemeCategorySection = () => { const loadThemes = async () => { const themesData = await fetchThemes(); setThemes(themesData); - console.log(themesData); }; loadThemes(); }, []); diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index a760ef57..1e574f13 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import {useEffect, useState} from 'react'; +import { useEffect, useState } from 'react'; import { Navigate } from 'react-router-dom'; import { fetchThemes } from '@/api/api'; From f596f90ef5615c0a7a5846b8672000e53aaa5e38 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Thu, 11 Jul 2024 10:12:43 +0900 Subject: [PATCH 06/15] =?UTF-8?q?docs=20:=20README=20step2=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ed279cea..fe400336 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,9 @@ - [o] url의 pathParams와 /api/vi/themes API를 사용 - [o] themeKey가 잘못된 경우 메인 페이지로 - [o] 요청시 한번에 20개의 상품 목록이 내려오도록 구현 + +## step2 요구사항 + +- [] 각 API에서 Loading 상태에 대한 UI 대응 +- [] 각 데이터가 없는 경우에 대한 UI 대응 +- [] HTTP status에 따라 Error 다르게 처리 From b4d3d700a4311f139dbcdbef3eac1ea74c7eb6c8 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Thu, 11 Jul 2024 10:40:28 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feat:=EA=B0=81=20api=EC=97=90=EC=84=9C=20?= =?UTF-8?q?loading,=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20UI=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- .../Home/GoodsRankingSection/index.tsx | 38 +++++++++++++-- .../Theme/ThemeGoodsSection/index.tsx | 48 +++++++++++++++++-- .../features/Theme/ThemeHeroSection/index.tsx | 31 ++++++++---- src/pages/Theme/index.tsx | 39 ++++++++++----- 5 files changed, 130 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index fe400336..980e4695 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,6 @@ ## step2 요구사항 -- [] 각 API에서 Loading 상태에 대한 UI 대응 -- [] 각 데이터가 없는 경우에 대한 UI 대응 +- [o] 각 API에서 Loading 상태에 대한 UI 대응 +- [o] 각 데이터가 없는 경우에 대한 UI 대응 - [] HTTP status에 따라 Error 다르게 처리 diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index d75b59a4..88e446c2 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -16,10 +16,24 @@ export const GoodsRankingSection = () => { }); const [goodsList, setGoodsList] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + useEffect(() => { const loadRankingProducts = async () => { - const products = await fetchRankingProducts(filterOption.targetType, filterOption.rankType); - setGoodsList(products); + setLoading(true); + setError(''); + try { + const products = await fetchRankingProducts(filterOption.targetType, filterOption.rankType); + setGoodsList(products); + if (products.length === 0) { + setError('No products found.'); + } + } catch (err) { + setError('Failed to fetch products. Please try again later.'); + } finally { + setLoading(false); + } }; loadRankingProducts(); @@ -30,7 +44,13 @@ export const GoodsRankingSection = () => { 실시간 급상승 선물랭킹 - + {loading ? ( + Loading... + ) : error ? ( + {error} + ) : ( + + )}
); @@ -58,3 +78,15 @@ const Title = styled.h2` line-height: 50px; } `; + +const LoadingMessage = styled.div` + color: #0070f3; + text-align: center; + margin-top: 20px; +`; + +const ErrorMessage = styled.div` + color: red; + text-align: center; + margin-top: 20px; +`; diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index ba8f768a..b87c3ad6 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -6,7 +6,7 @@ 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 { GoodsData } from '@/types'; +import type { GoodsData } from '@/types'; interface ThemeGoodsSectionProps { @@ -15,12 +15,39 @@ interface ThemeGoodsSectionProps { export const ThemeGoodsSection = ({ themeKey }: ThemeGoodsSectionProps) => { const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [fetchError, setFetchError] = useState(''); useEffect(() => { - fetchThemeProducts(themeKey).then(fetchedProducts => { - setProducts(fetchedProducts); - }).catch(console.error); + const fetchProducts = async () => { + try { + setLoading(true); + const fetchedProducts = await fetchThemeProducts(themeKey); + if (fetchedProducts.length === 0) { + setFetchError('No products found.'); + } else { + setProducts(fetchedProducts); + setFetchError(''); + } + } catch (error) { + console.error('Failed to fetch products:', error); + setFetchError('Failed to load products.'); + } finally { + setLoading(false); + } + }; + + fetchProducts(); }, [themeKey]); + + if (loading) { + return Loading products...; + } + + if (fetchError) { + return {fetchError}; + } + return ( @@ -54,3 +81,16 @@ const Wrapper = styled.section` padding: 40px 16px 360px; } `; + +const LoadingMessage = styled.div` + text-align: center; + padding: 20px; + font-size: 16px; +`; + +const ErrorMessage = styled.div` + color: red; + text-align: center; + padding: 20px; + font-size: 16px; +`; \ No newline at end of file diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 1e574f13..66609395 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,6 +1,5 @@ import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; -import { Navigate } from 'react-router-dom'; import { fetchThemes } from '@/api/api'; import { Container } from '@/components/common/layouts/Container'; @@ -11,21 +10,37 @@ interface ThemeHeroSectionProps { themeKey: string; } +const LoadingMessage = styled.div` + text-align: center; + padding: 20px; +`; + +const ErrorMessage = styled.div` + color: red; + text-align: center; + padding: 20px; +`; + export const ThemeHeroSection = ({ themeKey }: ThemeHeroSectionProps) => { const [theme, setTheme] = useState(null); const [loading, setLoading] = useState(true); - const [hasError, setHasError] = useState(false); + const [error, setError] = useState(''); useEffect(() => { const fetchThemeDetails = async () => { try { const themes = await fetchThemes(); const foundTheme = themes.find(t => t.key === themeKey); - if (!foundTheme) throw new Error('Theme not found'); - setTheme(foundTheme); + if (!foundTheme) { + setError('Theme not found'); + setTheme(null); + } else { + setTheme(foundTheme); + setError(''); + } } catch (fetchError) { console.error('Failed to fetch theme details:', fetchError); - setHasError(true); + setError('Failed to load theme data'); } finally { setLoading(false); } @@ -34,9 +49,9 @@ export const ThemeHeroSection = ({ themeKey }: ThemeHeroSectionProps) => { fetchThemeDetails(); }, [themeKey]); - if (loading) return
Loading...
; - if (hasError) return ; - if (!theme) return
Theme not found
; + if (loading) return Loading...; + if (error) return {error}; + if (!theme) return Theme not found; const { backgroundColor, label, title, description } = theme; diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx index cebd4583..6a65dedf 100644 --- a/src/pages/Theme/index.tsx +++ b/src/pages/Theme/index.tsx @@ -1,31 +1,30 @@ +import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; -import { Navigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { fetchThemes } from '@/api/api'; import { ThemeGoodsSection } from '@/components/features/Theme/ThemeGoodsSection'; -import { getCurrentTheme, ThemeHeroSection } from '@/components/features/Theme/ThemeHeroSection'; -import { RouterPath } from '@/routes/path'; -import { ThemeData } from '@/types'; +import { ThemeHeroSection } from '@/components/features/Theme/ThemeHeroSection'; export const ThemePage = () => { const { themeKey = '' } = useParams<{ themeKey: string }>(); - const [currentTheme, setCurrentTheme] = useState(null); const [loading, setLoading] = useState(true); - const [hasError, setHasError] = useState(false); + const [error, setError] = useState(''); useEffect(() => { (async () => { try { + setLoading(true); const themes = await fetchThemes(); - const foundTheme = getCurrentTheme(themeKey, themes); + const foundTheme = themes.find(t => t.key === themeKey); if (!foundTheme) { - setHasError(true); + setError('Theme not found.'); } else { - setCurrentTheme(foundTheme); + setError(''); } } catch (fetchError) { console.error('Failed to fetch themes', fetchError); - setHasError(true); + setError('An error occurred while fetching theme details.'); } finally { setLoading(false); } @@ -33,12 +32,13 @@ export const ThemePage = () => { }, [themeKey]); if (loading) { - return
Loading...
; + return Loading theme details...; } - if (hasError || !currentTheme) { - return ; + if (error) { + return {error}; } + return ( <> @@ -46,3 +46,16 @@ export const ThemePage = () => { ); }; + +const LoadingMessage = styled.div` + text-align: center; + padding: 20px; + font-size: 16px; +`; + +const ErrorMessage = styled.div` + color: red; + text-align: center; + padding: 20px; + font-size: 16px; +`; \ No newline at end of file From 023007b5974a8757a03893dfe97aeed53bcf4375 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Thu, 11 Jul 2024 10:45:46 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20http=20status=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20error=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/api/api.tsx | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 980e4695..42f80d19 100644 --- a/README.md +++ b/README.md @@ -25,4 +25,4 @@ - [o] 각 API에서 Loading 상태에 대한 UI 대응 - [o] 각 데이터가 없는 경우에 대한 UI 대응 -- [] HTTP status에 따라 Error 다르게 처리 +- [o] HTTP status에 따라 Error 다르게 처리 diff --git a/src/api/api.tsx b/src/api/api.tsx index a89c3ae7..00841433 100644 --- a/src/api/api.tsx +++ b/src/api/api.tsx @@ -25,18 +25,38 @@ export const fetchThemes = async (): Promise => { } - export const fetchRankingProducts = async(targetType:string, rankType:string) : Promise => { - const response = await axiosInstance.get('api/v1/ranking/products', { - params : { targetType, rankType } - }); - return response.data.products; + export const fetchRankingProducts = async (targetType: string, rankType: string): Promise => { + try { + const response = await axiosInstance.get('api/v1/ranking/products', { + params: { targetType, rankType } + }); + if (response.status === 200) { + return response.data.products; + } else { + console.error("Failed to get ranking products", response); + return []; + } + } catch (error) { + console.error("Failed to fetch ranking products", error); + throw new Error("Network error or server is down"); + } } export const fetchThemeProducts = async (themeKey: string, maxResults: number = 20): Promise => { - const response = await axiosInstance.get(`api/v1/themes/${themeKey}/products`, { - params: { maxResults } - }); - return response.data.products; + try { + const response = await axiosInstance.get(`api/v1/themes/${themeKey}/products`, { + params: { maxResults } + }); + if (response.status === 200) { + return response.data.products; + } else { + console.error("Failed to get theme products", response); + return []; + } + } catch (error) { + console.error("Failed to fetch theme products", error); + throw new Error("Network error or server is down"); + } }; export const fetchTheme = async (themeKey: string): Promise => { From fc31c4a4ed63035a40609a91b2e048e3a08fb341 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Thu, 11 Jul 2024 10:47:17 +0900 Subject: [PATCH 09/15] =?UTF-8?q?docs:=20README=20step3=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 42f80d19..5c353131 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,8 @@ - [o] 각 API에서 Loading 상태에 대한 UI 대응 - [o] 각 데이터가 없는 경우에 대한 UI 대응 - [o] HTTP status에 따라 Error 다르게 처리 + +## step3 요구사항 + +- [] 스크롤을 내리면 추가로 데이터 요청 +- [] API를 react-query로 구현 From e94d018679319ffcd2fad00a38c3864956ca7dd2 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Thu, 11 Jul 2024 15:24:01 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20api=EB=A5=BC=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 --- README.md | 2 +- package-lock.json | 113 ++++++++++++++---- package.json | 1 + src/App.tsx | 12 +- src/api/api.tsx | 34 ++++-- .../Home/GoodsRankingSection/index.tsx | 43 +++---- .../Theme/ThemeGoodsSection/index.tsx | 88 +++++++------- .../features/Theme/ThemeHeroSection/index.tsx | 54 +++------ src/pages/Theme/index.tsx | 43 +++---- src/types/index.ts | 5 + 10 files changed, 225 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 5c353131..1c330cf7 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,4 @@ ## step3 요구사항 - [] 스크롤을 내리면 추가로 데이터 요청 -- [] API를 react-query로 구현 +- [o] API를 react-query로 구현 diff --git a/package-lock.json b/package-lock.json index d0ee9525..2ae40839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-query": "^3.39.3", "react-router-dom": "^6.22.1" }, "devDependencies": { @@ -12687,8 +12688,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", @@ -12748,7 +12748,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" } @@ -12891,6 +12890,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", @@ -13619,8 +13633,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", @@ -15554,8 +15567,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", @@ -18731,8 +18743,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", @@ -19757,7 +19768,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" @@ -19766,8 +19776,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", @@ -23867,6 +23876,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", @@ -24643,6 +24657,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", @@ -24745,6 +24768,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", @@ -24985,6 +25013,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", @@ -25529,6 +25565,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", @@ -25566,7 +25607,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" } @@ -25850,7 +25890,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" } @@ -28285,6 +28324,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", @@ -28955,6 +29019,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", @@ -29149,7 +29218,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" }, @@ -29164,7 +29232,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" @@ -29174,7 +29241,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", @@ -29194,7 +29260,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" }, @@ -31856,6 +31921,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", @@ -33327,8 +33401,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 296e0a25..f3c576de 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "axios": "^1.7.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-query": "^3.39.3", "react-router-dom": "^6.22.1" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index f8602e3f..05eb3d35 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/api/api.tsx b/src/api/api.tsx index 00841433..1a463094 100644 --- a/src/api/api.tsx +++ b/src/api/api.tsx @@ -1,6 +1,7 @@ import axios from 'axios'; +import { useQuery } from 'react-query'; -import type {GoodsData,ThemeData } from '@/types'; +import type {GoodsData,InfiniteQueryResponse,ThemeData } from '@/types'; const axiosInstance = axios.create({ baseURL: 'https://react-gift-mock-api-leedyun.vercel.app/', @@ -42,16 +43,23 @@ export const fetchThemes = async (): Promise => { } } - export const fetchThemeProducts = async (themeKey: string, maxResults: number = 20): Promise => { + export const fetchThemeProducts = async ( + themeKey: string, + pageToken: string | undefined, + maxResults: number = 20 + ): Promise => { + const params = pageToken ? { pageToken, maxResults } : { maxResults }; try { - const response = await axiosInstance.get(`api/v1/themes/${themeKey}/products`, { - params: { maxResults } + const response = await axiosInstance.get(`/api/v1/themes/${themeKey}/products`, { + params }); if (response.status === 200) { - return response.data.products; + return { + products: response.data.products, + nextPageToken: response.data.nextPageToken + }; } else { - console.error("Failed to get theme products", response); - return []; + throw new Error('Failed to fetch products'); } } catch (error) { console.error("Failed to fetch theme products", error); @@ -73,4 +81,16 @@ export const fetchThemes = async (): Promise => { throw error; } }; + +export const useThemes = () => useQuery('themes', fetchThemes); +export const useRankingProducts = (targetType: string, rankType: string) => useQuery(['rankingProducts', targetType, rankType], () => fetchRankingProducts(targetType, rankType)); +export const useThemeProducts = (themeKey: string, pageToken?: string) => + useQuery( + ['themeProducts', themeKey, pageToken], + () => fetchThemeProducts(themeKey, pageToken), + { + keepPreviousData: true, + } + ); + \ No newline at end of file diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 88e446c2..d4c33c6b 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; +import { useQuery } from 'react-query'; import { fetchRankingProducts } from '@/api/api'; import { Container } from '@/components/common/layouts/Container'; @@ -15,39 +16,26 @@ export const GoodsRankingSection = () => { rankType: 'MANY_WISH', }); - const [goodsList, setGoodsList] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - useEffect(() => { - const loadRankingProducts = async () => { - setLoading(true); - setError(''); - try { - const products = await fetchRankingProducts(filterOption.targetType, filterOption.rankType); - setGoodsList(products); - if (products.length === 0) { - setError('No products found.'); - } - } catch (err) { - setError('Failed to fetch products. Please try again later.'); - } finally { - setLoading(false); - } - }; - - loadRankingProducts(); - }, [filterOption]); + const { data: goodsList, isLoading, isError, error } = useQuery( + ['rankingProducts', filterOption], + () => fetchRankingProducts(filterOption.targetType, filterOption.rankType), + { + keepPreviousData: true, + onError: (fetchError) => console.error('Failed to fetch products:', fetchError) + } + ); return ( 실시간 급상승 선물랭킹 - {loading ? ( + {isLoading ? ( Loading... - ) : error ? ( - {error} + ) : isError ? ( + Error: {error.message} + ) : !goodsList || goodsList.length === 0 ? ( + 보여줄 상품이 없어요! ) : ( )} @@ -86,7 +74,6 @@ const LoadingMessage = styled.div` `; const ErrorMessage = styled.div` - color: red; text-align: center; margin-top: 20px; `; diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index b87c3ad6..9a7e45f6 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,12 +1,12 @@ import styled from '@emotion/styled'; -import { useEffect,useState } from 'react'; +import { useInfiniteQuery } from 'react-query'; import { fetchThemeProducts } from '@/api/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'; +import type { GoodsData, InfiniteQueryResponse } from '@/types'; interface ThemeGoodsSectionProps { @@ -14,59 +14,51 @@ interface ThemeGoodsSectionProps { } export const ThemeGoodsSection = ({ themeKey }: ThemeGoodsSectionProps) => { - const [products, setProducts] = useState([]); - const [loading, setLoading] = useState(true); - const [fetchError, setFetchError] = useState(''); - - useEffect(() => { - const fetchProducts = async () => { - try { - setLoading(true); - const fetchedProducts = await fetchThemeProducts(themeKey); - if (fetchedProducts.length === 0) { - setFetchError('No products found.'); - } else { - setProducts(fetchedProducts); - setFetchError(''); - } - } catch (error) { - console.error('Failed to fetch products:', error); - setFetchError('Failed to load products.'); - } finally { - setLoading(false); - } - }; - - fetchProducts(); - }, [themeKey]); + const { + data, + isLoading, + isError, + error, + fetchNextPage, + hasNextPage, + } = useInfiniteQuery( + ['themeProducts', themeKey], + ({ pageParam = '' }) => fetchThemeProducts(themeKey, pageParam, 20), + { + getNextPageParam: (lastPage) => lastPage.nextPageToken ?? undefined, + } + ); - if (loading) { + if (isLoading) { return Loading products...; } - if (fetchError) { - return {fetchError}; + if (isError) { + return Error: {error?.message}; } + const handleScroll = (event: React.UIEvent) => { + const { scrollTop, clientHeight, scrollHeight } = event.currentTarget; + if (scrollHeight - scrollTop <= clientHeight * 1.2 && hasNextPage) { + fetchNextPage(); + } + }; + return ( - + - - {products.map(({ id, imageURL, name, price, brandInfo }) => ( - - ))} + + {data?.pages.map((page) => + page.products.map((product: GoodsData) => ( + + )) + )} @@ -74,6 +66,7 @@ export const ThemeGoodsSection = ({ themeKey }: ThemeGoodsSectionProps) => { }; const Wrapper = styled.section` + overflow-y: auto; width: 100%; padding: 28px 16px 180px; @@ -89,7 +82,6 @@ const LoadingMessage = styled.div` `; const ErrorMessage = styled.div` - color: red; text-align: center; padding: 20px; font-size: 16px; diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 66609395..b4aaff17 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,56 +1,37 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import type { AxiosError } from 'axios'; +import { useQuery } from 'react-query'; import { fetchThemes } from '@/api/api'; import { Container } from '@/components/common/layouts/Container'; import { breakpoints } from '@/styles/variants'; import type { ThemeData } from '@/types'; -interface ThemeHeroSectionProps { - themeKey: string; -} - const LoadingMessage = styled.div` text-align: center; padding: 20px; `; const ErrorMessage = styled.div` - color: red; text-align: center; padding: 20px; `; +interface ThemeHeroSectionProps { + themeKey: string; +} + +export const ThemeHeroSection = ({themeKey} : ThemeHeroSectionProps) => { + + const { data: themes, isLoading, isError, error } = useQuery(['themes'], fetchThemes); + + const theme = themes?.find((t: ThemeData) => t.key === themeKey); + + if (isLoading) return Loading...; + if (isError) { + const axiosError = error as AxiosError; + return Error: {axiosError?.message}; + } -export const ThemeHeroSection = ({ themeKey }: ThemeHeroSectionProps) => { - const [theme, setTheme] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - - useEffect(() => { - const fetchThemeDetails = async () => { - try { - const themes = await fetchThemes(); - const foundTheme = themes.find(t => t.key === themeKey); - if (!foundTheme) { - setError('Theme not found'); - setTheme(null); - } else { - setTheme(foundTheme); - setError(''); - } - } catch (fetchError) { - console.error('Failed to fetch theme details:', fetchError); - setError('Failed to load theme data'); - } finally { - setLoading(false); - } - }; - - fetchThemeDetails(); - }, [themeKey]); - - if (loading) return Loading...; - if (error) return {error}; if (!theme) return Theme not found; const { backgroundColor, label, title, description } = theme; @@ -66,6 +47,7 @@ export const ThemeHeroSection = ({ themeKey }: ThemeHeroSectionProps) => { ); }; + const Wrapper = styled.section<{ backgroundColor: string }>` padding: 27px 20px 23px; width: 100%; diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx index 6a65dedf..4f4d190a 100644 --- a/src/pages/Theme/index.tsx +++ b/src/pages/Theme/index.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import type { AxiosError } from 'axios'; +import { useQuery } from 'react-query'; import { useParams } from 'react-router-dom'; import { fetchThemes } from '@/api/api'; @@ -8,35 +9,23 @@ import { ThemeHeroSection } from '@/components/features/Theme/ThemeHeroSection'; export const ThemePage = () => { const { themeKey = '' } = useParams<{ themeKey: string }>(); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - - useEffect(() => { - (async () => { - try { - setLoading(true); - const themes = await fetchThemes(); - const foundTheme = themes.find(t => t.key === themeKey); - if (!foundTheme) { - setError('Theme not found.'); - } else { - setError(''); - } - } catch (fetchError) { - console.error('Failed to fetch themes', fetchError); - setError('An error occurred while fetching theme details.'); - } finally { - setLoading(false); - } - })(); - }, [themeKey]); - - if (loading) { + + const { data: themes, isLoading, isError, error } = useQuery('themes', fetchThemes); + + if (isLoading) { return Loading theme details...; } - if (error) { - return {error}; + const axiosError = error as AxiosError; + + if (isError) { + return Error: {axiosError.message}; + } + + const currentTheme = themes?.find((theme: { key: string; }) => theme.key === themeKey); + + if (!currentTheme) { + return Theme not found.; } return ( diff --git a/src/types/index.ts b/src/types/index.ts index d931c966..c16f81bc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -32,3 +32,8 @@ export type GoodsData = { imageURL: string; }; }; + +export interface InfiniteQueryResponse { + products: GoodsData[]; + nextPageToken?: string | null; +} \ No newline at end of file From ede33f57e1a5b72e3a0b12f7952e93b7971c012b Mon Sep 17 00:00:00 2001 From: leedohyun Date: Thu, 11 Jul 2024 15:55:21 +0900 Subject: [PATCH 11/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=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../Home/GoodsRankingSection/index.tsx | 4 +- .../Theme/ThemeGoodsSection/index.tsx | 40 ++++++++++++++----- .../features/Theme/ThemeHeroSection/index.tsx | 6 +-- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 1c330cf7..86ed5b44 100644 --- a/README.md +++ b/README.md @@ -29,5 +29,5 @@ ## step3 요구사항 -- [] 스크롤을 내리면 추가로 데이터 요청 +- [o] 스크롤을 내리면 추가로 데이터 요청 - [o] API를 react-query로 구현 diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index d4c33c6b..9a544215 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -16,7 +16,7 @@ export const GoodsRankingSection = () => { rankType: 'MANY_WISH', }); - const { data: goodsList, isLoading, isError, error } = useQuery( + const { data: goodsList, isLoading, isError } = useQuery( ['rankingProducts', filterOption], () => fetchRankingProducts(filterOption.targetType, filterOption.rankType), { @@ -33,7 +33,7 @@ export const GoodsRankingSection = () => { {isLoading ? ( Loading... ) : isError ? ( - Error: {error.message} + 에러가 발생했습니다. ) : !goodsList || goodsList.length === 0 ? ( 보여줄 상품이 없어요! ) : ( diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index 9a7e45f6..48985f5d 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import { useEffect, useRef } from 'react'; import { useInfiniteQuery } from 'react-query'; import { fetchThemeProducts } from '@/api/api'; @@ -14,38 +15,54 @@ interface ThemeGoodsSectionProps { } export const ThemeGoodsSection = ({ themeKey }: ThemeGoodsSectionProps) => { + const observerRef = useRef(null); const { data, isLoading, isError, - error, fetchNextPage, hasNextPage, } = useInfiniteQuery( ['themeProducts', themeKey], - ({ pageParam = '' }) => fetchThemeProducts(themeKey, pageParam, 20), + ({ pageParam = '' }) => fetchThemeProducts(themeKey, pageParam, 20), { getNextPageParam: (lastPage) => lastPage.nextPageToken ?? undefined, } ); + useEffect(() => { + const observer = new IntersectionObserver( + entries => { + const first = entries[0]; + if (first.isIntersecting && hasNextPage) { + fetchNextPage(); + } + }, + { threshold: 0.1 } + ); + + const currentElement = observerRef.current; + if (currentElement) { + observer.observe(currentElement); + } + + return () => { + if (currentElement) { + observer.unobserve(currentElement); + } + }; + }, [fetchNextPage, hasNextPage]); + if (isLoading) { return Loading products...; } if (isError) { - return Error: {error?.message}; + return 에러가 발생했습니다.; } - const handleScroll = (event: React.UIEvent) => { - const { scrollTop, clientHeight, scrollHeight } = event.currentTarget; - if (scrollHeight - scrollTop <= clientHeight * 1.2 && hasNextPage) { - fetchNextPage(); - } - }; - return ( - + {data?.pages.map((page) => @@ -59,6 +76,7 @@ export const ThemeGoodsSection = ({ themeKey }: ThemeGoodsSectionProps) => { /> )) )} +
diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index b4aaff17..7daa03ff 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,5 +1,4 @@ import styled from '@emotion/styled'; -import type { AxiosError } from 'axios'; import { useQuery } from 'react-query'; import { fetchThemes } from '@/api/api'; @@ -22,14 +21,13 @@ interface ThemeHeroSectionProps { export const ThemeHeroSection = ({themeKey} : ThemeHeroSectionProps) => { - const { data: themes, isLoading, isError, error } = useQuery(['themes'], fetchThemes); + const { data: themes, isLoading, isError } = useQuery(['themes'], fetchThemes); const theme = themes?.find((t: ThemeData) => t.key === themeKey); if (isLoading) return Loading...; if (isError) { - const axiosError = error as AxiosError; - return Error: {axiosError?.message}; + return 에러가 발생했습니다.; } if (!theme) return Theme not found; From a914267674fe36ffe2574bd51c364769194d8c63 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Thu, 11 Jul 2024 16:09:40 +0900 Subject: [PATCH 12/15] =?UTF-8?q?docs=20:=20step4=20=EC=A7=88=EB=AC=B8=20?= =?UTF-8?q?=EB=8B=B5=EB=B3=80?= 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 86ed5b44..0b591fb6 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,38 @@ - [o] 스크롤을 내리면 추가로 데이터 요청 - [o] API를 react-query로 구현 + +## 3주차 질문 + +- Q1. CORS 에러는 무엇이고 언제 발생하는지 설명해주세요. 이를 해결할 수 있는 방법에 대해서도 설명해주세요 + - CORS(Cross-Origin Resource Sharing) 에러는 웹 페이지가 다른 도메인의 리소스를 요청할 때 브라우저가 보안상의 이유로 그 요청을 차단하는 경우 발생 + - 해결 방법 + 1. 서버 설정 변경 : 서버에서 Access-Control-Allow-Origin 헤더를 설정하여, 특정 도메인에서의 요청을 허용 + 2. 프록시 서버 설정 : 모든 요청을 프록시를 통해 우회 + 3. CORS 미들웨어 사용 : Express의 cors 패키지 등을 사용하여 해결 +- Q2. 비동기 처리 방법인 callback, promise, async await에 대해 각각 장단점과 함께 설명해주세요. + + 1. Callback + + - 장점 : 간단한 비동기 작업에 쉽고 빠르게 사용 가능 + - 단점 : 복잡한 비동기 처리에서 콜백 지옥으로 인해 코드의 가독성과 유지보수 어려워짐 + + 2. Promise + + - 장점 : 콜백 지옥 문제를 해결하고, 비동기 처리를 체인 형식으로 연결하여 가독성을 높일 수 있음 + - 단점 : 여전히 코드가 중첩될 수 있고, 오류 처리가 직관적이지 않을 수 있음 + + 3. Async/Await + + - 장점 : 코드가 간결하고 가독성이 좋음. 오류 처리도 일반적인 try-catch문 사용 가능 + - 단점 : 내부에서 Promise를 사용하므로, 완전히 새로운 비동기 처리 메커니즘은 아니며, 오래된 환경에서는 사용이 어려울 수 있음 + +- Q3. react query의 주요 특징에 대해 설명하고, queryKey는 어떤 역할을 하는지 설명해주세요. + 1. React query 주요 특장 + - 서버 상태 관리 : 데이터 페칭, 캐싱, 동기화 및 업데이트 자동 관리 + - 효율적인 데이터 캐싱 : 자동으로 데이터를 캐시하고, 캐시에서 빠르게 데이터를 로드하여 성능 향상 + - 백그라운드 업데이트 : 애플리케이션의 데이터 신선도를 유지하기 위해 백그라운드에서 데이터를 자동으로 리페칭 가능 + - 비동기 로직 간소화 : 복잡한 비동기 로직을 간소화하며, 비동기 데이터 요청과 상태 관리 + 2. queryKey 역할 + - 식별자 역할 : queryKey는 데이터의 캐싱 및 무효화 제어. 동일한 queryKey를 가진 쿼리는 같은 캐시된 데이터 공유 가능 + - 쿼리 관리 : queryKey를 기반으로 쿼리의 상태를 관리하고, 해당 쿼리의 데이터가 변경되어 업데이트가 필요한지 판단 From 8617a95c49e67ed884f7220a48fe67ed1ebc19ae Mon Sep 17 00:00:00 2001 From: leedohyun Date: Sat, 13 Jul 2024 21:08:06 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat:=20react-query=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20API=20=ED=95=A8=EC=88=98=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/api.tsx | 78 ++++++++++++------------------------------------- 1 file changed, 18 insertions(+), 60 deletions(-) diff --git a/src/api/api.tsx b/src/api/api.tsx index 1a463094..4b5c9d1e 100644 --- a/src/api/api.tsx +++ b/src/api/api.tsx @@ -1,7 +1,7 @@ import axios from 'axios'; import { useQuery } from 'react-query'; -import type {GoodsData,InfiniteQueryResponse,ThemeData } from '@/types'; +import type { GoodsData,InfiniteQueryResponse,ThemeData } from '@/types'; const axiosInstance = axios.create({ baseURL: 'https://react-gift-mock-api-leedyun.vercel.app/', @@ -10,76 +10,34 @@ const axiosInstance = axios.create({ }, }); -export const fetchThemes = async (): Promise => { - try { - const response = await axiosInstance.get<{themes : ThemeData[]}>('api/v1/themes'); - if (response.status === 200 && response.data) { - return response.data.themes; - } else { - console.error("No themes data received", response); - return []; - } - } catch (error) { - console.error("Failed to fetch themes", error); - return []; - } - } + export const fetchThemes = async (): Promise => { + const response = await axiosInstance.get<{ themes: ThemeData[] }>('api/v1/themes'); + return response.data.themes; + }; - export const fetchRankingProducts = async (targetType: string, rankType: string): Promise => { - try { - const response = await axiosInstance.get('api/v1/ranking/products', { - params: { targetType, rankType } - }); - if (response.status === 200) { - return response.data.products; - } else { - console.error("Failed to get ranking products", response); - return []; - } - } catch (error) { - console.error("Failed to fetch ranking products", error); - throw new Error("Network error or server is down"); - } - } + const response = await axiosInstance.get<{ products: GoodsData[] }>('api/v1/ranking/products', { + params: { targetType, rankType }, + }); + return response.data.products; + }; export const fetchThemeProducts = async ( themeKey: string, - pageToken: string | undefined, + pageToken?: string, maxResults: number = 20 ): Promise => { const params = pageToken ? { pageToken, maxResults } : { maxResults }; - try { - const response = await axiosInstance.get(`/api/v1/themes/${themeKey}/products`, { - params - }); - if (response.status === 200) { - return { - products: response.data.products, - nextPageToken: response.data.nextPageToken - }; - } else { - throw new Error('Failed to fetch products'); - } - } catch (error) { - console.error("Failed to fetch theme products", error); - throw new Error("Network error or server is down"); - } + const response = await axiosInstance.get(`/api/v1/themes/${themeKey}/products`, { params }); + return { + products: response.data.products, + nextPageToken: response.data.nextPageToken, + }; }; export const fetchTheme = async (themeKey: string): Promise => { - try { - const response = await axiosInstance.get(`/api/v1/themes/${themeKey}`, { - }); - if (response.status === 200 && response.data) { - return response.data; - } else { - throw new Error("No theme data received"); - } - } catch (error) { - console.error("Failed to fetch theme", error); - throw error; - } + const response = await axiosInstance.get(`/api/v1/themes/${themeKey}`); + return response.data; }; export const useThemes = () => useQuery('themes', fetchThemes); From 2dde6b9ff3e1d52dfaee4fa9fb43a8d7e7cf87e4 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Sat, 13 Jul 2024 21:10:35 +0900 Subject: [PATCH 14/15] =?UTF-8?q?reafactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=94=8C=EB=9E=98=EA=B7=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/features/Home/GoodsRankingSection/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 9a544215..88840dcb 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -20,7 +20,6 @@ export const GoodsRankingSection = () => { ['rankingProducts', filterOption], () => fetchRankingProducts(filterOption.targetType, filterOption.rankType), { - keepPreviousData: true, onError: (fetchError) => console.error('Failed to fetch products:', fetchError) } ); From 85278b28606e5334b0309e2e428cdd99f9e9bf09 Mon Sep 17 00:00:00 2001 From: leedohyun Date: Sat, 13 Jul 2024 21:15:26 +0900 Subject: [PATCH 15/15] =?UTF-8?q?refactor=20:=20react-query=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=ED=85=8C=EB=A7=88=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Home/ThemeCategorySection/index.tsx | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index c0dc9ba0..29f19b7f 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { useEffect,useState } from 'react'; +import { useQuery } from 'react-query'; import { Link } from 'react-router-dom'; import { fetchThemes } from '@/api/api'; @@ -12,15 +12,15 @@ import type { ThemeData } from '@/types'; import { ThemeCategoryItem } from './ThemeCategoryItem'; export const ThemeCategorySection = () => { - const [themes, setThemes] = useState([]); - - useEffect(() => { - const loadThemes = async () => { - const themesData = await fetchThemes(); - setThemes(themesData); - }; - loadThemes(); - }, []); + const { data: themes, isLoading, isError } = useQuery('themes', fetchThemes); + + if (isLoading) { + return Loading...; + } + + if (isError) { + return 에러가 발생했습니다.; + } return ( @@ -30,12 +30,12 @@ export const ThemeCategorySection = () => { md: 6, }} > - {themes.map((theme, index) => ( - - + {themes && themes.map((theme, index) => ( + + ))} @@ -51,3 +51,14 @@ const Wrapper = styled.section` padding: 45px 52px 23px; } `; + +const LoadingMessage = styled.div` + color: #0070f3; + text-align: center; + margin-top: 20px; +`; + +const ErrorMessage = styled.div` + text-align: center; + margin-top: 20px; +`; \ No newline at end of file