Skip to content

Commit c0f4ff8

Browse files
authored
Merge pull request #9 from pythonkr/feature/add-main-server-hooks
feat: backend API에 대한 환경 및 context 변수들 추가
2 parents 7c9efc5 + ac42511 commit c0f4ff8

File tree

23 files changed

+522
-8
lines changed

23 files changed

+522
-8
lines changed

apps/pyconkr/src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import React from "react";
33
import { BrowserRouter, Route, Routes } from "react-router-dom";
44
import MainLayout from "./components/layout";
55
import { Test } from "./components/pages/test";
6+
import { IS_DEBUG_ENV } from "./consts/index.ts";
7+
8+
import * as Common from "@frontend/common";
69

710
export const App: React.FC = () => {
811
return (
912
<BrowserRouter>
1013
<Routes>
1114
<Route element={<MainLayout />}>
12-
<Route path="/" element={<Test />} />
15+
{ IS_DEBUG_ENV && <Route path="/debug" element={<Test />} /> }
16+
<Route path="*" element={<Common.Components.DynamicRoutePage />} />
1317
</Route>
1418
</Routes>
1519
</BrowserRouter>

apps/pyconkr/src/components/layout/Footer/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default function Footer({
6565
],
6666
icons = defaultIcons,
6767
}: FooterProps) {
68-
const { sendEmail } = Common.Hooks.useEmail();
68+
const { sendEmail } = Common.Hooks.Common.useEmail();
6969

7070
return (
7171
<FooterContainer>

apps/pyconkr/src/components/layout/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ const LayoutContainer = styled.div`
5555

5656
const MainContent = styled.main`
5757
flex: 1;
58+
59+
display: flex;
60+
flex-direction: column;
61+
justify-content: center;
62+
align-items: center;
5863
`;
5964

6065
export default function MainLayout() {

apps/pyconkr/src/components/pages/test.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import * as React from "react";
22

33
import { Box, Button } from "@mui/material";
44

5+
import { BackendTestPage } from '../../debug/page/backend_test';
56
import { MdiTestPage } from "../../debug/page/mdi_test";
67
import { ShopTestPage } from "../../debug/page/shop_test";
78

8-
type SelectedTabType = "shop" | "mdi";
9+
type SelectedTabType = "shop" | "mdi" | "backend";
910

1011
export const Test: React.FC = () => {
1112
const [selectedTab, setSelectedTab] = React.useState<SelectedTabType>("mdi");
@@ -18,8 +19,12 @@ export const Test: React.FC = () => {
1819
<Button variant="contained" onClick={() => setSelectedTab("mdi")}>
1920
MDI Test
2021
</Button>
22+
<Button variant="contained" onClick={() => setSelectedTab("backend")}>
23+
Backend Test
24+
</Button>
2125
{selectedTab === "shop" && <ShopTestPage />}
2226
{selectedTab === "mdi" && <MdiTestPage />}
27+
{selectedTab === "backend" && <BackendTestPage />}
2328
</Box>
2429
);
2530
};

apps/pyconkr/src/consts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const IS_DEBUG_ENV = import.meta.env.MODE === "development"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from "react";
2+
import * as R from "remeda";
3+
4+
import { Button, CircularProgress, MenuItem, Select, Stack } from '@mui/material';
5+
import { ErrorBoundary, Suspense } from '@suspensive/react';
6+
7+
import * as Common from "@frontend/common";
8+
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+
)
29+
30+
export const BackendTestPage: React.FC = () => {
31+
const inputRef = React.useRef<HTMLSelectElement>(null);
32+
const [pageId, setPageId] = React.useState<string | null>(null);
33+
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> : <>페이지를 선택해주세요.</>}
43+
</Stack>
44+
}

apps/pyconkr/src/main.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as Common from "@frontend/common";
1212
import * as Shop from "@frontend/shop";
1313

1414
import { App } from "./App.tsx";
15+
import { IS_DEBUG_ENV } from './consts/index.ts';
1516
import { globalStyles, muiTheme } from "./styles/globalStyles.ts";
1617

1718
const queryClient = new QueryClient({
@@ -35,8 +36,10 @@ const queryClient = new QueryClient({
3536
});
3637

3738
const CommonOptions: Common.Contexts.ContextOptions = {
38-
debug: import.meta.env.MODE === "development",
39+
debug: IS_DEBUG_ENV,
3940
baseUrl: '.',
41+
backendApiDomain: import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN,
42+
backendApiTimeout: 10000,
4043
};
4144

4245
const ShopOptions: Shop.Contexts.ContextOptions = {

apps/pyconkr/vite-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface ViteTypeOptions {
1111
}
1212

1313
interface ImportMetaEnv {
14+
readonly VITE_PYCONKR_BACKEND_API_DOMAIN: string;
1415
readonly VITE_PYCONKR_SHOP_API_DOMAIN: string;
1516
readonly VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME: string;
1617
readonly VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID: string;

dotenv/.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
VITE_PYCONKR_BACKEND_API_DOMAIN=https://rest-api.dev.pycon.kr
12
VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.dev.pycon.kr
23
VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=DEBUG_csrftoken
34
VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID=imp80859147

dotenv/.env.production

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
VITE_PYCONKR_BACKEND_API_DOMAIN=https://rest-api.pycon.kr
12
VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.pycon.kr
23
VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=csrftoken
34
VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID=imp96676915

packages/common/src/apis/client.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
2+
import * as R from "remeda";
3+
4+
import CommonSchemas from "../schemas";
5+
6+
const DEFAULT_ERROR_MESSAGE = "알 수 없는 문제가 발생했습니다, 잠시 후 다시 시도해주세요.";
7+
const DEFAULT_ERROR_RESPONSE = {
8+
type: "unknown",
9+
errors: [{ code: "unknown", detail: DEFAULT_ERROR_MESSAGE, attr: null }],
10+
};
11+
12+
export class BackendAPIClientError extends Error {
13+
readonly name = "BackendAPIClientError";
14+
readonly status: number;
15+
readonly detail: CommonSchemas.ErrorResponseSchema;
16+
readonly originalError: unknown;
17+
18+
constructor(error?: unknown) {
19+
let message: string = DEFAULT_ERROR_MESSAGE;
20+
let detail: CommonSchemas.ErrorResponseSchema = DEFAULT_ERROR_RESPONSE;
21+
let status = -1;
22+
23+
if (axios.isAxiosError(error)) {
24+
const response = error.response;
25+
26+
if (response) {
27+
status = response.status;
28+
detail = CommonSchemas.isObjectErrorResponseSchema(response.data)
29+
? response.data
30+
: {
31+
type: "axios_error",
32+
errors: [
33+
{
34+
code: "unknown",
35+
detail: R.isString(response.data)
36+
? response.data
37+
: DEFAULT_ERROR_MESSAGE,
38+
attr: null,
39+
},
40+
],
41+
};
42+
}
43+
} else if (error instanceof Error) {
44+
message = error.message;
45+
detail = {
46+
type: error.name || typeof error || "unknown",
47+
errors: [{ code: "unknown", detail: error.message, attr: null }],
48+
};
49+
}
50+
51+
super(message);
52+
this.originalError = error || null;
53+
this.status = status;
54+
this.detail = detail;
55+
}
56+
57+
isRequiredAuth(): boolean {
58+
return this.status === 401 || this.status === 403;
59+
}
60+
}
61+
62+
type AxiosRequestWithoutPayload = <T = any, R = AxiosResponse<T>, D = any>(
63+
url: string,
64+
config?: AxiosRequestConfig<D>
65+
) => Promise<R>;
66+
type AxiosRequestWithPayload = <T = any, R = AxiosResponse<T>, D = any>(
67+
url: string,
68+
data?: D,
69+
config?: AxiosRequestConfig<D>
70+
) => Promise<R>;
71+
72+
export class BackendAPIClient {
73+
readonly baseURL: string;
74+
private readonly backendAPI: AxiosInstance;
75+
76+
constructor(baseURL: string, timeout: number) {
77+
const headers = { "Content-Type": "application/json" };
78+
this.baseURL = baseURL;
79+
this.backendAPI = axios.create({ baseURL, timeout, headers });
80+
}
81+
82+
_safe_request_without_payload(
83+
requestFunc: AxiosRequestWithoutPayload
84+
): AxiosRequestWithoutPayload {
85+
return async <T = any, R = AxiosResponse<T>, D = any>(
86+
url: string,
87+
config?: AxiosRequestConfig<D>
88+
) => {
89+
try {
90+
return await requestFunc<T, R, D>(url, config);
91+
} catch (error) {
92+
throw new BackendAPIClientError(error);
93+
}
94+
};
95+
}
96+
97+
_safe_request_with_payload(
98+
requestFunc: AxiosRequestWithPayload
99+
): AxiosRequestWithPayload {
100+
return async <T = any, R = AxiosResponse<T>, D = any>(
101+
url: string,
102+
data: D,
103+
config?: AxiosRequestConfig<D>
104+
) => {
105+
try {
106+
return await requestFunc<T, R, D>(url, data, config);
107+
} catch (error) {
108+
throw new BackendAPIClientError(error);
109+
}
110+
};
111+
}
112+
113+
async get<T, D = any>(
114+
url: string,
115+
config?: AxiosRequestConfig<D>
116+
): Promise<T> {
117+
return (
118+
await this._safe_request_without_payload(this.backendAPI.get)<
119+
T,
120+
AxiosResponse<T>,
121+
D
122+
>(url, config)
123+
).data;
124+
}
125+
async post<T, D>(
126+
url: string,
127+
data: D,
128+
config?: AxiosRequestConfig<D>
129+
): Promise<T> {
130+
return (
131+
await this._safe_request_with_payload(this.backendAPI.post)<
132+
T,
133+
AxiosResponse<T>,
134+
D
135+
>(url, data, config)
136+
).data;
137+
}
138+
async put<T, D>(
139+
url: string,
140+
data: D,
141+
config?: AxiosRequestConfig<D>
142+
): Promise<T> {
143+
return (
144+
await this._safe_request_with_payload(this.backendAPI.put)<
145+
T,
146+
AxiosResponse<T>,
147+
D
148+
>(url, data, config)
149+
).data;
150+
}
151+
async patch<T, D>(
152+
url: string,
153+
data: D,
154+
config?: AxiosRequestConfig<D>
155+
): Promise<T> {
156+
return (
157+
await this._safe_request_with_payload(this.backendAPI.patch)<
158+
T,
159+
AxiosResponse<T>,
160+
D
161+
>(url, data, config)
162+
).data;
163+
}
164+
async delete<T, D = any>(
165+
url: string,
166+
config?: AxiosRequestConfig<D>
167+
): Promise<T> {
168+
return (
169+
await this._safe_request_without_payload(this.backendAPI.delete)<
170+
T,
171+
AxiosResponse<T>,
172+
D
173+
>(url, config)
174+
).data;
175+
}
176+
}

packages/common/src/apis/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import CommonSchemas from "../schemas";
2+
import { BackendAPIClient } from "./client";
3+
4+
namespace BackendAPIs {
5+
export const listSiteMaps = (client: BackendAPIClient) => () => client.get<CommonSchemas.FlattenedSiteMapSchema[]>("v1/cms/sitemap/");
6+
export const retrievePage = (client: BackendAPIClient) => (id: string) => client.get<CommonSchemas.PageSchema>(`v1/cms/page/${id}/`);
7+
}
8+
9+
export default BackendAPIs;

0 commit comments

Comments
 (0)