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] feature/#267 Suspense, ErrorBoundary, ErrorHandling 작업 구현 #278

Merged
merged 10 commits into from
Aug 14, 2023
13 changes: 13 additions & 0 deletions frontend/src/apis/getMapApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const getMapApi = (url: string) =>
fetch(url, {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
})
.then((data) => {
return data.json();
})
.catch((error) => {
throw new Error(`${error.message}`);
});
9 changes: 9 additions & 0 deletions frontend/src/assets/LoginErrorIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions frontend/src/assets/NotFoundIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions frontend/src/components/ErrorBoundary/ApiErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
children?: ReactNode;
fallback: React.ElementType;
}

interface State {
hasError: boolean;
info: Error | null;
}

class ApiErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
info: null,
};

public static getDerivedStateFromError(error: Error): State {
return { hasError: true, info: error };
}

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}

public render() {
const { hasError, info } = this.state;
const { children } = this.props;
if (hasError) {
return <this.props.fallback error={info} />;
}

return children;
}
}

export default ApiErrorBoundary;
38 changes: 38 additions & 0 deletions frontend/src/components/ErrorBoundary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
children?: ReactNode;
fallback: React.ElementType;
}

interface State {
hasError: boolean;
info: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
info: null,
};

public static getDerivedStateFromError(error: Error): State {
return { hasError: true, info: error };
}

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}

public render() {
const { hasError, info } = this.state;
const { children } = this.props;
if (hasError) {
return <this.props.fallback error={info} />;
}

return children;
}
}

export default ErrorBoundary;
33 changes: 33 additions & 0 deletions frontend/src/components/Loader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import styled, { keyframes } from 'styled-components';

const Loader = () => {
return (
<LoaderBackWrapper>
<LoaderWrapper />
</LoaderBackWrapper>
);
};

const Rotate = keyframes`
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
`;

const LoaderBackWrapper = styled.div`
border-radius: 10px;
`;

const LoaderWrapper = styled.div`
border: 10px solid #f3f3f3;
border-top: 10px solid #3498db;
border-radius: 50%;
width: 80px;
height: 80px;
animation: ${Rotate} 1s linear infinite;
`;

export default Loader;
45 changes: 45 additions & 0 deletions frontend/src/components/NotFound/LoginError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { styled } from 'styled-components';
import LoginErrorIcon from '../../assets/LoginErrorIcon.svg';
import Button from '../common/Button';
import Flex from '../common/Flex';
import Space from '../common/Space';
import Text from '../common/Text';

const LoginError = () => {
return (
<Flex
$flexDirection="column"
$justifyContent="center"
$alignItems="center"
width="100vw"
height="100vh"
>
<LoginErrorIcon />
<Space size={6} />
<Flex
$flexDirection="column"
$justifyContent="center"
$alignItems="center"
>
<Text color="black" $fontSize="extraLarge" $fontWeight="bold">
나만의 지도를 만들어 보세요.
</Text>
<Space size={5} />
<NotFoundButton variant="primary">카카오로 시작하기</NotFoundButton>
</Flex>
</Flex>
);
};

const NotFoundButton = styled(Button)`
width: 270px;
height: 56px;

background-color: rgb(255, 220, 0);

color: ${({ theme }) => theme.color.black};
font-weight: ${({ theme }) => theme.fontWeight['bold']};
border: 1px solid ${({ theme }) => theme.color.white};
`;

export default LoginError;
60 changes: 60 additions & 0 deletions frontend/src/components/NotFound/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { styled } from 'styled-components';
import NotFoundIcon from '../../assets/NotFoundIcon.svg';
import useNavigator from '../../hooks/useNavigator';
import Button from '../common/Button';
import Flex from '../common/Flex';
import Space from '../common/Space';
import Text from '../common/Text';

const NotFound = () => {
const { routePage } = useNavigator();

return (
<NotFoundContainer
$justifyContent="center"
$alignItems="center"
width="100vw"
height="100vh"
>
<NotFoundIcon />
<Space size={6} />
<Flex
$flexDirection="column"
$justifyContent="center"
$alignItems="center"
>
<Text color="black" $fontSize="extraLarge" $fontWeight="bold">
요청하신 페이지를 찾을 수 없습니다.
</Text>
<Text color="black" $fontSize="extraLarge" $fontWeight="bold">
주소를 확인해 주세요.
</Text>
<Space size={5} />
<NotFoundButton variant="secondary" onClick={() => routePage('/')}>
돌아가기
</NotFoundButton>
</Flex>
</NotFoundContainer>
);
};

const NotFoundContainer = styled(Flex)`
flex-direction: row;
@media screen and (max-width: 700px) {
flex-direction: column;
}
`;

