From 9de3360f75aba984347dfa1f32d15cd4156afa47 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Tue, 9 Jul 2024 20:21:02 +0900 Subject: [PATCH 01/14] =?UTF-8?q?docs:=20README.md=EC=97=90=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=95=A0=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index e69de29b..13d6bdd7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,18 @@ +## 1단계 - API 적용하기 + +### 기능 구현 목록 +- [ ] 첨부된 oas.yaml 파일을 토대로 Request, Response Type을 정의 +- [ ] React Query를 사용하지 말고 axios를 사용해서 구현 +- 첨부된 oas.yaml 파일과 mock API URL을 사용해서 API 구현 + - 메인 페이지 - Theme 카테고리 섹션 + - [ ] /api/v1/themes API를 사용하여 Section을 구현 + - [ ] Axios또는 React Query 등을 모두 활용해서 API 구현 + - 메인 페이지 - 실시간 급상승 선물랭킹 섹션 + - [ ] /api/v1/ranking/products API를 사용하여 Section을 구현 + - [ ] 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 함 + - Theme 페이지 - header + - [ ] url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 + - [ ] themeKey가 잘못 된 경우 메인 페이지로 연결 + - Theme 페이지 - 상품 목록 섹션 + - [ ] /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 + - [ ] API 요청 시 한번에 20개의 상품 목록이 내려오도록 구현 \ No newline at end of file From 4817cc468dd7692b2aed4fdea416802f078e7cc7 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 10 Jul 2024 01:42:06 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80-=20ThemeCategorySection=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- src/components/common/API/api.tsx | 7 + .../Home/ThemeCategorySection/index.tsx | 127 ++++++++---------- 3 files changed, 63 insertions(+), 75 deletions(-) create mode 100644 src/components/common/API/api.tsx diff --git a/README.md b/README.md index 13d6bdd7..1cf00ba6 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ - [ ] React Query를 사용하지 말고 axios를 사용해서 구현 - 첨부된 oas.yaml 파일과 mock API URL을 사용해서 API 구현 - 메인 페이지 - Theme 카테고리 섹션 - - [ ] /api/v1/themes API를 사용하여 Section을 구현 - - [ ] Axios또는 React Query 등을 모두 활용해서 API 구현 + - [x] /api/v1/themes API를 사용하여 Section을 구현 + - [x] Axios또는 React Query 등을 모두 활용해서 API 구현 - 메인 페이지 - 실시간 급상승 선물랭킹 섹션 - [ ] /api/v1/ranking/products API를 사용하여 Section을 구현 - [ ] 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 함 diff --git a/src/components/common/API/api.tsx b/src/components/common/API/api.tsx new file mode 100644 index 00000000..3a502453 --- /dev/null +++ b/src/components/common/API/api.tsx @@ -0,0 +1,7 @@ +import axios from 'axios'; + +const Api = axios.create({ + baseURL: 'https://react-gift-mock-api-userjmmm.vercel.app/', +}); + +export default Api; \ 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..90a251d3 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { Container } from '@/components/common/layouts/Container'; @@ -6,9 +7,51 @@ import { Grid } from '@/components/common/layouts/Grid'; import { getDynamicPath } from '@/routes/path'; import { breakpoints } from '@/styles/variants'; +import Api from '../../../common/API/api'; import { ThemeCategoryItem } from './ThemeCategoryItem'; -export const ThemeCategorySection = () => { +interface ThemeData { + id: number; + key: string; + label: string; + title: string; + description: string; + backgroundColor: string; +} + +interface FetchState { + isLoading: boolean; + isError: boolean; + data: T | null; +} + +export const ThemeCategorySection: React.FC = () => { + const [fetchState, setFetchState] = useState>({ + isLoading: true, + isError: false, + data: null, + }); + + useEffect(() => { + const fetchThemes = async () => { + setFetchState({ isLoading: true, isError: false, data: null }); + try { + const response = await Api.get<{ themes: ThemeData[] }>('/api/v1/themes'); + //console.log('API response:', response.data); + setFetchState({ isLoading: false, isError: false, data: response.data.themes }); + + } catch (error) { + console.error('Error fetching themes:', error); + setFetchState({ isLoading: false, isError: true, data: null }); + } + }; + + fetchThemes(); + }, []); + + if (fetchState.isLoading) return

Loading...

; + if (fetchState.isError) return

Failed to fetch themes

