Skip to content

Commit e33bd42

Browse files
committed
Handle NotFound for velog, velog post, velog series, tag
1 parent 1cec5d8 commit e33bd42

24 files changed

+343
-26
lines changed

.env

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
HTTPS=true
21
REACT_APP_API_HOST=https://v2dev.velog.io/
32
PUBLIC_URL=https://static.velog.io/

config/env.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ function getClientEnvironment(publicUrl) {
8585
}, {}),
8686
};
8787

88+
console.log(stringified);
89+
8890
const stringifiedForServerless = Object.keys(raw).reduce((env, key) => {
8991
env[`process.env.${key}`] = JSON.stringify(raw[key]);
9092
return env;

config/webpack.config.serverless.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
process.env.BABEL_ENV = 'production';
22
process.env.NODE_ENV = 'production';
33
process.env.REACT_APP_SSR = 'enabled';
4+
process.env.PUBLIC_URL = process.env.PUBLIC_URL || 'https://static.velog.io';
45

56
const CopyPlugin = require('copy-webpack-plugin');
67
const nodeExternals = require('webpack-node-externals');
@@ -19,7 +20,9 @@ const imageInlineSizeLimit = parseInt(
1920
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000',
2021
);
2122

22-
const publicUrl = paths.servedPath.slice(0, -1);
23+
console.log(process.env.PUBLIC_URL);
24+
const publicUrl = (paths.servedPath || process.env.PUBLIC_URL).slice(0, -1);
25+
console.log({ publicUrl });
2326
const env = getClientEnvironment(publicUrl);
2427

2528
module.exports = {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@loadable/babel-plugin": "^5.11.0",
1313
"@loadable/component": "^5.11.0",
1414
"@loadable/server": "^5.11.0",
15+
"@reduxjs/toolkit": "^1.2.2",
1516
"@svgr/webpack": "4.3.3",
1617
"@testing-library/jest-dom": "^4.2.4",
1718
"@testing-library/react": "^9.3.2",

src/App.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import RegisterPage from './pages/RegisterPage';
99
import { JazzbarProvider } from './lib/jazzbar';
1010
import PageTemplate from './components/base/PageTemplate';
1111
import VelogPageFallback from './containers/velog/VelogPageFallback';
12+
import ErrorBoundary from './components/error/ErrorBoundary';
13+
import NotFoundPage from './pages/NotFoundPage';
1214

1315
const loadableConfig = {
1416
fallback: <PageTemplate />,
@@ -43,20 +45,26 @@ interface AppProps {}
4345
const App: React.FC<AppProps> = props => {
4446
return (
4547
<JazzbarProvider>
46-
<Switch>
47-
<Route path="/" component={MainPage} exact />
48-
<Route path="/register" component={RegisterPage} />
49-
<Route path="/:mode(trending|recent|following)" component={MainPage} />
50-
<Route path="/@:username" component={VelogPage} />
51-
{/* <Route path="/@:username/:urlSlug" component={PostPage} /> */}
52-
<Route path="/email-login" component={EmailLoginPage} />
53-
<Route path="/write" component={WritePage} />
54-
<Route path="/search" component={SearchPage} />
55-
<Route path="/saves" component={SavesPage} />
56-
<Route path="/tags" component={TagsPage} />
57-
<Route path={['/policy/:type?']} component={PolicyPage} />
58-
<Route path="/setting" component={SettingPage} />
59-
</Switch>
48+
<ErrorBoundary>
49+
<Switch>
50+
<Route path="/" component={MainPage} exact />
51+
<Route path="/register" component={RegisterPage} />
52+
<Route
53+
path="/:mode(trending|recent|following)"
54+
component={MainPage}
55+
/>
56+
<Route path="/@:username" component={VelogPage} />
57+
{/* <Route path="/@:username/:urlSlug" component={PostPage} /> */}
58+
<Route path="/email-login" component={EmailLoginPage} />
59+
<Route path="/write" component={WritePage} />
60+
<Route path="/search" component={SearchPage} />
61+
<Route path="/saves" component={SavesPage} />
62+
<Route path="/tags" component={TagsPage} />
63+
<Route path={['/policy/:type?']} component={PolicyPage} />
64+
<Route path="/setting" component={SettingPage} />
65+
<Route component={NotFoundPage} />
66+
</Switch>
67+
</ErrorBoundary>
6068
<Core />
6169
</JazzbarProvider>
6270
);

src/components/common/StatusCode.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
import { Route } from 'react-router-dom';
3+
4+
export type StatusCodeProps = {
5+
statusCode: number;
6+
};
7+
8+
function StatusCode({ statusCode }: StatusCodeProps) {
9+
return (
10+
<Route
11+
render={({ staticContext }) => {
12+
if (staticContext) {
13+
staticContext.statusCode = statusCode;
14+
}
15+
return null;
16+
}}
17+
/>
18+
);
19+
}
20+
21+
export default StatusCode;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
4+
export type CrashErrorScreenProps = {};
5+
6+
function CrashErrorScreen(props: CrashErrorScreenProps) {
7+
return <div></div>;
8+
}
9+
10+
const Screen = styled.div`
11+
width: 100%;
12+
height: 100%;
13+
display: flex;
14+
`;
15+
16+
export default CrashErrorScreen;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import useNotFound from '../../lib/hooks/useNotFound';
3+
import NotFoundPage from '../../pages/NotFoundPage';
4+
5+
class ErrorBoundary extends React.Component {
6+
state = {
7+
hasError: false,
8+
};
9+
static getDerivedStateFromError(error: Error) {
10+
return { hasError: true };
11+
}
12+
componentDidCatch(error: Error, errorInfo: any) {
13+
console.log({
14+
error,
15+
errorInfo,
16+
stringifiedError: JSON.stringify(error || {}),
17+
});
18+
}
19+
render() {
20+
return (
21+
<ErrorBoundaryWrapper hasError={this.state.hasError}>
22+
{this.props.children}
23+
</ErrorBoundaryWrapper>
24+
);
25+
}
26+
}
27+
28+
type ErrorBoundaryWrapperProps = {
29+
children: React.ReactNode;
30+
hasError: boolean;
31+
};
32+
function ErrorBoundaryWrapper(props: ErrorBoundaryWrapperProps) {
33+
const { isNotFound } = useNotFound();
34+
35+
if (isNotFound) {
36+
return <NotFoundPage />;
37+
}
38+
39+
return <>{props.children}</>;
40+
}
41+
42+
export default ErrorBoundary;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react';
2+
import styled from 'styled-components';
3+
import media from '../../lib/styles/media';
4+
import palette from '../../lib/styles/palette';
5+
import Button from '../common/Button';
6+
7+
export type ErrorScreenTemplateProps = {
8+
image: string;
9+
message: string;
10+
buttonText?: string;
11+
onButtonClick?: () => void;
12+
};
13+
14+
function ErrorScreenTemplate({
15+
image,
16+
message,
17+
buttonText,
18+
onButtonClick,
19+
}: ErrorScreenTemplateProps) {
20+
return (
21+
<Screen>
22+
<img src={image} alt="error-image" />
23+
<div className="message">{message}</div>
24+
{buttonText && (
25+
<div className="button-wrapper">
26+
<Button size="large" onClick={onButtonClick}>
27+
{buttonText}
28+
</Button>
29+
</div>
30+
)}
31+
</Screen>
32+
);
33+
}
34+
35+
const Screen = styled.div`
36+
display: flex;
37+
width: 100%;
38+
height: 100%;
39+
align-items: center;
40+
justify-content: center;
41+
flex-direction: column;
42+
img {
43+
width: 20rem;
44+
height: auto;
45+
${media.small} {
46+
width: 12rem;
47+
}
48+
}
49+
.message {
50+
padding-left: 1rem;
51+
padding-right: 1rem;
52+
53+
font-size: 2.5rem;
54+
color: ${palette.gray8};
55+
margin-top: 2rem;
56+
${media.small} {
57+
margin-top: 1rem;
58+
}
59+
}
60+
.button-wrapper {
61+
margin-top: 2rem;
62+
${media.small} {
63+
margin-top: 1rem;
64+
}
65+
}
66+
`;
67+
68+
export default ErrorScreenTemplate;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
import ErrorScreenTemplate from './ErrorScreenTemplate';
3+
import { undrawPageNotFound } from '../../static/images';
4+
import { useHistory } from 'react-router-dom';
5+
import StatusCode from '../common/StatusCode';
6+
import useNotFound from '../../lib/hooks/useNotFound';
7+
8+
export type NotFoundErrorProps = {};
9+
10+
function NotFoundError(props: NotFoundErrorProps) {
11+
const history = useHistory();
12+
const { reset } = useNotFound();
13+
14+
return (
15+
<>
16+
<ErrorScreenTemplate
17+
image={undrawPageNotFound}
18+
message="아무것도 없네요!"
19+
buttonText="홈으로"
20+
onButtonClick={() => {
21+
history.push('/');
22+
reset();
23+
}}
24+
/>
25+
<StatusCode statusCode={404} />
26+
</>
27+
);
28+
}
29+
30+
export default NotFoundError;

src/containers/post/PostViewer.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import styled from 'styled-components';
3030
import { useMount } from 'react-use';
3131
import PostSkeleton from '../../components/post/PostSkeleton';
3232
import media from '../../lib/styles/media';
33+
import useNotFound from '../../lib/hooks/useNotFound';
3334

3435
const UserProfileWrapper = styled(VelogResponsive)`
3536
margin-top: 16rem;
@@ -88,17 +89,22 @@ const PostViewer: React.FC<PostViewerProps> = ({
8889
const [postView] = useMutation(POST_VIEW);
8990
const [likePost, { loading: loadingLike }] = useMutation(LIKE_POST);
9091
const [unlikePost, { loading: loadingUnlike }] = useMutation(UNLIKE_POST);
92+
const { showNotFound } = useNotFound();
9193

9294
const { loading, error, data } = readPost;
9395

9496
useEffect(() => {
97+
if (data && data.post === null) {
98+
showNotFound();
99+
return;
100+
}
95101
if (!data || !data.post) return;
96102
postView({
97103
variables: {
98104
id: data.post.id,
99105
},
100106
});
101-
}, [data, postView]);
107+
}, [data, postView, showNotFound]);
102108

103109
const prefetchLinkedPosts = useCallback(() => {
104110
if (!data || !data.post) return;
@@ -256,9 +262,7 @@ const PostViewer: React.FC<PostViewerProps> = ({
256262
const ownPost = post.user.id === userId;
257263
const message = ownPost
258264
? `제가 velog 에 게재한 "${post.title}" 포스트를 읽어보세요.`
259-
: `${post.user.username}님이 velog 에 게재한 "${
260-
post.title
261-
}" 포스트를 읽어보세요.`;
265+
: `${post.user.username}님이 velog 에 게재한 "${post.title}" 포스트를 읽어보세요.`;
262266

263267
shareTwitter(link, message);
264268
break;

src/containers/tags/TagDetailContainer.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useEffect } from 'react';
22
import TagDetail, { TagDetailSkeleton } from '../../components/tags/TagDetail';
33
import { useQuery } from '@apollo/react-hooks';
44
import { GET_TAG, GetTagResponse } from '../../lib/graphql/tags';
@@ -8,6 +8,7 @@ import useScrollPagination from '../../lib/hooks/useScrollPagination';
88
import PostCardList, {
99
PostCardListSkeleton,
1010
} from '../../components/common/PostCardList';
11+
import useNotFound from '../../lib/hooks/useNotFound';
1112

1213
export type TagDetailContainerProps = {
1314
tag: string;
@@ -26,6 +27,14 @@ function TagDetailContainer({ tag }: TagDetailContainerProps) {
2627
notifyOnNetworkStatusChange: true,
2728
});
2829

30+
const { showNotFound } = useNotFound();
31+
32+
useEffect(() => {
33+
if (tagDetail.data && !tagDetail.data.tag) {
34+
showNotFound();
35+
}
36+
}, [showNotFound, tagDetail.data]);
37+
2938
const onLoadMore = useCallback(
3039
(cursor: string) => {
3140
getPostList.fetchMore({

src/containers/velog/SeriesPosts.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import useToggle from '../../lib/hooks/useToggle';
1717
import SeriesActionButtons from '../../components/velog/SeriesActionButtons';
1818
import DraggableSeriesPostList from '../../components/velog/DraggableSeriesPostList';
1919
import useUser from '../../lib/hooks/useUser';
20+
import useNotFound from '../../lib/hooks/useNotFound';
2021

2122
export interface SeriesPostsProps {
2223
username: string;
@@ -51,11 +52,16 @@ const SeriesPosts: React.FC<SeriesPostsProps> = ({ username, urlSlug }) => {
5152
toggleEditing();
5253
}, [data, editSeries, nextName, order, toggleEditing]);
5354

55+
const { showNotFound } = useNotFound();
56+
5457
useEffect(() => {
58+
if (data && !data.series) {
59+
showNotFound();
60+
}
5561
if (!data || !data.series) return;
5662
setNextName(data.series.name);
5763
setOrder(data.series.series_posts.map(sp => sp.id));
58-
}, [data]);
64+
}, [data, showNotFound]);
5965

6066
const onChangeNextName = useCallback(
6167
(e: React.FormEvent<HTMLHeadingElement>) => {

0 commit comments

Comments
 (0)