From b7cf3ddf9bdcb781f88d6780e05e4dbfb804f3cb Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 18 May 2025 22:10:15 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20backend=20API=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=99=98=EA=B2=BD=20=EB=B0=8F=20context?= =?UTF-8?q?=20=EB=B3=80=EC=88=98=EB=93=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/main.tsx | 2 ++ apps/pyconkr/vite-env.d.ts | 1 + packages/common/src/contexts/index.ts | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/apps/pyconkr/src/main.tsx b/apps/pyconkr/src/main.tsx index d71c332..4aa9acd 100644 --- a/apps/pyconkr/src/main.tsx +++ b/apps/pyconkr/src/main.tsx @@ -37,6 +37,8 @@ const queryClient = new QueryClient({ const CommonOptions: Common.Contexts.ContextOptions = { debug: import.meta.env.MODE === "development", baseUrl: '.', + backendApiDomain: import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN, + backendApiTimeout: 10000, }; const ShopOptions: Shop.Contexts.ContextOptions = { diff --git a/apps/pyconkr/vite-env.d.ts b/apps/pyconkr/vite-env.d.ts index c3ecd2b..678a0d7 100644 --- a/apps/pyconkr/vite-env.d.ts +++ b/apps/pyconkr/vite-env.d.ts @@ -11,6 +11,7 @@ interface ViteTypeOptions { } interface ImportMetaEnv { + readonly VITE_PYCONKR_BACKEND_API_DOMAIN: string; readonly VITE_PYCONKR_SHOP_API_DOMAIN: string; readonly VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME: string; readonly VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID: string; diff --git a/packages/common/src/contexts/index.ts b/packages/common/src/contexts/index.ts index 7422117..ccbd8da 100644 --- a/packages/common/src/contexts/index.ts +++ b/packages/common/src/contexts/index.ts @@ -4,11 +4,15 @@ namespace GlobalContext { export type ContextOptions = { baseUrl: string; debug?: boolean; + backendApiDomain: string; + backendApiTimeout: number; } export const context = React.createContext({ baseUrl: "", debug: false, + backendApiDomain: "", + backendApiTimeout: 10000, }); } From 57c93f4baf4879d7459d3aaa86565f34bc06cbb9 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 18 May 2025 22:11:11 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20BackendAPIClient=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/apis/client.ts | 176 +++++++++++++++++++++++++++ packages/common/src/apis/index.ts | 9 ++ packages/common/src/index.ts | 1 + packages/common/src/schemas/index.ts | 68 +++++++++++ 4 files changed, 254 insertions(+) create mode 100644 packages/common/src/apis/client.ts create mode 100644 packages/common/src/apis/index.ts create mode 100644 packages/common/src/schemas/index.ts diff --git a/packages/common/src/apis/client.ts b/packages/common/src/apis/client.ts new file mode 100644 index 0000000..c78331a --- /dev/null +++ b/packages/common/src/apis/client.ts @@ -0,0 +1,176 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; +import * as R from "remeda"; + +import CommonSchemas from "../schemas"; + +const DEFAULT_ERROR_MESSAGE = "알 수 없는 문제가 발생했습니다, 잠시 후 다시 시도해주세요."; +const DEFAULT_ERROR_RESPONSE = { + type: "unknown", + errors: [{ code: "unknown", detail: DEFAULT_ERROR_MESSAGE, attr: null }], +}; + +export class BackendAPIClientError extends Error { + readonly name = "BackendAPIClientError"; + readonly status: number; + readonly detail: CommonSchemas.ErrorResponseSchema; + readonly originalError: unknown; + + constructor(error?: unknown) { + let message: string = DEFAULT_ERROR_MESSAGE; + let detail: CommonSchemas.ErrorResponseSchema = DEFAULT_ERROR_RESPONSE; + let status = -1; + + if (axios.isAxiosError(error)) { + const response = error.response; + + if (response) { + status = response.status; + detail = CommonSchemas.isObjectErrorResponseSchema(response.data) + ? response.data + : { + type: "axios_error", + errors: [ + { + code: "unknown", + detail: R.isString(response.data) + ? response.data + : DEFAULT_ERROR_MESSAGE, + attr: null, + }, + ], + }; + } + } else if (error instanceof Error) { + message = error.message; + detail = { + type: error.name || typeof error || "unknown", + errors: [{ code: "unknown", detail: error.message, attr: null }], + }; + } + + super(message); + this.originalError = error || null; + this.status = status; + this.detail = detail; + } + + isRequiredAuth(): boolean { + return this.status === 401 || this.status === 403; + } +} + +type AxiosRequestWithoutPayload = , D = any>( + url: string, + config?: AxiosRequestConfig +) => Promise; +type AxiosRequestWithPayload = , D = any>( + url: string, + data?: D, + config?: AxiosRequestConfig +) => Promise; + +export class BackendAPIClient { + readonly baseURL: string; + private readonly backendAPI: AxiosInstance; + + constructor(baseURL: string, timeout: number) { + const headers = { "Content-Type": "application/json" }; + this.baseURL = baseURL; + this.backendAPI = axios.create({ baseURL, timeout, headers }); + } + + _safe_request_without_payload( + requestFunc: AxiosRequestWithoutPayload + ): AxiosRequestWithoutPayload { + return async , D = any>( + url: string, + config?: AxiosRequestConfig + ) => { + try { + return await requestFunc(url, config); + } catch (error) { + throw new BackendAPIClientError(error); + } + }; + } + + _safe_request_with_payload( + requestFunc: AxiosRequestWithPayload + ): AxiosRequestWithPayload { + return async , D = any>( + url: string, + data: D, + config?: AxiosRequestConfig + ) => { + try { + return await requestFunc(url, data, config); + } catch (error) { + throw new BackendAPIClientError(error); + } + }; + } + + async get( + url: string, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_without_payload(this.backendAPI.get)< + T, + AxiosResponse, + D + >(url, config) + ).data; + } + async post( + url: string, + data: D, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_with_payload(this.backendAPI.post)< + T, + AxiosResponse, + D + >(url, data, config) + ).data; + } + async put( + url: string, + data: D, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_with_payload(this.backendAPI.put)< + T, + AxiosResponse, + D + >(url, data, config) + ).data; + } + async patch( + url: string, + data: D, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_with_payload(this.backendAPI.patch)< + T, + AxiosResponse, + D + >(url, data, config) + ).data; + } + async delete( + url: string, + config?: AxiosRequestConfig + ): Promise { + return ( + await this._safe_request_without_payload(this.backendAPI.delete)< + T, + AxiosResponse, + D + >(url, config) + ).data; + } +} diff --git a/packages/common/src/apis/index.ts b/packages/common/src/apis/index.ts new file mode 100644 index 0000000..c9534a3 --- /dev/null +++ b/packages/common/src/apis/index.ts @@ -0,0 +1,9 @@ +import CommonSchemas from "../schemas"; +import { BackendAPIClient } from "./client"; + +namespace BackendAPIs { + export const listSiteMaps = (client: BackendAPIClient) => () => client.get("v1/cms/sitemap/"); + export const retrievePage = (client: BackendAPIClient) => (id: string) => client.get(`v1/cms/page/${id}/`); +} + +export default BackendAPIs; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index b3b263b..d7814e7 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,5 +1,6 @@ export { default as Components } from "./components/index"; export { default as Contexts } from "./contexts/index"; export { default as Hooks } from "./hooks/index"; +export { default as Schemas } from "./schemas/index"; export { default as Utils } from "./utils/index"; diff --git a/packages/common/src/schemas/index.ts b/packages/common/src/schemas/index.ts new file mode 100644 index 0000000..e10f7dc --- /dev/null +++ b/packages/common/src/schemas/index.ts @@ -0,0 +1,68 @@ +import * as R from "remeda"; + +namespace BackendAPISchemas { + export type EmptyObject = Record; + + export type DetailedErrorSchema = { + code: string; + detail: string; + attr: string | null; + }; + + export type ErrorResponseSchema = { + type: string; + errors: DetailedErrorSchema[]; + }; + + export type FlattenedSiteMapSchema = { + id: string; + name: string; + order: number; + parent_sitemap: string | null; + page: string; + } + + export type NestedSiteMapSchema = { + id: string; + name: string; + order: number; + page: string; + children: NestedSiteMapSchema[]; + } + + export type SectionSchema = { + id: string; + css: string; + + order: number; + body: string; + } + + export type PageSchema = { + id: string; + css: string; + title: string; + subtitle: string; + sections: SectionSchema[]; + } + + export const isObjectErrorResponseSchema = ( + obj?: unknown + ): obj is BackendAPISchemas.ErrorResponseSchema => { + return ( + R.isPlainObject(obj) && + R.isString(obj.type) && + R.isArray(obj.errors) && + obj.errors.every((error) => { + return ( + R.isPlainObject(error) && + R.isString(error.code) && + R.isString(error.detail) && + (error.attr === null || R.isString(error.attr)) + ); + }) + ); + }; +} + +export default BackendAPISchemas; From ad8ac7bc346ba178d85752420ca7b479968c1b53 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 18 May 2025 22:11:39 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20Sitemap=20graph=20builder=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/utils/api.ts | 24 ++++++++++++++++++++++++ packages/common/src/utils/index.ts | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 packages/common/src/utils/api.ts diff --git a/packages/common/src/utils/api.ts b/packages/common/src/utils/api.ts new file mode 100644 index 0000000..5b6bf6c --- /dev/null +++ b/packages/common/src/utils/api.ts @@ -0,0 +1,24 @@ + +import BackendAPISchemas from '../schemas'; + +export const buildNestedSiteMap: (flattened: BackendAPISchemas.FlattenedSiteMapSchema[]) => BackendAPISchemas.NestedSiteMapSchema[] = (flattened) => { + const map: Record = {}; + const roots: BackendAPISchemas.NestedSiteMapSchema[] = []; + + flattened.forEach((item) => { + map[item.id] = { + ...item, + children: [], + }; + }); + + flattened.forEach((item) => { + if (item.parent_sitemap) { + map[item.parent_sitemap].children.push(map[item.id]); + } else { + roots.push(map[item.id]); + } + }); + + return roots; +} diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index d6c06c4..ae26abe 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -1,3 +1,4 @@ +import { buildNestedSiteMap as _buildNestedSiteMap } from './api'; import { getCookie as _getCookie } from "./cookie"; import { getFormValue as _getFormValue, @@ -5,6 +6,7 @@ import { } from "./form"; namespace Utils { + export const buildNestedSiteMap = _buildNestedSiteMap; export const getCookie = _getCookie; export const isFormValid = _isFormValid; export const getFormValue = _getFormValue; From b93b5d1351e45723a8898f38e943f2926374957d Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 18 May 2025 22:14:01 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20Backend=20API=20Hook=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/Footer/index.tsx | 2 +- packages/common/src/components/mdx.tsx | 2 +- packages/common/src/hooks/index.ts | 10 +++- packages/common/src/hooks/useAPI.ts | 50 +++++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 packages/common/src/hooks/useAPI.ts diff --git a/apps/pyconkr/src/components/layout/Footer/index.tsx b/apps/pyconkr/src/components/layout/Footer/index.tsx index b0a467f..7988b23 100644 --- a/apps/pyconkr/src/components/layout/Footer/index.tsx +++ b/apps/pyconkr/src/components/layout/Footer/index.tsx @@ -65,7 +65,7 @@ export default function Footer({ ], icons = defaultIcons, }: FooterProps) { - const { sendEmail } = Common.Hooks.useEmail(); + const { sendEmail } = Common.Hooks.Common.useEmail(); return ( diff --git a/packages/common/src/components/mdx.tsx b/packages/common/src/components/mdx.tsx index 5c6d57b..45ae95d 100644 --- a/packages/common/src/components/mdx.tsx +++ b/packages/common/src/components/mdx.tsx @@ -58,7 +58,7 @@ const lineFormatterForMDX = (line: string) => { export const MDXRenderer: React.FC<{ text: string }> = ({ text }) => { // 원래 MDX는 각 줄의 마지막에 공백 2개가 있어야 줄바꿈이 되고, 또 연속 줄바꿈은 무시되지만, // 편의성을 위해 렌더러 단에서 공백 2개를 추가하고 연속 줄바꿈을
로 변환합니다. - const { baseUrl, debug } = Hooks.useCommonContext(); + const { baseUrl } = Hooks.Common.useCommonContext(); const processedText = text .split("\n") diff --git a/packages/common/src/hooks/index.ts b/packages/common/src/hooks/index.ts index 1882485..430631b 100644 --- a/packages/common/src/hooks/index.ts +++ b/packages/common/src/hooks/index.ts @@ -1,9 +1,15 @@ +import BackendAPIHooks from './useAPI'; import { useCommonContext as useCommonContextHook } from './useCommonContext'; import { useEmail as useEmailHook } from './useEmail'; -namespace CommonHooks { +export namespace CommonHooks { export const useCommonContext = useCommonContextHook; export const useEmail = useEmailHook; +}; + +namespace Hooks { + export const Common = CommonHooks; + export const BackendAPI = BackendAPIHooks; } -export default CommonHooks; +export default Hooks; diff --git a/packages/common/src/hooks/useAPI.ts b/packages/common/src/hooks/useAPI.ts new file mode 100644 index 0000000..7b1cb91 --- /dev/null +++ b/packages/common/src/hooks/useAPI.ts @@ -0,0 +1,50 @@ +import * as React from "react"; + +import { useSuspenseQuery } from "@tanstack/react-query"; + +import BackendAPIs from "../apis"; +import { BackendAPIClient } from '../apis/client'; +import BackendContext from '../contexts'; +import Utils from "../utils"; + +const QUERY_KEYS = { + SITEMAP: ["query", "sitemap"], + SITEMAP_LIST: ["query", "sitemap", "list"], + SITEMAP_NESTED_LIST: ["query", "sitemap", "list", "nested"], + PAGE: ["query", "page"], +}; + +namespace BackendAPIHooks { + export const useBackendContext = () => { + const context = React.useContext(BackendContext.context); + if (!context) throw new Error("useBackendContext must be used within a CommonProvider"); + return context; + } + + const clientDecorator = (func:(client: BackendAPIClient) => T): T => { + const { backendApiDomain, backendApiTimeout } = useBackendContext(); + return func(new BackendAPIClient(backendApiDomain, backendApiTimeout)); + } + + export const useFlattenSiteMapQuery = () => useSuspenseQuery({ + queryKey: QUERY_KEYS.SITEMAP_LIST, + queryFn: clientDecorator(BackendAPIs.listSiteMaps), + meta: { invalidates: [ QUERY_KEYS.SITEMAP ] }, + }); + + export const useNestedSiteMapQuery = () => useSuspenseQuery({ + queryKey: QUERY_KEYS.SITEMAP_LIST, + queryFn: async () => { + const { data } = useFlattenSiteMapQuery(); + return Utils.buildNestedSiteMap(data); + }, + meta: { invalidates: [ QUERY_KEYS.SITEMAP_NESTED_LIST ] }, + }); + + export const usePageQuery = (id: string) => useSuspenseQuery({ + queryKey: [ ...QUERY_KEYS.PAGE, id ], + queryFn: () => clientDecorator(BackendAPIs.retrievePage)(id), + }); +} + +export default BackendAPIHooks; From 6cd2481c50a0c9a42fc937fa71f8e9d0b04fa11d Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 18 May 2025 22:15:14 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20Backend=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/components/pages/test.tsx | 7 ++- apps/pyconkr/src/debug/page/backend_test.tsx | 61 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 apps/pyconkr/src/debug/page/backend_test.tsx diff --git a/apps/pyconkr/src/components/pages/test.tsx b/apps/pyconkr/src/components/pages/test.tsx index 228e364..214f290 100644 --- a/apps/pyconkr/src/components/pages/test.tsx +++ b/apps/pyconkr/src/components/pages/test.tsx @@ -2,10 +2,11 @@ import * as React from "react"; import { Box, Button } from "@mui/material"; +import { BackendTestPage } from '../../debug/page/backend_test'; import { MdiTestPage } from "../../debug/page/mdi_test"; import { ShopTestPage } from "../../debug/page/shop_test"; -type SelectedTabType = "shop" | "mdi"; +type SelectedTabType = "shop" | "mdi" | "backend"; export const Test: React.FC = () => { const [selectedTab, setSelectedTab] = React.useState("mdi"); @@ -18,8 +19,12 @@ export const Test: React.FC = () => { + {selectedTab === "shop" && } {selectedTab === "mdi" && } + {selectedTab === "backend" && } ); }; diff --git a/apps/pyconkr/src/debug/page/backend_test.tsx b/apps/pyconkr/src/debug/page/backend_test.tsx new file mode 100644 index 0000000..006c2ae --- /dev/null +++ b/apps/pyconkr/src/debug/page/backend_test.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; +import * as R from "remeda"; + +import { Button, MenuItem, Select, Stack } from '@mui/material'; +import { ErrorBoundary, Suspense } from '@suspensive/react'; + +import * as Common from "@frontend/common"; + +const SiteMapRenderer: React.FC = () => { + const { data } = Common.Hooks.BackendAPI.useNestedSiteMapQuery(); + return
{JSON.stringify(data, null, 2)}
+}; + +const parseCss = (t: unknown): React.CSSProperties => R.isString(t) && !R.isEmpty(t) && JSON.parse(t) + +const PageRenderer: React.FC<{ id: string }> = ({ id }) => { + const { data } = Common.Hooks.BackendAPI.usePageQuery(id); + + return
+ { + data.sections.map( + (s) =>
+ +
+ ) + } +
+}; + +const PageIdSelector: React.FC<{ inputRef: React.Ref }> = ({ inputRef }) => { + const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(); + const pageIdList = data.map((sitemap) => sitemap.page); + + return +} + +const SuspenseWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( + + + {children} + + +) + +export const BackendTestPage: React.FC = () => { + const inputRef = React.useRef(null); + const [pageId, setPageId] = React.useState(null); + + return +
+ +
+ +
+ +
+ {R.isString(pageId) ? : <>페이지를 선택해주세요.} +
+} From 464ca3e2711506e44fd865f81fb3f7f8d6f911c6 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 18 May 2025 22:15:37 +0900 Subject: [PATCH 06/14] =?UTF-8?q?chore:=20Backend=20API=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=A0=95=EC=9D=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dotenv/.env.development | 1 + dotenv/.env.production | 1 + 2 files changed, 2 insertions(+) diff --git a/dotenv/.env.development b/dotenv/.env.development index 1f5fdf5..41f9c73 100644 --- a/dotenv/.env.development +++ b/dotenv/.env.development @@ -1,3 +1,4 @@ +VITE_PYCONKR_BACKEND_API_DOMAIN=https://rest-api.dev.pycon.kr VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.dev.pycon.kr VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=DEBUG_csrftoken VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID=imp80859147 diff --git a/dotenv/.env.production b/dotenv/.env.production index a4b0532..0aac883 100644 --- a/dotenv/.env.production +++ b/dotenv/.env.production @@ -1,3 +1,4 @@ +VITE_PYCONKR_BACKEND_API_DOMAIN=https://rest-api.pycon.kr VITE_PYCONKR_SHOP_API_DOMAIN=https://shop-api.pycon.kr VITE_PYCONKR_SHOP_CSRF_COOKIE_NAME=csrftoken VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID=imp96676915 From 186656437787b595c94fcc17c4ce3200f5cd36a2 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 18 May 2025 22:35:38 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20Namespace=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EC=A4=91=20=EB=88=84=EB=9D=BD=EB=90=9C=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/components/error_handler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/components/error_handler.tsx b/packages/common/src/components/error_handler.tsx index 6930cbf..5bcd37d 100644 --- a/packages/common/src/components/error_handler.tsx +++ b/packages/common/src/components/error_handler.tsx @@ -42,7 +42,7 @@ const SimplifiedErrorFallback: React.FC<{ reset: () => void }> = ({ reset }) => export const ErrorFallback: React.FC<{ error: Error, reset: () => void }> = ({ error, reset }) => { const InnerErrorFallback: React.FC<{ error: Error, reset: () => void }> = ({ error, reset }) => { - const { debug } = CommonContext.useCommonContext(); + const { debug } = CommonContext.Common.useCommonContext(); return debug ? : ; } From dc000ac934884ccedbaaf0c65f35f1b44d3b2142 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 18 May 2025 22:36:39 +0900 Subject: [PATCH 08/14] =?UTF-8?q?chore:=20=EB=A1=9C=EB=94=A9=20=EC=A4=91?= =?UTF-8?q?=20indicator=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/debug/page/backend_test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/pyconkr/src/debug/page/backend_test.tsx b/apps/pyconkr/src/debug/page/backend_test.tsx index 006c2ae..9e27dc3 100644 --- a/apps/pyconkr/src/debug/page/backend_test.tsx +++ b/apps/pyconkr/src/debug/page/backend_test.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import * as R from "remeda"; -import { Button, MenuItem, Select, Stack } from '@mui/material'; +import { Button, CircularProgress, MenuItem, Select, Stack } from '@mui/material'; import { ErrorBoundary, Suspense } from '@suspensive/react'; import * as Common from "@frontend/common"; @@ -38,7 +38,7 @@ const PageIdSelector: React.FC<{ inputRef: React.Ref }> = ({ const SuspenseWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( - + }> {children} From 75d322d4485a444a2a13b9eafc473b401c994b15 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 19 May 2025 09:51:00 +0900 Subject: [PATCH 09/14] =?UTF-8?q?chore:=20=EB=94=94=EB=B2=84=EA=B9=85=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EC=9E=84=EC=9D=84=20=EB=82=98=ED=83=80?= =?UTF-8?q?=EB=82=B4=EB=8A=94=20=EC=A0=84=EC=97=AD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/consts/index.ts | 1 + apps/pyconkr/src/main.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 apps/pyconkr/src/consts/index.ts diff --git a/apps/pyconkr/src/consts/index.ts b/apps/pyconkr/src/consts/index.ts new file mode 100644 index 0000000..9ce1470 --- /dev/null +++ b/apps/pyconkr/src/consts/index.ts @@ -0,0 +1 @@ +export const IS_DEBUG_ENV = import.meta.env.MODE === "development" diff --git a/apps/pyconkr/src/main.tsx b/apps/pyconkr/src/main.tsx index 4aa9acd..6d2e1b8 100644 --- a/apps/pyconkr/src/main.tsx +++ b/apps/pyconkr/src/main.tsx @@ -12,6 +12,7 @@ import * as Common from "@frontend/common"; import * as Shop from "@frontend/shop"; import { App } from "./App.tsx"; +import { IS_DEBUG_ENV } from './consts/index.ts'; import { globalStyles, muiTheme } from "./styles/globalStyles.ts"; const queryClient = new QueryClient({ @@ -35,7 +36,7 @@ const queryClient = new QueryClient({ }); const CommonOptions: Common.Contexts.ContextOptions = { - debug: import.meta.env.MODE === "development", + debug: IS_DEBUG_ENV, baseUrl: '.', backendApiDomain: import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN, backendApiTimeout: 10000, From f0a14088f54c11a09e97fbbc461ecf9ade5cf5c6 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 19 May 2025 09:51:34 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20=EC=8A=A4=ED=82=A4=EB=A7=88?= =?UTF-8?q?=EC=97=90=20route=5Fcode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/schemas/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/common/src/schemas/index.ts b/packages/common/src/schemas/index.ts index e10f7dc..65816a6 100644 --- a/packages/common/src/schemas/index.ts +++ b/packages/common/src/schemas/index.ts @@ -16,19 +16,21 @@ namespace BackendAPISchemas { export type FlattenedSiteMapSchema = { id: string; + route_code: string; name: string; order: number; parent_sitemap: string | null; page: string; - } + }; export type NestedSiteMapSchema = { id: string; + route_code: string; name: string; order: number; page: string; - children: NestedSiteMapSchema[]; - } + children: { [key: string]: NestedSiteMapSchema }; + }; export type SectionSchema = { id: string; @@ -36,7 +38,7 @@ namespace BackendAPISchemas { order: number; body: string; - } + }; export type PageSchema = { id: string; @@ -44,7 +46,7 @@ namespace BackendAPISchemas { title: string; subtitle: string; sections: SectionSchema[]; - } + }; export const isObjectErrorResponseSchema = ( obj?: unknown From e9e05f04ac0744c44f2e7517d2aabf9febf672ca Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 19 May 2025 09:52:14 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20=ED=98=84=EC=9E=AC=20route?= =?UTF-8?q?=EC=97=90=EC=84=9C=20page=20id=EB=A5=BC=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/utils/api.ts | 39 +++++++++++++++++++++++++----- packages/common/src/utils/index.ts | 8 +++++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/common/src/utils/api.ts b/packages/common/src/utils/api.ts index 5b6bf6c..7e9dc28 100644 --- a/packages/common/src/utils/api.ts +++ b/packages/common/src/utils/api.ts @@ -1,24 +1,51 @@ +import * as R from "remeda"; -import BackendAPISchemas from '../schemas'; +import BackendAPISchemas from "../schemas"; -export const buildNestedSiteMap: (flattened: BackendAPISchemas.FlattenedSiteMapSchema[]) => BackendAPISchemas.NestedSiteMapSchema[] = (flattened) => { +export const buildNestedSiteMap: ( + flattened: BackendAPISchemas.FlattenedSiteMapSchema[] +) => { [key: string]: BackendAPISchemas.NestedSiteMapSchema } = (flattened) => { const map: Record = {}; const roots: BackendAPISchemas.NestedSiteMapSchema[] = []; + const siteMapIdRouteCodeMap = flattened.reduce((acc, item) => { + acc[item.id] = item.route_code; + return acc; + }, {} as Record); + flattened.forEach((item) => { map[item.id] = { ...item, - children: [], + children: {}, }; }); flattened.forEach((item) => { if (item.parent_sitemap) { - map[item.parent_sitemap].children.push(map[item.id]); + map[item.parent_sitemap].children[siteMapIdRouteCodeMap[item.id]] = map[item.id]; } else { roots.push(map[item.id]); } }); - return roots; -} + return roots.reduce((acc, item) => { + acc[item.route_code] = item; + return acc; + }, {} as Record); +}; + +export const findSiteMapUsingRoute = (route: string, siteMapData: BackendAPISchemas.NestedSiteMapSchema): BackendAPISchemas.NestedSiteMapSchema | null => { + const currentRouteCodes = ['', ...route.split('/').filter((code) => !R.isEmpty(code))]; + + let currentSitemap: BackendAPISchemas.NestedSiteMapSchema | null | undefined = siteMapData.children[currentRouteCodes[0]]; + if (currentSitemap === undefined) return null; + + for (const routeCode of currentRouteCodes.slice(1)) { + if ((currentSitemap = currentSitemap.children[routeCode] || null) === null) { + break; + } + } + return currentSitemap; +}; + +export const parseCss = (t: unknown): React.CSSProperties => (R.isString(t) && !R.isEmpty(t) && JSON.parse(t)) || {}; diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index ae26abe..45f5f5e 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -1,4 +1,8 @@ -import { buildNestedSiteMap as _buildNestedSiteMap } from './api'; +import { + buildNestedSiteMap as _buildNestedSiteMap, + findSiteMapUsingRoute as _findSiteMapUsingRoute, + parseCss as _parseCss, +} from './api'; import { getCookie as _getCookie } from "./cookie"; import { getFormValue as _getFormValue, @@ -7,6 +11,8 @@ import { namespace Utils { export const buildNestedSiteMap = _buildNestedSiteMap; + export const findSiteMapUsingRoute = _findSiteMapUsingRoute; + export const parseCss = _parseCss; export const getCookie = _getCookie; export const isFormValid = _isFormValid; export const getFormValue = _getFormValue; From 7e71a966ec8a1eca9cd9e039d5096fefea5751bd Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 19 May 2025 09:52:25 +0900 Subject: [PATCH 12/14] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20hook=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/hooks/useAPI.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/common/src/hooks/useAPI.ts b/packages/common/src/hooks/useAPI.ts index 7b1cb91..082e827 100644 --- a/packages/common/src/hooks/useAPI.ts +++ b/packages/common/src/hooks/useAPI.ts @@ -5,12 +5,10 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import BackendAPIs from "../apis"; import { BackendAPIClient } from '../apis/client'; import BackendContext from '../contexts'; -import Utils from "../utils"; const QUERY_KEYS = { SITEMAP: ["query", "sitemap"], SITEMAP_LIST: ["query", "sitemap", "list"], - SITEMAP_NESTED_LIST: ["query", "sitemap", "list", "nested"], PAGE: ["query", "page"], }; @@ -32,15 +30,6 @@ namespace BackendAPIHooks { meta: { invalidates: [ QUERY_KEYS.SITEMAP ] }, }); - export const useNestedSiteMapQuery = () => useSuspenseQuery({ - queryKey: QUERY_KEYS.SITEMAP_LIST, - queryFn: async () => { - const { data } = useFlattenSiteMapQuery(); - return Utils.buildNestedSiteMap(data); - }, - meta: { invalidates: [ QUERY_KEYS.SITEMAP_NESTED_LIST ] }, - }); - export const usePageQuery = (id: string) => useSuspenseQuery({ queryKey: [ ...QUERY_KEYS.PAGE, id ], queryFn: () => clientDecorator(BackendAPIs.retrievePage)(id), From 4d09e6218a2f024e510c1729a82e22e1aedc2369 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 19 May 2025 09:53:04 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20=EA=B2=BD=EB=A1=9C=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=ED=95=98=EB=8A=94=20Component=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/components/layout/index.tsx | 5 ++ apps/pyconkr/src/debug/page/backend_test.tsx | 25 +----- .../common/src/components/dynamic_route.tsx | 79 +++++++++++++++++++ packages/common/src/components/index.ts | 6 ++ 4 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 packages/common/src/components/dynamic_route.tsx diff --git a/apps/pyconkr/src/components/layout/index.tsx b/apps/pyconkr/src/components/layout/index.tsx index bea488d..d54228d 100644 --- a/apps/pyconkr/src/components/layout/index.tsx +++ b/apps/pyconkr/src/components/layout/index.tsx @@ -55,6 +55,11 @@ const LayoutContainer = styled.div` const MainContent = styled.main` flex: 1; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; `; export default function MainLayout() { diff --git a/apps/pyconkr/src/debug/page/backend_test.tsx b/apps/pyconkr/src/debug/page/backend_test.tsx index 9e27dc3..ecc3a30 100644 --- a/apps/pyconkr/src/debug/page/backend_test.tsx +++ b/apps/pyconkr/src/debug/page/backend_test.tsx @@ -7,32 +7,15 @@ import { ErrorBoundary, Suspense } from '@suspensive/react'; import * as Common from "@frontend/common"; const SiteMapRenderer: React.FC = () => { - const { data } = Common.Hooks.BackendAPI.useNestedSiteMapQuery(); - return
{JSON.stringify(data, null, 2)}
-}; - -const parseCss = (t: unknown): React.CSSProperties => R.isString(t) && !R.isEmpty(t) && JSON.parse(t) - -const PageRenderer: React.FC<{ id: string }> = ({ id }) => { - const { data } = Common.Hooks.BackendAPI.usePageQuery(id); - - return
- { - data.sections.map( - (s) =>
- -
- ) - } -
+ const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(); + return
{JSON.stringify(Common.Utils.buildNestedSiteMap(data), null, 2)}
}; const PageIdSelector: React.FC<{ inputRef: React.Ref }> = ({ inputRef }) => { const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(); - const pageIdList = data.map((sitemap) => sitemap.page); return } @@ -56,6 +39,6 @@ export const BackendTestPage: React.FC = () => {

- {R.isString(pageId) ? : <>페이지를 선택해주세요.} + {R.isString(pageId) ? : <>페이지를 선택해주세요.} } diff --git a/packages/common/src/components/dynamic_route.tsx b/packages/common/src/components/dynamic_route.tsx new file mode 100644 index 0000000..f9e0295 --- /dev/null +++ b/packages/common/src/components/dynamic_route.tsx @@ -0,0 +1,79 @@ +import * as React from "react"; +import { useLocation } from 'react-router-dom'; +import * as R from "remeda"; + +import { CircularProgress } from '@mui/material'; +import { ErrorBoundary, Suspense } from '@suspensive/react'; + +import styled from '@emotion/styled'; +import Components from '../components'; +import Hooks from "../hooks"; +import Schemas from "../schemas"; +import Utils from '../utils'; + +const InitialPageStyle: React.CSSProperties = { + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'column', +} + +const InitialSectionStyle: React.CSSProperties = { + width: '100%', +} + +export const PageRenderer: React.FC<{ id: string }> = ({ id }) => { + const { data } = Hooks.BackendAPI.usePageQuery(id); + + return
+ { + data.sections.map( + (s) =>
+ +
+ ) + } +
+}; + +const AsyncDynamicRoutePage: React.FC = () => { + const location = useLocation(); + const { data } = Hooks.BackendAPI.useFlattenSiteMapQuery(); + const nestedSiteMap = Utils.buildNestedSiteMap(data); + + const currentRouteCodes = ['', ...location.pathname.split('/').filter((code) => !R.isEmpty(code))]; + + let currentSitemap: Schemas.NestedSiteMapSchema | null | undefined = nestedSiteMap[currentRouteCodes[0]]; + if (currentSitemap === undefined) { + return <>404 Not Found; + } + + for (const routeCode of currentRouteCodes.slice(1)) { + if ((currentSitemap = currentSitemap.children[routeCode]) === undefined) { + break; + } + } + + return R.isNullish(currentSitemap) + ? <>404 Not Found + : +} + +const FullPage = styled.div` + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +` + +export const DynamicRoutePage: React.FC = () => + + }> + + + +; diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index d93d8e9..6f1efa3 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -1,10 +1,16 @@ import { CommonContextProvider as CommonContextProviderComponent } from './common_context'; +import { + DynamicRoutePage as DynamicRoutePageComponent, + PageRenderer as PageRendererComponent, +} from './dynamic_route'; import { ErrorFallback as ErrorFallbackComponent } from './error_handler'; import { MDXRenderer as MDXRendererComponent } from "./mdx"; import { PythonKorea as PythonKoreaComponent } from './pythonkorea'; namespace Components { export const CommonContextProvider = CommonContextProviderComponent; + export const DynamicRoutePage = DynamicRoutePageComponent; + export const PageRenderer = PageRendererComponent; export const MDXRenderer = MDXRendererComponent; export const PythonKorea = PythonKoreaComponent; export const ErrorFallback = ErrorFallbackComponent; From ac42511fa02c774842a850073c2c4a0d8a10e97f Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 19 May 2025 09:53:26 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat:=20=EA=B2=BD=EB=A1=9C=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20=EB=9D=BC=EC=9A=B0=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/App.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/pyconkr/src/App.tsx b/apps/pyconkr/src/App.tsx index 9869c8b..d08e701 100644 --- a/apps/pyconkr/src/App.tsx +++ b/apps/pyconkr/src/App.tsx @@ -3,13 +3,17 @@ import React from "react"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import MainLayout from "./components/layout"; import { Test } from "./components/pages/test"; +import { IS_DEBUG_ENV } from "./consts/index.ts"; + +import * as Common from "@frontend/common"; export const App: React.FC = () => { return ( }> - } /> + { IS_DEBUG_ENV && } /> } + } />