Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

경북대 FE_이정민_3주차 과제 Step3 #87

Open
wants to merge 18 commits into
base: userjmmm
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9de3360
docs: README.md에 구현할 기능 목록 정리
Jul 9, 2024
4817cc4
feat: 메인 페이지- ThemeCategorySection 구현
Jul 9, 2024
f6df8eb
feat: ThemeData에 imageURL 받아오도록 추가
Jul 9, 2024
1f441f6
feat: 메인페이지 - GoodsRankingSection 필터 조건에 맞게 구현
Jul 9, 2024
fb9b0d5
feat: Theme 페이지 - header section 구현
Jul 9, 2024
5c4521d
feat: themKey가 잘못된 경우 useNavigate로 리디렉션 구현
Jul 9, 2024
6b08761
feat: /api/v1/themes/{themeKey}/products API를 사용하여 상품 목록을 구현
Jul 9, 2024
923c885
feat: API 요청 시 한번에 20개의 상품 목록이 내려오도록 구현
Jul 10, 2024
402dba8
feat: Loading 상태를 보여주는 UI component 생성
Jul 11, 2024
98221da
feat: 1단계에서 사용 중인 API에 Loading Component 적용
Jul 11, 2024
b12d2cd
feat: 데이터가 없는 경우에 대한 UI component 생성
Jul 11, 2024
bb2665e
feat: Http Status에 따라 Error UI component 생성
Jul 11, 2024
f638bf9
feat: 1단계에서 사용 중인 API에 적용하기
Jul 11, 2024
08ecc59
refactor: step1에서 수정한 코드에 맞춰서 ThemeGoodsSection/index.tsx 수정
Jul 12, 2024
2a1328d
refactor: API 요청 시 한번에 20개의 상품 목록이 내려오도록 요구사항 수정
Jul 12, 2024
acf2775
refactor: 필요없는 코드 제거
Jul 12, 2024
1145c6c
feat: 스크롤을 내리면 추가로 데이터를 요청하여 보여지도록 구현
Jul 12, 2024
d04fb69
feat: 1단계에서 구현한 API를 react-query를 사용해서 구현
Jul 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## 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 핸들링 하기

### 기능 구현 목록
- 각 API에서 Loading 상태에 대한 UI 대응
- [x] Loading 상태를 보여주는 UI component 만들기
- [x] 1단계에서 사용 중인 API에 적용하기

- [x] 데이터가 없는 경우에 대한 UI component 만들기
- [x] Http Status에 따라 Error UI component 만들기
- [x] 1단계에서 사용 중인 API에 적용하기


## 3단계 - 테마 별 선물 추천 API에 페이지네이션 구현하기 & React Query 사용해보기

### 기능 구현 목록
- [x] 스크롤을 내리면 추가로 데이터를 요청하여 보여지도록 구현
- [x] 1단계에서 구현한 API를 react-query를 사용해서 구현
12 changes: 9 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AuthProvider>
<Routes />
</AuthProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Routes />
</AuthProvider>
</QueryClientProvider>
);
};

Expand Down
20 changes: 20 additions & 0 deletions src/components/common/API/api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import axios from 'axios';

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) {
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 });
}
}
};

export default Api;
27 changes: 27 additions & 0 deletions src/components/common/Status/emptyData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from "@emotion/styled";

interface EmptyDataProps {
message?: string;
}

const EmptyData = ({ message = "보여줄 데이터가 없어요 🤨" }: EmptyDataProps) => {
return (
<EmptyDataContainer>
{message}
</EmptyDataContainer>
)
};

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;
37 changes: 37 additions & 0 deletions src/components/common/Status/errorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import styled from "@emotion/styled";

interface ErrorMessageProps {
code?: string;
message: string;
}

const ErrorMessage = ({ code, message }: ErrorMessageProps) => {
return (
<ErrorContainer>
{code && <Code>(Error Code: {code}) </Code>} {message} <Emoji>😔</Emoji>
</ErrorContainer>
)
};

const ErrorContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
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;
35 changes: 35 additions & 0 deletions src/components/common/Status/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';

const Loading: React.FC = () => {
return (
<LoadingContainer>
<Spinner />
</LoadingContainer>
);
};

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;
77 changes: 72 additions & 5 deletions src/components/features/Home/GoodsRankingSection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,93 @@
import styled from '@emotion/styled';
import { useState } from 'react';
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';
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<T> {
isLoading: boolean;
isError: boolean;
errorCode?: string;
errorMessage?: string;
data: T | null;
}

export const GoodsRankingSection: React.FC = () => {
const [filterOption, setFilterOption] = useState<RankingFilterOption>({
targetType: 'ALL',
rankType: 'MANY_WISH',
});

// GoodsMockData를 21번 반복 생성
const [fetchState, setFetchState] = useState<FetchState<ProductData[]>>({
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) {
const axiosError = error as AxiosError;
setFetchState({
isLoading: false,
isError: true,
data: null,
errorMessage: axiosError.message,
errorCode: axiosError.code,
});
}
};

fetchRankingProducts(filterOption);
}, [filterOption]);

return (
<Wrapper>
<Container>
<Title>실시간 급상승 선물랭킹</Title>
<GoodsRankingFilter filterOption={filterOption} onFilterOptionChange={setFilterOption} />
<GoodsRankingList goodsList={GoodsMockList} />
{fetchState.isLoading ? (
<Loading />
) : fetchState.isError ? (
<ErrorMessage code= {fetchState.errorCode} message={fetchState.errorMessage || '데이터를 불러오는 중에 문제가 발생했습니다.'} />
) : fetchState.data && fetchState.data.length > 0 ? (
<GoodsRankingList goodsList={fetchState.data} />
) : (
<EmptyData />
)}
</Container>
</Wrapper>
);
Expand Down Expand Up @@ -50,3 +115,5 @@ const Title = styled.h2`
line-height: 50px;
}
`;

export default GoodsRankingSection;
Loading