const NotFoundButton = styled(Button)`
width: 116px;
height: 48px;

font-weight: ${({ theme }) => theme.fontWeight['bold']};

&:hover {
color: ${({ theme }) => theme.color.white};
background-color: ${({ theme }) => theme.color.primary};
border: 1px solid ${({ theme }) => theme.color.primary};
}
`;
export default NotFound;
4 changes: 2 additions & 2 deletions frontend/src/components/PinPreview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ export interface PinPreviewProps {
pinTitle: string;
pinLocation: string;
pinInformation: string;
setSelectedPinId: (value: number) => void;
setSelectedPinId: React.Dispatch<React.SetStateAction<number | null>>;
pinId: number;
topicId: string | undefined;
tagPins: string[];
setTagPins: (value: string[]) => void;
setTagPins: React.Dispatch<React.SetStateAction<string[]>>;
taggedPinIds: number[];
setTaggedPinIds: React.Dispatch<React.SetStateAction<number[]>>;
setIsEditPinDetail: React.Dispatch<React.SetStateAction<boolean>>;
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/components/PinsOfTopic/PinsOfTopicSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Flex from '../common/Flex';
import PinSkeleton from '../common/PinSkeleton';
import Space from '../common/Space';
import TopicSkeleton from '../common/TopicSkeleton';

const PinsOfTopicSkeleton = () => {
return (
<Flex $flexDirection="column">
<Space size={7} />
<TopicSkeleton skeletonType="horizon" />
<Space size={7} />
<PinSkeleton />
<Space size={6} />
<PinSkeleton />
<Space size={6} />
<PinSkeleton />
</Flex>
);
};

export default PinsOfTopicSkeleton;
67 changes: 67 additions & 0 deletions frontend/src/components/PinsOfTopic/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { TopicInfoType } from '../../types/Topic';
import Space from '../common/Space';
import PinPreview from '../PinPreview';
import TopicInfo from '../TopicInfo';

interface PinsOfTopicProps {
topicId: string | undefined;
tagPins: string[];
topicDetail: TopicInfoType[];
taggedPinIds: number[];
setSelectedPinId: React.Dispatch<React.SetStateAction<number | null>>;
setTagPins: React.Dispatch<React.SetStateAction<string[]>>;
setTaggedPinIds: React.Dispatch<React.SetStateAction<number[]>>;
setIsEditPinDetail: React.Dispatch<React.SetStateAction<boolean>>;
}

const PinsOfTopic = ({
topicId,
tagPins,
topicDetail,
taggedPinIds,
setSelectedPinId,
setTagPins,
setTaggedPinIds,
setIsEditPinDetail,
}: PinsOfTopicProps) => {
return (
<>
{topicDetail.map((topic, idx) => {
return (
<ul key={topic.id}>
{idx !== 0 && <Space size={5} />}
<TopicInfo
fullUrl={topicId}
topicId={topicId?.split(',')[idx]}
topicParticipant={1}
pinNumber={topic.pinCount}
topicTitle={topic.name}
topicOwner={'토픽을 만든 사람'}
topicDescription={topic.description}
/>
{topic.pins.map((pin) => (
<li key={pin.id}>
<Space size={3} />
<PinPreview
pinTitle={pin.name}
pinLocation={pin.address}
pinInformation={pin.description}
setSelectedPinId={setSelectedPinId}
pinId={Number(pin.id)}
topicId={topicId}
tagPins={tagPins}
setTagPins={setTagPins}
taggedPinIds={taggedPinIds}
setTaggedPinIds={setTaggedPinIds}
setIsEditPinDetail={setIsEditPinDetail}
/>
</li>
))}
</ul>
);
})}
</>
);
};

export default PinsOfTopic;
19 changes: 19 additions & 0 deletions frontend/src/components/SeeAllCardList/SeeAllCardListSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Flex from '../common/Flex';
import Space from '../common/Space';
import TopicSkeleton from '../common/TopicSkeleton';

const SeeAllCardListSkeleton = () => {
return (
<Flex $flexDirection="column">
<TopicSkeleton skeletonType="horizon" />
<Space size={3} />
<TopicSkeleton skeletonType="horizon" />
<Space size={3} />
<TopicSkeleton skeletonType="horizon" />
<Space size={3} />
<TopicSkeleton skeletonType="horizon" />
</Flex>
);
};

export default SeeAllCardListSkeleton;
15 changes: 15 additions & 0 deletions frontend/src/components/TopicCardList/TopicCardListSeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Flex from '../common/Flex';
import Space from '../common/Space';
import TopicSkeleton from '../common/TopicSkeleton';

const TopicCardListSeleton = () => {
return (
<Flex>
<TopicSkeleton skeletonType="vertical" />
<Space size={3} />
<TopicSkeleton skeletonType="vertical" />
</Flex>
);
};

export default TopicCardListSeleton;
Loading
Loading