Skip to content

Commit 81f6960

Browse files
committed
chore: 수정한 API Hook 반영 및 전반적인 리팩토링
1 parent 959aa14 commit 81f6960

File tree

4 files changed

+104
-92
lines changed

4 files changed

+104
-92
lines changed
Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,35 @@
11
import * as React from "react";
2-
import * as R from "remeda";
32

4-
import { Button, CircularProgress, MenuItem, Select, Stack } from '@mui/material';
5-
import { ErrorBoundary, Suspense } from '@suspensive/react';
3+
import { CircularProgress, MenuItem, Select, SelectProps, Stack } from '@mui/material';
4+
import { Suspense } from '@suspensive/react';
65

76
import * as Common from "@frontend/common";
87

9-
const SiteMapRenderer: React.FC = () => {
10-
const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery();
11-
return <pre style={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(Common.Utils.buildNestedSiteMap(data), null, 2)}</pre>
12-
};
13-
14-
const PageIdSelector: React.FC<{ inputRef: React.Ref<HTMLSelectElement> }> = ({ inputRef }) => {
15-
const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery();
16-
17-
return <Select inputRef={inputRef}>
18-
{data.map((siteMap) => <MenuItem key={siteMap.id} value={siteMap.page}>{siteMap.name}</MenuItem>)}
19-
</Select>
20-
}
21-
22-
const SuspenseWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
23-
<ErrorBoundary fallback={Common.Components.ErrorFallback}>
24-
<Suspense fallback={<CircularProgress />}>
25-
{children}
26-
</Suspense>
27-
</ErrorBoundary>
28-
)
8+
const SiteMapRenderer: React.FC = Suspense.with(
9+
{ fallback: <CircularProgress /> },
10+
() => {
11+
const backendClient = Common.Hooks.BackendAPI.useBackendClient();
12+
const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendClient);
13+
return <pre style={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(Common.Utils.buildNestedSiteMap(data), null, 2)}</pre>
14+
}
15+
);
16+
17+
const PageIdSelector: React.FC<{ onChange: SelectProps['onChange'] }> = Suspense.with(
18+
{ fallback: <CircularProgress /> },
19+
({ onChange }) => {
20+
const backendClient = Common.Hooks.BackendAPI.useBackendClient();
21+
const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendClient);
22+
23+
return <Select onChange={onChange}>{data.map((s) => <MenuItem key={s.id} value={s.page}>{s.name}</MenuItem>)}</Select>
24+
}
25+
);
2926

3027
export const BackendTestPage: React.FC = () => {
31-
const inputRef = React.useRef<HTMLSelectElement>(null);
3228
const [pageId, setPageId] = React.useState<string | null>(null);
3329

34-
return <Stack>
35-
<br />
36-
<SuspenseWrapper><SiteMapRenderer /></SuspenseWrapper>
37-
<br />
38-
<SuspenseWrapper><PageIdSelector inputRef={inputRef} /></SuspenseWrapper>
39-
<br />
40-
<Button variant="outlined" onClick={() => setPageId(inputRef.current?.value ?? null)}>페이지 렌더링</Button>
41-
<br />
42-
{R.isString(pageId) ? <SuspenseWrapper><Common.Components.PageRenderer id={pageId} /></SuspenseWrapper> : <>페이지를 선택해주세요.</>}
30+
return <Stack spacing={2}>
31+
<SiteMapRenderer />
32+
<PageIdSelector onChange={(e) => setPageId(e.target.value as string)} />
33+
{Common.Utils.isFilledString(pageId) ? <Common.Components.PageRenderer id={pageId} /> : <>페이지를 선택해주세요.</>}
4334
</Stack>
44-
}
35+
};
Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as React from "react";
2-
import { useLocation } from 'react-router-dom';
2+
import { useLocation, useParams } from 'react-router-dom';
33
import * as R from "remeda";
44

55
import { CircularProgress } from '@mui/material';
66
import { ErrorBoundary, Suspense } from '@suspensive/react';
77

8-
import styled from '@emotion/styled';
8+
import { AxiosError, AxiosResponse } from 'axios';
9+
import { BackendAPIClientError } from '../apis/client';
910
import Hooks from "../hooks";
1011
import Schemas from "../schemas";
1112
import Utils from '../utils';
@@ -14,67 +15,87 @@ import { MDXRenderer } from './mdx';
1415

