From 27c1907b9d8ac3e9f90d7730b79f6841e397e9b6 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 6 Jun 2025 23:01:14 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20@mui/system=20dependency=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 --- package.json | 1 + pnpm-lock.yaml | 69 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 12edad0..fd9796b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@mdx-js/rollup": "^3.1.0", "@mui/icons-material": "^7.1.0", "@mui/material": "^7.1.0", + "@mui/system": "^7.1.1", "@rjsf/core": "6.0.0-beta.10", "@rjsf/mui": "6.0.0-beta.10", "@rjsf/utils": "6.0.0-beta.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfc5976..691ea8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@mui/material': specifier: ^7.1.0 version: 7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/system': + specifier: ^7.1.1 + version: 7.1.1(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0) '@rjsf/core': specifier: 6.0.0-beta.10 version: 6.0.0-beta.10(@rjsf/utils@6.0.0-beta.10(react@19.1.0))(react@19.1.0) @@ -695,8 +698,8 @@ packages: '@types/react': optional: true - '@mui/private-theming@7.1.0': - resolution: {integrity: sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==} + '@mui/private-theming@7.1.1': + resolution: {integrity: sha512-M8NbLUx+armk2ZuaxBkkMk11ultnWmrPlN0Xe3jUEaBChg/mcxa5HWIWS1EE4DF36WRACaAHVAvyekWlDQf0PQ==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -705,8 +708,8 @@ packages: '@types/react': optional: true - '@mui/styled-engine@7.1.0': - resolution: {integrity: sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==} + '@mui/styled-engine@7.1.1': + resolution: {integrity: sha512-R2wpzmSN127j26HrCPYVQ53vvMcT5DaKLoWkrfwUYq3cYytL6TQrCH8JBH3z79B6g4nMZZVoaXrxO757AlShaw==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.4.1 @@ -718,8 +721,8 @@ packages: '@emotion/styled': optional: true - '@mui/system@7.1.0': - resolution: {integrity: sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==} + '@mui/system@7.1.1': + resolution: {integrity: sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -742,6 +745,14 @@ packages: '@types/react': optional: true + '@mui/types@7.4.3': + resolution: {integrity: sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/utils@7.1.0': resolution: {integrity: sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==} engines: {node: '>=14.0.0'} @@ -752,6 +763,16 @@ packages: '@types/react': optional: true + '@mui/utils@7.1.1': + resolution: {integrity: sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@napi-rs/wasm-runtime@0.2.10': resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==} @@ -4125,7 +4146,7 @@ snapshots: dependencies: '@babel/runtime': 7.27.1 '@mui/core-downloads-tracker': 7.1.0 - '@mui/system': 7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0) + '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0) '@mui/types': 7.4.2(@types/react@19.1.4) '@mui/utils': 7.1.0(@types/react@19.1.4)(react@19.1.0) '@popperjs/core': 2.11.8 @@ -4142,16 +4163,16 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0) '@types/react': 19.1.4 - '@mui/private-theming@7.1.0(@types/react@19.1.4)(react@19.1.0)': + '@mui/private-theming@7.1.1(@types/react@19.1.4)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.1 - '@mui/utils': 7.1.0(@types/react@19.1.4)(react@19.1.0) + '@mui/utils': 7.1.1(@types/react@19.1.4)(react@19.1.0) prop-types: 15.8.1 react: 19.1.0 optionalDependencies: '@types/react': 19.1.4 - '@mui/styled-engine@7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(react@19.1.0)': + '@mui/styled-engine@7.1.1(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.1 '@emotion/cache': 11.14.0 @@ -4164,13 +4185,13 @@ snapshots: '@emotion/react': 11.14.0(@types/react@19.1.4)(react@19.1.0) '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0) - '@mui/system@7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0)': + '@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.1 - '@mui/private-theming': 7.1.0(@types/react@19.1.4)(react@19.1.0) - '@mui/styled-engine': 7.1.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(react@19.1.0) - '@mui/types': 7.4.2(@types/react@19.1.4) - '@mui/utils': 7.1.0(@types/react@19.1.4)(react@19.1.0) + '@mui/private-theming': 7.1.1(@types/react@19.1.4)(react@19.1.0) + '@mui/styled-engine': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.4)(react@19.1.0))(@types/react@19.1.4)(react@19.1.0))(react@19.1.0) + '@mui/types': 7.4.3(@types/react@19.1.4) + '@mui/utils': 7.1.1(@types/react@19.1.4)(react@19.1.0) clsx: 2.1.1 csstype: 3.1.3 prop-types: 15.8.1 @@ -4186,6 +4207,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.4 + '@mui/types@7.4.3(@types/react@19.1.4)': + dependencies: + '@babel/runtime': 7.27.1 + optionalDependencies: + '@types/react': 19.1.4 + '@mui/utils@7.1.0(@types/react@19.1.4)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.1 @@ -4198,6 +4225,18 @@ snapshots: optionalDependencies: '@types/react': 19.1.4 + '@mui/utils@7.1.1(@types/react@19.1.4)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/types': 7.4.3(@types/react@19.1.4) + '@types/prop-types': 15.7.14 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.0 + react-is: 19.1.0 + optionalDependencies: + '@types/react': 19.1.4 + '@napi-rs/wasm-runtime@0.2.10': dependencies: '@emnapi/core': 1.4.3 From a4deb78f739e0af1d19c409d512e8e07074bc300 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 6 Jun 2025 23:02:05 +0900 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=EC=83=88=EB=A1=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=9C=20=ED=95=84=EB=93=9C=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/backendAPI.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/common/src/schemas/backendAPI.ts b/packages/common/src/schemas/backendAPI.ts index 9f3b1f8..22797cc 100644 --- a/packages/common/src/schemas/backendAPI.ts +++ b/packages/common/src/schemas/backendAPI.ts @@ -45,6 +45,10 @@ namespace BackendAPISchemas { css: string; title: string; subtitle: string; + + show_top_title_banner: boolean; + show_bottom_sponsor_banner: boolean; + sections: SectionSchema[]; }; From 71cee4a32fd54f7560d64597af56dd9f678c40a6 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 6 Jun 2025 23:03:18 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20API=EC=97=90=20=EC=96=B8=EC=96=B4?= =?UTF-8?q?=20=EC=A7=80=EC=9B=90=EC=9D=84=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/apis/client.ts | 12 ++++++++++-- packages/common/src/apis/index.ts | 3 ++- .../common/src/components/mdx_components/map.tsx | 6 ++++-- packages/common/src/contexts/index.ts | 2 ++ packages/common/src/hooks/useAPI.ts | 8 ++++---- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/common/src/apis/client.ts b/packages/common/src/apis/client.ts index f8a79df..c54aea3 100644 --- a/packages/common/src/apis/client.ts +++ b/packages/common/src/apis/client.ts @@ -59,6 +59,8 @@ export class BackendAPIClientError extends Error { } } +type supportedLanguages = "ko" | "en"; + type AxiosRequestWithoutPayload = , D = unknown>( url: string, config?: AxiosRequestConfig @@ -70,6 +72,7 @@ type AxiosRequestWithPayload = , D = unknown>( ) => Promise; export class BackendAPIClient { + readonly language: supportedLanguages; readonly baseURL: string; protected readonly csrfCookieName: string; private readonly backendAPI: AxiosInstance; @@ -78,9 +81,14 @@ export class BackendAPIClient { baseURL: string, timeout: number, csrfCookieName: string = "csrftoken", - withCredentials: boolean = false + withCredentials: boolean = false, + language: supportedLanguages = "ko" ) { - const headers = { "Content-Type": "application/json" }; + const headers = { + "Content-Type": "application/json", + "Accept-Language": language, + }; + this.language = language; this.baseURL = baseURL; this.csrfCookieName = csrfCookieName; this.backendAPI = axios.create({ diff --git a/packages/common/src/apis/index.ts b/packages/common/src/apis/index.ts index cca823d..3f679b6 100644 --- a/packages/common/src/apis/index.ts +++ b/packages/common/src/apis/index.ts @@ -1,7 +1,8 @@ -import { BackendAPIClient } from "./client"; +import { BackendAPIClient, BackendAPIClientError as _BackendAPIClientError } from "./client"; import BackendAPISchemas from "../schemas/backendAPI"; namespace BackendAPIs { + export const BackendAPIClientError = _BackendAPIClientError; export const listSiteMaps = (client: BackendAPIClient) => () => client.get("v1/cms/sitemap/"); export const retrievePage = (client: BackendAPIClient) => (id: string) => diff --git a/packages/common/src/components/mdx_components/map.tsx b/packages/common/src/components/mdx_components/map.tsx index 486926a..89fa785 100644 --- a/packages/common/src/components/mdx_components/map.tsx +++ b/packages/common/src/components/mdx_components/map.tsx @@ -2,13 +2,14 @@ import { Box, Button, Stack, Tab, Tabs } from "@mui/material"; import * as React from "react"; import { renderToStaticMarkup } from "react-dom/server"; +import { useCommonContext } from "../../hooks/useCommonContext"; + type SupportedMapType = "kakao" | "google" | "naver"; const MAP_TYPES: SupportedMapType[] = ["kakao", "google", "naver"]; type LangType = "ko" | "en"; export type MapPropType = { - language: LangType; geo: { lat: number; lng: number; @@ -54,7 +55,8 @@ const MapData: { [key in SupportedMapType]: MapDataType } = { }, }; -export const Map: React.FC = ({ language, geo, placeName, placeCode, googleMapIframeSrc }) => { +export const Map: React.FC = ({ geo, placeName, placeCode, googleMapIframeSrc }) => { + const { language } = useCommonContext(); const kakaoMapRef = React.useRef(null); const [mapState, setMapState] = React.useState({ tab: 0 }); const selectedMapType = MAP_TYPES[mapState.tab] || "kakao"; diff --git a/packages/common/src/contexts/index.ts b/packages/common/src/contexts/index.ts index ad75691..69ffc1d 100644 --- a/packages/common/src/contexts/index.ts +++ b/packages/common/src/contexts/index.ts @@ -3,6 +3,7 @@ import * as React from "react"; namespace GlobalContext { export type ContextOptions = { + language: "ko" | "en"; frontendDomain?: string; baseUrl: string; debug?: boolean; @@ -13,6 +14,7 @@ namespace GlobalContext { }; export const context = React.createContext({ + language: "ko", frontendDomain: "", baseUrl: "", debug: false, diff --git a/packages/common/src/hooks/useAPI.ts b/packages/common/src/hooks/useAPI.ts index 974eba1..1c0fa59 100644 --- a/packages/common/src/hooks/useAPI.ts +++ b/packages/common/src/hooks/useAPI.ts @@ -18,19 +18,19 @@ namespace BackendAPIHooks { }; export const useBackendClient = () => { - const { backendApiDomain, backendApiTimeout } = useBackendContext(); - return new BackendAPIClient(backendApiDomain, backendApiTimeout); + const { language, backendApiDomain, backendApiTimeout } = useBackendContext(); + return new BackendAPIClient(backendApiDomain, backendApiTimeout, "", false, language); }; export const useFlattenSiteMapQuery = (client: BackendAPIClient) => useSuspenseQuery({ - queryKey: QUERY_KEYS.SITEMAP_LIST, + queryKey: [client.language, ...QUERY_KEYS.SITEMAP_LIST], queryFn: BackendAPIs.listSiteMaps(client), }); export const usePageQuery = (client: BackendAPIClient, id: string) => useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PAGE, id], + queryKey: [client.language, ...QUERY_KEYS.PAGE, id], queryFn: () => BackendAPIs.retrievePage(client)(id), }); } From 5a6e580f76e135df19aed64e6f31ef61c0f29af2 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 6 Jun 2025 23:05:18 +0900 Subject: [PATCH 4/6] =?UTF-8?q?chore:=20AppContext=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr-admin/src/main.tsx | 1 + apps/pyconkr/src/App.tsx | 64 ++++---- .../src/components/pages/dynamic_route.tsx | 138 ++++++++++++++++++ apps/pyconkr/src/consts/local_stroage.ts | 1 + apps/pyconkr/src/contexts/SponsorContext.tsx | 27 ---- apps/pyconkr/src/contexts/app_context.tsx | 28 ++++ apps/pyconkr/src/main.tsx | 83 ++++++----- .../common/src/components/dynamic_route.tsx | 133 ----------------- packages/common/src/components/index.ts | 8 - 9 files changed, 256 insertions(+), 227 deletions(-) create mode 100644 apps/pyconkr/src/components/pages/dynamic_route.tsx create mode 100644 apps/pyconkr/src/consts/local_stroage.ts delete mode 100644 apps/pyconkr/src/contexts/SponsorContext.tsx create mode 100644 apps/pyconkr/src/contexts/app_context.tsx delete mode 100644 packages/common/src/components/dynamic_route.tsx diff --git a/apps/pyconkr-admin/src/main.tsx b/apps/pyconkr-admin/src/main.tsx index a2055cf..6545c53 100644 --- a/apps/pyconkr-admin/src/main.tsx +++ b/apps/pyconkr-admin/src/main.tsx @@ -32,6 +32,7 @@ const queryClient = new QueryClient({ const CommonOptions: Common.Contexts.ContextOptions = { debug: true, + language: "ko", baseUrl: ".", frontendDomain: import.meta.env.VITE_PYCONKR_FRONTEND_DOMAIN, backendApiDomain: import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN, diff --git a/apps/pyconkr/src/App.tsx b/apps/pyconkr/src/App.tsx index f02b9f1..1471c9c 100644 --- a/apps/pyconkr/src/App.tsx +++ b/apps/pyconkr/src/App.tsx @@ -1,36 +1,48 @@ import * as Common from "@frontend/common"; -import React from "react"; -import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; +import * as React from "react"; +import { Route, Routes, useLocation } from "react-router-dom"; +import * as R from "remeda"; -import MainLayout from "./components/layout"; -import { Test } from "./components/pages/test"; -import { IS_DEBUG_ENV } from "./consts/index.ts"; -import { SponsorProvider } from "./contexts/SponsorContext"; +import MainLayout from "./components/layout/index.tsx"; +import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx"; +import { Test } from "./components/pages/test.tsx"; +import { IS_DEBUG_ENV } from "./consts"; +import { useAppContext } from "./contexts/app_context"; +import BackendAPISchemas from "../../../packages/common/src/schemas/backendAPI"; -// 스폰서를 표시할 페이지 경로 설정 -const SPONSOR_VISIBLE_PATHS = ["/"]; +export const App: React.FC = () => { + const backendAPIClient = Common.Hooks.BackendAPI.useBackendClient(); + const { data: flatSiteMap } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendAPIClient); + const siteMapNode = Common.Utils.buildNestedSiteMap(flatSiteMap)?.[""]; -const AppContent = () => { const location = useLocation(); - const shouldShowSponsor = SPONSOR_VISIBLE_PATHS.includes(location.pathname); + const { setAppContext, language } = useAppContext(); - return ( - - - }> - {IS_DEBUG_ENV && } />} - } /> - } /> - - - - ); -}; + React.useEffect(() => { + (async () => { + const currentRouteCodes = ["", ...location.pathname.split("/").filter((code) => !R.isEmpty(code))]; + const currentSiteMapDepth: (BackendAPISchemas.NestedSiteMapSchema | undefined)[] = [siteMapNode]; + + for (const routeCode of currentRouteCodes.splice(1)) { + currentSiteMapDepth.push(currentSiteMapDepth.at(-1)?.children[routeCode]); + if (R.isNullish(currentSiteMapDepth.at(-1))) { + console.warn(`Route not found in site map: ${routeCode}`); + break; + } + } + + setAppContext((ps) => ({ ...ps, siteMapNode, currentSiteMapDepth })); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location, language, flatSiteMap]); -export const App: React.FC = () => { return ( - - - + + }> + {IS_DEBUG_ENV && } />} + } /> + } /> + + ); }; diff --git a/apps/pyconkr/src/components/pages/dynamic_route.tsx b/apps/pyconkr/src/components/pages/dynamic_route.tsx new file mode 100644 index 0000000..2c036b2 --- /dev/null +++ b/apps/pyconkr/src/components/pages/dynamic_route.tsx @@ -0,0 +1,138 @@ +import * as Common from "@frontend/common"; +import { CircularProgress, Stack, Theme } from "@mui/material"; +import { ErrorBoundary, Suspense } from "@suspensive/react"; +import { AxiosError, AxiosResponse } from "axios"; +import * as React from "react"; +import { useLocation, useParams } from "react-router-dom"; +import * as R from "remeda"; + +import { useAppContext } from "../../contexts/app_context"; + +const initialPageStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties = + (additionalStyle) => (theme) => ({ + width: "100%", + display: "flex", + justifyContent: "flex-start", + alignItems: "center", + flexDirection: "column", + + marginTop: theme.spacing(4), + + ...(!R.isEmpty(additionalStyle) + ? additionalStyle + : { + [theme.breakpoints.down("lg")]: { + marginTop: theme.spacing(2), + }, + [theme.breakpoints.down("sm")]: { + marginTop: theme.spacing(1), + }, + }), + }); + +const initialSectionStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties = + (additionalStyle) => (theme) => ({ + width: "100%", + maxWidth: "1200px", + display: "flex", + justifyContent: "flex-start", + alignItems: "center", + paddingRight: theme.spacing(16), + paddingLeft: theme.spacing(16), + + "& .markdown-body": { width: "100%" }, + ...(!R.isEmpty(additionalStyle) + ? additionalStyle + : { + [theme.breakpoints.down("lg")]: { + paddingRight: theme.spacing(4), + paddingLeft: theme.spacing(4), + }, + [theme.breakpoints.down("sm")]: { + paddingRight: theme.spacing(2), + paddingLeft: theme.spacing(2), + }, + }), + }); + +const LoginRequired: React.FC = () => <>401 Login Required; +const PermissionDenied: React.FC = () => <>403 Permission Denied; +const PageNotFound: React.FC = () => <>404 Not Found; +const CenteredLoadingPage: React.FC = () => ( + + + +); + +const throwPageNotFound: (message: string) => never = (message) => { + const errorStr = `RouteRenderer: ${message}`; + const axiosError = new AxiosError(errorStr, errorStr, undefined, undefined, { + status: 404, + } as AxiosResponse); + throw new Common.BackendAPIs.BackendAPIClientError(axiosError); +}; + +const RouteErrorFallback: React.FC<{ error: Error; reset: () => void }> = ({ error, reset }) => { + if (error instanceof Common.BackendAPIs.BackendAPIClientError) { + switch (error.status) { + case 401: + return ; + case 403: + return ; + case 404: + return ; + default: + return ; + } + } + return ; +}; + +export const PageRenderer: React.FC<{ id: string }> = ErrorBoundary.with( + { fallback: RouteErrorFallback }, + Suspense.with({ fallback: }, ({ id }) => { + const { setAppContext } = useAppContext(); + const backendClient = Common.Hooks.BackendAPI.useBackendClient(); + const { data } = Common.Hooks.BackendAPI.usePageQuery(backendClient, id); + + React.useEffect(() => { + setAppContext((prev) => ({ + ...prev, + title: data.title, + shouldShowTitleBanner: data.show_top_title_banner, + shouldShowSponsorBanner: data.show_bottom_sponsor_banner, + })); + }, [data, setAppContext]); + + return ( + + {data.sections.map((s) => ( + + + + ))} + + ); + }) +); + +export const RouteRenderer: React.FC = ErrorBoundary.with( + { fallback: RouteErrorFallback }, + Suspense.with({ fallback: }, () => { + const location = useLocation(); + const { siteMapNode, currentSiteMapDepth } = useAppContext(); + + if (!siteMapNode) return ; + + const routeInfo = !R.isEmpty(currentSiteMapDepth) && currentSiteMapDepth[currentSiteMapDepth.length - 1]; + if (!routeInfo) throwPageNotFound(`Route ${location} not found`); + + return ; + }) +); + +export const PageIdParamRenderer: React.FC = Suspense.with({ fallback: }, () => { + const { id } = useParams(); + if (!id) throwPageNotFound("Page ID is required"); + return ; +}); diff --git a/apps/pyconkr/src/consts/local_stroage.ts b/apps/pyconkr/src/consts/local_stroage.ts new file mode 100644 index 0000000..d2e88da --- /dev/null +++ b/apps/pyconkr/src/consts/local_stroage.ts @@ -0,0 +1 @@ +export const LOCAL_STORAGE_LANGUAGE_KEY = "language"; diff --git a/apps/pyconkr/src/contexts/SponsorContext.tsx b/apps/pyconkr/src/contexts/SponsorContext.tsx deleted file mode 100644 index f7ada60..0000000 --- a/apps/pyconkr/src/contexts/SponsorContext.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { createContext, useContext, useState, ReactNode } from "react"; - -interface SponsorContextType { - isVisible: boolean; - setIsVisible: (value: boolean) => void; -} - -const SponsorContext = createContext(undefined); - -interface SponsorProviderProps { - children: ReactNode; - initialVisibility?: boolean; -} - -export function SponsorProvider({ children, initialVisibility = false }: SponsorProviderProps) { - const [isVisible, setIsVisible] = useState(initialVisibility); - - return {children}; -} - -export function useSponsor() { - const context = useContext(SponsorContext); - if (context === undefined) { - throw new Error("useSponsor must be used within a SponsorProvider"); - } - return context; -} diff --git a/apps/pyconkr/src/contexts/app_context.tsx b/apps/pyconkr/src/contexts/app_context.tsx new file mode 100644 index 0000000..bbf4500 --- /dev/null +++ b/apps/pyconkr/src/contexts/app_context.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; + +import BackendAPISchemas from "../../../../packages/common/src/schemas/backendAPI"; + +type LanguageType = "ko" | "en"; + +export type AppContextType = { + language: LanguageType; + shouldShowTitleBanner: boolean; + shouldShowSponsorBanner: boolean; + + siteMapNode?: BackendAPISchemas.NestedSiteMapSchema; + sponsors: unknown; + title: string; + currentSiteMapDepth: (BackendAPISchemas.NestedSiteMapSchema | undefined)[]; + + setAppContext: React.Dispatch>>; +}; + +export const AppContext = React.createContext(undefined); + +export const useAppContext = (): AppContextType => { + const context = React.useContext(AppContext); + if (!context) { + throw new Error("useAppContext must be used within an AppContextProvider"); + } + return context; +}; diff --git a/apps/pyconkr/src/main.tsx b/apps/pyconkr/src/main.tsx index 560f5ba..0dba916 100644 --- a/apps/pyconkr/src/main.tsx +++ b/apps/pyconkr/src/main.tsx @@ -8,10 +8,13 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { SnackbarProvider } from "notistack"; import * as React from "react"; import * as ReactDom from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; import { App } from "./App.tsx"; import { IS_DEBUG_ENV } from "./consts"; +import { LOCAL_STORAGE_LANGUAGE_KEY } from "./consts/local_stroage.ts"; import { PyConKRMDXComponents } from "./consts/mdx_components.ts"; +import { AppContext, AppContextType } from "./contexts/app_context.tsx"; import { globalStyles, muiTheme } from "./styles/globalStyles.ts"; const queryClient = new QueryClient({ @@ -32,6 +35,7 @@ const queryClient = new QueryClient({ }); const CommonOptions: Common.Contexts.ContextOptions = { + language: "ko", debug: IS_DEBUG_ENV, baseUrl: ".", backendApiDomain: import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN, @@ -46,37 +50,50 @@ const ShopOptions: Shop.Contexts.ContextOptions = { shopImpAccountId: import.meta.env.VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID, }; -ReactDom.createRoot(document.getElementById("root")!).render( - - - - - - - - - - - 문제가 발생했습니다, 새로고침을 해주세요. - - } - > - - - - } - > - - - - - - - - - +const SuspenseFallback = ( + + + ); + +const MainApp: React.FC = () => { + const [appState, setAppContext] = React.useState>({ + language: (localStorage.getItem(LOCAL_STORAGE_LANGUAGE_KEY) as "ko" | "en" | null) ?? "ko", + shouldShowTitleBanner: true, + shouldShowSponsorBanner: false, + + currentSiteMapDepth: [], + + sponsors: null, + title: "PyCon Korea 2025", + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +ReactDom.createRoot(document.getElementById("root")!).render(); diff --git a/packages/common/src/components/dynamic_route.tsx b/packages/common/src/components/dynamic_route.tsx deleted file mode 100644 index 1ab3397..0000000 --- a/packages/common/src/components/dynamic_route.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { CircularProgress, Stack, Theme } from "@mui/material"; -import { ErrorBoundary, Suspense } from "@suspensive/react"; -import { AxiosError, AxiosResponse } from "axios"; -import * as React from "react"; -import { useLocation, useParams } from "react-router-dom"; -import * as R from "remeda"; - -import { BackendAPIClientError } from "../apis/client"; -import Hooks from "../hooks"; -import BackendAPISchemas from "../schemas/backendAPI"; -import Utils from "../utils"; -import { ErrorFallback } from "./error_handler"; -import { MDXRenderer } from "./mdx"; - -const initialPageStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties = - (additionalStyle) => (theme) => ({ - width: "100%", - display: "flex", - justifyContent: "flex-start", - alignItems: "center", - flexDirection: "column", - - marginTop: theme.spacing(8), - - ...(additionalStyle - ? additionalStyle - : { - [theme.breakpoints.down("md")]: { - marginTop: theme.spacing(4), - }, - [theme.breakpoints.down("sm")]: { - marginTop: theme.spacing(2), - }, - }), - ...additionalStyle, - }); - -const initialSectionStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties = - (additionalStyle) => (theme) => ({ - width: "100%", - maxWidth: "1000px", - display: "flex", - justifyContent: "flex-start", - alignItems: "center", - paddingRight: "2rem", - paddingLeft: "2rem", - - "& .markdown-body": { width: "100%" }, - ...(additionalStyle - ? additionalStyle - : { - [theme.breakpoints.down("md")]: { - paddingRight: "1rem", - paddingLeft: "1rem", - }, - [theme.breakpoints.down("sm")]: { - paddingRight: "0.5rem", - paddingLeft: "0.5rem", - }, - }), - }); - -const LoginRequired: React.FC = () => <>401 Login Required; -const PermissionDenied: React.FC = () => <>403 Permission Denied; -const PageNotFound: React.FC = () => <>404 Not Found; - -const throwPageNotFound: (message: string) => never = (message) => { - const errorStr = `RouteRenderer: ${message}`; - const axiosError = new AxiosError(errorStr, errorStr, undefined, undefined, { - status: 404, - } as AxiosResponse); - throw new BackendAPIClientError(axiosError); -}; - -const RouteErrorFallback: React.FC<{ error: Error; reset: () => void }> = ({ error, reset }) => { - if (error instanceof BackendAPIClientError) { - switch (error.status) { - case 401: - return ; - case 403: - return ; - case 404: - return ; - default: - return ; - } - } - return ; -}; - -export const PageRenderer: React.FC<{ id?: string }> = ErrorBoundary.with( - { fallback: RouteErrorFallback }, - Suspense.with({ fallback: }, ({ id }) => { - const backendClient = Hooks.BackendAPI.useBackendClient(); - const { data } = Hooks.BackendAPI.usePageQuery(backendClient, id || ""); - - return ( - - {data.sections.map((s) => ( - - - - ))} - - ); - }) -); - -export const RouteRenderer: React.FC = ErrorBoundary.with( - { fallback: RouteErrorFallback }, - Suspense.with({ fallback: }, () => { - const location = useLocation(); - - const backendClient = Hooks.BackendAPI.useBackendClient(); - const { data } = Hooks.BackendAPI.useFlattenSiteMapQuery(backendClient); - const nestedSiteMap = Utils.buildNestedSiteMap(data); - - const currentRouteCodes = ["", ...location.pathname.split("/").filter((code) => !R.isEmpty(code))]; - let currentSitemap: BackendAPISchemas.NestedSiteMapSchema | undefined = nestedSiteMap[currentRouteCodes[0]]; - if (currentSitemap === undefined) throwPageNotFound(`Route ${location} not found`); - - for (const routeCode of currentRouteCodes.slice(1)) - if ((currentSitemap = currentSitemap.children[routeCode]) === undefined) - throwPageNotFound(`Route ${location} not found`); - - return ; - }) -); - -export const PageIdParamRenderer: React.FC = Suspense.with({ fallback: }, () => { - const { id } = useParams(); - return ; -}); diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index b409d89..257b7f4 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -1,10 +1,5 @@ import { CenteredPage as CenteredPageComponent } from "./centered_page"; import { CommonContextProvider as CommonContextProviderComponent } from "./common_context"; -import { - PageIdParamRenderer as PageIdParamRendererComponent, - PageRenderer as PageRendererComponent, - RouteRenderer as RouteRendererComponent, -} from "./dynamic_route"; import { ErrorFallback as ErrorFallbackComponent } from "./error_handler"; import { LottieDebugPanel as LottieDebugPanelComponent, @@ -24,9 +19,6 @@ import { PythonKorea as PythonKoreaComponent } from "./pythonkorea"; namespace Components { export const CenteredPage = CenteredPageComponent; export const CommonContextProvider = CommonContextProviderComponent; - export const RouteRenderer = RouteRendererComponent; - export const PageRenderer = PageRendererComponent; - export const PageIdParamRenderer = PageIdParamRendererComponent; export const MDXEditor = MDXEditorComponent; export const MDXRenderer = MDXRendererComponent; export const PythonKorea = PythonKoreaComponent; From e71d63e9eb9274a5d8d0f1db6d2ddc8110a5c4cc Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 6 Jun 2025 23:06:02 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20GnB=20=EB=B0=8F=20Footer=EB=A5=BC?= =?UTF-8?q?=20API=EC=97=90=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/layout/BreadCrumb/index.tsx | 97 +++-- .../src/components/layout/Header/index.tsx | 316 ++++++++++++-- .../layout/LanguageSelector/index.tsx | 40 +- .../components/layout/LoginButton/index.tsx | 23 +- .../src/components/layout/Nav/index.tsx | 392 ------------------ .../src/components/layout/Sponsor/index.tsx | 4 - apps/pyconkr/src/components/layout/index.tsx | 14 +- apps/pyconkr/src/debug/page/backend_test.tsx | 8 +- apps/pyconkr/src/debug/page/map_test.tsx | 1 - apps/pyconkr/src/hooks/useMenu.ts | 52 --- 10 files changed, 369 insertions(+), 578 deletions(-) delete mode 100644 apps/pyconkr/src/components/layout/Nav/index.tsx delete mode 100644 apps/pyconkr/src/hooks/useMenu.ts diff --git a/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx b/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx index 1a6fdaf..464b7af 100644 --- a/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx +++ b/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx @@ -1,56 +1,71 @@ -import styled from "@emotion/styled"; -import { useEffect, useState } from "react"; +import { Stack, styled } from "@mui/material"; +import * as React from "react"; +import { Link } from "react-router-dom"; +import * as R from "remeda"; -export default function BreadCrumb() { - const [breadcrumbInfo, setBreadcrumbInfo] = useState({ - paths: [{ text: "홈", href: "/" }], - title: "파이콘 한국 행동강령(CoC)", - }); +import BackendAPISchemas from "../../../../../../packages/common/src/schemas/backendAPI"; - useEffect(() => { - const mockPathInfo = { - paths: [ - { text: "홈", href: "/" }, - { text: "파이콘 한국", href: "/about" }, - ], - title: "파이콘 한국 행동강령(CoC)", - }; - setBreadcrumbInfo(mockPathInfo); - }, []); +type BreadCrumbPropType = { + title: string; + parentSiteMaps: (BackendAPISchemas.NestedSiteMapSchema | undefined)[]; +}; +export const BreadCrumb: React.FC = ({ title, parentSiteMaps }) => { + let route = "/"; return ( - - {breadcrumbInfo.paths.map((item, index) => ( - - {index > 0 && >} - {item.text} - - ))} + + {parentSiteMaps + .slice(1, -1) + .filter((routeInfo) => R.isNonNullish(routeInfo)) + .map(({ route_code, name }, index) => { + route += `${route_code}/`; + return ( + + {index > 0 && >} + + + ); + })} - {breadcrumbInfo.title} + {title} ); -} +}; -const BreadCrumbContainer = styled.div` - width: 100%; - padding: 14px 117px; - background-color: rgba(255, 255, 255, 0.7); - background-image: linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.45)); - box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - gap: 5px; -`; +const BreadCrumbContainer = styled(Stack)(({ theme }) => ({ + position: "fixed", + + top: "3.625rem", + width: "100%", + height: "4.5rem", + background: "linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.45))", + boxShadow: "0 1px 10px rgba(0, 0, 0, 0.1)", + backdropFilter: "blur(10px)", + + gap: "0.25rem", + justifyContent: "center", + alignItems: "flex-start", + + zIndex: theme.zIndex.appBar - 1, + + paddingRight: "8rem", + paddingLeft: "8rem", + + [theme.breakpoints.down("lg")]: { + paddingRight: "2rem", + paddingLeft: "2rem", + }, + [theme.breakpoints.down("sm")]: { + paddingRight: "1rem", + paddingLeft: "1rem", + }, +})); -const BreadcrumbPathContainer = styled.div` +const BreadcrumbPathContainer = styled(Stack)` font-size: 9.75px; font-weight: 300; color: #000000; - display: flex; - align-items: center; - gap: 0; a { color: #000000; @@ -67,7 +82,7 @@ const BreadcrumbPathContainer = styled.div` } `; -const PageTitle = styled.h1` +const PageTitle = styled("h1")` font-size: 27px; font-weight: 600; color: #000000; diff --git a/apps/pyconkr/src/components/layout/Header/index.tsx b/apps/pyconkr/src/components/layout/Header/index.tsx index d9aab15..b8301fd 100644 --- a/apps/pyconkr/src/components/layout/Header/index.tsx +++ b/apps/pyconkr/src/components/layout/Header/index.tsx @@ -1,57 +1,287 @@ -import styled from "@emotion/styled"; import * as Common from "@frontend/common"; -import { useNavigate } from "react-router-dom"; +import { ArrowForwardIos } from "@mui/icons-material"; +import { Box, Button, CircularProgress, Divider, Stack, styled, SxProps, Theme, Typography } from "@mui/material"; +import { MUIStyledCommonProps } from "@mui/system"; +import * as React from "react"; +import { Link } from "react-router-dom"; +import * as R from "remeda"; -import BreadCrumb from "../BreadCrumb"; +import BackendAPISchemas from "../../../../../../packages/common/src/schemas/backendAPI"; +import { useAppContext } from "../../../contexts/app_context"; import LanguageSelector from "../LanguageSelector"; import LoginButton from "../LoginButton"; -import Nav from "../Nav"; + +type MenuType = BackendAPISchemas.NestedSiteMapSchema; +type MenuOrUndefinedType = MenuType | undefined; + +type NavigationStateType = { + depth1?: MenuType; + depth2?: MenuType; + depth3?: MenuType; +}; + +const HeaderHeight: React.CSSProperties["height"] = "3.625rem"; +const BreadCrumbHeight: React.CSSProperties["height"] = "4.5rem"; const Header: React.FC = () => { - const navigate = useNavigate(); + const { title, language, siteMapNode, currentSiteMapDepth, shouldShowTitleBanner } = useAppContext(); + const [navState, setNavState] = React.useState({}); + + const resetDepths = () => setNavState({}); + const setDepth1 = (depth1: MenuOrUndefinedType) => setNavState({ depth1 }); + const setDepth2 = (depth2: MenuOrUndefinedType) => setNavState((ps) => ({ ...ps, depth2, depth3: undefined })); + const setDepth3 = (depth3: MenuOrUndefinedType) => setNavState((ps) => ({ ...ps, depth3 })); + + const getDepth2Route = (nextRoute?: string) => (navState.depth1?.route_code || "") + `/${nextRoute || ""}`; + const getDepth3Route = (nextRoute?: string) => getDepth2Route(navState.depth2?.route_code) + `/${nextRoute || ""}`; + + React.useEffect(resetDepths, [language]); + + let breadCrumbRoute = ""; + let breadCrumbArray = currentSiteMapDepth.slice(1, -1); + if (R.isEmpty(breadCrumbArray)) breadCrumbArray = currentSiteMapDepth.slice(0, -1); + + const headerContainerStyle: SxProps = shouldShowTitleBanner + ? {} + : { + backgroundColor: "transparent", + [":hover"]: { backgroundColor: (theme) => theme.palette.primary.light }, + }; return ( - <> - - navigate("/")}> - - -