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주차 과제 Step2 #57

Open
wants to merge 14 commits into
base: userjmmm
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## 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에 적용하기
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 });
}
Comment on lines +12 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굳이 이렇게 만들어주는 이유가 있을까요?
에러 객체에서 필요한 정보가 코드와 설명 이외에도 있으면 어떻게 대응하실 예정일까요?

}
};

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 = () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서는 왜 React.FC를 사용하셨나요?
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;
Comment on lines +38 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error?: { code?: string; message?: string }

error와 관련된 것들을 모아서 하나의 객체로 처리하면 어떨까요?

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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타입 단언이 좋은 선택일까요?
굳이 타입 단언을 하지 않고도 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 />
)}
Comment on lines +82 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삼항연산자를 중첩 사용하여 가독성이 좋지 않은 것 같아요. 조금 더 깔끔하게 표현해볼 수 있을까요?

</Container>
</Wrapper>
);
Expand Down Expand Up @@ -50,3 +115,5 @@ const Title = styled.h2`
line-height: 50px;
}
`;

export default GoodsRankingSection;
Loading