1516
const InitialPageStyle: React.CSSProperties = {
1617
width: '100%',
17-
height: '100%',
1818
display: 'flex',
1919
justifyContent: 'center',
2020
alignItems: 'center',
2121
flexDirection: 'column',
22-
}
22+
};
2323

2424
const InitialSectionStyle: React.CSSProperties = {
2525
width: '100%',
26-
}
26+
};
27+
28+
const LoginRequired: React.FC = () => <>401 Login Required</>;
29+
const PermissionDenied: React.FC = () => <>403 Permission Denied</>;
30+
const PageNotFound: React.FC = () => <>404 Not Found</>;
2731

28-
export const PageRenderer: React.FC<{ id: string }> = ({ id }) => {
29-
const { data } = Hooks.BackendAPI.usePageQuery(id);
32+
const throwPageNotFound = (message: string) => {
33+
const errorStr = `RouteRenderer: ${message}`;
34+
const axiosError = new AxiosError(errorStr, errorStr, undefined, undefined, { status: 404 } as AxiosResponse);
35+
throw new BackendAPIClientError(axiosError);
36+
};
3037

31-
return <div style={{ ...InitialPageStyle, ...Utils.parseCss(data.css) }}>
32-
{
33-
data.sections.map(
34-
(s) => <div style={{ ...InitialSectionStyle, ...Utils.parseCss(s.css) }} key={s.id}>
35-
<MDXRenderer text={s.body} />
36-
</div>
37-
)
38+
const RouteErrorFallback: React.FC<{ error: Error, reset: () => void }> = ({ error, reset }) => {
39+
if (error instanceof BackendAPIClientError) {
40+
switch (error.status) {
41+
case 401: return <LoginRequired />;
42+
case 403: return <PermissionDenied />;
43+
case 404: return <PageNotFound />;
44+
default: return <ErrorFallback error={error} reset={reset} />;
3845
}
39-
</div>
46+
}
47+
return <ErrorFallback error={error} reset={reset} />;
4048
};
4149

42-
const AsyncDynamicRoutePage: React.FC = () => {
43-
const location = useLocation();
44-
const { data } = Hooks.BackendAPI.useFlattenSiteMapQuery();
45-
const nestedSiteMap = Utils.buildNestedSiteMap(data);
50+
export const PageRenderer: React.FC<{ id?: string }> = ErrorBoundary.with(
51+
{ fallback: RouteErrorFallback },
52+
Suspense.with(
53+
{ fallback: <CircularProgress /> },
54+
({ id }) => {
55+
const backendClient = Hooks.BackendAPI.useBackendClient();
56+
const { data } = Hooks.BackendAPI.usePageQuery(backendClient, id || '');
57+
58+
return <div style={{ ...InitialPageStyle, ...Utils.parseCss(data.css) }}>
59+
{
60+
data.sections.map(
61+
(s) => <div style={{ ...InitialSectionStyle, ...Utils.parseCss(s.css) }} key={s.id}>
62+
<MDXRenderer text={s.body} />
63+
</div>
64+
)
65+
}
66+
</div>
67+
}
68+
)
69+
);
4670

47-
const currentRouteCodes = ['', ...location.pathname.split('/').filter((code) => !R.isEmpty(code))];
71+
export const RouteRenderer: React.FC = ErrorBoundary.with(
72+
{ fallback: RouteErrorFallback },
73+
Suspense.with(
74+
{ fallback: <CircularProgress /> },
75+
() => {
76+
const location = useLocation();
4877

49-
let currentSitemap: Schemas.NestedSiteMapSchema | null | undefined = nestedSiteMap[currentRouteCodes[0]];
50-
if (currentSitemap === undefined) {
51-
return <>404 Not Found</>;
52-
}
78+
const backendClient = Hooks.BackendAPI.useBackendClient();
79+
const { data } = Hooks.BackendAPI.useFlattenSiteMapQuery(backendClient);
80+
const nestedSiteMap = Utils.buildNestedSiteMap(data);
5381

54-
for (const routeCode of currentRouteCodes.slice(1)) {
55-
if ((currentSitemap = currentSitemap.children[routeCode]) === undefined) {
56-
break;
57-
}
58-
}
82+
const currentRouteCodes = ['', ...location.pathname.split('/').filter((code) => !R.isEmpty(code))];
83+
let currentSitemap: Schemas.NestedSiteMapSchema | undefined = nestedSiteMap[currentRouteCodes[0]];
84+
if (currentSitemap === undefined) throwPageNotFound(`Route ${location} not found`);
5985

60-
return R.isNullish(currentSitemap)
61-
? <>404 Not Found</>
62-
: <PageRenderer id={currentSitemap.page} />
63-
}
86+
for (const routeCode of currentRouteCodes.slice(1))
87+
if ((currentSitemap = currentSitemap.children[routeCode]) === undefined)
88+
throwPageNotFound(`Route ${location} not found`);
6489

65-
const FullPage = styled.div`
66-
width: 100%;
67-
height: 100%;
68-
display: flex;
69-
justify-content: center;
70-
align-items: center;
71-
flex-direction: column;
72-
`
90+
return <PageRenderer id={currentSitemap.page} />
91+
}
92+
)
93+
);
7394

74-
export const DynamicRoutePage: React.FC = () => <FullPage>
75-
<ErrorBoundary fallback={ErrorFallback}>
76-
<Suspense fallback={<CircularProgress />}>
77-
<AsyncDynamicRoutePage />
78-
</Suspense>
79-
</ErrorBoundary>
80-
</FullPage>;
95+
export const PageIdParamRenderer: React.FC = Suspense.with(
96+
{ fallback: <CircularProgress /> },
97+
() => {
98+
const { id } = useParams();
99+
return <PageRenderer id={id} />
100+
}
101+
);

packages/common/src/components/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { CommonContextProvider as CommonContextProviderComponent } from './common_context';
22
import {
3-
DynamicRoutePage as DynamicRoutePageComponent,
3+
PageIdParamRenderer as PageIdParamRendererComponent,
44
PageRenderer as PageRendererComponent,
5+
RouteRenderer as RouteRendererComponent,
56
} from './dynamic_route';
67
import { ErrorFallback as ErrorFallbackComponent } from './error_handler';
78
import { MDXRenderer as MDXRendererComponent } from "./mdx";
89
import { PythonKorea as PythonKoreaComponent } from './pythonkorea';
910

1011
namespace Components {
1112
export const CommonContextProvider = CommonContextProviderComponent;
12-
export const DynamicRoutePage = DynamicRoutePageComponent;
13+
export const RouteRenderer = RouteRendererComponent;
1314
export const PageRenderer = PageRendererComponent;
15+
export const PageIdParamRenderer = PageIdParamRendererComponent;
1416
export const MDXRenderer = MDXRendererComponent;
1517
export const PythonKorea = PythonKoreaComponent;
1618
export const ErrorFallback = ErrorFallbackComponent;

packages/common/src/hooks/useAPI.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { BackendAPIClient } from '../apis/client';
77
import BackendContext from '../contexts';
88

99
const QUERY_KEYS = {
10-
SITEMAP: ["query", "sitemap"],
1110
SITEMAP_LIST: ["query", "sitemap", "list"],
1211
PAGE: ["query", "page"],
1312
};
@@ -19,20 +18,19 @@ namespace BackendAPIHooks {
1918
return context;
2019
}
2120

22-
const clientDecorator = <T = CallableFunction>(func:(client: BackendAPIClient) => T): T => {
21+
export const useBackendClient = () => {
2322
const { backendApiDomain, backendApiTimeout } = useBackendContext();
24-
return func(new BackendAPIClient(backendApiDomain, backendApiTimeout));
23+
return new BackendAPIClient(backendApiDomain, backendApiTimeout);
2524
}
2625

27-
export const useFlattenSiteMapQuery = () => useSuspenseQuery({
26+
export const useFlattenSiteMapQuery = (client: BackendAPIClient) => useSuspenseQuery({
2827
queryKey: QUERY_KEYS.SITEMAP_LIST,
29-
queryFn: clientDecorator(BackendAPIs.listSiteMaps),
30-
meta: { invalidates: [ QUERY_KEYS.SITEMAP ] },
28+
queryFn: BackendAPIs.listSiteMaps(client),
3129
});
3230

33-
export const usePageQuery = (id: string) => useSuspenseQuery({
31+
export const usePageQuery = (client: BackendAPIClient, id: string) => useSuspenseQuery({
3432
queryKey: [ ...QUERY_KEYS.PAGE, id ],
35-
queryFn: () => clientDecorator(BackendAPIs.retrievePage)(id),
33+
queryFn: () => (BackendAPIs.retrievePage)(client)(id),
3634
});
3735
}
3836

0 commit comments

Comments
 (0)