; + return ( @@ -18,78 +61,14 @@ export const ThemeCategorySection = () => { md: 6, }} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {fetchState.data?.map((theme) => ( + + + + ))} @@ -103,3 +82,5 @@ const Wrapper = styled.section` padding: 45px 52px 23px; } `; + +export default ThemeCategorySection; \ No newline at end of file From f6df8ebbe826860c982bba6cfe92a3e17dc1a8d1 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 10 Jul 2024 02:46:18 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20ThemeData=EC=97=90=20imageURL=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A4=EB=8F=84=EB=A1=9D=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/common/API/api.tsx | 10 ++++++++++ .../features/Home/ThemeCategorySection/index.tsx | 13 ++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/common/API/api.tsx b/src/components/common/API/api.tsx index 3a502453..c45e57d4 100644 --- a/src/components/common/API/api.tsx +++ b/src/components/common/API/api.tsx @@ -4,4 +4,14 @@ const Api = axios.create({ baseURL: 'https://react-gift-mock-api-userjmmm.vercel.app/', }); +export const fetchData = async (endpoint: string, params = {}) => { + try { + const response = await Api.get(endpoint, { params }); + return response.data; + } catch (error) { + console.error(`Error fetching data from ${endpoint}:`, error); + throw error; + } +}; + export default Api; \ 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 90a251d3..93167c53 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -7,7 +7,7 @@ import { Grid } from '@/components/common/layouts/Grid'; import { getDynamicPath } from '@/routes/path'; import { breakpoints } from '@/styles/variants'; -import Api from '../../../common/API/api'; +import { fetchData } from '../../../common/API/api'; import { ThemeCategoryItem } from './ThemeCategoryItem'; interface ThemeData { @@ -17,6 +17,7 @@ interface ThemeData { title: string; description: string; backgroundColor: string; + imageURL?: string; } interface FetchState { @@ -36,10 +37,8 @@ export const ThemeCategorySection: React.FC = () => { const fetchThemes = async () => { setFetchState({ isLoading: true, isError: false, data: null }); try { - const response = await Api.get<{ themes: ThemeData[] }>('/api/v1/themes'); - //console.log('API response:', response.data); - setFetchState({ isLoading: false, isError: false, data: response.data.themes }); - + const data = await fetchData('/api/v1/themes'); + setFetchState({ isLoading: false, isError: false, data: data.themes }); } catch (error) { console.error('Error fetching themes:', error); setFetchState({ isLoading: false, isError: true, data: null }); @@ -64,7 +63,7 @@ export const ThemeCategorySection: React.FC = () => { {fetchState.data?.map((theme) => ( @@ -83,4 +82,4 @@ const Wrapper = styled.section` } `; -export default ThemeCategorySection; \ No newline at end of file +export default ThemeCategorySection; From 1f441f6b5e43d3b081f0d4e5ef99397e0fb6de44 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 10 Jul 2024 03:18:25 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20-=20GoodsRankingSection=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=A1=B0=EA=B1=B4=EC=97=90=20=EB=A7=9E=EA=B2=8C=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 --- README.md | 4 +- .../Home/GoodsRankingSection/index.tsx | 81 +++++++++++++++++-- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1cf00ba6..8e17bd26 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ - [x] /api/v1/themes API를 사용하여 Section을 구현 - [x] Axios또는 React Query 등을 모두 활용해서 API 구현 - 메인 페이지 - 실시간 급상승 선물랭킹 섹션 - - [ ] /api/v1/ranking/products API를 사용하여 Section을 구현 - - [ ] 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 함 + - [x] /api/v1/ranking/products API를 사용하여 Section을 구현 + - [x] 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 함 - Theme 페이지 - header - [ ] url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 - [ ] themeKey가 잘못 된 경우 메인 페이지로 연결 diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 9464d67c..2c427206 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -1,28 +1,80 @@ import styled from '@emotion/styled'; -import { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Container } from '@/components/common/layouts/Container'; import { breakpoints } from '@/styles/variants'; import type { RankingFilterOption } from '@/types'; -import { GoodsMockList } from '@/types/mock'; +import { fetchData } from '../../../common/API/api'; import { GoodsRankingFilter } from './Filter'; import { GoodsRankingList } from './List'; -export const GoodsRankingSection = () => { +interface ProductData { + id: number; + name: string; + imageURL: string; + wish: { + wishCount: number; + isWished: boolean; + }; + price: { + basicPrice: number; + discountRate: number; + sellingPrice: number; + }; + brandInfo: { + id: number; + name: string; + imageURL: string; + }; +} + +interface FetchState { + isLoading: boolean; + isError: boolean; + data: T | null; +} + +export const GoodsRankingSection: React.FC = () => { const [filterOption, setFilterOption] = useState({ targetType: 'ALL', rankType: 'MANY_WISH', }); - // GoodsMockData를 21번 반복 생성 + const [fetchState, setFetchState] = useState>({ + isLoading: true, + isError: false, + data: null, + }); + + useEffect(() => { + const fetchRankingProducts = async (filters: RankingFilterOption) => { + setFetchState({ isLoading: true, isError: false, data: null }); + try { + const response = await fetchData('/api/v1/ranking/products', filters); + setFetchState({ isLoading: false, isError: false, data: response.products }); + } catch (error) { + setFetchState({ isLoading: false, isError: true, data: null }); + } + }; + + fetchRankingProducts(filterOption); + }, [filterOption]); return ( 실시간 급상승 선물랭킹 - + {fetchState.isLoading ? ( + Loading... + ) : fetchState.isError ? ( + 데이터를 불러오는 중에 문제가 발생했습니다. + ) : fetchState.data && fetchState.data.length > 0 ? ( + + ) : ( + 보여줄 상품이 없어요! + )} ); @@ -50,3 +102,22 @@ const Title = styled.h2` line-height: 50px; } `; + +const ErrorMessage = styled.p` + text-align: center; + margin-top: 20px; +`; + +const LoadingMessage = styled.p` + color: #000; + text-align: center; + margin-top: 20px; +`; + +const EmptyMessage = styled.p` + color: #000; + text-align: center; + margin-top: 20px; +`; + +export default GoodsRankingSection; \ No newline at end of file From fb9b0d5204b613b4f0ebfc5134645b886d4b54bf Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 10 Jul 2024 03:59:36 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20Theme=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20-=20header=20section=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 +- .../features/Theme/ThemeHeroSection/index.tsx | 54 ++++++++++++++++--- src/pages/Theme/index.tsx | 13 ++--- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 8e17bd26..82939151 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - [x] /api/v1/ranking/products API를 사용하여 Section을 구현 - [x] 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 함 - Theme 페이지 - header - - [ ] url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 + - [x] url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 - [ ] themeKey가 잘못 된 경우 메인 페이지로 연결 - Theme 페이지 - 상품 목록 섹션 - [ ] /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 36cfc038..2b422a63 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,19 +1,59 @@ import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; +import { fetchData } from '@/components/common/API/api'; import { Container } from '@/components/common/layouts/Container'; import { breakpoints } from '@/styles/variants'; -import type { ThemeData } from '@/types'; -import { ThemeMockList } from '@/types/mock'; + +interface ThemeData { + id: number; + key: string; + label: string; + title: string; + description: string; + backgroundColor: string; +} + +interface FetchState { + isLoading: boolean; + isError: boolean; + data: T | null; +} type Props = { themeKey: string; }; -export const ThemeHeroSection = ({ themeKey }: Props) => { - const currentTheme = getCurrentTheme(themeKey, ThemeMockList); +const ThemeHeroSection: React.FC = ({ themeKey }) => { + const [fetchState, setFetchState] = useState>({ + isLoading: true, + isError: false, + data: null, + }); + + useEffect(() => { + const fetchTheme = async (key: string) => { + setFetchState({ isLoading: true, isError: false, data: null }); + try { + const data = await fetchData('/api/v1/themes', { key }); + setFetchState({ isLoading: false, isError: false, data: data.themes }); + } catch (error) { + console.error('Error fetching themes:', error); + setFetchState({ isLoading: false, isError: true, data: null }); + } + }; + if (themeKey) { + fetchTheme(themeKey); + } + }, [themeKey]); + + if (fetchState.isLoading) return

Loading...

; + if (fetchState.isError) return

데이터를 불러오는 중에 문제가 발생했습니다.

; + + const currentTheme = fetchState.data?.find((theme) => theme.key === themeKey); if (!currentTheme) { - return null; + return

테마를 찾을 수 없습니다.

; } const { backgroundColor, label, title, description } = currentTheme; @@ -83,6 +123,4 @@ const Description = styled.p` } `; -export const getCurrentTheme = (themeKey: string, themeList: ThemeData[]) => { - return themeList.find((theme) => theme.key === themeKey); -}; +export default ThemeHeroSection; \ No newline at end of file diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx index 4d02e6c1..09aa2d58 100644 --- a/src/pages/Theme/index.tsx +++ b/src/pages/Theme/index.tsx @@ -1,17 +1,10 @@ -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 ( <> @@ -20,3 +13,5 @@ export const ThemePage = () => { ); }; + +export default ThemePage; \ No newline at end of file From 5c4521d4cd71fbb4a07eed1b7c9cb8f9e1320d87 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 10 Jul 2024 04:19:50 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20themKey=EA=B0=80=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=EA=B2=BD=EC=9A=B0=20useNavigate=EB=A1=9C?= =?UTF-8?q?=20=EB=A6=AC=EB=94=94=EB=A0=89=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 --- README.md | 2 +- .../features/Theme/ThemeHeroSection/index.tsx | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 82939151..b41e896e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - [x] 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 함 - Theme 페이지 - header - [x] url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 - - [ ] themeKey가 잘못 된 경우 메인 페이지로 연결 + - [x] themeKey가 잘못 된 경우 메인 페이지로 연결 - Theme 페이지 - 상품 목록 섹션 - [ ] /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 - [ ] API 요청 시 한번에 20개의 상품 목록이 내려오도록 구현 \ 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 2b422a63..9df6424a 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { fetchData } from '@/components/common/API/api'; import { Container } from '@/components/common/layouts/Container'; @@ -31,6 +32,8 @@ const ThemeHeroSection: React.FC = ({ themeKey }) => { data: null, }); + const navigate = useNavigate(); + useEffect(() => { const fetchTheme = async (key: string) => { setFetchState({ isLoading: true, isError: false, data: null }); @@ -48,12 +51,19 @@ const ThemeHeroSection: React.FC = ({ themeKey }) => { } }, [themeKey]); + useEffect(() => { + if (!fetchState.isLoading && !fetchState.isError && !fetchState.data?.find((theme) => theme.key === themeKey)) { + console.log('Invalid theme key, redirecting...'); + navigate('/'); + } + }, [fetchState, themeKey, navigate]); + if (fetchState.isLoading) return

Loading...

; if (fetchState.isError) return

데이터를 불러오는 중에 문제가 발생했습니다.

; const currentTheme = fetchState.data?.find((theme) => theme.key === themeKey); if (!currentTheme) { - return

테마를 찾을 수 없습니다.

; + return null; } const { backgroundColor, label, title, description } = currentTheme; From 6b08761cc182683e6863c13ec62ec7621dfbf158 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 10 Jul 2024 04:48:33 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20/api/v1/themes/{themeKey}/product?= =?UTF-8?q?s=20API=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../Theme/ThemeGoodsSection/index.tsx | 57 ++++++++++++++++++- .../features/Theme/ThemeHeroSection/index.tsx | 6 +- src/pages/Theme/index.tsx | 2 +- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b41e896e..4aba349d 100644 --- a/README.md +++ b/README.md @@ -14,5 +14,5 @@ - [x] url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 - [x] themeKey가 잘못 된 경우 메인 페이지로 연결 - Theme 페이지 - 상품 목록 섹션 - - [ ] /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 + - [x] /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 - [ ] API 요청 시 한번에 20개의 상품 목록이 내려오도록 구현 \ 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..b3e9958f 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,16 +1,65 @@ import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; +import { fetchData } from '@/components/common/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'; + +interface ProductData { + id: number; + name: string; + imageURL: string; + price: { + sellingPrice: number; + }; + brandInfo: { + name: string; + }; +} + +interface FetchState { + isLoading: boolean; + isError: boolean; + data: T | null; +} type Props = { themeKey: string; }; -export const ThemeGoodsSection = ({}: Props) => { +const ThemeGoodsSection: React.FC = ({ themeKey }) => { + const [fetchState, setFetchState] = useState>({ + isLoading: true, + isError: false, + data: null, + }); + + useEffect(() => { + const fetchProducts = async (key: string) => { + setFetchState({ isLoading: true, isError: false, data: null }); + try { + const data = await fetchData(`/api/v1/themes/${key}/products`); + setFetchState({ isLoading: false, isError: false, data: data.products }); + } catch (error) { + console.error('Error fetching products:', error); + setFetchState({ isLoading: false, isError: true, data: null }); + } + }; + + if (themeKey) { + fetchProducts(themeKey); + } + }, [themeKey]); + + if (fetchState.isLoading) + return

Loading...

; + if (fetchState.isError) + return

데이터를 불러오는 중에 문제가 발생했습니다.

; + if (!fetchState.data || fetchState.data.length === 0) + return

상품이 없습니다.

; + return ( @@ -21,7 +70,7 @@ export const ThemeGoodsSection = ({}: Props) => { }} gap={16} > - {GoodsMockList.map(({ id, imageURL, name, price, brandInfo }) => ( + {fetchState.data.map(({ id, imageURL, name, price, brandInfo }) => ( = ({ themeKey }) => { } }, [fetchState, themeKey, navigate]); - if (fetchState.isLoading) return

Loading...

; - if (fetchState.isError) return

데이터를 불러오는 중에 문제가 발생했습니다.

; + if (fetchState.isLoading) + return

Loading...

; + if (fetchState.isError) + return

데이터를 불러오는 중에 문제가 발생했습니다.

; const currentTheme = fetchState.data?.find((theme) => theme.key === themeKey); if (!currentTheme) { diff --git a/src/pages/Theme/index.tsx b/src/pages/Theme/index.tsx index 09aa2d58..744e4658 100644 --- a/src/pages/Theme/index.tsx +++ b/src/pages/Theme/index.tsx @@ -1,6 +1,6 @@ import { useParams } from 'react-router-dom'; -import { ThemeGoodsSection } from '@/components/features/Theme/ThemeGoodsSection'; +import ThemeGoodsSection from '@/components/features/Theme/ThemeGoodsSection'; import ThemeHeroSection from '@/components/features/Theme/ThemeHeroSection'; export const ThemePage = () => { From 923c885f48d33f3f9362f19fb1d57928dffa0046 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Wed, 10 Jul 2024 17:19:32 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=8B=9C=20=ED=95=9C=EB=B2=88=EC=97=90=2020=EA=B0=9C=EC=9D=98?= =?UTF-8?q?=20=EC=83=81=ED=92=88=20=EB=AA=A9=EB=A1=9D=EC=9D=B4=20=EB=82=B4?= =?UTF-8?q?=EB=A0=A4=EC=98=A4=EB=8F=84=EB=A1=9D=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 +- .../Theme/ThemeGoodsSection/index.tsx | 111 ++++++++++++++---- .../features/Theme/ThemeHeroSection/index.tsx | 6 +- src/index.tsx | 3 - 4 files changed, 93 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4aba349d..cdc3bbd3 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,4 @@ - [x] themeKey가 잘못 된 경우 메인 페이지로 연결 - Theme 페이지 - 상품 목록 섹션 - [x] /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 - - [ ] API 요청 시 한번에 20개의 상품 목록이 내려오도록 구현 \ No newline at end of file + - [x] API 요청 시 한번에 20개의 상품 목록이 내려오도록 구현 \ 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 b3e9958f..bb4b2220 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { fetchData } from '@/components/common/API/api'; import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default'; @@ -19,10 +19,26 @@ interface ProductData { }; } +interface ApiResponse { + products: ProductData[]; + nextPageToken: string | null; + pageInfo: { + totalResults: number; + resultsPerPage: number; + }; +} + interface FetchState { isLoading: boolean; isError: boolean; - data: T | null; + data: T; + hasMore: boolean; + nextPageToken: string | null; +} + +interface FetchParams { + maxResults: number; + pageToken?: string; } type Props = { @@ -33,32 +49,69 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => { const [fetchState, setFetchState] = useState>({ isLoading: true, isError: false, - data: null, + data: [], + hasMore: true, + nextPageToken: null, }); - useEffect(() => { - const fetchProducts = async (key: string) => { - setFetchState({ isLoading: true, isError: false, data: null }); - try { - const data = await fetchData(`/api/v1/themes/${key}/products`); - setFetchState({ isLoading: false, isError: false, data: data.products }); - } catch (error) { - console.error('Error fetching products:', error); - setFetchState({ isLoading: false, isError: true, data: null }); - } - }; + const observerRef = useRef(null); + const loadMoreRef = useRef(null); + + const fetchProducts = useCallback(async (key: string, pageToken: string | null) => { + setFetchState(prevState => ({ ...prevState, isLoading: true })); + + try { + const params: FetchParams = { maxResults: 20 }; + if (pageToken) params.pageToken = pageToken; + const data: ApiResponse = await fetchData(`/api/v1/themes/${key}/products`, params); + setFetchState(prevState => ({ + isLoading: false, + isError: false, + data: [...prevState.data, ...data.products], + hasMore: data.products.length > 0, + nextPageToken: data.nextPageToken, + })); + } catch (error) { + console.error('Error fetching products:', error); + setFetchState(prevState => ({ ...prevState, isLoading: false, isError: true })); + } + }, []); + + // 초기 로딩 비동기 통신 + useEffect(() => { if (themeKey) { - fetchProducts(themeKey); + setFetchState({ isLoading: true, isError: false, data: [], hasMore: true, nextPageToken: null }); + fetchProducts(themeKey, null); } - }, [themeKey]); + }, [themeKey, fetchProducts]); - if (fetchState.isLoading) - return

Loading...

; + // ref(스크롤)의 변화를 감지하고 다음 페이지를 불러오는 비동기 처리 + useEffect(() => { + if (fetchState.hasMore && !fetchState.isLoading) { + if (observerRef.current) observerRef.current.disconnect(); + + observerRef.current = new IntersectionObserver(entries => { + if (entries[0].isIntersecting && fetchState.nextPageToken) { + fetchProducts(themeKey, fetchState.nextPageToken); + } + }, { + threshold: 1.0, + }); + + if (loadMoreRef.current) { + observerRef.current.observe(loadMoreRef.current); + } + } + return () => observerRef.current?.disconnect(); + }, [fetchState.hasMore, fetchState.isLoading, fetchState.nextPageToken, themeKey, fetchProducts]); + + if (fetchState.isLoading && fetchState.data.length === 0) + return Loading...; if (fetchState.isError) - return

데이터를 불러오는 중에 문제가 발생했습니다.

; + return 데이터를 불러오는 중에 문제가 발생했습니다.; if (!fetchState.data || fetchState.data.length === 0) - return

상품이 없습니다.

; + return 상품이 없습니다.; return ( @@ -81,6 +134,7 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => { ))}
+
); }; @@ -94,4 +148,19 @@ const Wrapper = styled.section` } `; -export default ThemeGoodsSection; \ No newline at end of file +const ErrorMessage = styled.p` + text-align: center; + margin-top: 20px; +`; + +const LoadingMessage = styled.p` + text-align: center; + margin-top: 20px; +`; + +const EmptyMessage = styled.p` + text-align: center; + margin-top: 20px; +`; + +export default ThemeGoodsSection; diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 59691846..290c9c23 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -58,10 +58,8 @@ const ThemeHeroSection: React.FC = ({ themeKey }) => { } }, [fetchState, themeKey, navigate]); - if (fetchState.isLoading) - return

Loading...

; - if (fetchState.isError) - return

데이터를 불러오는 중에 문제가 발생했습니다.

; + if (fetchState.isLoading)return

Loading...

; + if (fetchState.isError) return

데이터를 불러오는 중에 문제가 발생했습니다.

; const currentTheme = fetchState.data?.find((theme) => theme.key === themeKey); if (!currentTheme) { diff --git a/src/index.tsx b/src/index.tsx index ab5f7ad6..c322b323 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,10 @@ import '@/styles'; -import React from 'react'; import ReactDOM from 'react-dom/client'; import App from '@/App'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( - - , ); From 402dba8db4e5ce4de21bfe0ca29a892d8f809878 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Thu, 11 Jul 2024 14:32:59 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20Loading=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A5=BC=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8A=94=20UI=20compone?= =?UTF-8?q?nt=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 ++++++---------- src/components/common/Status/loading.tsx | 35 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 src/components/common/Status/loading.tsx diff --git a/README.md b/README.md index cdc3bbd3..11a41279 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,10 @@ -## 1단계 - API 적용하기 +## 2단계 - Error, Loading Status 핸들링 하기 ### 기능 구현 목록 -- [ ] 첨부된 oas.yaml 파일을 토대로 Request, Response Type을 정의 -- [ ] React Query를 사용하지 말고 axios를 사용해서 구현 -- 첨부된 oas.yaml 파일과 mock API URL을 사용해서 API 구현 - - 메인 페이지 - Theme 카테고리 섹션 - - [x] /api/v1/themes API를 사용하여 Section을 구현 - - [x] Axios또는 React Query 등을 모두 활용해서 API 구현 - - 메인 페이지 - 실시간 급상승 선물랭킹 섹션 - - [x] /api/v1/ranking/products API를 사용하여 Section을 구현 - - [x] 필터 조건을 선택 하면 해당 조건에 맞게 API를 요청하여 보여지게 함 - - Theme 페이지 - header - - [x] url의 pathParams와 /api/v1/themes API를 사용하여 Section을 구현 - - [x] themeKey가 잘못 된 경우 메인 페이지로 연결 - - Theme 페이지 - 상품 목록 섹션 - - [x] /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현 - - [x] API 요청 시 한번에 20개의 상품 목록이 내려오도록 구현 \ No newline at end of file +- 각 API에서 Loading 상태에 대한 UI 대응 +- [x] Loading 상태를 보여주는 UI component 만들기 +- [ ] 1단계에서 사용 중인 API에 적용하기 + +- [ ] 데이터가 없는 경우에 대한 UI component 만들기 +- [ ] Http Status에 따라 Error UI component 만들기 +- [ ] 1단계에서 사용 중인 API에 적용하기 \ No newline at end of file diff --git a/src/components/common/Status/loading.tsx b/src/components/common/Status/loading.tsx new file mode 100644 index 00000000..a2229c83 --- /dev/null +++ b/src/components/common/Status/loading.tsx @@ -0,0 +1,35 @@ +import { keyframes } from '@emotion/react'; +import styled from '@emotion/styled'; + +const Loading: React.FC = () => { + return ( + + + + ); +}; + +const spin = keyframes` + to { + transform: rotate(360deg); + } +`; + +const LoadingContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +`; + +const Spinner = styled.div` + border: 4px solid rgba(0, 0, 0, 0.1); + border-left-color: #22a6b3; + border-radius: 50%; + width: 40px; + height: 40px; + animation: ${spin} 1s linear infinite; +`; + +export default Loading; \ No newline at end of file From 98221daa6131be8dc18120a3ad89e770f9983282 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Thu, 11 Jul 2024 14:40:43 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=201=EB=8B=A8=EA=B3=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=20=EC=A4=91=EC=9D=B8=20API?= =?UTF-8?q?=EC=97=90=20Loading=20Component=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../features/Home/GoodsRankingSection/index.tsx | 9 ++------- .../features/Home/ThemeCategorySection/index.tsx | 4 +++- .../features/Theme/ThemeGoodsSection/index.tsx | 8 ++------ src/components/features/Theme/ThemeHeroSection/index.tsx | 7 +++++-- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 11a41279..9d0d907e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ### 기능 구현 목록 - 각 API에서 Loading 상태에 대한 UI 대응 - [x] Loading 상태를 보여주는 UI component 만들기 -- [ ] 1단계에서 사용 중인 API에 적용하기 +- [x] 1단계에서 사용 중인 API에 적용하기 - [ ] 데이터가 없는 경우에 대한 UI component 만들기 - [ ] Http Status에 따라 Error UI component 만들기 diff --git a/src/components/features/Home/GoodsRankingSection/index.tsx b/src/components/features/Home/GoodsRankingSection/index.tsx index 2c427206..ffbbb8fb 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; import React, { useEffect, useState } from 'react'; import { Container } from '@/components/common/layouts/Container'; +import Loading from '@/components/common/Status/loading'; import { breakpoints } from '@/styles/variants'; import type { RankingFilterOption } from '@/types'; @@ -67,7 +68,7 @@ export const GoodsRankingSection: React.FC = () => { 실시간 급상승 선물랭킹 {fetchState.isLoading ? ( - Loading... + ) : fetchState.isError ? ( 데이터를 불러오는 중에 문제가 발생했습니다. ) : fetchState.data && fetchState.data.length > 0 ? ( @@ -108,12 +109,6 @@ const ErrorMessage = styled.p` margin-top: 20px; `; -const LoadingMessage = styled.p` - color: #000; - text-align: center; - margin-top: 20px; -`; - const EmptyMessage = styled.p` color: #000; text-align: center; diff --git a/src/components/features/Home/ThemeCategorySection/index.tsx b/src/components/features/Home/ThemeCategorySection/index.tsx index 93167c53..c8c68a18 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'; import { Container } from '@/components/common/layouts/Container'; import { Grid } from '@/components/common/layouts/Grid'; +import Loading from '@/components/common/Status/loading'; import { getDynamicPath } from '@/routes/path'; import { breakpoints } from '@/styles/variants'; @@ -48,7 +49,8 @@ export const ThemeCategorySection: React.FC = () => { fetchThemes(); }, []); - if (fetchState.isLoading) return

Loading...

; + if (fetchState.isLoading) + return ; if (fetchState.isError) return

Failed to fetch themes

; return ( diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index bb4b2220..a635d720 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -5,6 +5,7 @@ import { fetchData } from '@/components/common/API/api'; import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default'; import { Container } from '@/components/common/layouts/Container'; import { Grid } from '@/components/common/layouts/Grid'; +import Loading from '@/components/common/Status/loading'; import { breakpoints } from '@/styles/variants'; interface ProductData { @@ -107,7 +108,7 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => { }, [fetchState.hasMore, fetchState.isLoading, fetchState.nextPageToken, themeKey, fetchProducts]); if (fetchState.isLoading && fetchState.data.length === 0) - return Loading...; + return ; if (fetchState.isError) return 데이터를 불러오는 중에 문제가 발생했습니다.; if (!fetchState.data || fetchState.data.length === 0) @@ -153,11 +154,6 @@ const ErrorMessage = styled.p` margin-top: 20px; `; -const LoadingMessage = styled.p` - text-align: center; - margin-top: 20px; -`; - const EmptyMessage = styled.p` text-align: center; margin-top: 20px; diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 290c9c23..73e00ea7 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { fetchData } from '@/components/common/API/api'; import { Container } from '@/components/common/layouts/Container'; +import Loading from '@/components/common/Status/loading'; import { breakpoints } from '@/styles/variants'; interface ThemeData { @@ -58,8 +59,10 @@ const ThemeHeroSection: React.FC = ({ themeKey }) => { } }, [fetchState, themeKey, navigate]); - if (fetchState.isLoading)return

Loading...

; - if (fetchState.isError) return

데이터를 불러오는 중에 문제가 발생했습니다.

; + if (fetchState.isLoading) + return ; + if (fetchState.isError) + return

데이터를 불러오는 중에 문제가 발생했습니다.

; const currentTheme = fetchState.data?.find((theme) => theme.key === themeKey); if (!currentTheme) { From b12d2cd2d818ae9be9ef327b58c7536f42968e39 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Thu, 11 Jul 2024 14:56:32 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=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=ED=95=9C=20UI=20component=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/components/common/Status/emptyData.tsx | 27 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/components/common/Status/emptyData.tsx diff --git a/README.md b/README.md index 9d0d907e..ce93a961 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,6 @@ - [x] Loading 상태를 보여주는 UI component 만들기 - [x] 1단계에서 사용 중인 API에 적용하기 -- [ ] 데이터가 없는 경우에 대한 UI component 만들기 +- [x] 데이터가 없는 경우에 대한 UI component 만들기 - [ ] Http Status에 따라 Error UI component 만들기 - [ ] 1단계에서 사용 중인 API에 적용하기 \ No newline at end of file diff --git a/src/components/common/Status/emptyData.tsx b/src/components/common/Status/emptyData.tsx new file mode 100644 index 00000000..742a16dc --- /dev/null +++ b/src/components/common/Status/emptyData.tsx @@ -0,0 +1,27 @@ +import styled from "@emotion/styled"; + +interface EmptyDataProps { + message?: string; +} + +const EmptyData = ({ message = "보여줄 데이터가 없어요 🤨" }: EmptyDataProps) => { + return ( + + {message} + + ) +}; + +const EmptyDataContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + color: #555; + font-size: 18px; + text-align: center; + padding: 20px; +`; + +export default EmptyData; \ No newline at end of file From bb2665ef24f97607a4affbf9993c7603949197a6 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Thu, 11 Jul 2024 15:20:18 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20Http=20Status=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20Error=20UI=20component=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/components/common/API/api.tsx | 9 +++++--- src/components/common/Status/errorMessage.tsx | 23 +++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/components/common/Status/errorMessage.tsx diff --git a/README.md b/README.md index ce93a961..5dc882ee 100644 --- a/README.md +++ b/README.md @@ -6,5 +6,5 @@ - [x] 1단계에서 사용 중인 API에 적용하기 - [x] 데이터가 없는 경우에 대한 UI component 만들기 -- [ ] Http Status에 따라 Error UI component 만들기 +- [x] Http Status에 따라 Error UI component 만들기 - [ ] 1단계에서 사용 중인 API에 적용하기 \ No newline at end of file diff --git a/src/components/common/API/api.tsx b/src/components/common/API/api.tsx index c45e57d4..243690a9 100644 --- a/src/components/common/API/api.tsx +++ b/src/components/common/API/api.tsx @@ -8,9 +8,12 @@ export const fetchData = async (endpoint: string, params = {}) => { try { const response = await Api.get(endpoint, { params }); return response.data; - } catch (error) { - console.error(`Error fetching data from ${endpoint}:`, error); - throw error; + } catch (error: any) { + if (axios.isAxiosError(error)) { + const { status, data } = error.response || {}; + const errorMessage = data?.description || '알 수 없는 오류가 발생했어요.'; + throw new Error(JSON.stringify({ status, message: errorMessage })); + } } }; diff --git a/src/components/common/Status/errorMessage.tsx b/src/components/common/Status/errorMessage.tsx new file mode 100644 index 00000000..754006a8 --- /dev/null +++ b/src/components/common/Status/errorMessage.tsx @@ -0,0 +1,23 @@ +import styled from "@emotion/styled"; + +interface ErrorMessageProps { + message: string; +} + +const ErrorMessage = ({ message }: ErrorMessageProps) => { + return {message}; +}; + +const ErrorContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + color: #d32f2f; + font-size: 18px; + text-align: center; + padding: 20px; +`; + +export default ErrorMessage; \ No newline at end of file From f638bf996dd76d06f1d8cd72a5b4be513ec6c4d8 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Thu, 11 Jul 2024 20:42:48 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=201=EB=8B=A8=EA=B3=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=20=EC=A4=91=EC=9D=B8=20API?= =?UTF-8?q?=EC=97=90=20=EC=A0=81=EC=9A=A9=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/components/common/API/api.tsx | 12 ++++---- src/components/common/Status/errorMessage.tsx | 20 +++++++++++-- .../Home/GoodsRankingSection/index.tsx | 29 ++++++++++--------- .../Home/ThemeCategorySection/index.tsx | 24 +++++++++++++-- .../Theme/ThemeGoodsSection/index.tsx | 29 ++++++++++--------- .../features/Theme/ThemeHeroSection/index.tsx | 16 ++++++++-- 7 files changed, 88 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 5dc882ee..36effb02 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,4 @@ - [x] 데이터가 없는 경우에 대한 UI component 만들기 - [x] Http Status에 따라 Error UI component 만들기 -- [ ] 1단계에서 사용 중인 API에 적용하기 \ No newline at end of file +- [x] 1단계에서 사용 중인 API에 적용하기 \ No newline at end of file diff --git a/src/components/common/API/api.tsx b/src/components/common/API/api.tsx index 243690a9..fe39bbaf 100644 --- a/src/components/common/API/api.tsx +++ b/src/components/common/API/api.tsx @@ -8,12 +8,12 @@ export const fetchData = async (endpoint: string, params = {}) => { try { const response = await Api.get(endpoint, { params }); return response.data; - } catch (error: any) { - if (axios.isAxiosError(error)) { - const { status, data } = error.response || {}; - const errorMessage = data?.description || '알 수 없는 오류가 발생했어요.'; - throw new Error(JSON.stringify({ status, message: errorMessage })); - } + } catch (error) { + if (axios.isAxiosError(error)) { // axios는 자동으로 JSON 변환해줌 + const code = error.response?.status?.toString() || 'UNKNOWN_ERROR'; + const description = error.response?.data?.description || '알 수 없는 오류가 발생했어요.'; + throw Object.assign(new Error(), {code, description }); + } } }; diff --git a/src/components/common/Status/errorMessage.tsx b/src/components/common/Status/errorMessage.tsx index 754006a8..68057b2c 100644 --- a/src/components/common/Status/errorMessage.tsx +++ b/src/components/common/Status/errorMessage.tsx @@ -1,11 +1,16 @@ import styled from "@emotion/styled"; interface ErrorMessageProps { + code?: string; message: string; } -const ErrorMessage = ({ message }: ErrorMessageProps) => { - return {message}; +const ErrorMessage = ({ code, message }: ErrorMessageProps) => { + return ( + + {code && (Error Code: {code}) } {message} 😔 + + ) }; const ErrorContainer = styled.div` @@ -14,10 +19,19 @@ const ErrorContainer = styled.div` align-items: center; height: 100%; width: 100%; - color: #d32f2f; font-size: 18px; text-align: center; padding: 20px; `; +const Code = styled.span` + margin-right: 10px; + font-size: 16px; + color: #d32f2f; +`; + +const Emoji = styled.span` + margin-left: 5px; +`; + export default ErrorMessage; \ 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 ffbbb8fb..85b6b410 100644 --- a/src/components/features/Home/GoodsRankingSection/index.tsx +++ b/src/components/features/Home/GoodsRankingSection/index.tsx @@ -1,7 +1,10 @@ import styled from '@emotion/styled'; +import type { AxiosError } from 'axios'; import React, { useEffect, useState } from 'react'; import { Container } from '@/components/common/layouts/Container'; +import EmptyData from '@/components/common/Status/emptyData'; +import ErrorMessage from '@/components/common/Status/errorMessage'; import Loading from '@/components/common/Status/loading'; import { breakpoints } from '@/styles/variants'; import type { RankingFilterOption } from '@/types'; @@ -33,6 +36,8 @@ interface ProductData { interface FetchState { isLoading: boolean; isError: boolean; + errorCode?: string; + errorMessage?: string; data: T | null; } @@ -55,7 +60,14 @@ export const GoodsRankingSection: React.FC = () => { const response = await fetchData('/api/v1/ranking/products', filters); setFetchState({ isLoading: false, isError: false, data: response.products }); } catch (error) { - setFetchState({ isLoading: false, isError: true, data: null }); + const axiosError = error as AxiosError; + setFetchState({ + isLoading: false, + isError: true, + data: null, + errorMessage: axiosError.message, + errorCode: axiosError.code, + }); } }; @@ -70,11 +82,11 @@ export const GoodsRankingSection: React.FC = () => { {fetchState.isLoading ? ( ) : fetchState.isError ? ( - 데이터를 불러오는 중에 문제가 발생했습니다. + ) : fetchState.data && fetchState.data.length > 0 ? ( ) : ( - 보여줄 상품이 없어요! + )} @@ -104,15 +116,4 @@ const Title = styled.h2` } `; -const ErrorMessage = styled.p` - text-align: center; - margin-top: 20px; -`; - -const EmptyMessage = styled.p` - color: #000; - text-align: center; - margin-top: 20px; -`; - export default GoodsRankingSection; \ 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 c8c68a18..f8a2c926 100644 --- a/src/components/features/Home/ThemeCategorySection/index.tsx +++ b/src/components/features/Home/ThemeCategorySection/index.tsx @@ -1,9 +1,11 @@ import styled from '@emotion/styled'; +import type { AxiosError } from 'axios'; import React, { 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 ErrorMessage from '@/components/common/Status/errorMessage'; import Loading from '@/components/common/Status/loading'; import { getDynamicPath } from '@/routes/path'; import { breakpoints } from '@/styles/variants'; @@ -11,6 +13,7 @@ import { breakpoints } from '@/styles/variants'; import { fetchData } from '../../../common/API/api'; import { ThemeCategoryItem } from './ThemeCategoryItem'; + interface ThemeData { id: number; key: string; @@ -24,6 +27,8 @@ interface ThemeData { interface FetchState { isLoading: boolean; isError: boolean; + errorCode?: string; + errorMessage?: string; data: T | null; } @@ -41,8 +46,14 @@ export const ThemeCategorySection: React.FC = () => { const data = await fetchData('/api/v1/themes'); setFetchState({ isLoading: false, isError: false, data: data.themes }); } catch (error) { - console.error('Error fetching themes:', error); - setFetchState({ isLoading: false, isError: true, data: null }); + const axiosError = error as AxiosError; + setFetchState({ + isLoading: false, + isError: true, + data: null, + errorCode: axiosError.code, + errorMessage: axiosError.message, + }); } }; @@ -51,7 +62,14 @@ export const ThemeCategorySection: React.FC = () => { if (fetchState.isLoading) return ; - if (fetchState.isError) return

Failed to fetch themes

; + if (fetchState.isError) + return ( + + ); + return ( diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index a635d720..b229c77e 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,10 +1,13 @@ import styled from '@emotion/styled'; +import type { AxiosError } from 'axios'; import { useCallback, useEffect, useRef, useState } from 'react'; import { fetchData } from '@/components/common/API/api'; import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default'; import { Container } from '@/components/common/layouts/Container'; import { Grid } from '@/components/common/layouts/Grid'; +import EmptyData from '@/components/common/Status/emptyData'; +import ErrorMessage from '@/components/common/Status/errorMessage'; import Loading from '@/components/common/Status/loading'; import { breakpoints } from '@/styles/variants'; @@ -32,6 +35,8 @@ interface ApiResponse { interface FetchState { isLoading: boolean; isError: boolean; + errorCode?: string; + errorMessage?: string; data: T; hasMore: boolean; nextPageToken: string | null; @@ -74,8 +79,14 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => { nextPageToken: data.nextPageToken, })); } catch (error) { - console.error('Error fetching products:', error); - setFetchState(prevState => ({ ...prevState, isLoading: false, isError: true })); + const axiosError = error as AxiosError; + setFetchState((prevState) => ({ + ...prevState, + isLoading: false, + isError: true, + errorCode: axiosError.code, + errorMessage: axiosError.message, + })); } }, []); @@ -110,9 +121,9 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => { if (fetchState.isLoading && fetchState.data.length === 0) return ; if (fetchState.isError) - return 데이터를 불러오는 중에 문제가 발생했습니다.; + return ; if (!fetchState.data || fetchState.data.length === 0) - return 상품이 없습니다.; + return return ( @@ -149,14 +160,4 @@ const Wrapper = styled.section` } `; -const ErrorMessage = styled.p` - text-align: center; - margin-top: 20px; -`; - -const EmptyMessage = styled.p` - text-align: center; - margin-top: 20px; -`; - export default ThemeGoodsSection; diff --git a/src/components/features/Theme/ThemeHeroSection/index.tsx b/src/components/features/Theme/ThemeHeroSection/index.tsx index 73e00ea7..13ee2f21 100644 --- a/src/components/features/Theme/ThemeHeroSection/index.tsx +++ b/src/components/features/Theme/ThemeHeroSection/index.tsx @@ -1,9 +1,11 @@ import styled from '@emotion/styled'; +import type { AxiosError } from 'axios'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { fetchData } from '@/components/common/API/api'; import { Container } from '@/components/common/layouts/Container'; +import ErrorMessage from '@/components/common/Status/errorMessage'; import Loading from '@/components/common/Status/loading'; import { breakpoints } from '@/styles/variants'; @@ -19,6 +21,8 @@ interface ThemeData { interface FetchState { isLoading: boolean; isError: boolean; + errorMessage?: string; + errorCode?: string; data: T | null; } @@ -42,8 +46,14 @@ const ThemeHeroSection: React.FC = ({ themeKey }) => { const data = await fetchData('/api/v1/themes', { key }); setFetchState({ isLoading: false, isError: false, data: data.themes }); } catch (error) { - console.error('Error fetching themes:', error); - setFetchState({ isLoading: false, isError: true, data: null }); + const axiosError = error as AxiosError; + setFetchState({ + isLoading: false, + isError: true, + data: null, + errorMessage: axiosError.message, + errorCode: axiosError.code, + }); } }; @@ -62,7 +72,7 @@ const ThemeHeroSection: React.FC = ({ themeKey }) => { if (fetchState.isLoading) return ; if (fetchState.isError) - return

데이터를 불러오는 중에 문제가 발생했습니다.

; + return ; const currentTheme = fetchState.data?.find((theme) => theme.key === themeKey); if (!currentTheme) { From 08ecc59fd7c84eb3c5f397a687e0739c0016e9b9 Mon Sep 17 00:00:00 2001 From: userjmmm Date: Fri, 12 Jul 2024 14:50:37 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20step1=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=20=EC=BD=94=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=EC=84=9C=20ThemeGoodsSection/index.tsx=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Theme/ThemeGoodsSection/index.tsx | 84 +++++-------------- 1 file changed, 19 insertions(+), 65 deletions(-) diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx index b229c77e..d8aca79e 100644 --- a/src/components/features/Theme/ThemeGoodsSection/index.tsx +++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import type { AxiosError } from 'axios'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback,useEffect, useState } from 'react'; import { fetchData } from '@/components/common/API/api'; import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default'; @@ -23,28 +23,12 @@ interface ProductData { }; } -interface ApiResponse { - products: ProductData[]; - nextPageToken: string | null; - pageInfo: { - totalResults: number; - resultsPerPage: number; - }; -} - interface FetchState { isLoading: boolean; isError: boolean; errorCode?: string; errorMessage?: string; data: T; - hasMore: boolean; - nextPageToken: string | null; -} - -interface FetchParams { - maxResults: number; - pageToken?: string; } type Props = { @@ -56,74 +40,45 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => { isLoading: true, isError: false, data: [], - hasMore: true, - nextPageToken: null, - }); - - const observerRef = useRef(null); - const loadMoreRef = useRef(null); - const fetchProducts = useCallback(async (key: string, pageToken: string | null) => { - setFetchState(prevState => ({ ...prevState, isLoading: true })); + }); + const fetchProducts = useCallback(async (key: string) => { try { - const params: FetchParams = { maxResults: 20 }; - if (pageToken) params.pageToken = pageToken; - - const data: ApiResponse = await fetchData(`/api/v1/themes/${key}/products`, params); - setFetchState(prevState => ({ + const data = await fetchData(`/api/v1/themes/${key}/products?maxResults=20`); + setFetchState({ isLoading: false, isError: false, - data: [...prevState.data, ...data.products], - hasMore: data.products.length > 0, - nextPageToken: data.nextPageToken, - })); + data: data.products, + }); } catch (error) { + console.error('Error fetching products:', error); const axiosError = error as AxiosError; - setFetchState((prevState) => ({ - ...prevState, + setFetchState({ isLoading: false, isError: true, - errorCode: axiosError.code, + data: [], errorMessage: axiosError.message, - })); + errorCode: axiosError.code, + }); + } }, []); - // 초기 로딩 비동기 통신 +//초기 로딩 비동기 통신 useEffect(() => { if (themeKey) { - setFetchState({ isLoading: true, isError: false, data: [], hasMore: true, nextPageToken: null }); - fetchProducts(themeKey, null); + setFetchState({ isLoading: true, isError: false, data: []}); + fetchProducts(themeKey); } }, [themeKey, fetchProducts]); - // ref(스크롤)의 변화를 감지하고 다음 페이지를 불러오는 비동기 처리 - useEffect(() => { - if (fetchState.hasMore && !fetchState.isLoading) { - if (observerRef.current) observerRef.current.disconnect(); - - observerRef.current = new IntersectionObserver(entries => { - if (entries[0].isIntersecting && fetchState.nextPageToken) { - fetchProducts(themeKey, fetchState.nextPageToken); - } - }, { - threshold: 1.0, - }); - - if (loadMoreRef.current) { - observerRef.current.observe(loadMoreRef.current); - } - } - return () => observerRef.current?.disconnect(); - }, [fetchState.hasMore, fetchState.isLoading, fetchState.nextPageToken, themeKey, fetchProducts]); - - if (fetchState.isLoading && fetchState.data.length === 0) + if (fetchState.isLoading) return ; if (fetchState.isError) return ; if (!fetchState.data || fetchState.data.length === 0) - return + return ; return ( @@ -146,7 +101,6 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => { ))} -
); }; @@ -160,4 +114,4 @@ const Wrapper = styled.section` } `; -export default ThemeGoodsSection; +export default ThemeGoodsSection; \ No newline at end of file