From 9de3360f75aba984347dfa1f32d15cd4156afa47 Mon Sep 17 00:00:00 2001
From: userjmmm
Date: Tue, 9 Jul 2024 20:21:02 +0900
Subject: [PATCH 01/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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/18] =?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
From 2a1328df9457dcf574901977da62547e02288552 Mon Sep 17 00:00:00 2001
From: userjmmm
Date: Fri, 12 Jul 2024 14:27:38 +0900
Subject: [PATCH 15/18] =?UTF-8?q?refactor:=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=EC=9A=94=EA=B5=AC?=
=?UTF-8?q?=EC=82=AC=ED=95=AD=20=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 | 117 ------------------
1 file changed, 117 deletions(-)
diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx
index d8aca79e..e69de29b 100644
--- a/src/components/features/Theme/ThemeGoodsSection/index.tsx
+++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx
@@ -1,117 +0,0 @@
-import styled from '@emotion/styled';
-import type { AxiosError } from 'axios';
-import { useCallback,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 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';
-
-interface ProductData {
- id: number;
- name: string;
- imageURL: string;
- price: {
- sellingPrice: number;
- };
- brandInfo: {
- name: string;
- };
-}
-
-interface FetchState {
- isLoading: boolean;
- isError: boolean;
- errorCode?: string;
- errorMessage?: string;
- data: T;
-}
-
-type Props = {
- themeKey: string;
-};
-
-const ThemeGoodsSection: React.FC = ({ themeKey }) => {
- const [fetchState, setFetchState] = useState>({
- isLoading: true,
- isError: false,
- data: [],
-
- });
-
- const fetchProducts = useCallback(async (key: string) => {
- try {
- const data = await fetchData(`/api/v1/themes/${key}/products?maxResults=20`);
- setFetchState({
- isLoading: false,
- isError: false,
- data: data.products,
- });
- } catch (error) {
- console.error('Error fetching products:', error);
- const axiosError = error as AxiosError;
- setFetchState({
- isLoading: false,
- isError: true,
- data: [],
- errorMessage: axiosError.message,
- errorCode: axiosError.code,
- });
-
- }
- }, []);
-
-//초기 로딩 비동기 통신
- useEffect(() => {
- if (themeKey) {
- setFetchState({ isLoading: true, isError: false, data: []});
- fetchProducts(themeKey);
- }
- }, [themeKey, fetchProducts]);
-
- if (fetchState.isLoading)
- return ;
- if (fetchState.isError)
- return ;
- if (!fetchState.data || fetchState.data.length === 0)
- return ;
-
- return (
-
-
-
- {fetchState.data.map(({ id, imageURL, name, price, brandInfo }) => (
-
- ))}
-
-
-
- );
-};
-
-const Wrapper = styled.section`
- width: 100%;
- padding: 28px 16px 180px;
-
- @media screen and (min-width: ${breakpoints.sm}) {
- padding: 40px 16px 360px;
- }
-`;
-
-export default ThemeGoodsSection;
\ No newline at end of file
From acf27758c3e11db5d25fdeafb65d1e5d49443475 Mon Sep 17 00:00:00 2001
From: userjmmm
Date: Fri, 12 Jul 2024 14:56:13 +0900
Subject: [PATCH 16/18] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?=
=?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Theme/ThemeGoodsSection/index.tsx | 117 ++++++++++++++++++
1 file changed, 117 insertions(+)
diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx
index e69de29b..d8aca79e 100644
--- a/src/components/features/Theme/ThemeGoodsSection/index.tsx
+++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx
@@ -0,0 +1,117 @@
+import styled from '@emotion/styled';
+import type { AxiosError } from 'axios';
+import { useCallback,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 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';
+
+interface ProductData {
+ id: number;
+ name: string;
+ imageURL: string;
+ price: {
+ sellingPrice: number;
+ };
+ brandInfo: {
+ name: string;
+ };
+}
+
+interface FetchState {
+ isLoading: boolean;
+ isError: boolean;
+ errorCode?: string;
+ errorMessage?: string;
+ data: T;
+}
+
+type Props = {
+ themeKey: string;
+};
+
+const ThemeGoodsSection: React.FC = ({ themeKey }) => {
+ const [fetchState, setFetchState] = useState>({
+ isLoading: true,
+ isError: false,
+ data: [],
+
+ });
+
+ const fetchProducts = useCallback(async (key: string) => {
+ try {
+ const data = await fetchData(`/api/v1/themes/${key}/products?maxResults=20`);
+ setFetchState({
+ isLoading: false,
+ isError: false,
+ data: data.products,
+ });
+ } catch (error) {
+ console.error('Error fetching products:', error);
+ const axiosError = error as AxiosError;
+ setFetchState({
+ isLoading: false,
+ isError: true,
+ data: [],
+ errorMessage: axiosError.message,
+ errorCode: axiosError.code,
+ });
+
+ }
+ }, []);
+
+//초기 로딩 비동기 통신
+ useEffect(() => {
+ if (themeKey) {
+ setFetchState({ isLoading: true, isError: false, data: []});
+ fetchProducts(themeKey);
+ }
+ }, [themeKey, fetchProducts]);
+
+ if (fetchState.isLoading)
+ return ;
+ if (fetchState.isError)
+ return ;
+ if (!fetchState.data || fetchState.data.length === 0)
+ return ;
+
+ return (
+
+
+
+ {fetchState.data.map(({ id, imageURL, name, price, brandInfo }) => (
+
+ ))}
+
+
+
+ );
+};
+
+const Wrapper = styled.section`
+ width: 100%;
+ padding: 28px 16px 180px;
+
+ @media screen and (min-width: ${breakpoints.sm}) {
+ padding: 40px 16px 360px;
+ }
+`;
+
+export default ThemeGoodsSection;
\ No newline at end of file
From 1145c6c0ebbd7ca46a7bb17d474d54c97e2c8dbe Mon Sep 17 00:00:00 2001
From: userjmmm
Date: Fri, 12 Jul 2024 18:57:24 +0900
Subject: [PATCH 17/18] =?UTF-8?q?feat:=20=EC=8A=A4=ED=81=AC=EB=A1=A4?=
=?UTF-8?q?=EC=9D=84=20=EB=82=B4=EB=A6=AC=EB=A9=B4=20=EC=B6=94=EA=B0=80?=
=?UTF-8?q?=EB=A1=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=9A=94?=
=?UTF-8?q?=EC=B2=AD=ED=95=98=EC=97=AC=20=EB=B3=B4=EC=97=AC=EC=A7=80?=
=?UTF-8?q?=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 | 29 ++++-
.../Theme/ThemeGoodsSection/index.tsx | 118 ++++++++++++------
2 files changed, 110 insertions(+), 37 deletions(-)
diff --git a/README.md b/README.md
index 36effb02..378863c6 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,23 @@
+## 1단계 - API 적용하기
+
+### 기능 구현 목록
+- [ ] 첨부된 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개의 상품 목록이 내려오도록 구현
+
+
## 2단계 - Error, Loading Status 핸들링 하기
### 기능 구현 목록
@@ -7,4 +27,11 @@
- [x] 데이터가 없는 경우에 대한 UI component 만들기
- [x] Http Status에 따라 Error UI component 만들기
-- [x] 1단계에서 사용 중인 API에 적용하기
\ No newline at end of file
+- [x] 1단계에서 사용 중인 API에 적용하기
+
+
+## 3단계 - 테마 별 선물 추천 API에 페이지네이션 구현하기 & React Query 사용해보기
+
+### 기능 구현 목록
+- [x] 스크롤을 내리면 추가로 데이터를 요청하여 보여지도록 구현
+- [ ] 1단계에서 구현한 API를 react-query를 사용해서 구현
\ 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 d8aca79e..2f427eb3 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, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { fetchData } from '@/components/common/API/api';
import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default';
@@ -23,12 +23,28 @@ interface ProductData {
};
}
+interface ApiResponse {
+ products: ProductData[];
+ nextPageToken: string | null;
+ pageInfo: {
+ totalResults: number;
+ resultsPerPage: number;
+ };
+}
+
interface FetchState {
isLoading: boolean;
isError: boolean;
- errorCode?: string;
errorMessage?: string;
+ errorCode?: string;
data: T;
+ hasMore: boolean;
+ nextPageToken: string | null;
+}
+
+interface FetchParams {
+ maxResults: number;
+ pageToken?: string;
}
type Props = {
@@ -40,45 +56,74 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => {
isLoading: true,
isError: false,
data: [],
-
+ hasMore: true,
+ nextPageToken: null,
});
- const fetchProducts = useCallback(async (key: string) => {
- try {
- const data = await fetchData(`/api/v1/themes/${key}/products?maxResults=20`);
- setFetchState({
- isLoading: false,
- isError: false,
- data: data.products,
- });
- } catch (error) {
- console.error('Error fetching products:', error);
- const axiosError = error as AxiosError;
- setFetchState({
- isLoading: false,
- isError: true,
- data: [],
- errorMessage: axiosError.message,
- errorCode: axiosError.code,
- });
-
- }
- }, []);
+const observerRef = useRef(null);
+const loadMoreRef = useRef(null);
-//초기 로딩 비동기 통신
- useEffect(() => {
- if (themeKey) {
- setFetchState({ isLoading: true, isError: false, data: []});
- fetchProducts(themeKey);
+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) {
+ const axiosError = error as AxiosError;
+ setFetchState({
+ isLoading: false,
+ isError: true,
+ errorMessage: axiosError.message,
+ errorCode: axiosError.code,
+ data: [],
+ hasMore: false,
+ nextPageToken: null,
+ });
+ }
+}, []);
+
+useEffect(() => {
+ if (themeKey) {
+ setFetchState({ isLoading: true, isError: false, data: [], hasMore: true, nextPageToken: null });
+ fetchProducts(themeKey, null);
+ }
+}, [themeKey, fetchProducts]);
+
+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);
}
- }, [themeKey, fetchProducts]);
+ }
+ return () => observerRef.current?.disconnect();
+}, [fetchState.hasMore, fetchState.isLoading, fetchState.nextPageToken, themeKey, fetchProducts]);
- if (fetchState.isLoading)
- return ;
- if (fetchState.isError)
- return ;
- if (!fetchState.data || fetchState.data.length === 0)
- return ;
+if (fetchState.isLoading && fetchState.data.length === 0)
+ return ;
+if (fetchState.isError)
+ return ;
+if (!fetchState.data || fetchState.data.length === 0)
+ return ;
return (
@@ -101,6 +146,7 @@ const ThemeGoodsSection: React.FC = ({ themeKey }) => {
))}
+
);
};
From d04fb695b1c1026b41eb7c4f1d9c5350d0c751a0 Mon Sep 17 00:00:00 2001
From: userjmmm
Date: Fri, 12 Jul 2024 21:33:15 +0900
Subject: [PATCH 18/18] =?UTF-8?q?feat:=201=EB=8B=A8=EA=B3=84=EC=97=90?=
=?UTF-8?q?=EC=84=9C=20=EA=B5=AC=ED=98=84=ED=95=9C=20API=EB=A5=BC=20react-?=
=?UTF-8?q?query=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=B4=EC=84=9C=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 | 2 +-
src/App.tsx | 12 +-
.../Theme/ThemeGoodsSection/index.tsx | 148 ++++++++----------
3 files changed, 73 insertions(+), 89 deletions(-)
diff --git a/README.md b/README.md
index 378863c6..08b96d4d 100644
--- a/README.md
+++ b/README.md
@@ -34,4 +34,4 @@
### 기능 구현 목록
- [x] 스크롤을 내리면 추가로 데이터를 요청하여 보여지도록 구현
-- [ ] 1단계에서 구현한 API를 react-query를 사용해서 구현
\ No newline at end of file
+- [x] 1단계에서 구현한 API를 react-query를 사용해서 구현
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 26d8766c..e48f1a9b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,11 +1,17 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
import { AuthProvider } from './provider/Auth';
import { Routes } from './routes';
+const queryClient = new QueryClient();
+
const App = () => {
return (
-
-
-
+
+
+
+
+
);
};
diff --git a/src/components/features/Theme/ThemeGoodsSection/index.tsx b/src/components/features/Theme/ThemeGoodsSection/index.tsx
index 2f427eb3..70f6de56 100644
--- a/src/components/features/Theme/ThemeGoodsSection/index.tsx
+++ b/src/components/features/Theme/ThemeGoodsSection/index.tsx
@@ -1,6 +1,7 @@
import styled from '@emotion/styled';
+import { useInfiniteQuery } from '@tanstack/react-query';
import type { AxiosError } from 'axios';
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useEffect, useRef } from 'react';
import { fetchData } from '@/components/common/API/api';
import { DefaultGoodsItems } from '@/components/common/GoodsItem/Default';
@@ -32,16 +33,6 @@ interface ApiResponse {
};
}
-interface FetchState {
- isLoading: boolean;
- isError: boolean;
- errorMessage?: string;
- errorCode?: string;
- data: T;
- hasMore: boolean;
- nextPageToken: string | null;
-}
-
interface FetchParams {
maxResults: number;
pageToken?: string;
@@ -51,79 +42,65 @@ type Props = {
themeKey: string;
};
-const ThemeGoodsSection: React.FC = ({ themeKey }) => {
- const [fetchState, setFetchState] = useState>({
- 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 }));
-
- 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) {
- const axiosError = error as AxiosError;
- setFetchState({
- isLoading: false,
- isError: true,
- errorMessage: axiosError.message,
- errorCode: axiosError.code,
- data: [],
- hasMore: false,
- nextPageToken: null,
- });
- }
-}, []);
+const fetchProducts = async (themeKey: string, pageParam: string | null): Promise => {
+ const params: FetchParams = { maxResults: 20 };
+ if (pageParam)
+ params.pageToken = pageParam;
-useEffect(() => {
- if (themeKey) {
- setFetchState({ isLoading: true, isError: false, data: [], hasMore: true, nextPageToken: null });
- fetchProducts(themeKey, null);
- }
-}, [themeKey, fetchProducts]);
+ return fetchData(`/api/v1/themes/${themeKey}/products`, params);
+};
-useEffect(() => {
- if (fetchState.hasMore && !fetchState.isLoading) {
- if (observerRef.current) observerRef.current.disconnect();
+const ThemeGoodsSection: React.FC = ({ themeKey }) => {
+ const observerRef = useRef(null);
+ const loadMoreRef = useRef(null);
+
+ const {
+ data,
+ error,
+ isLoading,
+ isError,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = useInfiniteQuery({
+ queryKey: ['products', themeKey],
+ queryFn: ({ pageParam = null }) => fetchProducts(themeKey, pageParam as string || null),
+ getNextPageParam: (lastPage) => lastPage.nextPageToken,
+ initialPageParam: null,
+ });
- observerRef.current = new IntersectionObserver(entries => {
- if (entries[0].isIntersecting && fetchState.nextPageToken) {
- fetchProducts(themeKey, fetchState.nextPageToken);
+ useEffect(() => {
+ if (hasNextPage && !isFetchingNextPage) {
+ if (observerRef.current) observerRef.current.disconnect();
+
+ observerRef.current = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].isIntersecting) {
+ void fetchNextPage();
+ }
+ },
+ {
+ threshold: 1.0,
+ }
+ );
+
+ if (loadMoreRef.current) {
+ observerRef.current.observe(loadMoreRef.current);
}
- }, {
- 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 ;
-if (fetchState.isError)
- return ;
-if (!fetchState.data || fetchState.data.length === 0)
- return ;
+ return () => observerRef.current?.disconnect();
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
+
+ if (isLoading)
+ return ;
+ if (isError)
+ return ;
+ if (!data || data.pages[0].products.length === 0)
+ return ;
+
+ const products = data.pages.flatMap(
+ (page) => page.products
+ );
return (
@@ -135,18 +112,19 @@ if (!fetchState.data || fetchState.data.length === 0)
}}
gap={16}
>
- {fetchState.data.map(({ id, imageURL, name, price, brandInfo }) => (
+ {products.map((product: ProductData) => (
))}
+ {isFetchingNextPage && }
);
};