From a5f090171e62213972f3fd172fd25c2053249786 Mon Sep 17 00:00:00 2001 From: y00eunji Date: Wed, 18 Jun 2025 14:24:53 +0900 Subject: [PATCH 001/324] =?UTF-8?q?feat=20:=20=EB=AA=A8=EB=B0=94=EC=9D=BC?= =?UTF-8?q?=20GNB=20=EB=A7=88=ED=81=AC=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/Header/Mobile/HamburgerButton.tsx | 46 +++ .../layout/Header/Mobile/MobileHeader.tsx | 95 +++++ .../Header/Mobile/MobileLanguageToggle.tsx | 65 ++++ .../layout/Header/Mobile/MobileNavigation.tsx | 354 ++++++++++++++++++ .../src/components/layout/Header/index.tsx | 9 +- .../components/layout/SignInButton/index.tsx | 57 ++- apps/pyconkr/src/styles/globalStyles.ts | 12 + types/emotion.d.ts | 24 ++ 8 files changed, 650 insertions(+), 12 deletions(-) create mode 100644 apps/pyconkr/src/components/layout/Header/Mobile/HamburgerButton.tsx create mode 100644 apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx create mode 100644 apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx create mode 100644 apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/HamburgerButton.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/HamburgerButton.tsx new file mode 100644 index 00000000..c859d123 --- /dev/null +++ b/apps/pyconkr/src/components/layout/Header/Mobile/HamburgerButton.tsx @@ -0,0 +1,46 @@ +import { IconButton, styled } from "@mui/material"; +import * as React from "react"; + +interface HamburgerButtonProps { + isOpen: boolean; + onClick: () => void; + isMainPath?: boolean; +} + +export const HamburgerButton: React.FC = ({ isOpen, onClick, isMainPath = true }) => { + return ( + + + + + + + + ); +}; + +const StyledIconButton = styled(IconButton)<{ isMainPath: boolean }>(({ theme, isMainPath }) => ({ + padding: 0, + width: 26, + height: 18, + color: isMainPath ? theme.palette.mobileHeader.main.text : theme.palette.mobileHeader.sub.text, +})); + +const HamburgerIcon = styled("div")<{ isOpen: boolean; isMainPath: boolean }>(({ isOpen, theme, isMainPath }) => ({ + width: 26, + height: 18, + position: "relative", + cursor: "pointer", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + + "& span": { + display: "block", + height: isOpen ? 3 : 2, + width: "100%", + backgroundColor: isMainPath ? theme.palette.mobileHeader.main.text : theme.palette.mobileHeader.sub.text, + borderRadius: 1, + transition: "height 0.3s ease", + }, +})); diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx new file mode 100644 index 00000000..37206f2a --- /dev/null +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx @@ -0,0 +1,95 @@ +import * as Common from "@frontend/common"; +import { Box, Stack, styled, Typography } from "@mui/material"; +import * as React from "react"; +import { Link, useLocation } from "react-router-dom"; + +import { HamburgerButton } from "./HamburgerButton"; +import { MobileLanguageToggle } from "./MobileLanguageToggle"; +import { MobileNavigation } from "./MobileNavigation"; +import { useAppContext } from "../../../../contexts/app_context"; + +interface MobileHeaderProps { + isNavigationOpen?: boolean; + onToggleNavigation?: () => void; +} + +export const MobileHeader: React.FC = ({ isNavigationOpen = false, onToggleNavigation }) => { + const { siteMapNode, language } = useAppContext(); + const location = useLocation(); + const [internalNavigationOpen, setInternalNavigationOpen] = React.useState(false); + + const navigationOpen = onToggleNavigation ? isNavigationOpen : internalNavigationOpen; + const toggleNavigation = onToggleNavigation || (() => setInternalNavigationOpen(!internalNavigationOpen)); + + const isMainPath = location.pathname === "/"; + + const handleLanguageChange = (newLanguage: string) => { + // TODO: 언어 변경 로직 구현 + console.log("Language changed to:", newLanguage); + }; + + return ( + <> + + + + + + + + + 파이콘 한국 2025 + + + + + + + + + + toggleNavigation()} siteMapNode={siteMapNode} /> + + ); +}; + +const MobileHeaderContainer = styled("header")<{ isOpen: boolean; isMainPath: boolean }>(({ theme, isOpen, isMainPath }) => ({ + position: "fixed", + top: 0, + left: 0, + right: 0, + + display: isOpen ? "none" : "flex", + alignItems: "center", + justifyContent: "space-between", + + width: "100%", + height: 60, + + padding: "15px 23px", + + backgroundColor: isMainPath ? "rgba(182, 216, 215, 0.1)" : "#B6D8D7", + backdropFilter: "blur(8px)", + WebkitBackdropFilter: "blur(8px)", + color: isMainPath ? "white" : "rgba(18, 109, 127, 0.6)", + + zIndex: theme.zIndex.appBar + 100000, +})); + +const LeftContent = styled(Box)({ + display: "flex", + alignItems: "center", + gap: 17, +}); + +const LogoAndTextContainer = styled(Box)({ + display: "flex", + alignItems: "center", +}); diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx new file mode 100644 index 00000000..da5d6160 --- /dev/null +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx @@ -0,0 +1,65 @@ +import { ButtonBase, styled } from "@mui/material"; +import * as React from "react"; + +interface MobileLanguageToggleProps { + currentLanguage: string; + onLanguageChange: (newLanguage: string) => void; + isMainPath?: boolean; +} + +export const MobileLanguageToggle: React.FC = ({ currentLanguage, onLanguageChange, isMainPath = true }) => { + return ( + + onLanguageChange("ko")}> + KO + + onLanguageChange("en")}> + EN + + + ); +}; + +const ToggleContainer = styled("div")<{ isMainPath: boolean }>(({ isMainPath }) => ({ + display: "flex", + width: 94, + height: 29, + border: "1px solid white", + borderRadius: 15, + padding: 2, + gap: 2, + backgroundColor: isMainPath ? "transparent" : "rgba(255, 255, 255, 0.1)", +})); + +const LanguageButton = styled(ButtonBase)<{ isActive: boolean; isMainPath: boolean }>(({ isActive, isMainPath }) => ({ + flex: 1, + height: "100%", + borderRadius: 13, + fontSize: 12, + fontWeight: 400, + transition: "all 0.2s ease", + + color: isMainPath ? "white" : "rgba(18, 109, 127, 0.6)", + backgroundColor: "transparent", + + ...(isActive && { + backgroundColor: isMainPath ? "rgba(255, 255, 255, 0.7)" : "rgba(255, 255, 255, 0.9)", + color: isMainPath ? "#888888" : "#126D7F", + fontWeight: 600, + }), + + "&:hover": { + backgroundColor: isActive + ? isMainPath + ? "rgba(255, 255, 255, 0.8)" + : "rgba(255, 255, 255, 1)" + : isMainPath + ? "rgba(255, 255, 255, 0.1)" + : "rgba(255, 255, 255, 0.3)", + }, + + WebkitFontSmoothing: "antialiased", + MozOsxFontSmoothing: "grayscale", + textRendering: "optimizeLegibility", + WebkitTextStroke: "0.5px transparent", +})); diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx new file mode 100644 index 00000000..7d1ec70a --- /dev/null +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx @@ -0,0 +1,354 @@ +import * as Common from "@frontend/common"; +import BackendAPISchemas from "@frontend/common/src/schemas/backendAPI"; +import { ArrowBack, ArrowForward } from "@mui/icons-material"; +import { Box, Button, Chip, Drawer, IconButton, Stack, styled, Typography } from "@mui/material"; +import * as React from "react"; +import { Link, useLocation } from "react-router-dom"; +import * as R from "remeda"; + +import { HamburgerButton } from "./HamburgerButton"; +import { MobileLanguageToggle } from "./MobileLanguageToggle"; +import { useAppContext } from "../../../../contexts/app_context"; +import { SignInButton } from "../../SignInButton"; + +type MenuType = BackendAPISchemas.NestedSiteMapSchema; + +interface MobileNavigationProps { + isOpen: boolean; + onClose: () => void; + siteMapNode?: MenuType; +} + +type NavigationLevel = "depth1" | "depth2" | "depth3"; + +interface NavigationState { + level: NavigationLevel; + depth1?: MenuType; + depth2?: MenuType; + breadcrumbs: { name: string; level: NavigationLevel }[]; +} + +export const MobileNavigation: React.FC = ({ isOpen, onClose, siteMapNode }) => { + const { language } = useAppContext(); + const location = useLocation(); + const [navState, setNavState] = React.useState({ + level: "depth1", + breadcrumbs: [], + }); + + const isMainPath = location.pathname === "/"; + + const resetNavigation = () => { + setNavState({ + level: "depth1", + breadcrumbs: [], + }); + }; + + const navigateToDepth2 = (depth1: MenuType) => { + setNavState({ + level: "depth2", + depth1, + breadcrumbs: [{ name: depth1.name, level: "depth1" }], + }); + }; + + const navigateToDepth3 = (depth2: MenuType) => { + setNavState((prev) => ({ + ...prev, + level: "depth3", + depth2, + breadcrumbs: [...prev.breadcrumbs, { name: depth2.name, level: "depth2" }], + })); + }; + + const goBack = () => { + if (navState.level === "depth3") { + setNavState((prev) => ({ + ...prev, + level: "depth2", + depth2: undefined, + breadcrumbs: prev.breadcrumbs.slice(0, -1), + })); + } else if (navState.level === "depth2") { + resetNavigation(); + } + }; + + const handleClose = () => { + onClose(); + resetNavigation(); + }; + + const handleLanguageChange = (newLanguage: string) => { + // TODO: 언어 변경 로직 구현 + console.log("Language changed to:", newLanguage); + }; + + const renderDepth1Menu = () => { + if (!siteMapNode) return null; + + return ( + + {Object.values(siteMapNode.children) + .filter((s) => !s.hide) + .map((menu) => ( + + + {menu.name} + + {!R.isEmpty(menu.children) && ( + navigateToDepth2(menu)}> + + + )} + + ))} + + ); + }; + + const renderDepth2Menu = () => { + if (!navState.depth1) return null; + + return ( + + + + + + {navState.depth1.name} + + + + + + {Object.values(navState.depth1.children) + .filter((s) => !s.hide) + .map((menu) => ( + + + + + {!R.isEmpty(menu.children) && ( + navigateToDepth3(menu)}> + + + )} + + ))} + + + ); + }; + + const renderDepth3Menu = () => { + if (!navState.depth2) return null; + + return ( + + + + + + {navState.depth2.name} + + + + + + {Object.values(navState.depth2.children) + .filter((s) => !s.hide) + .map((menu) => ( + + + + ))} + + + ); + }; + + return ( + + + + + + + + + + 파이콘 한국 2025 + + + + + + + {navState.level === "depth1" && renderDepth1Menu()} + {navState.level === "depth2" && renderDepth2Menu()} + {navState.level === "depth3" && renderDepth3Menu()} + + + + + + + + + + + ); +}; + +const StyledDrawer = styled(Drawer)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ + "& .MuiDrawer-paper": { + width: "70vw", + background: isMainPath + ? `linear-gradient(0deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)), + linear-gradient(0deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15))` + : "#B6D8D7", + backdropFilter: isMainPath ? "blur(10px)" : "none", + WebkitBackdropFilter: isMainPath ? "blur(10px)" : "none", + color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", + borderTopRightRadius: 15, + borderBottomRightRadius: 15, + }, +})); + +const DrawerContent = styled(Box)({ + height: "100%", + display: "flex", + flexDirection: "column", +}); + +const NavigationHeader = styled(Box)<{ isMainPath: boolean }>({ + display: "flex", + alignItems: "center", + padding: "23px 23px 10px 23px", + position: "relative", + gap: 17, +}); + +const NavigationContent = styled(Box)({ + flex: 1, + overflow: "auto", +}); + +const MenuContainer = styled(Stack)({ + padding: "20px 0", + gap: "25px", +}); + +const MenuItem = styled(Box)<{ isMainPath?: boolean }>({ + display: "flex", + alignItems: "center", + padding: "0 23px", + gap: 23, +}); + +const MenuLink = styled(Link)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ + color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", + textDecoration: "none", + fontSize: "20px", + fontWeight: 600, +})); + +const MenuArrowButton = styled(IconButton)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ + color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", + padding: 8, +})); + +const BackButton = styled(Button)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ + display: "flex", + alignItems: "center", + color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", + textTransform: "none", + padding: "0 15px 0 0", + minWidth: "auto", + minHeight: "auto", +})); + +const MenuChip = styled(Chip)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ + backgroundColor: isMainPath ? "rgba(212, 212, 212, 0.5)" : "rgba(18, 109, 127, 0.2)", + color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", + height: 40, + borderRadius: 15, + padding: "10px 13px", + fontSize: "16px", + fontWeight: 600, + + "& .MuiChip-label": { + padding: 0, + }, + + "&:hover": { + backgroundColor: isMainPath ? "rgba(212, 212, 212, 0.7)" : "rgba(18, 109, 127, 0.3)", + }, +})); + +const BottomActions = styled(Stack)<{ isMainPath: boolean }>({ + padding: "20px 23px", + gap: 50, + alignItems: "center", +}); + +const HeaderTitle = styled(Typography)<{ isMainPath: boolean }>(({ theme, isMainPath }) => ({ + color: isMainPath ? theme.palette.mobileHeader.main.text : theme.palette.mobileHeader.sub.text, + fontSize: 18, + fontWeight: 600, +})); + +const LogoAndTextContainer = styled(Box)({ + display: "flex", + alignItems: "center", +}); + +const NavigationMenuSection = styled(Box)({ + padding: "20px 23px", +}); + +const Depth2Header = styled(Box)<{ isMainPath: boolean }>({ + display: "flex", + alignItems: "center", + height: "auto", + marginBottom: 10, +}); + +const Depth2Title = styled(Typography)<{ isMainPath: boolean }>(({ isMainPath }) => ({ + color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", + fontSize: 20, + fontWeight: 800, +})); + +const Depth2Divider = styled(Box)<{ isMainPath: boolean }>(({ isMainPath }) => ({ + height: 1, + backgroundColor: isMainPath ? "rgba(255, 255, 255, 0.3)" : "rgba(18, 109, 127, 0.3)", + marginBottom: 21, +})); + +const Depth2MenuList = styled(Stack)({ + gap: 15, +}); + +const Depth2MenuItem = styled(Box)({ + display: "flex", + alignItems: "center", + gap: 10, +}); + +const Depth3MenuGrid = styled(Box)({ + height: 260, + display: "flex", + flexDirection: "column", + flexWrap: "wrap", + alignContent: "flex-start", + gap: 15, + overflow: "hidden", +}); diff --git a/apps/pyconkr/src/components/layout/Header/index.tsx b/apps/pyconkr/src/components/layout/Header/index.tsx index 6219cf23..87dcfc13 100644 --- a/apps/pyconkr/src/components/layout/Header/index.tsx +++ b/apps/pyconkr/src/components/layout/Header/index.tsx @@ -1,6 +1,6 @@ import * as Common from "@frontend/common"; import { ArrowForwardIos } from "@mui/icons-material"; -import { Box, Button, CircularProgress, Divider, Stack, styled, SxProps, Theme, Typography } from "@mui/material"; +import { Box, Button, CircularProgress, Divider, Stack, styled, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"; import { MUIStyledCommonProps } from "@mui/system"; import * as React from "react"; import { Link } from "react-router-dom"; @@ -11,6 +11,7 @@ import { useAppContext } from "../../../contexts/app_context"; import { CartBadgeButton } from "../CartBadgeButton"; import LanguageSelector from "../LanguageSelector"; import { SignInButton } from "../SignInButton"; +import { MobileHeader } from "./Mobile/MobileHeader"; type MenuType = BackendAPISchemas.NestedSiteMapSchema; type MenuOrUndefinedType = MenuType | undefined; @@ -26,6 +27,8 @@ const BreadCrumbHeight: React.CSSProperties["height"] = "4.5rem"; const Header: React.FC = () => { const { title, language, siteMapNode, currentSiteMapDepth, shouldShowTitleBanner } = useAppContext(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); const [navState, setNavState] = React.useState({}); const resetDepths = () => setNavState({}); @@ -38,6 +41,10 @@ const Header: React.FC = () => { React.useEffect(resetDepths, [language]); + if (isMobile) { + return ; + } + let breadCrumbRoute = ""; let breadCrumbArray = currentSiteMapDepth.slice(1, -1); if (R.isEmpty(breadCrumbArray)) breadCrumbArray = currentSiteMapDepth.slice(0, -1); diff --git a/apps/pyconkr/src/components/layout/SignInButton/index.tsx b/apps/pyconkr/src/components/layout/SignInButton/index.tsx index 112a0a37..ad87538a 100644 --- a/apps/pyconkr/src/components/layout/SignInButton/index.tsx +++ b/apps/pyconkr/src/components/layout/SignInButton/index.tsx @@ -1,5 +1,6 @@ import * as Shop from "@frontend/shop"; -import { Button } from "@mui/material"; +import { Login } from "@mui/icons-material"; +import { Button, Stack } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import { useNavigate } from "react-router-dom"; @@ -9,15 +10,45 @@ type InnerSignInButtonImplPropType = { loading?: boolean; signedIn?: boolean; onSignOut?: () => void; + isMobile?: boolean; + isMainPath?: boolean; }; -const InnerSignInButtonImpl: React.FC = ({ loading, signedIn, onSignOut }) => { +const InnerSignInButtonImpl: React.FC = ({ loading, signedIn, onSignOut, isMobile = false, isMainPath = true }) => { const navigate = useNavigate(); const { language } = useAppContext(); const signInBtnStr = language === "ko" ? "로그인" : "Sign In"; const signOutBtnStr = language === "ko" ? "로그아웃" : "Sign Out"; + if (isMobile) { + return ( + + ); + } + return ( @@ -60,15 +77,27 @@ const InnerSignInButtonImpl: React.FC = ({ loadin ); }; -export const SignInButton: React.FC<{ isMobile?: boolean; isMainPath?: boolean }> = ({ isMobile = false, isMainPath = true }) => { +export const SignInButton: React.FC<{ isMobile?: boolean; isMainPath?: boolean; onClose?: () => void }> = ({ + isMobile = false, + isMainPath = true, + onClose, +}) => { const SignInWithErrorBoundary = ErrorBoundary.with( - { fallback: }, - Suspense.with({ fallback: }, () => { + { fallback: }, + Suspense.with({ fallback: }, () => { const shopAPIClient = Shop.Hooks.useShopClient(); const signOutMutation = Shop.Hooks.useSignOutMutation(shopAPIClient); const { data } = Shop.Hooks.useUserStatus(shopAPIClient); - return ; + return ( + + ); }) ); From 4bc0be59a61c030efd9f2600389e1bcfed5e3c3a Mon Sep 17 00:00:00 2001 From: y00eunji Date: Wed, 18 Jun 2025 15:25:35 +0900 Subject: [PATCH 003/324] =?UTF-8?q?feat=20:=20=EB=AA=A8=EB=B0=94=EC=9D=BC?= =?UTF-8?q?=20UI=20=EC=96=B8=EC=96=B4=20=EC=A0=84=ED=99=98=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/Header/Mobile/MobileHeader.tsx | 9 ++------- .../Header/Mobile/MobileLanguageToggle.tsx | 17 ++++++++++++----- .../layout/Header/Mobile/MobileNavigation.tsx | 9 +-------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx index 37206f2a..51b22a30 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx @@ -14,7 +14,7 @@ interface MobileHeaderProps { } export const MobileHeader: React.FC = ({ isNavigationOpen = false, onToggleNavigation }) => { - const { siteMapNode, language } = useAppContext(); + const { siteMapNode } = useAppContext(); const location = useLocation(); const [internalNavigationOpen, setInternalNavigationOpen] = React.useState(false); @@ -23,11 +23,6 @@ export const MobileHeader: React.FC = ({ isNavigationOpen = f const isMainPath = location.pathname === "/"; - const handleLanguageChange = (newLanguage: string) => { - // TODO: 언어 변경 로직 구현 - console.log("Language changed to:", newLanguage); - }; - return ( <> @@ -52,7 +47,7 @@ export const MobileHeader: React.FC = ({ isNavigationOpen = f - + toggleNavigation()} siteMapNode={siteMapNode} /> diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx index da5d6160..a0d7f712 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx @@ -1,19 +1,26 @@ import { ButtonBase, styled } from "@mui/material"; import * as React from "react"; +import { LOCAL_STORAGE_LANGUAGE_KEY } from "../../../../consts/local_stroage"; +import { useAppContext } from "../../../../contexts/app_context"; + interface MobileLanguageToggleProps { - currentLanguage: string; - onLanguageChange: (newLanguage: string) => void; isMainPath?: boolean; } -export const MobileLanguageToggle: React.FC = ({ currentLanguage, onLanguageChange, isMainPath = true }) => { +export const MobileLanguageToggle: React.FC = ({ isMainPath = true }) => { + const { language, setAppContext } = useAppContext(); + + const handleLanguageChange = (newLanguage: "ko" | "en") => { + localStorage.setItem(LOCAL_STORAGE_LANGUAGE_KEY, newLanguage); + setAppContext((ps) => ({ ...ps, language: newLanguage })); + }; return ( - onLanguageChange("ko")}> + handleLanguageChange("ko")}> KO - onLanguageChange("en")}> + handleLanguageChange("en")}> EN diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx index 7d1ec70a..edea3415 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx @@ -8,7 +8,6 @@ import * as R from "remeda"; import { HamburgerButton } from "./HamburgerButton"; import { MobileLanguageToggle } from "./MobileLanguageToggle"; -import { useAppContext } from "../../../../contexts/app_context"; import { SignInButton } from "../../SignInButton"; type MenuType = BackendAPISchemas.NestedSiteMapSchema; @@ -29,7 +28,6 @@ interface NavigationState { } export const MobileNavigation: React.FC = ({ isOpen, onClose, siteMapNode }) => { - const { language } = useAppContext(); const location = useLocation(); const [navState, setNavState] = React.useState({ level: "depth1", @@ -80,11 +78,6 @@ export const MobileNavigation: React.FC = ({ isOpen, onCl resetNavigation(); }; - const handleLanguageChange = (newLanguage: string) => { - // TODO: 언어 변경 로직 구현 - console.log("Language changed to:", newLanguage); - }; - const renderDepth1Menu = () => { if (!siteMapNode) return null; @@ -198,7 +191,7 @@ export const MobileNavigation: React.FC = ({ isOpen, onCl - + From 91a3950115dec908c00073cf1639e3e64cb01e8e Mon Sep 17 00:00:00 2001 From: y00eunji Date: Wed, 18 Jun 2025 15:33:24 +0900 Subject: [PATCH 004/324] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20U?= =?UTF-8?q?I=20=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=ED=85=8C?= =?UTF-8?q?=EB=A7=88=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Header/Mobile/MobileLanguageToggle.tsx | 24 +++--- .../layout/Header/Mobile/MobileNavigation.tsx | 37 ++++----- apps/pyconkr/src/styles/globalStyles.ts | 41 ++++++++++ types/emotion.d.ts | 80 +++++++++++++++++++ 4 files changed, 152 insertions(+), 30 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx index a0d7f712..f38217fd 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx @@ -27,7 +27,7 @@ export const MobileLanguageToggle: React.FC = ({ isMa ); }; -const ToggleContainer = styled("div")<{ isMainPath: boolean }>(({ isMainPath }) => ({ +const ToggleContainer = styled("div")<{ isMainPath: boolean }>(({ theme, isMainPath }) => ({ display: "flex", width: 94, height: 29, @@ -35,10 +35,12 @@ const ToggleContainer = styled("div")<{ isMainPath: boolean }>(({ isMainPath }) borderRadius: 15, padding: 2, gap: 2, - backgroundColor: isMainPath ? "transparent" : "rgba(255, 255, 255, 0.1)", + backgroundColor: isMainPath + ? theme.palette.mobileNavigation.main.languageToggle.background + : theme.palette.mobileNavigation.sub.languageToggle.background, })); -const LanguageButton = styled(ButtonBase)<{ isActive: boolean; isMainPath: boolean }>(({ isActive, isMainPath }) => ({ +const LanguageButton = styled(ButtonBase)<{ isActive: boolean; isMainPath: boolean }>(({ theme, isActive, isMainPath }) => ({ flex: 1, height: "100%", borderRadius: 13, @@ -46,23 +48,25 @@ const LanguageButton = styled(ButtonBase)<{ isActive: boolean; isMainPath: boole fontWeight: 400, transition: "all 0.2s ease", - color: isMainPath ? "white" : "rgba(18, 109, 127, 0.6)", + color: isMainPath ? theme.palette.mobileHeader.main.text : theme.palette.mobileHeader.sub.text, backgroundColor: "transparent", ...(isActive && { - backgroundColor: isMainPath ? "rgba(255, 255, 255, 0.7)" : "rgba(255, 255, 255, 0.9)", - color: isMainPath ? "#888888" : "#126D7F", + backgroundColor: isMainPath + ? theme.palette.mobileNavigation.main.languageToggle.active.background + : theme.palette.mobileNavigation.sub.languageToggle.active.background, + color: isMainPath ? theme.palette.mobileHeader.main.activeLanguage : theme.palette.mobileHeader.sub.activeLanguage, fontWeight: 600, }), "&:hover": { backgroundColor: isActive ? isMainPath - ? "rgba(255, 255, 255, 0.8)" - : "rgba(255, 255, 255, 1)" + ? theme.palette.mobileNavigation.main.languageToggle.active.hover + : theme.palette.mobileNavigation.sub.languageToggle.active.hover : isMainPath - ? "rgba(255, 255, 255, 0.1)" - : "rgba(255, 255, 255, 0.3)", + ? theme.palette.mobileNavigation.main.languageToggle.inactive.hover + : theme.palette.mobileNavigation.sub.languageToggle.inactive.hover, }, WebkitFontSmoothing: "antialiased", diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx index edea3415..21ef26b3 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx @@ -200,16 +200,13 @@ export const MobileNavigation: React.FC = ({ isOpen, onCl ); }; -const StyledDrawer = styled(Drawer)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ +const StyledDrawer = styled(Drawer)<{ isMainPath?: boolean }>(({ theme, isMainPath = true }) => ({ "& .MuiDrawer-paper": { width: "70vw", - background: isMainPath - ? `linear-gradient(0deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)), - linear-gradient(0deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15))` - : "#B6D8D7", + background: isMainPath ? theme.palette.mobileNavigation.main.background : theme.palette.mobileNavigation.sub.background, backdropFilter: isMainPath ? "blur(10px)" : "none", WebkitBackdropFilter: isMainPath ? "blur(10px)" : "none", - color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", + color: isMainPath ? theme.palette.mobileNavigation.main.text : theme.palette.mobileNavigation.sub.text, borderTopRightRadius: 15, borderBottomRightRadius: 15, }, @@ -246,31 +243,31 @@ const MenuItem = styled(Box)<{ isMainPath?: boolean }>({ gap: 23, }); -const MenuLink = styled(Link)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ - color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", +const MenuLink = styled(Link)<{ isMainPath?: boolean }>(({ theme, isMainPath = true }) => ({ + color: isMainPath ? theme.palette.mobileNavigation.main.text : theme.palette.mobileNavigation.sub.text, textDecoration: "none", fontSize: "20px", fontWeight: 600, })); -const MenuArrowButton = styled(IconButton)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ - color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", +const MenuArrowButton = styled(IconButton)<{ isMainPath?: boolean }>(({ theme, isMainPath = true }) => ({ + color: isMainPath ? theme.palette.mobileNavigation.main.text : theme.palette.mobileNavigation.sub.text, padding: 8, })); -const BackButton = styled(Button)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ +const BackButton = styled(Button)<{ isMainPath?: boolean }>(({ theme, isMainPath = true }) => ({ display: "flex", alignItems: "center", - color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", + color: isMainPath ? theme.palette.mobileNavigation.main.text : theme.palette.mobileNavigation.sub.text, textTransform: "none", padding: "0 15px 0 0", minWidth: "auto", minHeight: "auto", })); -const MenuChip = styled(Chip)<{ isMainPath?: boolean }>(({ isMainPath = true }) => ({ - backgroundColor: isMainPath ? "rgba(212, 212, 212, 0.5)" : "rgba(18, 109, 127, 0.2)", - color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", +const MenuChip = styled(Chip)<{ isMainPath?: boolean }>(({ theme, isMainPath = true }) => ({ + backgroundColor: isMainPath ? theme.palette.mobileNavigation.main.chip.background : theme.palette.mobileNavigation.sub.chip.background, + color: isMainPath ? theme.palette.mobileNavigation.main.text : theme.palette.mobileNavigation.sub.text, height: 40, borderRadius: 15, padding: "10px 13px", @@ -282,7 +279,7 @@ const MenuChip = styled(Chip)<{ isMainPath?: boolean }>(({ isMainPath = true }) }, "&:hover": { - backgroundColor: isMainPath ? "rgba(212, 212, 212, 0.7)" : "rgba(18, 109, 127, 0.3)", + backgroundColor: isMainPath ? theme.palette.mobileNavigation.main.chip.hover : theme.palette.mobileNavigation.sub.chip.hover, }, })); @@ -314,15 +311,15 @@ const Depth2Header = styled(Box)<{ isMainPath: boolean }>({ marginBottom: 10, }); -const Depth2Title = styled(Typography)<{ isMainPath: boolean }>(({ isMainPath }) => ({ - color: isMainPath ? "white" : "rgba(18, 109, 127, 0.9)", +const Depth2Title = styled(Typography)<{ isMainPath: boolean }>(({ theme, isMainPath }) => ({ + color: isMainPath ? theme.palette.mobileNavigation.main.text : theme.palette.mobileNavigation.sub.text, fontSize: 20, fontWeight: 800, })); -const Depth2Divider = styled(Box)<{ isMainPath: boolean }>(({ isMainPath }) => ({ +const Depth2Divider = styled(Box)<{ isMainPath: boolean }>(({ theme, isMainPath }) => ({ height: 1, - backgroundColor: isMainPath ? "rgba(255, 255, 255, 0.3)" : "rgba(18, 109, 127, 0.3)", + backgroundColor: isMainPath ? theme.palette.mobileNavigation.main.divider : theme.palette.mobileNavigation.sub.divider, marginBottom: 21, })); diff --git a/apps/pyconkr/src/styles/globalStyles.ts b/apps/pyconkr/src/styles/globalStyles.ts index 5cf92f5e..d08c4f06 100644 --- a/apps/pyconkr/src/styles/globalStyles.ts +++ b/apps/pyconkr/src/styles/globalStyles.ts @@ -36,6 +36,47 @@ export const muiTheme = createTheme({ activeLanguage: "#126D7F", }, }, + mobileNavigation: { + main: { + background: + "linear-gradient(0deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)), linear-gradient(0deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15))", + text: "#FFFFFF", + chip: { + background: "rgba(212, 212, 212, 0.5)", + hover: "rgba(212, 212, 212, 0.7)", + }, + divider: "rgba(255, 255, 255, 0.3)", + languageToggle: { + background: "transparent", + active: { + background: "rgba(255, 255, 255, 0.7)", + hover: "rgba(255, 255, 255, 0.8)", + }, + inactive: { + hover: "rgba(255, 255, 255, 0.1)", + }, + }, + }, + sub: { + background: "#B6D8D7", + text: "rgba(18, 109, 127, 0.9)", + chip: { + background: "rgba(18, 109, 127, 0.2)", + hover: "rgba(18, 109, 127, 0.3)", + }, + divider: "rgba(18, 109, 127, 0.3)", + languageToggle: { + background: "rgba(255, 255, 255, 0.1)", + active: { + background: "rgba(255, 255, 255, 0.9)", + hover: "rgba(255, 255, 255, 1)", + }, + inactive: { + hover: "rgba(255, 255, 255, 0.3)", + }, + }, + }, + }, text: { primary: "#000000", secondary: "#666666", diff --git a/types/emotion.d.ts b/types/emotion.d.ts index 6ad28f26..f1196eac 100644 --- a/types/emotion.d.ts +++ b/types/emotion.d.ts @@ -16,6 +16,46 @@ declare module "@mui/material/styles" { activeLanguage: string; }; }; + mobileNavigation: { + main: { + background: string; + text: string; + chip: { + background: string; + hover: string; + }; + divider: string; + languageToggle: { + background: string; + active: { + background: string; + hover: string; + }; + inactive: { + hover: string; + }; + }; + }; + sub: { + background: string; + text: string; + chip: { + background: string; + hover: string; + }; + divider: string; + languageToggle: { + background: string; + active: { + background: string; + hover: string; + }; + inactive: { + hover: string; + }; + }; + }; + }; } interface PaletteOptions { @@ -32,6 +72,46 @@ declare module "@mui/material/styles" { activeLanguage: string; }; }; + mobileNavigation?: { + main: { + background: string; + text: string; + chip: { + background: string; + hover: string; + }; + divider: string; + languageToggle: { + background: string; + active: { + background: string; + hover: string; + }; + inactive: { + hover: string; + }; + }; + }; + sub: { + background: string; + text: string; + chip: { + background: string; + hover: string; + }; + divider: string; + languageToggle: { + background: string; + active: { + background: string; + hover: string; + }; + inactive: { + hover: string; + }; + }; + }; + }; } interface PaletteColor { From e07e6367c2973c70ebc473cfbcf52047f17c4cd8 Mon Sep 17 00:00:00 2001 From: y00eunji Date: Wed, 18 Jun 2025 15:41:34 +0900 Subject: [PATCH 005/324] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EB=B0=8F=20=EB=82=B4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/Header/Mobile/MobileHeader.tsx | 8 ++++---- .../components/layout/Header/Mobile/MobileNavigation.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx index 51b22a30..03b2f928 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileHeader.tsx @@ -56,7 +56,7 @@ export const MobileHeader: React.FC = ({ isNavigationOpen = f }; const MobileHeaderContainer = styled("header")<{ isOpen: boolean; isMainPath: boolean }>(({ theme, isOpen, isMainPath }) => ({ - position: "fixed", + position: isMainPath ? "fixed" : "sticky", top: 0, left: 0, right: 0, @@ -71,11 +71,11 @@ const MobileHeaderContainer = styled("header")<{ isOpen: boolean; isMainPath: bo padding: "15px 23px", backgroundColor: isMainPath ? "rgba(182, 216, 215, 0.1)" : "#B6D8D7", - backdropFilter: "blur(8px)", - WebkitBackdropFilter: "blur(8px)", + backdropFilter: isMainPath ? "blur(8px)" : "none", + WebkitBackdropFilter: isMainPath ? "blur(8px)" : "none", color: isMainPath ? "white" : "rgba(18, 109, 127, 0.6)", - zIndex: theme.zIndex.appBar + 100000, + zIndex: isMainPath ? theme.zIndex.appBar + 100000 : theme.zIndex.appBar, })); const LeftContent = styled(Box)({ diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx index 21ef26b3..860f4742 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx @@ -192,7 +192,7 @@ export const MobileNavigation: React.FC = ({ isOpen, onCl - + From 97ae5db45d8dcc64b4e7855be1dfec214465504f Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 00:48:47 +0900 Subject: [PATCH 006/324] =?UTF-8?q?chore:=20buildFlatSiteMap=EB=A1=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EB=90=9C=20sitemap=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=EC=97=90=20route=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=82=BD=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/utils/api.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/common/src/utils/api.ts b/packages/common/src/utils/api.ts index eb67aa57..7c4c1231 100644 --- a/packages/common/src/utils/api.ts +++ b/packages/common/src/utils/api.ts @@ -36,14 +36,15 @@ export const buildNestedSiteMap = (flat: T[]) => { }; export const buildFlatSiteMap = (nested: GNestedSiteMap) => { - const flat: T[] = []; + const flat: (T & { route: string })[] = []; - const traverse = (node: GNestedSiteMap) => { - flat.push(node); - node.children.forEach(traverse); + const traverse = (node: GNestedSiteMap, parentRoute: string) => { + const route = parentRoute ? `${parentRoute}/${node.route_code}` : node.route_code; + flat.push({ ...node, route }); + node.children.forEach((n: GNestedSiteMap) => traverse(n, route)); }; - traverse(nested); + traverse(nested, ""); return flat; }; From c64b2eaba68a12abffe318c5621f7f67913eef8a Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 00:49:11 +0900 Subject: [PATCH 007/324] =?UTF-8?q?feat:=20=ED=9B=84=EC=9B=90=EC=82=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=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/index.ts | 1 + packages/common/src/hooks/useAPI.ts | 6 ++++++ packages/common/src/schemas/backendAPI.ts | 13 +++++++++++++ 3 files changed, 20 insertions(+) diff --git a/packages/common/src/apis/index.ts b/packages/common/src/apis/index.ts index 2caa7769..04b9166d 100644 --- a/packages/common/src/apis/index.ts +++ b/packages/common/src/apis/index.ts @@ -5,6 +5,7 @@ namespace BackendAPIs { export const BackendAPIClientError = _BackendAPIClientError; export const listSiteMaps = (client: BackendAPIClient) => () => client.get("v1/cms/sitemap/"); export const retrievePage = (client: BackendAPIClient) => (id: string) => client.get(`v1/cms/page/${id}/`); + export const listSponsors = (client: BackendAPIClient) => () => client.get("v1/event/sponsor/"); } export default BackendAPIs; diff --git a/packages/common/src/hooks/useAPI.ts b/packages/common/src/hooks/useAPI.ts index 1c0fa599..7615d5f4 100644 --- a/packages/common/src/hooks/useAPI.ts +++ b/packages/common/src/hooks/useAPI.ts @@ -33,6 +33,12 @@ namespace BackendAPIHooks { queryKey: [client.language, ...QUERY_KEYS.PAGE, id], queryFn: () => BackendAPIs.retrievePage(client)(id), }); + + export const useSponsorQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: [client.language, "sponsor", "list"], + queryFn: BackendAPIs.listSponsors(client), + }); } export default BackendAPIHooks; diff --git a/packages/common/src/schemas/backendAPI.ts b/packages/common/src/schemas/backendAPI.ts index c983f864..f862d3d3 100644 --- a/packages/common/src/schemas/backendAPI.ts +++ b/packages/common/src/schemas/backendAPI.ts @@ -31,6 +31,7 @@ namespace BackendAPISchemas { order: number; page: string; hide: boolean; + parent_sitemap: string | null; children: NestedSiteMapSchema[]; }; @@ -54,6 +55,18 @@ namespace BackendAPISchemas { sections: SectionSchema[]; }; + export type SponsorSchema = { + id: string; + name: string; + order: number; + sponsors: { + id: string; + name: string; + logo: string; + sitemap_id: string | null; + }[]; + }; + export const isObjectErrorResponseSchema = (obj?: unknown): obj is BackendAPISchemas.ErrorResponseSchema => { return ( R.isPlainObject(obj) && From e9e6b2854a065ce0ee4b99bee05e13507da27d7a Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 00:49:21 +0900 Subject: [PATCH 008/324] =?UTF-8?q?feat:=20=ED=9B=84=EC=9B=90=EC=82=AC=20A?= =?UTF-8?q?PI=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/Sponsor/index.tsx | 219 ++++++++---------- apps/pyconkr/src/components/layout/index.tsx | 2 +- 2 files changed, 99 insertions(+), 122 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Sponsor/index.tsx b/apps/pyconkr/src/components/layout/Sponsor/index.tsx index c98904fe..82b89bcc 100644 --- a/apps/pyconkr/src/components/layout/Sponsor/index.tsx +++ b/apps/pyconkr/src/components/layout/Sponsor/index.tsx @@ -1,121 +1,98 @@ -import styled from "@emotion/styled"; -import { useEffect, useState } from "react"; - -import SponsorExample from "../../../assets/sponsorExample.svg?react"; - -interface Sponsor { - id: number; - name: string; - Logo: React.ComponentType; -} - -export default function Sponsor() { - const [sponsors, setSponsors] = useState([]); - - // 16개의 임시 스폰서 데이터 생성 - useEffect(() => { - const fetchSponsors = () => { - const dummySponsors = Array(16) - .fill(null) - .map((_, index) => ({ - id: index + 1, - name: `후원사 ${index + 1}`, - Logo: SponsorExample, - })); - - setSponsors(dummySponsors); - }; - - fetchSponsors(); - }, []); - - return ( - - - 후원사 목록 - - - {sponsors.map((sponsor) => ( - - - {sponsor.name} - - - ))} - - - ); -} - -const SponsorSection = styled.section` - width: 1067px; - margin: 0 auto; - margin-bottom: 140px; -`; - -const SponsorTitle = styled.h4` - font-weight: 600; - font-size: 37px; - text-align: center; - margin: 0; -`; - -const SponsorGrid = styled.div` - margin-top: 101px; - display: grid; - grid-template-columns: repeat(4, 1fr); - column-gap: 35px; - row-gap: 75px; - justify-items: center; -`; - -const SponsorItem = styled.div` - width: 240px; - height: 75px; -`; - -const SponsorButton = styled.button` - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background: none; - border: none; - padding: 0; - cursor: pointer; - transition: transform 0.2s ease; - - &:focus { - outline: 2px solid #007aff; - outline-offset: 4px; - border-radius: 4px; - } - - &:focus:not(:focus-visible) { - outline: none; - } - - &:focus-visible { - outline: 2px solid #007aff; - outline-offset: 4px; - border-radius: 4px; - } - - &:hover { - // transform: scale(1.05); - } - - .sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; - } -`; +import * as Common from "@frontend/common"; +import { CircularProgress, Divider, Stack, Typography, styled } from "@mui/material"; +import { ErrorBoundary, Suspense } from "@suspensive/react"; +import { Link } from "react-router-dom"; + +import { useAppContext } from "../../../contexts/app_context"; + +const LogoHeight: React.CSSProperties["height"] = "8rem"; +const LogoWidth: React.CSSProperties["width"] = "15rem"; + +const SponsorContainer = styled(Stack)({ + width: "100%", + alignItems: "center", + justifyContent: "center", +}); + +const SponsorSection = styled(Stack)({ + margin: "8rem 8rem 4rem 8rem", + width: "100%", + maxWidth: "1300px", +}); + +const SponsorStack = styled(Stack)({ + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "center", + alignItems: "center", + padding: "0 1rem", + gap: "2rem", +}); + +const LogoImageContainer = styled(Stack)({ + alignItems: "center", + justifyContent: "center", + alignContent: "stretch", + height: LogoHeight, + maxHeight: LogoHeight, + maxWidth: LogoWidth, +}); + +const LogoImage = styled("img")({ + height: `calc(${LogoHeight} * 0.9)`, // 90% of LogoHeight + minHeight: LogoHeight, + minWidth: `calc(${LogoWidth} * 0.9)`, // 80% of LogoWidth + maxWidth: "100%", + maxHeight: "100%", + objectFit: "contain", +}); + +export const Sponsor: React.FC = ErrorBoundary.with( + { + fallback: ( + + 후원사 정보를 불러오는 중 문제가 발생했습니다, +
+ 잠시 후 다시 시도해 주세요. +
+ ), + }, + Suspense.with({ fallback: }, () => { + const { siteMapNode } = useAppContext(); + const backendAPIClient = Common.Hooks.BackendAPI.useBackendClient(); + const { data: sponsorData } = Common.Hooks.BackendAPI.useSponsorQuery(backendAPIClient); + + if (!siteMapNode) return ; + + const flatSiteMap = Common.Utils.buildFlatSiteMap(siteMapNode); + const flatSiteMapObj = flatSiteMap.reduce((a, i) => ({ ...a, [i.id]: i }), {} as Record); + + return ( + + + + + {sponsorData + .filter((t) => t.sponsors.length) + .map((sponsorTier, i, a) => ( + + + + {sponsorTier.sponsors.map((sponsor) => { + const sponsorImg = ( + + + + ); + return sponsor.sitemap_id ? : sponsorImg; + })} + + {i !== a.length - 1 && } + + ))} + + + + ); + }) +); diff --git a/apps/pyconkr/src/components/layout/index.tsx b/apps/pyconkr/src/components/layout/index.tsx index 8cfb3980..ee0da09c 100644 --- a/apps/pyconkr/src/components/layout/index.tsx +++ b/apps/pyconkr/src/components/layout/index.tsx @@ -4,7 +4,7 @@ import { Outlet } from "react-router-dom"; import Footer from "./Footer"; import Header from "./Header"; -import Sponsor from "./Sponsor"; +import { Sponsor } from "./Sponsor"; import { useAppContext } from "../../contexts/app_context"; export default function MainLayout() { From 8413af62bbf31db2f4882c0c4227d11ab1481b18 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 00:56:52 +0900 Subject: [PATCH 009/324] =?UTF-8?q?feat:=20ShopAPI=EC=9D=98=20=EB=8B=A4?= =?UTF-8?q?=EA=B5=AD=EC=96=B4=20=EC=A7=80=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/shop/src/apis/client.ts | 6 ++++-- packages/shop/src/hooks/index.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/shop/src/apis/client.ts b/packages/shop/src/apis/client.ts index 785588aa..368a4a99 100644 --- a/packages/shop/src/apis/client.ts +++ b/packages/shop/src/apis/client.ts @@ -63,17 +63,19 @@ type AxiosRequestWithPayload = , D = unknown>( export class ShopAPIClient { readonly baseURL: string; + readonly language: "ko" | "en"; protected readonly csrfCookieName: string; private readonly shopAPI: AxiosInstance; - constructor(baseURL: string, csrfCookieName: string, timeout: number) { + constructor(baseURL: string, csrfCookieName: string, timeout: number, language: "ko" | "en") { this.baseURL = baseURL; + this.language = language; this.csrfCookieName = csrfCookieName; this.shopAPI = axios.create({ baseURL, timeout, withCredentials: true, - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", "Accept-Language": language }, }); this.shopAPI.interceptors.request.use( (config) => { diff --git a/packages/shop/src/hooks/index.ts b/packages/shop/src/hooks/index.ts index d7ce04b1..cd196c60 100644 --- a/packages/shop/src/hooks/index.ts +++ b/packages/shop/src/hooks/index.ts @@ -36,13 +36,13 @@ namespace ShopHooks { }; export const useShopClient = () => { - const { shopApiDomain, shopApiCSRFCookieName, shopApiTimeout } = useShopContext(); - return new ShopAPIClient(shopApiDomain, shopApiCSRFCookieName, shopApiTimeout); + const { shopApiDomain, shopApiCSRFCookieName, shopApiTimeout, language } = useShopContext(); + return new ShopAPIClient(shopApiDomain, shopApiCSRFCookieName, shopApiTimeout, language); }; export const useUserStatus = (client: ShopAPIClient) => useSuspenseQuery({ - queryKey: QUERY_KEYS.USER, + queryKey: [client.language, ...QUERY_KEYS.USER], queryFn: ShopAPIs.retrieveUserInfo(client), retry: 3, }); @@ -71,13 +71,13 @@ namespace ShopHooks { export const useProducts = (client: ShopAPIClient, qs?: ShopSchemas.ProductListQueryParams) => useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PRODUCT_LIST, qs ? JSON.stringify(qs) : ""], + queryKey: [client.language, ...QUERY_KEYS.PRODUCT_LIST, qs ? JSON.stringify(qs) : ""], queryFn: () => ShopAPIs.listProducts(client)(qs), }); export const useCart = (client: ShopAPIClient) => useSuspenseQuery({ - queryKey: QUERY_KEYS.CART_INFO, + queryKey: [client.language, ...QUERY_KEYS.CART_INFO], queryFn: ShopAPIs.retrieveCart(client), }); @@ -111,7 +111,7 @@ namespace ShopHooks { export const useOrders = (client: ShopAPIClient) => useSuspenseQuery({ - queryKey: QUERY_KEYS.ORDER_LIST, + queryKey: [client.language, ...QUERY_KEYS.ORDER_LIST], queryFn: ShopAPIs.listOrders(client), }); From fdca5eacc6d88778abfb76775f5b886ff21be0e0 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 01:05:29 +0900 Subject: [PATCH 010/324] =?UTF-8?q?feat:=20=ED=9B=84=EC=9B=90=EC=82=AC=20?= =?UTF-8?q?=EB=A1=9C=EA=B3=A0=EC=97=90=20tooltip=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/Sponsor/index.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Sponsor/index.tsx b/apps/pyconkr/src/components/layout/Sponsor/index.tsx index 82b89bcc..b77a0ad4 100644 --- a/apps/pyconkr/src/components/layout/Sponsor/index.tsx +++ b/apps/pyconkr/src/components/layout/Sponsor/index.tsx @@ -1,5 +1,5 @@ import * as Common from "@frontend/common"; -import { CircularProgress, Divider, Stack, Typography, styled } from "@mui/material"; +import { CircularProgress, Divider, Stack, Tooltip, Typography, TypographyProps, styled } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import { Link } from "react-router-dom"; @@ -67,21 +67,30 @@ export const Sponsor: React.FC = ErrorBoundary.with( const flatSiteMap = Common.Utils.buildFlatSiteMap(siteMapNode); const flatSiteMapObj = flatSiteMap.reduce((a, i) => ({ ...a, [i.id]: i }), {} as Record); + const textProps: TypographyProps = { + textAlign: "center", + fontWeight: "bold", + }; + return ( - + {sponsorData .filter((t) => t.sponsors.length) .map((sponsorTier, i, a) => ( - + {sponsorTier.sponsors.map((sponsor) => { + const sponsorName = sponsor.name.replace(/\\n/g, "\n"); + const sponsorNameContent = ; const sponsorImg = ( - + + + ); return sponsor.sitemap_id ? : sponsorImg; From ae01317789fe445b14653a92de8d788e8df7d6f5 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 15:48:13 +0900 Subject: [PATCH 011/324] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=EC=9D=B4=20=EB=8F=99=EC=9E=91=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/hooks/useAPI.ts | 7 ++++--- packages/shop/src/hooks/index.ts | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/common/src/hooks/useAPI.ts b/packages/common/src/hooks/useAPI.ts index 7615d5f4..705e2b12 100644 --- a/packages/common/src/hooks/useAPI.ts +++ b/packages/common/src/hooks/useAPI.ts @@ -8,6 +8,7 @@ import BackendContext from "../contexts"; const QUERY_KEYS = { SITEMAP_LIST: ["query", "sitemap", "list"], PAGE: ["query", "page"], + SPONSOR_LIST: ["query", "sponsor", "list"], }; namespace BackendAPIHooks { @@ -24,19 +25,19 @@ namespace BackendAPIHooks { export const useFlattenSiteMapQuery = (client: BackendAPIClient) => useSuspenseQuery({ - queryKey: [client.language, ...QUERY_KEYS.SITEMAP_LIST], + queryKey: [...QUERY_KEYS.SITEMAP_LIST, client.language], queryFn: BackendAPIs.listSiteMaps(client), }); export const usePageQuery = (client: BackendAPIClient, id: string) => useSuspenseQuery({ - queryKey: [client.language, ...QUERY_KEYS.PAGE, id], + queryKey: [...QUERY_KEYS.PAGE, id, client.language], queryFn: () => BackendAPIs.retrievePage(client)(id), }); export const useSponsorQuery = (client: BackendAPIClient) => useSuspenseQuery({ - queryKey: [client.language, "sponsor", "list"], + queryKey: [...QUERY_KEYS.SPONSOR_LIST, client.language], queryFn: BackendAPIs.listSponsors(client), }); } diff --git a/packages/shop/src/hooks/index.ts b/packages/shop/src/hooks/index.ts index cd196c60..b117c2e8 100644 --- a/packages/shop/src/hooks/index.ts +++ b/packages/shop/src/hooks/index.ts @@ -42,7 +42,7 @@ namespace ShopHooks { export const useUserStatus = (client: ShopAPIClient) => useSuspenseQuery({ - queryKey: [client.language, ...QUERY_KEYS.USER], + queryKey: [...QUERY_KEYS.USER, client.language], queryFn: ShopAPIs.retrieveUserInfo(client), retry: 3, }); @@ -71,13 +71,13 @@ namespace ShopHooks { export const useProducts = (client: ShopAPIClient, qs?: ShopSchemas.ProductListQueryParams) => useSuspenseQuery({ - queryKey: [client.language, ...QUERY_KEYS.PRODUCT_LIST, qs ? JSON.stringify(qs) : ""], + queryKey: [...QUERY_KEYS.PRODUCT_LIST, qs ? JSON.stringify(qs) : "", client.language], queryFn: () => ShopAPIs.listProducts(client)(qs), }); export const useCart = (client: ShopAPIClient) => useSuspenseQuery({ - queryKey: [client.language, ...QUERY_KEYS.CART_INFO], + queryKey: [...QUERY_KEYS.CART_INFO, client.language], queryFn: ShopAPIs.retrieveCart(client), }); @@ -111,7 +111,7 @@ namespace ShopHooks { export const useOrders = (client: ShopAPIClient) => useSuspenseQuery({ - queryKey: [client.language, ...QUERY_KEYS.ORDER_LIST], + queryKey: [...QUERY_KEYS.ORDER_LIST, client.language], queryFn: ShopAPIs.listOrders(client), }); From 3a41f66ebd0d5c70c8a52b4bb14fbf8ab1afe041 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 17:24:29 +0900 Subject: [PATCH 012/324] =?UTF-8?q?feat:=20SponsorTag=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=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/routes.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/pyconkr-admin/src/routes.tsx b/apps/pyconkr-admin/src/routes.tsx index 91088cdf..27d588ea 100644 --- a/apps/pyconkr-admin/src/routes.tsx +++ b/apps/pyconkr-admin/src/routes.tsx @@ -10,6 +10,7 @@ import { ManageAccounts, NoteAlt, StickyNote2, + Tag, } from "@mui/icons-material"; import { AdminEditorCreateRoutePage, AdminEditorModifyRoutePage } from "./components/layouts/admin_editor"; @@ -84,6 +85,14 @@ export const RouteDefinitions: RouteDef[] = [ app: "event", resource: "sponsortier", }, + { + type: "routeDefinition", + key: "event-sponsortag", + icon: Tag, + title: "후원사 태그", + app: "event", + resource: "sponsortag", + }, { type: "routeDefinition", key: "event-sponsor", From 6a76b16523c86c046c119c9769438223dd743e20 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 17:26:07 +0900 Subject: [PATCH 013/324] =?UTF-8?q?feat:=20=ED=9B=84=EC=9B=90=EC=82=AC?= =?UTF-8?q?=EC=97=90=20=ED=83=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/Sponsor/index.tsx | 99 ++++++++++++++++--- packages/common/src/schemas/backendAPI.ts | 1 + 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Sponsor/index.tsx b/apps/pyconkr/src/components/layout/Sponsor/index.tsx index b77a0ad4..0c42461f 100644 --- a/apps/pyconkr/src/components/layout/Sponsor/index.tsx +++ b/apps/pyconkr/src/components/layout/Sponsor/index.tsx @@ -1,5 +1,5 @@ import * as Common from "@frontend/common"; -import { CircularProgress, Divider, Stack, Tooltip, Typography, TypographyProps, styled } from "@mui/material"; +import { Badge, CircularProgress, Divider, Stack, Tooltip, Typography, TypographyProps, styled } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import { Link } from "react-router-dom"; @@ -7,6 +7,8 @@ import { useAppContext } from "../../../contexts/app_context"; const LogoHeight: React.CSSProperties["height"] = "8rem"; const LogoWidth: React.CSSProperties["width"] = "15rem"; +const LogoContainerHeight: React.CSSProperties["height"] = `calc(${LogoHeight} + 2rem)`; +const LogoContainerWidth: React.CSSProperties["width"] = `calc(${LogoWidth} + 4rem)`; const SponsorContainer = styled(Stack)({ width: "100%", @@ -26,27 +28,89 @@ const SponsorStack = styled(Stack)({ justifyContent: "center", alignItems: "center", padding: "0 1rem", - gap: "2rem", + gap: "4rem", }); -const LogoImageContainer = styled(Stack)({ +const LogoImageEqualWidthContainer = styled(Stack)(({ theme }) => ({ + position: "relative", alignItems: "center", justifyContent: "center", alignContent: "stretch", - height: LogoHeight, - maxHeight: LogoHeight, + height: LogoContainerHeight, + maxHeight: LogoContainerHeight, + minWidth: LogoContainerWidth, + maxWidth: LogoContainerWidth, + border: `1px solid ${theme.palette.primary.light}`, + borderRadius: "0.5rem", + + transition: "all 0.3s ease-in-out", + + "&:hover": { + borderColor: theme.palette.primary.dark, + boxShadow: theme.shadows[3], + }, +})); + +const LogoImageContainer = styled(Stack)({ + width: "auto", + height: "auto", + minHeight: LogoHeight, maxWidth: LogoWidth, + maxHeight: LogoHeight, + objectFit: "contain", + alignItems: "center", + justifyContent: "center", + margin: "4rem 8rem", }); const LogoImage = styled("img")({ - height: `calc(${LogoHeight} * 0.9)`, // 90% of LogoHeight + width: "auto", + height: "auto", minHeight: LogoHeight, - minWidth: `calc(${LogoWidth} * 0.9)`, // 80% of LogoWidth - maxWidth: "100%", - maxHeight: "100%", + maxWidth: LogoWidth, + maxHeight: LogoHeight, objectFit: "contain", }); +const LogoBadgeContainer = styled(Stack)({ + position: "absolute", + width: "auto", + height: "auto", + top: "0.5rem", + right: "-0.5rem", + flexDirection: "column", + alignItems: "flex-end", + justifyContent: "center", + gap: "0.25rem", +}); + +const LogoBadge = styled(Badge)(({ theme }) => ({ + alignItems: "flex-end", + + "& .MuiBadge-badge": { + position: "relative", + borderRadius: "0.25rem", + // height: "1rem", + padding: "0 0.5rem", + backgroundColor: theme.palette.primary.main, + // color: theme.palette.primary.main, + // border: `1px solid ${theme.palette.primary.main}`, + borderEndEndRadius: "0", + transform: "none", + + "&:after": { + content: '""', + position: "absolute", + bottom: "-8px", + right: "-0.1px", + width: 0, + height: 0, + border: "solid 4px", + borderColor: `${theme.palette.primary.dark} transparent transparent ${theme.palette.primary.dark}`, + }, + }, +})); + export const Sponsor: React.FC = ErrorBoundary.with( { fallback: ( @@ -87,11 +151,18 @@ export const Sponsor: React.FC = ErrorBoundary.with( const sponsorName = sponsor.name.replace(/\\n/g, "\n"); const sponsorNameContent = ; const sponsorImg = ( - - - - - + + + + {sponsor.tags.map((tag, i) => ( + + ))} + + + + + + ); return sponsor.sitemap_id ? : sponsorImg; })} diff --git a/packages/common/src/schemas/backendAPI.ts b/packages/common/src/schemas/backendAPI.ts index f862d3d3..dcf9b4aa 100644 --- a/packages/common/src/schemas/backendAPI.ts +++ b/packages/common/src/schemas/backendAPI.ts @@ -64,6 +64,7 @@ namespace BackendAPISchemas { name: string; logo: string; sitemap_id: string | null; + tags: string[]; }[]; }; From d8a691946da734ea8562e432d9721cf54a009c5b Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 20 Jun 2025 17:35:31 +0900 Subject: [PATCH 014/324] =?UTF-8?q?fix:=20=ED=9B=84=EC=9B=90=EC=82=AC=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EA=B8=80=EC=9E=90=20=EC=83=89=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/components/layout/Sponsor/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Sponsor/index.tsx b/apps/pyconkr/src/components/layout/Sponsor/index.tsx index 0c42461f..16d85b26 100644 --- a/apps/pyconkr/src/components/layout/Sponsor/index.tsx +++ b/apps/pyconkr/src/components/layout/Sponsor/index.tsx @@ -90,11 +90,9 @@ const LogoBadge = styled(Badge)(({ theme }) => ({ "& .MuiBadge-badge": { position: "relative", borderRadius: "0.25rem", - // height: "1rem", padding: "0 0.5rem", backgroundColor: theme.palette.primary.main, - // color: theme.palette.primary.main, - // border: `1px solid ${theme.palette.primary.main}`, + color: "white", borderEndEndRadius: "0", transform: "none", From 813e71620d9603b3d900dbc2e66c26b89448dc36 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 21 Jun 2025 21:47:07 +0900 Subject: [PATCH 015/324] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=94=94=EB=B2=84=EA=B9=85=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/components/pages/test.tsx | 4 +- apps/pyconkr/src/debug/page/backend_test.tsx | 39 -------------------- 2 files changed, 1 insertion(+), 42 deletions(-) delete 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 8cc4c538..da436f0f 100644 --- a/apps/pyconkr/src/components/pages/test.tsx +++ b/apps/pyconkr/src/components/pages/test.tsx @@ -1,14 +1,13 @@ import { Button, Stack } from "@mui/material"; import * as React from "react"; -import { BackendTestPage } from "../../debug/page/backend_test"; import { ComponentTestPage } from "../../debug/page/component_test"; import { MapTestPage } from "../../debug/page/map_test"; import { MdiTestPage } from "../../debug/page/mdi_test"; import { ShopTestPage } from "../../debug/page/shop_test"; const LOCAL_STORAGE_KEY = "selectedTab"; -type SelectedTabType = "shop" | "mdi" | "backend" | "map" | "component"; +type SelectedTabType = "shop" | "mdi" | "map" | "component"; const getTabFromLocalStorage = (): SelectedTabType => (localStorage.getItem(LOCAL_STORAGE_KEY) as SelectedTabType) || "shop"; @@ -20,7 +19,6 @@ const setTabToLocalStorage = (tab: SelectedTabType) => { const TabList: { [key in SelectedTabType]: React.ReactNode } = { shop: , mdi: , - backend: , map: , component: , }; diff --git a/apps/pyconkr/src/debug/page/backend_test.tsx b/apps/pyconkr/src/debug/page/backend_test.tsx deleted file mode 100644 index db0e5885..00000000 --- a/apps/pyconkr/src/debug/page/backend_test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as Common from "@frontend/common"; -import { CircularProgress, MenuItem, Select, SelectProps, Stack } from "@mui/material"; -import { Suspense } from "@suspensive/react"; -import * as React from "react"; - -import { PageRenderer } from "../../components/pages/dynamic_route"; - -const SiteMapRenderer: React.FC = Suspense.with({ fallback: }, () => { - const backendClient = Common.Hooks.BackendAPI.useBackendClient(); - const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendClient); - return
{JSON.stringify(Common.Utils.buildNestedSiteMap(data), null, 2)}
; -}); - -const PageIdSelector: React.FC<{ onChange: SelectProps["onChange"] }> = Suspense.with({ fallback: }, ({ onChange }) => { - const backendClient = Common.Hooks.BackendAPI.useBackendClient(); - const { data } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendClient); - - return ( - - ); -}); - -export const BackendTestPage: React.FC = () => { - const [pageId, setPageId] = React.useState(null); - - return ( - - - setPageId(e.target.value as string)} /> - {Common.Utils.isFilledString(pageId) ? : <>페이지를 선택해주세요.} - - ); -}; From e2e783079a4f59465aa6aff258907f0e8fc9aa51 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 21 Jun 2025 22:39:34 +0900 Subject: [PATCH 016/324] =?UTF-8?q?feat:=20sitemap=EB=A1=9C=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=A7=81=ED=81=AC=EB=A5=BC=20=EA=B0=88=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/Header/index.tsx | 16 +++++++++++++--- .../src/components/pages/dynamic_route.tsx | 6 +++++- packages/common/src/schemas/backendAPI.ts | 6 ++++-- packages/common/src/schemas/backendAdminAPI.ts | 6 ++++-- packages/common/src/utils/api.ts | 1 - 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Header/index.tsx b/apps/pyconkr/src/components/layout/Header/index.tsx index 87dcfc13..ef68bcf3 100644 --- a/apps/pyconkr/src/components/layout/Header/index.tsx +++ b/apps/pyconkr/src/components/layout/Header/index.tsx @@ -72,7 +72,13 @@ const Header: React.FC = () => { {Object.values(siteMapNode.children) .filter((s) => !s.hide) .map((r) => ( - + @@ -100,7 +106,9 @@ const Header: React.FC = () => { onMouseEnter={() => setDepth2(r)} // 하위 depth가 있는 경우, 하위 depth를 선택할 수 있도록 유지하기 위해 depth2도 유지합니다. onMouseLeave={() => R.isEmpty(navState.depth2?.children ?? {}) && setDepth2(undefined)} - to={getDepth2Route(r.route_code)} + target={R.isString(r.external_link) ? "_blank" : undefined} + rel={R.isString(r.external_link) ? "noopener noreferrer" : undefined} + to={r.external_link || getDepth2Route(r.route_code)} /> ))}
@@ -120,7 +128,9 @@ const Header: React.FC = () => { onClick={resetDepths} onMouseEnter={() => setDepth3(r)} onMouseLeave={() => setDepth3(undefined)} - to={getDepth3Route(r?.route_code)} + target={R.isString(r.external_link) ? "_blank" : undefined} + rel={R.isString(r.external_link) ? "noopener noreferrer" : undefined} + to={r.external_link || getDepth3Route(r?.route_code)} /> ))}
diff --git a/apps/pyconkr/src/components/pages/dynamic_route.tsx b/apps/pyconkr/src/components/pages/dynamic_route.tsx index 0cc1cc29..021a9244 100644 --- a/apps/pyconkr/src/components/pages/dynamic_route.tsx +++ b/apps/pyconkr/src/components/pages/dynamic_route.tsx @@ -135,7 +135,11 @@ export const RouteRenderer: React.FC = ErrorBoundary.with( Suspense.with({ fallback: }, () => { const { siteMapNode, currentSiteMapDepth } = useAppContext(); const routeInfo = !R.isEmpty(currentSiteMapDepth) && currentSiteMapDepth[currentSiteMapDepth.length - 1]; - return !(siteMapNode && routeInfo) ? : ; + + if (!(siteMapNode && routeInfo)) return ; + if (R.isString(routeInfo.page)) return ; + if (R.isString(routeInfo.external_link)) window.location.replace(routeInfo.external_link); + return ; }) ); diff --git a/packages/common/src/schemas/backendAPI.ts b/packages/common/src/schemas/backendAPI.ts index dcf9b4aa..2f0ce819 100644 --- a/packages/common/src/schemas/backendAPI.ts +++ b/packages/common/src/schemas/backendAPI.ts @@ -20,8 +20,9 @@ namespace BackendAPISchemas { name: string; order: number; parent_sitemap: string | null; - page: string; hide: boolean; + page: string | null; + external_link: string | null; }; export type NestedSiteMapSchema = { @@ -29,10 +30,11 @@ namespace BackendAPISchemas { route_code: string; name: string; order: number; - page: string; hide: boolean; parent_sitemap: string | null; children: NestedSiteMapSchema[]; + page: string | null; + external_link: string | null; }; export type SectionSchema = { diff --git a/packages/common/src/schemas/backendAdminAPI.ts b/packages/common/src/schemas/backendAdminAPI.ts index cd60b54f..24d92b50 100644 --- a/packages/common/src/schemas/backendAdminAPI.ts +++ b/packages/common/src/schemas/backendAdminAPI.ts @@ -63,8 +63,9 @@ namespace BackendAdminAPISchemas { name_en: string; order: number; parent_sitemap: string | null; - page: string; hide: boolean; + page: string | null; + external_link: string | null; }; export type NestedSiteMapSchema = { @@ -74,9 +75,10 @@ namespace BackendAdminAPISchemas { name_en: string; order: number; parent_sitemap: string | null; - page: string; hide: boolean; children: NestedSiteMapSchema[]; + page: string | null; + external_link: string | null; }; export type PageSectionBulkUpdateSchema = PageSectionSchema | Omit; diff --git a/packages/common/src/utils/api.ts b/packages/common/src/utils/api.ts index 7c4c1231..a9312f82 100644 --- a/packages/common/src/utils/api.ts +++ b/packages/common/src/utils/api.ts @@ -5,7 +5,6 @@ type GFlatSiteMap = { route_code: string; order: number; parent_sitemap: string | null; - page: string; hide: boolean; }; type GNestedSiteMap = T & { children: GNestedSiteMap[] }; From 18e4781b08287e85a70271a24e4aeb7e7f68d12e Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 22 Jun 2025 16:37:27 +0900 Subject: [PATCH 017/324] =?UTF-8?q?feat:=20=EC=88=9C=EC=88=98=20markdown?= =?UTF-8?q?=20editor=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20MDXRenderer?= =?UTF-8?q?=EA=B0=80=20JSX=EB=A5=BC=20=EB=AA=BB=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20md=20=EB=AA=A8=EB=93=9C=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/pages/page/editor.tsx | 2 +- .../src/components/pages/dynamic_route.tsx | 2 +- apps/pyconkr/src/debug/page/mdi_test.tsx | 2 +- packages/common/src/components/index.ts | 2 + packages/common/src/components/md_editor.tsx | 63 +++++++++++++++++++ packages/common/src/components/mdx.tsx | 11 +++- packages/common/src/components/mdx_editor.tsx | 5 ++ .../shop/src/components/features/product.tsx | 2 +- 8 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 packages/common/src/components/md_editor.tsx diff --git a/apps/pyconkr-admin/src/components/pages/page/editor.tsx b/apps/pyconkr-admin/src/components/pages/page/editor.tsx index 74ee04b7..82586518 100644 --- a/apps/pyconkr-admin/src/components/pages/page/editor.tsx +++ b/apps/pyconkr-admin/src/components/pages/page/editor.tsx @@ -52,7 +52,7 @@ const SectionTextEditor: React.FC = ({ disabled, defa - + diff --git a/apps/pyconkr/src/components/pages/dynamic_route.tsx b/apps/pyconkr/src/components/pages/dynamic_route.tsx index 021a9244..bc39578d 100644 --- a/apps/pyconkr/src/components/pages/dynamic_route.tsx +++ b/apps/pyconkr/src/components/pages/dynamic_route.tsx @@ -117,7 +117,7 @@ const InnerPageRenderer: React.FC<{ id: string }> = Suspense.with({ fallback: {data.sections.map((s) => ( - + ))} diff --git a/apps/pyconkr/src/debug/page/mdi_test.tsx b/apps/pyconkr/src/debug/page/mdi_test.tsx index 33cca026..f68d8332 100644 --- a/apps/pyconkr/src/debug/page/mdi_test.tsx +++ b/apps/pyconkr/src/debug/page/mdi_test.tsx @@ -28,7 +28,7 @@ export const MdiTestPage: React.FC = () => { - + ); diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index 3edd8621..80dca845 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -8,6 +8,7 @@ import { LottiePlayer as LottiePlayerComponent, NetworkLottiePlayer as NetworkLottiePlayerComponent, } from "./lottie"; +import { MarkdownEditor as MarkdownEditorComponent } from "./md_editor"; import { MDXRenderer as MDXRendererComponent } from "./mdx"; import { Confetti as ConfettiComponent } from "./mdx_components/confetti"; import { @@ -29,6 +30,7 @@ import { PythonKorea as PythonKoreaComponent } from "./pythonkorea"; namespace Components { export const CenteredPage = CenteredPageComponent; export const CommonContextProvider = CommonContextProviderComponent; + export const MarkdownEditor = MarkdownEditorComponent; export const MDXEditor = MDXEditorComponent; export const MDXRenderer = MDXRendererComponent; export const PythonKorea = PythonKoreaComponent; diff --git a/packages/common/src/components/md_editor.tsx b/packages/common/src/components/md_editor.tsx new file mode 100644 index 00000000..84abc2cf --- /dev/null +++ b/packages/common/src/components/md_editor.tsx @@ -0,0 +1,63 @@ +import { Stack } from "@mui/material"; +import MDEditor, { ICommand, commands } from "@uiw/react-md-editor"; +import * as React from "react"; + +type MDEditorProps = { + disabled?: boolean; + defaultValue?: string; + onChange?: (value?: string) => void; + extraCommands?: ICommand[]; +}; + +const TextEditorStyle: React.CSSProperties = { + flexGrow: 1, + width: "100%", + maxWidth: "100%", + + wordBreak: "break-word", + whiteSpace: "pre-wrap", + overflowWrap: "break-word", + + fieldSizing: "content", +} as React.CSSProperties; + +export const MarkdownEditor: React.FC = ({ disabled, defaultValue, onChange, extraCommands }) => ( + + + +); diff --git a/packages/common/src/components/mdx.tsx b/packages/common/src/components/mdx.tsx index 51e40554..ddeb74d2 100644 --- a/packages/common/src/components/mdx.tsx +++ b/packages/common/src/components/mdx.tsx @@ -84,7 +84,13 @@ const lineFormatterForMDX = (line: string) => { return `${trimmedLine} \n`; }; -export const MDXRenderer: React.FC<{ text: string; resetKey?: number }> = ({ text, resetKey }) => { +type MDXRendererPropType = { + text: string; + resetKey?: number; + format?: "mdx" | "md"; +}; + +export const MDXRenderer: React.FC = ({ text, resetKey, format }) => { const { baseUrl, mdxComponents } = Hooks.Common.useCommonContext(); const [state, setState] = React.useState<{ component: React.ReactNode; @@ -106,6 +112,7 @@ export const MDXRenderer: React.FC<{ text: string; resetKey?: number }> = ({ tex const { default: RenderResult } = await evaluate(processedText, { ...runtime, ...provider, + format: format || "md", baseUrl, remarkPlugins: [remarkGfm], }); @@ -120,7 +127,7 @@ export const MDXRenderer: React.FC<{ text: string; resetKey?: number }> = ({ tex setRenderResult(); } })(); - }, [text, resetKey, state.resetKey, baseUrl, mdxComponents]); + }, [text, resetKey, format, state.resetKey, baseUrl, mdxComponents]); return ( diff --git a/packages/common/src/components/mdx_editor.tsx b/packages/common/src/components/mdx_editor.tsx index 90e3eb7d..4f5422ef 100644 --- a/packages/common/src/components/mdx_editor.tsx +++ b/packages/common/src/components/mdx_editor.tsx @@ -253,15 +253,18 @@ export const MDXEditor: React.FC = ({ disabled, defaultValue, on }), commands.bold, commands.italic, + commands.strikethrough, commands.code, commands.link, commands.divider, commands.quote, commands.codeBlock, + commands.table, commands.hr, commands.divider, commands.unorderedListCommand, commands.orderedListCommand, + commands.checkedListCommand, commands.divider, commands.group([], { name: "custom components", @@ -277,6 +280,8 @@ export const MDXEditor: React.FC = ({ disabled, defaultValue, on children: (props) => , buttonProps: { "aria-label": "Insert image" }, }), + commands.divider, + commands.help, ]} extraCommands={extraCommands} style={TextEditorStyle} diff --git a/packages/shop/src/components/features/product.tsx b/packages/shop/src/components/features/product.tsx index 9fd31ad6..49008206 100644 --- a/packages/shop/src/components/features/product.tsx +++ b/packages/shop/src/components/features/product.tsx @@ -248,7 +248,7 @@ const ProductItem: React.FC = ({ disabled: rootDisabled, la return ( <> - +
{R.isNullish(notPurchasableReason) ? ( From ef7cc1855f9e3be4674cf5c4ab002685bc35b514 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 22 Jun 2025 17:55:01 +0900 Subject: [PATCH 018/324] =?UTF-8?q?fix:=20MarkdownEditor=EC=97=90=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=EB=90=9C=20props=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/components/md_editor.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/common/src/components/md_editor.tsx b/packages/common/src/components/md_editor.tsx index 84abc2cf..fdc66657 100644 --- a/packages/common/src/components/md_editor.tsx +++ b/packages/common/src/components/md_editor.tsx @@ -4,6 +4,8 @@ import * as React from "react"; type MDEditorProps = { disabled?: boolean; + name?: string; + value?: string; defaultValue?: string; onChange?: (value?: string) => void; extraCommands?: ICommand[]; @@ -21,16 +23,17 @@ const TextEditorStyle: React.CSSProperties = { fieldSizing: "content", } as React.CSSProperties; -export const MarkdownEditor: React.FC = ({ disabled, defaultValue, onChange, extraCommands }) => ( +export const MarkdownEditor: React.FC = ({ disabled, name, defaultValue, value, onChange, extraCommands }) => ( Date: Sun, 22 Jun 2025 17:55:33 +0900 Subject: [PATCH 019/324] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EC=97=90=EB=94=94=ED=84=B0=EC=97=90=20=EB=A7=88=ED=81=AC?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=ED=95=84=EB=93=9C=20=EC=A7=80=EC=9B=90=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 --- .../src/components/layouts/admin_editor.tsx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx index e8e0170a..5f589d17 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx @@ -13,6 +13,7 @@ import { OutlinedSelectProps, Select, Stack, + styled, Tab, Table, TableBody, @@ -142,6 +143,38 @@ const M2MSelect: Field = ErrorBoundary.with( }) ); +const MUIStyledFieldset = styled("fieldset")(({ theme }) => ({ + color: theme.palette.text.secondary, + margin: 0, + + border: `1px solid ${theme.palette.info}`, + borderRadius: theme.shape.borderRadius, +})); + +const MDEditorField: Field = ErrorBoundary.with( + { fallback: Common.Components.ErrorFallback }, + ({ disabled, formData, name, onChange: rawOnChange }) => { + const [valueState, setValueState] = React.useState(formData?.toString() || ""); + const onChange = (value?: string) => { + setValueState(value); + rawOnChange(value, undefined, name); + }; + return ( + + + + + + + + + + + + ); + } +); + type ReadOnlyValueFieldStateType = { loading: boolean; blob: Blob | null; @@ -369,7 +402,7 @@ const InnerAdminEditor: React.FC = Err onSubmit={onSubmitFunc} disabled={disabled} showErrorList={false} - fields={{ file: FileField, m2m_select: M2MSelect }} + fields={{ file: FileField, m2m_select: M2MSelect, markdown: MDEditorField }} /> From 331db6001c16885a80f5859ce057e25844e65ed8 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 22 Jun 2025 18:53:13 +0900 Subject: [PATCH 020/324] =?UTF-8?q?fix:=20M2MSelect=EA=B0=80=20=ED=95=AD?= =?UTF-8?q?=EC=83=81=20required=EC=9D=B4=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr-admin/src/components/layouts/admin_editor.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx index 5f589d17..5946d465 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx @@ -92,6 +92,9 @@ const fieldPropsToSelectedProps = (props: FieldProps): OutlinedSelectProps & { d onBlur: rawOnBlur, onChange: rawOnChange, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + required: _, + schema, errorSchema, uiSchema, From 12b0da1dc59cd32be465dda16833895ff3159e15 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 22 Jun 2025 19:57:13 +0900 Subject: [PATCH 021/324] =?UTF-8?q?feat:=20=ED=9B=84=EC=9B=90=EC=82=AC=20?= =?UTF-8?q?=EC=83=81=EC=84=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 --- .../src/components/layouts/admin_editor.tsx | 15 ++- apps/pyconkr/src/App.tsx | 7 +- .../components/layout/PageLayout/index.tsx | 23 +++++ .../src/components/layout/Sponsor/index.tsx | 42 ++++---- apps/pyconkr/src/components/pages/sign_in.tsx | 29 +----- .../src/components/pages/sponsor_detail.tsx | 95 +++++++++++++++++++ apps/pyconkr/src/contexts/app_context.tsx | 2 +- apps/pyconkr/src/main.tsx | 1 - packages/common/src/apis/index.ts | 2 +- packages/common/src/components/mdx.tsx | 6 +- packages/common/src/schemas/backendAPI.ts | 4 +- 11 files changed, 167 insertions(+), 59 deletions(-) create mode 100644 apps/pyconkr/src/components/layout/PageLayout/index.tsx create mode 100644 apps/pyconkr/src/components/pages/sponsor_detail.tsx diff --git a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx index 5946d465..d0681ee7 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx @@ -154,6 +154,17 @@ const MUIStyledFieldset = styled("fieldset")(({ theme }) => ({ borderRadius: theme.shape.borderRadius, })); +const MDRendererContainer = styled(Box)(({ theme }) => ({ + width: "50%", + maxWidth: "50%", + backgroundColor: "#fff", + + "& .markdown-body": { + width: "100%", + p: { margin: theme.spacing(2, 0) }, + }, +})); + const MDEditorField: Field = ErrorBoundary.with( { fallback: Common.Components.ErrorFallback }, ({ disabled, formData, name, onChange: rawOnChange }) => { @@ -169,9 +180,9 @@ const MDEditorField: Field = ErrorBoundary.with( - + - + ); diff --git a/apps/pyconkr/src/App.tsx b/apps/pyconkr/src/App.tsx index 88f6e7fd..da9f719e 100644 --- a/apps/pyconkr/src/App.tsx +++ b/apps/pyconkr/src/App.tsx @@ -6,6 +6,7 @@ import * as R from "remeda"; import MainLayout from "./components/layout/index.tsx"; import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx"; import { ShopSignInPage } from "./components/pages/sign_in.tsx"; +import { SponsorDetailPage } from "./components/pages/sponsor_detail.tsx"; import { Test } from "./components/pages/test.tsx"; import { IS_DEBUG_ENV } from "./consts"; import { useAppContext } from "./contexts/app_context"; @@ -13,6 +14,7 @@ import BackendAPISchemas from "../../../packages/common/src/schemas/backendAPI"; export const App: React.FC = () => { const backendAPIClient = Common.Hooks.BackendAPI.useBackendClient(); + const { data: sponsorTiers } = Common.Hooks.BackendAPI.useSponsorQuery(backendAPIClient); const { data: flatSiteMap } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendAPIClient); const siteMapNode = Common.Utils.buildNestedSiteMap(flatSiteMap)?.[""]; @@ -35,16 +37,17 @@ export const App: React.FC = () => { } } - setAppContext((ps) => ({ ...ps, siteMapNode, currentSiteMapDepth })); + setAppContext((ps) => ({ ...ps, siteMapNode, sponsorTiers, currentSiteMapDepth })); })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location, language, flatSiteMap]); + }, [location, language, flatSiteMap, sponsorTiers]); return ( }> {IS_DEBUG_ENV && } />} } /> + } /> } /> } /> diff --git a/apps/pyconkr/src/components/layout/PageLayout/index.tsx b/apps/pyconkr/src/components/layout/PageLayout/index.tsx new file mode 100644 index 00000000..322bb1c9 --- /dev/null +++ b/apps/pyconkr/src/components/layout/PageLayout/index.tsx @@ -0,0 +1,23 @@ +import { Stack, styled } from "@mui/material"; + +export const PageLayout = styled(Stack)(({ theme }) => ({ + height: "75%", + width: "100%", + maxWidth: "1200px", + + justifyContent: "flex-start", + alignItems: "center", + + paddingTop: theme.spacing(8), + paddingBottom: theme.spacing(8), + + paddingRight: theme.spacing(16), + paddingLeft: theme.spacing(16), + + [theme.breakpoints.down("lg")]: { + padding: theme.spacing(4), + }, + [theme.breakpoints.down("sm")]: { + padding: theme.spacing(2), + }, +})); diff --git a/apps/pyconkr/src/components/layout/Sponsor/index.tsx b/apps/pyconkr/src/components/layout/Sponsor/index.tsx index 16d85b26..315a060f 100644 --- a/apps/pyconkr/src/components/layout/Sponsor/index.tsx +++ b/apps/pyconkr/src/components/layout/Sponsor/index.tsx @@ -1,4 +1,3 @@ -import * as Common from "@frontend/common"; import { Badge, CircularProgress, Divider, Stack, Tooltip, Typography, TypographyProps, styled } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import { Link } from "react-router-dom"; @@ -120,14 +119,8 @@ export const Sponsor: React.FC = ErrorBoundary.with( ), }, Suspense.with({ fallback: }, () => { - const { siteMapNode } = useAppContext(); - const backendAPIClient = Common.Hooks.BackendAPI.useBackendClient(); - const { data: sponsorData } = Common.Hooks.BackendAPI.useSponsorQuery(backendAPIClient); - - if (!siteMapNode) return ; - - const flatSiteMap = Common.Utils.buildFlatSiteMap(siteMapNode); - const flatSiteMapObj = flatSiteMap.reduce((a, i) => ({ ...a, [i.id]: i }), {} as Record); + const { sponsorTiers } = useAppContext(); + if (!sponsorTiers) return ; const textProps: TypographyProps = { textAlign: "center", @@ -139,7 +132,7 @@ export const Sponsor: React.FC = ErrorBoundary.with( - {sponsorData + {sponsorTiers .filter((t) => t.sponsors.length) .map((sponsorTier, i, a) => ( @@ -148,21 +141,22 @@ export const Sponsor: React.FC = ErrorBoundary.with( {sponsorTier.sponsors.map((sponsor) => { const sponsorName = sponsor.name.replace(/\\n/g, "\n"); const sponsorNameContent = ; - const sponsorImg = ( - - - - {sponsor.tags.map((tag, i) => ( - - ))} - - - - - - + return ( + + + + + {sponsor.tags.map((tag, i) => ( + + ))} + + + + + + + ); - return sponsor.sitemap_id ? : sponsorImg; })} {i !== a.length - 1 && } diff --git a/apps/pyconkr/src/components/pages/sign_in.tsx b/apps/pyconkr/src/components/pages/sign_in.tsx index ecb14904..905b2ad2 100644 --- a/apps/pyconkr/src/components/pages/sign_in.tsx +++ b/apps/pyconkr/src/components/pages/sign_in.tsx @@ -1,34 +1,13 @@ import * as Shop from "@frontend/shop"; import { AccountCircleOutlined, Google } from "@mui/icons-material"; -import { Backdrop, Button, ButtonProps, CircularProgress, Stack, styled, Typography } from "@mui/material"; +import { Backdrop, Button, ButtonProps, CircularProgress, Stack, Typography } from "@mui/material"; import { Suspense } from "@suspensive/react"; import { enqueueSnackbar, OptionsObject } from "notistack"; import * as React from "react"; import { useNavigate } from "react-router-dom"; import { useAppContext } from "../../contexts/app_context"; - -const SignInPageContainer = styled(Stack)(({ theme }) => ({ - height: "75%", - width: "100%", - maxWidth: "1200px", - - justifyContent: "flex-start", - alignItems: "center", - - paddingTop: theme.spacing(8), - paddingBottom: theme.spacing(8), - - paddingRight: theme.spacing(16), - paddingLeft: theme.spacing(16), - - [theme.breakpoints.down("lg")]: { - padding: theme.spacing(4), - }, - [theme.breakpoints.down("sm")]: { - padding: theme.spacing(2), - }, -})); +import { PageLayout } from "../layout/PageLayout"; type PageeStateType = { openBackdrop: boolean; @@ -110,14 +89,14 @@ export const ShopSignInPage: React.FC = Suspense.with({ fallback: - + {btnProps.map((props, index) => ( + {!hideCreateNew && ( + + )} @@ -57,8 +60,8 @@ const InnerAdminList: React.FC = ErrorBoundary.with( {item.str_repr} - {hideCreatedAt === true && {new Date(item.created_at).toLocaleString()}} - {hideUpdatedAt === true && {new Date(item.updated_at).toLocaleString()}} + {!hideCreatedAt && {new Date(item.created_at).toLocaleString()}} + {!hideUpdatedAt && {new Date(item.updated_at).toLocaleString()}} ))} From eed12747095bbfdd905ca1565bef8ae1cd35a389 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 5 Jul 2025 19:55:29 +0900 Subject: [PATCH 044/324] =?UTF-8?q?fix:=20=EC=B0=B8=EA=B0=80=EC=9E=90=20?= =?UTF-8?q?=ED=8F=AC=ED=83=88=EC=97=90=EC=84=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=8B=AC=EC=82=AC=EC=9D=98=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr-participant-portal/src/components/pages/home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pyconkr-participant-portal/src/components/pages/home.tsx b/apps/pyconkr-participant-portal/src/components/pages/home.tsx index 6dcb35cd..e35008c7 100644 --- a/apps/pyconkr-participant-portal/src/components/pages/home.tsx +++ b/apps/pyconkr-participant-portal/src/components/pages/home.tsx @@ -124,7 +124,7 @@ const InnerLandingPage: React.FC = () => { } - onClick={() => navigate(`/session/${audit.instance_id}/modification-audit/${audit.id}`)} + onClick={() => navigate(`/session/${audit.instance_id}/`)} /> )) From 2e1da22d048c3d572fedbe6998a43ec05ea03fb4 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 5 Jul 2025 19:56:07 +0900 Subject: [PATCH 045/324] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20A?= =?UTF-8?q?PI=EC=9D=98=20=EC=88=98=EC=A0=95=20=EC=8B=AC=EC=82=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=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/admin_api.ts | 23 +++++++++++ packages/common/src/hooks/useAdminAPI.ts | 26 +++++++++++++ .../common/src/schemas/backendAdminAPI.ts | 38 +++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/packages/common/src/apis/admin_api.ts b/packages/common/src/apis/admin_api.ts index 852163d0..39e36fa5 100644 --- a/packages/common/src/apis/admin_api.ts +++ b/packages/common/src/apis/admin_api.ts @@ -77,6 +77,29 @@ namespace BackendAdminAPIs { `v1/admin-api/cms/page/${pageId}/section/bulk-update/`, data ); + + export const approveModificationAudit = (client: BackendAPIClient, id: string) => (reason?: string | null) => + client.patch( + `v1/admin-api/modification-audit/modification-audit/${id}/approve/`, + { reason: reason ?? null } + ); + + export const rejectModificationAudit = (client: BackendAPIClient, id: string) => (reason?: string | null) => + client.patch( + `v1/admin-api/modification-audit/modification-audit/${id}/reject/`, + { reason: reason ?? null } + ); + + export const previewModificationAudit = + (client: BackendAPIClient, app: string, resource: string, instanceId: string, auditId: string) => + async () => { + try { + return await client.get(`v1/admin-api/${app}/${resource}/${instanceId}/preview/${auditId}/`); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + return null; + } + }; } export default BackendAdminAPIs; diff --git a/packages/common/src/hooks/useAdminAPI.ts b/packages/common/src/hooks/useAdminAPI.ts index f0b44c9f..a165d377 100644 --- a/packages/common/src/hooks/useAdminAPI.ts +++ b/packages/common/src/hooks/useAdminAPI.ts @@ -19,6 +19,8 @@ const MUTATION_KEYS = { ADMIN_CREATE: ["mutation", "admin", "create"], ADMIN_UPDATE: ["mutation", "admin", "update"], ADMIN_REMOVE: ["mutation", "admin", "remove"], + ADMIN_APPROVE_MODIFICATION_AUDIT: ["mutation", "admin", "approve", "modification-audit"], + ADMIN_REJECT_MODIFICATION_AUDIT: ["mutation", "admin", "reject", "modification-audit"], }; namespace BackendAdminAPIHooks { @@ -122,6 +124,30 @@ namespace BackendAdminAPIHooks { mutationKey: [...MUTATION_KEYS.ADMIN_UPDATE, "cms", "page", pageId, "section"], mutationFn: BackendAdminAPIs.bulkUpdateSections(client, pageId), }); + + export const useApproveModificationAuditMutation = (client: BackendAPIClient, id: string) => + useMutation({ + mutationKey: MUTATION_KEYS.ADMIN_APPROVE_MODIFICATION_AUDIT, + mutationFn: BackendAdminAPIs.approveModificationAudit(client, id), + }); + + export const useRejectModificationAuditMutation = (client: BackendAPIClient, id: string) => + useMutation({ + mutationKey: MUTATION_KEYS.ADMIN_REJECT_MODIFICATION_AUDIT, + mutationFn: BackendAdminAPIs.rejectModificationAudit(client, id), + }); + + export const useModificationAuditPreviewRetrieveQuery = ( + client: BackendAPIClient, + app: string, + resource: string, + instanceId: string, + auditId: string + ) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.ADMIN_RETRIEVE, app, resource, instanceId, "preview", auditId], + queryFn: BackendAdminAPIs.previewModificationAudit(client, app, resource, instanceId, auditId), + }); } export default BackendAdminAPIHooks; diff --git a/packages/common/src/schemas/backendAdminAPI.ts b/packages/common/src/schemas/backendAdminAPI.ts index 24d92b50..4f6b8056 100644 --- a/packages/common/src/schemas/backendAdminAPI.ts +++ b/packages/common/src/schemas/backendAdminAPI.ts @@ -82,6 +82,44 @@ namespace BackendAdminAPISchemas { }; export type PageSectionBulkUpdateSchema = PageSectionSchema | Omit; + + export type PresentationSchema = { + id: string; // UUID + type: string; // UUID of the presentation type + categories: string[]; // Array of category UUIDs + title_ko: string; + title_en: string; + summary_ko: string; + summary_en: string; + description_ko: string; + description_en: string; + image: string | null; + }; + + export type ModificationAuditSchema = { + id: string; // UUID + status: "requested" | "approved" | "rejected" | "cancelled"; // Status of the modification request + created_at: string; // ISO 8601 timestamp + updated_at: string; // ISO 8601 timestamp + modification_data: string; // JSON string containing the modification data + str_repr: string; // String representation of the modification audit, e.g., "Presentation Title - Status" + comments: { + id: string; // UUID of the comment + content: string; // Content of the comment + created_at: string; // ISO 8601 timestamp + created_by: { + id: number; // User ID of the commenter + nickname: string; // Nickname of the commenter + is_superuser: boolean; // Whether the commenter is a staff member + }; + updated_at: string; // ISO 8601 timestamp + }[]; + instance: { + app: string; + model: string; + id: string; // UUID of the instance being modified, e.g., presentation ID + }; + }; } export default BackendAdminAPISchemas; From cf8c65c05623c3acbc613d33bbcfd17f0e9dbdd6 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 6 Jul 2025 03:03:59 +0900 Subject: [PATCH 046/324] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC?= =?UTF-8?q?=EC=9D=98=20=EC=88=98=EC=A0=95=20=EC=8B=AC=EC=82=AC=20=EC=A4=91?= =?UTF-8?q?=EA=B0=84=20=EA=B5=AC=ED=98=84=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layouts/global.tsx | 104 ++++++----- .../pages/modification_audit/components.tsx | 174 ++++++++++++++++++ .../pages/modification_audit/dialogs.tsx | 90 +++++++++ .../pages/modification_audit/hooks.tsx | 16 ++ .../pages/modification_audit/pages.tsx | 67 +++++++ .../modification_audit/sub_pages/index.tsx | 14 ++ .../sub_pages/presentation_preview.tsx | 29 +++ .../modification_audit/sub_pages/types.d.ts | 7 + .../sub_pages/userext_preview.tsx | 23 +++ apps/pyconkr-admin/src/routes.tsx | 44 +++-- packages/common/src/components/fieldset.tsx | 26 +++ packages/common/src/components/index.ts | 2 + packages/common/src/hooks/useAdminAPI.ts | 16 +- .../common/src/schemas/backendAdminAPI.ts | 2 + 14 files changed, 549 insertions(+), 65 deletions(-) create mode 100644 apps/pyconkr-admin/src/components/pages/modification_audit/components.tsx create mode 100644 apps/pyconkr-admin/src/components/pages/modification_audit/dialogs.tsx create mode 100644 apps/pyconkr-admin/src/components/pages/modification_audit/hooks.tsx create mode 100644 apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx create mode 100644 apps/pyconkr-admin/src/components/pages/modification_audit/sub_pages/index.tsx create mode 100644 apps/pyconkr-admin/src/components/pages/modification_audit/sub_pages/presentation_preview.tsx create mode 100644 apps/pyconkr-admin/src/components/pages/modification_audit/sub_pages/types.d.ts create mode 100644 apps/pyconkr-admin/src/components/pages/modification_audit/sub_pages/userext_preview.tsx create mode 100644 packages/common/src/components/fieldset.tsx diff --git a/apps/pyconkr-admin/src/components/layouts/global.tsx b/apps/pyconkr-admin/src/components/layouts/global.tsx index 77e924c4..ff8e52e1 100644 --- a/apps/pyconkr-admin/src/components/layouts/global.tsx +++ b/apps/pyconkr-admin/src/components/layouts/global.tsx @@ -25,7 +25,7 @@ import { MiniVariantAppBar, MiniVariantDrawer } from "./sidebar"; export type RouteDef = | { - type: "routeDefinition"; + type: "autoAdminRouteDefinition"; key: string; // Unique key for the route icon: typeof SvgIcon; title: string; @@ -35,6 +35,15 @@ export type RouteDef = hideOnSidebar?: boolean; placeOnBottom?: boolean; } + | { + type: "routeDefinition"; + key: string; // Unique key for the route + icon: typeof SvgIcon; + title: string; + route: string; + hideOnSidebar?: boolean; + placeOnBottom?: boolean; + } | { type: "separator"; key: string; // Unique key for the route @@ -72,48 +81,57 @@ export const Layout: React.FC<{ routes: RouteDef[] }> = ({ routes }) => { const [state, dispatch] = React.useState({ showDrawer: false }); const toggleDrawer = () => dispatch((ps) => ({ ...ps, showDrawer: !ps.showDrawer })); - const SidebarItem: React.FC<{ routeInfo: RouteDef }> = ({ routeInfo }) => - routeInfo.type === "separator" ? ( - - {state.showDrawer ? ( - - - - ) : ( - ({ - width: t.spacing(7), - [t.breakpoints.up("sm")]: { width: t.spacing(8) }, - })} - > - - - )} - - ) : ( - - navigate(routeInfo.route || `/${routeInfo.app}/${routeInfo.resource}`)} - > - - - - {state.showDrawer && } - - - ); + const SidebarItem: React.FC<{ routeInfo: RouteDef }> = ({ routeInfo }) => { + switch (routeInfo.type) { + case "separator": + return ( + + {state.showDrawer ? ( + + + + ) : ( + ({ + width: t.spacing(7), + [t.breakpoints.up("sm")]: { width: t.spacing(8) }, + })} + > + + + )} + + ); + case "routeDefinition": + case "autoAdminRouteDefinition": + return ( + + navigate(routeInfo.type === "autoAdminRouteDefinition" ? `/${routeInfo.app}/${routeInfo.resource}` : routeInfo.route)} + > + + + + {state.showDrawer && } + + + ); + default: + return null; + } + }; const menuButtonStyle: (t: Theme) => React.CSSProperties = (t) => ({ width: `calc(${t.spacing(7)} + 1px)`, diff --git a/apps/pyconkr-admin/src/components/pages/modification_audit/components.tsx b/apps/pyconkr-admin/src/components/pages/modification_audit/components.tsx new file mode 100644 index 00000000..28e4b91a --- /dev/null +++ b/apps/pyconkr-admin/src/components/pages/modification_audit/components.tsx @@ -0,0 +1,174 @@ +import * as Common from "@frontend/common"; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Stack, + styled, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, + TextFieldProps, + Typography, +} from "@mui/material"; +import * as React from "react"; + +type SharedPreviewFieldProps = { + originalDataset: Record; + previewDataset: Record; + name: string; + label: string; +}; + +type PreviewFieldProps = Omit & SharedPreviewFieldProps; + +export const PreviewTextField: React.FC = ({ originalDataset, previewDataset, name, ...props }) => { + const textFieldSx: TextFieldProps["sx"] = { + "& .MuiInputBase-input, & .Mui-disabled": { + color: "black", + WebkitTextFillColor: "black", + "-webkit-text-fill-color": "black", + }, + }; + const textFieldProps: TextFieldProps = { + fullWidth: true, + disabled: true, + variant: "outlined", + value: previewDataset[name] || "(값 없음)", + sx: textFieldSx, + ...props, + }; + const modifiedTextFieldProps: TextFieldProps = { ...textFieldProps, sx: { ...textFieldSx, backgroundColor: "rgba(255, 255, 0, 0.1)" } }; + const originalTextFieldProps: TextFieldProps = { ...textFieldProps, sx: { ...textFieldSx, backgroundColor: "rgba(0, 64, 64, 0.1)" } }; + const isModified = originalDataset[name] !== previewDataset[name]; + + return originalDataset[name] === previewDataset[name] ? ( + + ) : ( + + + + + + 기존 값을 보려면 여기를 클릭해주세요. + + + + + + + + ); +}; + +export const PreviewMarkdownField: React.FC = ({ originalDataset, previewDataset, name, label }) => { + return originalDataset[name] === previewDataset[name] ? ( + + + + + + ) : ( + + + + + + + + + + 기존 값을 보려면 여기를 클릭해주세요. + + + + + + + + + + + + ); +}; + +const ImageFallback: React.FC = () => ( + + + +); + +const WidthSpecifiedFallbackImage = styled(Common.Components.FallbackImage)({ + maxWidth: "20rem", + objectFit: "cover", +}); + +export const PreviewImageField: React.FC = ({ originalDataset, previewDataset, name, label }) => { + const originalImage = originalDataset[name] as string; + const previewImage = previewDataset[name] as string; + + return originalImage === previewImage ? ( + + } /> + + ) : ( + + + + + + } /> + + 기존 이미지를 보려면 여기를 클릭해주세요. + + + + + } /> + + + + + ); +}; + +type SimplifiedModificationAudit = { + id: string; + created_at: string; + created_by: string; + status: string; +}; + +export const ModificationAuditProperties: React.FC<{ audit: SimplifiedModificationAudit }> = ({ audit }) => ( +
+ + + 속성 + + + + + + 심사 ID + {audit.id} + + + 심사 요청 시간 + {new Date(audit.created_at).toLocaleString()} + + + 심사 요청자 + {audit.created_by} + + + 심사 상태 + {audit.status} + + +
+); diff --git a/apps/pyconkr-admin/src/components/pages/modification_audit/dialogs.tsx b/apps/pyconkr-admin/src/components/pages/modification_audit/dialogs.tsx new file mode 100644 index 00000000..e5c43ff1 --- /dev/null +++ b/apps/pyconkr-admin/src/components/pages/modification_audit/dialogs.tsx @@ -0,0 +1,90 @@ +import * as Common from "@frontend/common"; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"; +import { enqueueSnackbar, OptionsObject } from "notistack"; +import * as React from "react"; + +type SubmitConfirmDialogProps = { + open: boolean; + onClose: () => void; + modificationAuditId: string; +}; + +export const ApproveSubmitConfirmDialog: React.FC = ({ open, onClose, modificationAuditId }) => { + const backendAdminClient = Common.Hooks.BackendAdminAPI.useBackendAdminClient(); + const approveModificationAuditMutation = Common.Hooks.BackendAdminAPI.useApproveModificationAuditMutation(backendAdminClient, modificationAuditId); + + const addSnackbar = (c: string | React.ReactNode, variant: OptionsObject["variant"]) => + enqueueSnackbar(c, { variant, anchorOrigin: { vertical: "bottom", horizontal: "center" } }); + + const onApproveClick = () => { + approveModificationAuditMutation.mutate(undefined, { + onSuccess: () => { + addSnackbar("수정 심사가 승인되었습니다.", "success"); + onClose(); + }, + onError: (error) => { + console.error("Approve modification audit failed:", error); + let errorMessage = error instanceof Error ? error.message : "An unknown error occurred."; + if (error instanceof Common.BackendAPIs.BackendAPIClientError) errorMessage = error.message; + addSnackbar(errorMessage, "error"); + }, + }); + }; + + return ( + + 수정 심사 승인 확인 + + + 승인하는 경우 바로 내용이 반영되어 홈페이지에 노출되게 됩니다. +
+ 승인 후에는 수정 심사를 반려할 수 없으니, 내용을 한번 더 확인해 주세요. +
+
+ +
+ ); +}; + +export const RejectSubmitConfirmDialog: React.FC = ({ open, onClose, modificationAuditId }) => { + const backendAdminClient = Common.Hooks.BackendAdminAPI.useBackendAdminClient(); + const rejectModificationAuditMutation = Common.Hooks.BackendAdminAPI.useRejectModificationAuditMutation(backendAdminClient, modificationAuditId); + + const addSnackbar = (c: string | React.ReactNode, variant: OptionsObject["variant"]) => + enqueueSnackbar(c, { variant, anchorOrigin: { vertical: "bottom", horizontal: "center" } }); + + const onRejectClick = () => { + rejectModificationAuditMutation.mutate(undefined, { + onSuccess: () => { + addSnackbar("수정 심사가 반려되었습니다.", "success"); + onClose(); + }, + onError: (error) => { + console.error("Reject modification audit failed:", error); + let errorMessage = error instanceof Error ? error.message : "An unknown error occurred."; + if (error instanceof Common.BackendAPIs.BackendAPIClientError) errorMessage = error.message; + addSnackbar(errorMessage, "error"); + }, + }); + }; + + return ( + + 수정 심사 반려 확인 + + + 수정 심사를 반려하시겠습니까? +
+ 반려 후에는 다시 승인할 수 없습니다! +
+
+ +
+ ); +}; diff --git a/apps/pyconkr-admin/src/components/pages/modification_audit/hooks.tsx b/apps/pyconkr-admin/src/components/pages/modification_audit/hooks.tsx new file mode 100644 index 00000000..df5f96d1 --- /dev/null +++ b/apps/pyconkr-admin/src/components/pages/modification_audit/hooks.tsx @@ -0,0 +1,16 @@ +import * as Common from "@frontend/common"; + +const AdminAPIHooks = Common.Hooks.BackendAdminAPI; + +export const useModificationAuditData = >(auditId: string) => { + const backendAdminClient = AdminAPIHooks.useBackendAdminClient(); + const { data: audit } = AdminAPIHooks.useModificationAuditRetrieveQuery(backendAdminClient, auditId); + const app = audit?.instance.app || ""; + const model = audit?.instance.model || ""; + const objId = audit?.instance.id || ""; + + const { data: originalData } = AdminAPIHooks.useRetrieveQuery(backendAdminClient, app, model, objId); + const { data: previewData } = AdminAPIHooks.useModificationAuditPreviewQuery(backendAdminClient, app, model, objId, auditId); + + return !audit || !originalData || !previewData ? null : { audit, originalData, previewData }; +}; diff --git a/apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx b/apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx new file mode 100644 index 00000000..d9222b14 --- /dev/null +++ b/apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx @@ -0,0 +1,67 @@ +import * as Common from "@frontend/common"; +import { Box, Button, CircularProgress, Divider, Stack, Typography } from "@mui/material"; +import { ErrorBoundary, Suspense } from "@suspensive/react"; +import * as React from "react"; +import { Navigate, useParams } from "react-router-dom"; + +import { ModificationAuditProperties } from "./components"; +import { ApproveSubmitConfirmDialog, RejectSubmitConfirmDialog } from "./dialogs"; +import { useModificationAuditData } from "./hooks"; +import { SubModificationAuditPage } from "./sub_pages"; +import { BackendAdminSignInGuard } from "../../elements/admin_signin_guard"; + +type EditorStateType = { actionStatus?: "approve" | "reject" }; + +const InnerAdminModificationAuditEditor: React.FC = () => { + const [editorState, setEditorState] = React.useState({}); + const { id } = useParams<{ id?: string }>(); + const auditData = useModificationAuditData>(id || ""); + + if (!auditData) return ; + + const { audit } = auditData; + const { status, instance } = audit; + const { app, model } = instance; + const btnDisabled = status !== "requested"; + + const closeSubmitConfirmDialog = () => setEditorState((ps) => ({ ...ps, actionStatus: undefined })); + const openApproveSubmitConfirmDialog = () => setEditorState((ps) => ({ ...ps, actionStatus: "approve" })); + const openRejectSubmitConfirmDialog = () => setEditorState((ps) => ({ ...ps, actionStatus: "reject" })); + + return ( + <> + + + + + {app.toUpperCase()} > {model.toUpperCase()} > 수정 심사 + + + + + + + + {btnDisabled && } + + ); })} - - + + - {sortedRoomList.map((room) => { - return ( - - {room} - - ); - })} + {sortedRoomList.map((room) => ( + + + + ))} - - - + } /> {/* dummy first row */} {Object.entries(selectedTableData).map(([time, roomData], i, a) => { const hasSession = Object.values(rooms).some((c) => c >= 1) || Object.values(roomData).some((room) => room !== undefined); if (!hasSession) { if (breakCount > 1) { breakCount--; - return ; + return ; } else { // 지금부터 다음 세션이 존재하기 전까지의 휴식 시간을 계산합니다. breakCount = 1; @@ -205,55 +221,59 @@ export const SessionTimeTable: React.FC = ErrorBoundary.with( // I really hate this, but I can't think of a better way to do this. const height = (TD_HEIGHT * breakCount) / (breakCount <= 2 ? 1 : 3); + const isLast = i === a.length - 1; + const duration = breakCount * 10; // 10 minutes per row return ( {time} - - - {i !== a.length - 1 && {"휴식"}} - + `1px solid ${t.palette.divider} !important`, + borderBottom: isLast ? "transparernt" : (t) => `1px solid ${t.palette.divider} !important`, + }} + > + + {!isLast && } + ); } } - // 만약 세션 타입이 아닌 발표가 존재하는 경우, 해당 줄에서는 colSpan이 roomCount인 column을 생성합니다. - const nonSessionTypeData = Object.values(roomData).find((room) => room !== undefined && !room.session.isSession); - if (nonSessionTypeData) { - Object.keys(rooms).forEach((room) => (rooms[room] = nonSessionTypeData.rowSpan - 1)); + // 만약 동일 세션이 모든 방에서 진행되는 경우, 해당 줄에서는 colSpan이 roomCount인 column을 생성합니다. + const sessionIds = new Set(Object.values(roomData).map((room) => room?.session.id)); + const firstSessionInfo = Object.values(roomData)[0]; + if (sessionIds.size === 1 && firstSessionInfo !== undefined) { + Object.keys(rooms).forEach((room) => (rooms[room] = firstSessionInfo.rowSpan - 1)); return ( - {time} - + + ); } return ( - {time} + {sortedRoomList.map((room) => { const roomDatum = roomData[room]; if (roomDatum === undefined) { // 진행 중인 세션이 없는 경우, 해당 줄에서는 해당 room의 빈 column을 생성합니다. - if (rooms[room] <= 0) return ; + if (rooms[room] <= 0) return ; // 진행 중인 세션이 있는 경우, 이번 줄에서는 해당 세션들만큼 column을 생성하지 않습니다. rooms[room] -= 1; return null; @@ -268,28 +288,11 @@ export const SessionTimeTable: React.FC = ErrorBoundary.with( - +
); }) ); -const WarningText = styled(Typography)({ - paddingLeft: "1rem", - backgroundColor: "unset", - textAlign: "right", - margin: 0, - padding: 0, - border: "unset", - fontSize: "1rem", - lineHeight: 2, - fontWeight: 300, -}); - -const SessionTimeTableItemTagContainer = styled(Stack)({ - alignItems: "center", - justifyContent: "center", -}); - const SessionDateItemContainer = styled(Stack)({ alignItems: "center", justifyContent: "center", @@ -314,14 +317,6 @@ const SessionDateSubTitle = styled(Typography)<{ isSelected: boolean }>(({ theme color: isSelected ? theme.palette.primary.main : theme.palette.primary.light, })); -const RestTitle = styled(Typography)({ - fontSize: "1em", - fontWeight: 500, - lineHeight: 1.25, - textDecoration: "none", - whiteSpace: "pre-wrap", -}); - const SessionTitle = styled(Typography)({ fontSize: "1.125em", fontWeight: 600, @@ -330,21 +325,6 @@ const SessionTitle = styled(Typography)({ whiteSpace: "pre-wrap", }); -const RoomTitle = styled(Typography)({ - fontSize: "1.25em", - fontWeight: 500, -}); - -const ColoredDivider = styled(StyledDivider)(({ theme }) => ({ - color: theme.palette.primary.main, -})); - -const SessionSpeakerItemContainer = styled(Stack)({ - display: "flex", - alignItems: "center", - justifyContent: "center", -}); - const SessionTable = styled(Table)({ width: "100%", maxWidth: "60rem", @@ -401,34 +381,15 @@ const SessionTableRow = styled(TableRow)({ }); const SessionTableCell = styled(TableCell)({ + padding: "0 0.5rem", alignItems: "center", justifyContent: "center", border: "unset", }); -const SessionDateTabContainer = styled(Box)({ - display: "flex", - gap: "2rem", - justifyContent: "center", - alignItems: "center", - button: { - backgroundColor: "unset", - border: "unset", - "&.selected": { - color: `rgba(255, 255, 255, 1)`, - }, - }, - "h1, h2, h3, h4, h5, h6": { - margin: 0, - color: "inherit", - }, -}); - -const SessionBox = styled(Box)(({ theme }) => ({ +const SessionBox = styled(Stack)(({ theme }) => ({ height: "100%", - // margin: "0.25rem", padding: "0.25rem", - display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", @@ -489,8 +450,7 @@ const SessionBox = styled(Box)(({ theme }) => ({ }, })); -const SessionTableContainer = styled(Box)({ - display: "flex", +const SessionTableContainer = styled(Stack)({ flexDirection: "column", alignItems: "center", justifyContent: "center", diff --git a/packages/common/src/schemas/backendAPI.ts b/packages/common/src/schemas/backendAPI.ts index 40510cee..57ce3908 100644 --- a/packages/common/src/schemas/backendAPI.ts +++ b/packages/common/src/schemas/backendAPI.ts @@ -82,7 +82,6 @@ namespace BackendAPISchemas { description: string; slideshow_url: string | null; image: string | null; - isSession: boolean; categories: { id: string; name: string; @@ -96,18 +95,9 @@ namespace BackendAPISchemas { room_schedules: { id: string; room_name: string; - event_id: number; - event_name: string; - start_at: Date; - end_at: Date; - }; - call_for_presentation_schedules: { - id: string; - presentation_type_name: string; - start_at: Date; - end_at: Date; - next_call_for_presentation_schedule: string; - }; + start_at: string; + end_at: string; + }[]; }; export const isObjectErrorResponseSchema = (obj?: unknown): obj is BackendAPISchemas.ErrorResponseSchema => { From 28643de509ade2613f8fd8960e00084d6a5a5544 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 28 Jul 2025 02:02:22 +0900 Subject: [PATCH 084/324] =?UTF-8?q?fix:=20=EB=B0=9C=ED=91=9C=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=ED=91=9C=EC=97=90=EC=84=9C=20=EB=B0=9C=ED=91=9C?= =?UTF-8?q?=EC=9E=90=20=EB=AA=A9=EB=A1=9D=EC=9D=B4=20=EB=84=98=EC=B9=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/src/components/mdx_components/session_timetable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/components/mdx_components/session_timetable.tsx b/packages/common/src/components/mdx_components/session_timetable.tsx index ff6d9bf4..a4fc2970 100644 --- a/packages/common/src/components/mdx_components/session_timetable.tsx +++ b/packages/common/src/components/mdx_components/session_timetable.tsx @@ -124,7 +124,7 @@ const SessionColumn: React.FC<{ sx={{ height: sessionBoxHeight, gap: 0.75, padding: "0.5rem" }} > - + {session.speakers.map((speaker) => ( ))} From d12f1885d70e7e52dade17ade3097555028bf8c7 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 28 Jul 2025 02:03:23 +0900 Subject: [PATCH 085/324] =?UTF-8?q?fix:=20=EC=A0=9C=EB=AA=A9=20overflow=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 --- .../common/src/components/mdx_components/session_timetable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/components/mdx_components/session_timetable.tsx b/packages/common/src/components/mdx_components/session_timetable.tsx index a4fc2970..5140bafc 100644 --- a/packages/common/src/components/mdx_components/session_timetable.tsx +++ b/packages/common/src/components/mdx_components/session_timetable.tsx @@ -11,7 +11,7 @@ import { CenteredPage } from "../centered_page"; import { ErrorFallback } from "../error_handler"; import { StyledDivider } from "./styled_divider"; -const TD_HEIGHT = 3.5; +const TD_HEIGHT = 4; const TD_WIDTH = 15; const TD_WIDTH_MOBILE = 20; From 7c3b7ac978f2e4590222017c3aa1cb244c49e74e Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Mon, 28 Jul 2025 02:04:16 +0900 Subject: [PATCH 086/324] =?UTF-8?q?fix:=20=EC=8B=A4=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=20=ED=8F=AC=ED=95=A8=EB=90=9C=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/components/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index 2fb6a6a4..6f6d732f 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -30,7 +30,6 @@ import { import { StyledFullWidthButton as StyledFullWidthButtonComponent } from "./mdx_components/styled_full_width_button"; import { MDXEditor as MDXEditorComponent } from "./mdx_editor"; import { PythonKorea as PythonKoreaComponent } from "./pythonkorea"; -import { ScrollRestoration as ScrollRestorationComponent } from "./scroll_restoration"; namespace Components { export const CenteredPage = CenteredPageComponent; @@ -47,7 +46,6 @@ namespace Components { export const LinkHandler = LinkHandlerComponent; export const DndFileInput = DndFileInputComponent; export const Fieldset = FieldsetComponent; - export const ScrollRestoration = ScrollRestorationComponent; export namespace MDX { export const Confetti = ConfettiComponent; From 035eaad11b7d867aae4ba3e185f7f6fbe3dda753 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Thu, 31 Jul 2025 22:39:25 +0900 Subject: [PATCH 087/324] =?UTF-8?q?feat:=20=EB=B0=9C=ED=91=9C=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=97=90=20=EC=8B=9C=EA=B0=84=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/pages/presentation_detail.tsx | 85 +++++++++++++++---- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/apps/pyconkr/src/components/pages/presentation_detail.tsx b/apps/pyconkr/src/components/pages/presentation_detail.tsx index 3e14cb5c..dd603e67 100644 --- a/apps/pyconkr/src/components/pages/presentation_detail.tsx +++ b/apps/pyconkr/src/components/pages/presentation_detail.tsx @@ -1,6 +1,7 @@ import * as Common from "@frontend/common"; -import { Box, Chip, CircularProgress, Divider, Stack, styled, Typography } from "@mui/material"; +import { Box, Chip, CircularProgress, Divider, Stack, styled, Table, TableBody, TableCell, TableRow, Typography } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; +import { DateTime } from "luxon"; import * as React from "react"; import { Navigate, useParams } from "react-router-dom"; @@ -148,6 +149,31 @@ export const PresentationDetailPage: React.FC = ErrorBoundary.with( const speakersStr = language === "ko" ? "발표자" : "Speakers"; // const slideShowStr = language === "ko" ? "발표 슬라이드" : "Presentation Slideshow"; + const datetimeLabel = language === "ko" ? "발표 시각" : "Presentation Time"; + const datetimeSeparator = language === "ko" ? " ~ " : " - "; + const minText = language === "ko" ? "분" : "min."; + + // 동일 시간별로 모아서 보여줌. 단, 방은 콤마(,)로 join해서 보여줌 + const scheduleMap: Record = presentation.room_schedules.reduce( + (acc, schedule) => { + const startAt = DateTime.fromISO(schedule.start_at).setLocale(language); + const endAt = DateTime.fromISO(schedule.end_at).setLocale(language); + if (!startAt.isValid || !endAt.isValid) return acc; // 유효하지 않은 날짜는 무시 + + const duration = Number.parseInt(endAt.diff(startAt, ["minutes"]).minutes.toString()); + const startAtFormatted = startAt.toLocaleString(DateTime.DATETIME_MED); + // 동일 일자인 경우, 시간만 표시 + const endAtFormatted = endAt.toLocaleString(startAt.hasSame(endAt, "day") ? DateTime.TIME_SIMPLE : DateTime.DATETIME_MED); + + const key = `${startAtFormatted} ${datetimeSeparator} ${endAtFormatted} (${duration}${minText})`; + const roomText = schedule.room_name.replace("\\n", "\n"); + if (!acc[key]) acc[key] = [roomText]; + else acc[key].push(roomText); + return acc; + }, + {} as Record + ); + React.useEffect(() => { setAppContext((prev) => ({ ...prev, @@ -159,7 +185,11 @@ export const PresentationDetailPage: React.FC = ErrorBoundary.with( return ( - + {presentation.summary && ( )} - - {presentation.categories.length ? ( - <> - - - - {presentation.categories.map((c) => ( - - ))} - - - - - ) : null} + + + {presentation.room_schedules.length + ? Object.entries(scheduleMap).map(([datetime, rooms], index) => ( + + {index === 0 && ( + + + + )} + + + + + {rooms.map((room, index) => ( + + ))} + + + + + )) + : null} + {presentation.categories.length ? ( + + } /> + + + {presentation.categories.map((c) => ( + + ))} + + + + ) : null} + +
{/* {presentation.slideshow_url && ( <> From d272c9932a2028b54281f2a53ce4b8aac4842388 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 1 Aug 2025 08:03:31 +0900 Subject: [PATCH 088/324] =?UTF-8?q?fix:=20=EB=88=84=EB=9D=BD=EB=90=9C=20li?= =?UTF-8?q?ne-break=20handling=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/src/components/mdx_components/session_timetable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/components/mdx_components/session_timetable.tsx b/packages/common/src/components/mdx_components/session_timetable.tsx index 5140bafc..1023c55a 100644 --- a/packages/common/src/components/mdx_components/session_timetable.tsx +++ b/packages/common/src/components/mdx_components/session_timetable.tsx @@ -123,7 +123,7 @@ const SessionColumn: React.FC<{ className={clickable ? "clickable" : ""} sx={{ height: sessionBoxHeight, gap: 0.75, padding: "0.5rem" }} > - + {session.speakers.map((speaker) => ( From 8fdb4f481c876762ccf05566f04e97b9b1517e57 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Fri, 1 Aug 2025 10:15:00 +0900 Subject: [PATCH 089/324] =?UTF-8?q?fix:=20=ED=82=A4=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B0=9C=ED=91=9C=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B5=A4=20=ED=91=9C=EA=B0=80=20=EA=B9=A8=EC=A7=80?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/components/pages/presentation_detail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/pyconkr/src/components/pages/presentation_detail.tsx b/apps/pyconkr/src/components/pages/presentation_detail.tsx index dd603e67..0e878992 100644 --- a/apps/pyconkr/src/components/pages/presentation_detail.tsx +++ b/apps/pyconkr/src/components/pages/presentation_detail.tsx @@ -203,7 +203,7 @@ export const PresentationDetailPage: React.FC = ErrorBoundary.with( ? Object.entries(scheduleMap).map(([datetime, rooms], index) => ( {index === 0 && ( - + )} From d400f5e564eeaa2e12163411def3811e3beeef03 Mon Sep 17 00:00:00 2001 From: y00eunji Date: Mon, 4 Aug 2025 20:54:18 +0900 Subject: [PATCH 090/324] =?UTF-8?q?feat=20:=20children=EC=9D=B4=20?= =?UTF-8?q?=ED=95=9C=20=EA=B0=9C=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=EC=A4=91?= =?UTF-8?q?=20hide=EA=B0=80=20true=EB=A9=B4=20=ED=99=94=EC=82=B4=ED=91=9C?= =?UTF-8?q?=20=EC=95=88=EC=83=9D=EA=B8=B0=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/Header/Mobile/MobileNavigation.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx index 860f4742..e1e307c2 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx @@ -90,7 +90,7 @@ export const MobileNavigation: React.FC = ({ isOpen, onCl {menu.name} - {!R.isEmpty(menu.children) && ( + {!R.isEmpty(menu.children) && Object.values(menu.children).some((child) => !child.hide) && ( navigateToDepth2(menu)}> @@ -123,7 +123,7 @@ export const MobileNavigation: React.FC = ({ isOpen, onCl - {!R.isEmpty(menu.children) && ( + {!R.isEmpty(menu.children) && Object.values(menu.children).some((child) => !child.hide) && ( navigateToDepth3(menu)}> From d267f090df51903b9c396d0db886879881c30ddd Mon Sep 17 00:00:00 2001 From: y00eunji Date: Mon, 4 Aug 2025 20:56:53 +0900 Subject: [PATCH 091/324] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=9E=90=EC=8B=9D=20=EB=A9=94=EB=89=B4=EA=B0=80=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20=EB=B2=84=ED=8A=BC=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/Header/Mobile/MobileNavigation.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx index e1e307c2..2729ed26 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx @@ -87,9 +87,15 @@ export const MobileNavigation: React.FC = ({ isOpen, onCl .filter((s) => !s.hide) .map((menu) => ( - - {menu.name} - + {!R.isEmpty(menu.children) && Object.values(menu.children).some((child) => !child.hide) ? ( + navigateToDepth2(menu)}> + {menu.name} + + ) : ( + + {menu.name} + + )} {!R.isEmpty(menu.children) && Object.values(menu.children).some((child) => !child.hide) && ( navigateToDepth2(menu)}> @@ -250,6 +256,17 @@ const MenuLink = styled(Link)<{ isMainPath?: boolean }>(({ theme, isMainPath = t fontWeight: 600, })); +const MenuButton = styled(Button)<{ isMainPath?: boolean }>(({ theme, isMainPath = true }) => ({ + color: isMainPath ? theme.palette.mobileNavigation.main.text : theme.palette.mobileNavigation.sub.text, + textTransform: "none", + fontSize: "20px", + fontWeight: 600, + padding: 0, + minWidth: "auto", + minHeight: "auto", + justifyContent: "flex-start", +})); + const MenuArrowButton = styled(IconButton)<{ isMainPath?: boolean }>(({ theme, isMainPath = true }) => ({ color: isMainPath ? theme.palette.mobileNavigation.main.text : theme.palette.mobileNavigation.sub.text, padding: 8, From 81923d5b08a9fd6f697ccf67ff445a51894134a6 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Wed, 6 Aug 2025 01:04:50 +0900 Subject: [PATCH 092/324] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20F?= =?UTF-8?q?ooter=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/Footer/Mobile/MobileFooter.tsx | 221 ++++++++++++++++++ .../src/components/layout/Footer/index.tsx | 111 +++++---- 2 files changed, 282 insertions(+), 50 deletions(-) create mode 100644 apps/pyconkr/src/components/layout/Footer/Mobile/MobileFooter.tsx diff --git a/apps/pyconkr/src/components/layout/Footer/Mobile/MobileFooter.tsx b/apps/pyconkr/src/components/layout/Footer/Mobile/MobileFooter.tsx new file mode 100644 index 00000000..f8d9f87d --- /dev/null +++ b/apps/pyconkr/src/components/layout/Footer/Mobile/MobileFooter.tsx @@ -0,0 +1,221 @@ +import styled from "@emotion/styled"; +import * as Common from "@frontend/common"; +import { Article, Email, Facebook, GitHub, Instagram, LinkedIn, X, YouTube } from "@mui/icons-material"; +import * as React from "react"; + +import FlickrIcon from "@apps/pyconkr/assets/thirdparty/flickr.svg?react"; + +import { useAppContext } from "../../../../contexts/app_context"; + +interface IconItem { + icon: React.FC<{ width?: number; height?: number }>; + alt: string; + href: string; +} + +const defaultIcons: IconItem[] = [ + { + icon: Facebook, + alt: "facebook", + href: "https://www.facebook.com/pyconkorea/", + }, + { + icon: YouTube, + alt: "YouTube", + href: "https://www.youtube.com/c/PyConKRtube", + }, + { icon: X, alt: "X", href: "https://x.com/PyConKR" }, + { icon: GitHub, alt: "github", href: "https://github.com/pythonkr" }, + { + icon: Instagram, + alt: "Instagram", + href: "https://www.instagram.com/pycon_korea/", + }, + { + icon: LinkedIn, + alt: "LinkedIn", + href: "https://www.linkedin.com/company/pyconkorea/", + }, + { icon: Article, alt: "blog", href: "https://blog.pycon.kr/" }, + { + icon: FlickrIcon, + alt: "Flickr", + href: "https://www.flickr.com/photos/126829363@N08/", + }, +]; + +const Bar: React.FC = () =>
|
; + +export default function MobileFooter() { + const { sendEmail } = Common.Hooks.Common.useEmail(); + const { language } = useAppContext(); + + const title = language === "ko" ? "Weave with Python, 파이콘 한국 2025" : "Weave with Python, Pycon KR 2025"; + const committeeTitle = + language === "ko" + ? "파이콘 한국 2025는 파이콘 한국 준비위원회가 만들고 있습니다" + : "PyCon Korea 2025 is organized by the PyCon Korea Organizing Committee"; + const djangoTitle = language === "ko" ? "파이썬 웹 프레임워크 Django로 만들었습니다" : "Built with the Django web framework for Python"; + + const links = [ + { + text: language === "ko" ? "파이콘 한국 행동 강령(CoC)" : "PyCon Korea Code of Conduct", + href: "https://pythonkr.github.io/pycon-code-of-conduct/ko/coc/a_intent_and_purpose.html", + }, + { + text: language === "ko" ? "서비스 이용 약관" : "Terms of Service", + href: "/about/terms-of-service", + }, + { + text: language === "ko" ? "개인 정보 처리 방침" : "Privacy Policy", + href: "/about/privacy-policy", + }, + ]; + + return ( + + + +
+ +
+ +
+ +
+
+ + {links.map((link, index) => ( + + + {link.text} + + {index < links.length - 1 && |} + + ))} + + + + + {defaultIcons.map((icon) => ( + + + ))} + +
+
+ ); +} + +const FooterContainer = styled.footer` + background: linear-gradient(to bottom, #ffffff 0%, #e4fdff 25%, #92c9cc 50%, #5cadb3 75%, #095a5f 100%); + color: ${({ theme }) => theme.palette.common.white}; + font-size: 0.75rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + max-height: 16rem; + padding: 5rem 0 1rem 0; +`; + +const FooterContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; +`; + +const FooterText = styled.div` + padding: 0 2rem; + margin: 0.1rem; + + font-size: 9pt; + + a > button { + margin-left: 0.25rem; + padding: 0.05rem 0.25rem; + font-size: 8pt; + color: ${({ theme }) => theme.palette.common.white}; + border-color: ${({ theme }) => theme.palette.common.white}; + + gap: 0.25rem; + + & span { + margin-left: -2px; + margin-right: 0; + + & svg { + font-size: 12pt !important; + } + } + } + + strong { + font-size: 12pt; + } +`; + +const FooterBoldText = styled.text` + font-weight: 600; +`; + +const FooterNormalText = styled.text` + font-weight: 400; +`; + +const FooterSlogan = styled.div` + text-align: center; +`; + +const FooterLinkSlogan = styled.div` + display: flex; + gap: 0.3rem; +`; + +const FooterLinks = styled.div` + display: flex; + align-items: center; + gap: 0.3rem; +`; + +const FooterIcons = styled.div` + display: flex; + align-items: center; + gap: 9px; +`; + +const Link = styled.a` + color: ${({ theme }) => theme.palette.common.white}; + text-decoration: none; + &:hover { + text-decoration: underline; + } +`; + +const Separator = styled.span` + color: ${({ theme }) => theme.palette.common.white}; + opacity: 0.5; + margin: 0.05rem 0; +`; + +const IconLink = styled.a` + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; + + &:hover { + opacity: 0.8; + } + + img { + width: 20px; + height: 20px; + } +`; diff --git a/apps/pyconkr/src/components/layout/Footer/index.tsx b/apps/pyconkr/src/components/layout/Footer/index.tsx index 3f940f42..4c59f084 100644 --- a/apps/pyconkr/src/components/layout/Footer/index.tsx +++ b/apps/pyconkr/src/components/layout/Footer/index.tsx @@ -1,12 +1,13 @@ import styled from "@emotion/styled"; import * as Common from "@frontend/common"; import { Article, Email, Facebook, GitHub, Instagram, LinkedIn, OpenInNew, X, YouTube } from "@mui/icons-material"; -import { Button } from "@mui/material"; +import { Button, useMediaQuery, useTheme } from "@mui/material"; import * as React from "react"; import FlickrIcon from "@apps/pyconkr/assets/thirdparty/flickr.svg?react"; import { useAppContext } from "../../../contexts/app_context"; +import MobileFooter from "./Mobile/MobileFooter"; interface IconItem { icon: React.FC<{ width?: number; height?: number }>; @@ -49,6 +50,9 @@ const Bar: React.FC = () =>
- - - {corpPasamoStr} -
- {corpAddressStr} - - {corpRepresentatorStr} - - {corpPhoneStr} - - {corpCompanyNumberStr} -
- - -
- {corpMailOrderSalesRegistrationNumberStr} - - {hostingProviderStr} - - {contractEmailStr} - pyconkr@pycon.kr - - - {links.map((link, index) => ( - - - {link.text} - - {index < links.length - 1 && |} - - ))} - - - - - {defaultIcons.map((icon) => ( - - + {copyrightStr} + + + ); + } } const FooterContainer = styled.footer` @@ -195,6 +205,7 @@ const FooterLinks = styled.div` align-items: center; gap: 0.625rem; `; + const FooterIcons = styled.div` display: flex; align-items: center; From 9ca41633474d22de98d8ec26ae2c540314f7549a Mon Sep 17 00:00:00 2001 From: earthyoung Date: Wed, 6 Aug 2025 12:53:21 +0900 Subject: [PATCH 093/324] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20=EC=95=84=EC=BD=94=EB=94=94=EC=96=B8=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layout/Footer/index.tsx | 5 +- .../src/assets/pyconkr2025_hostlogo_big.png | Bin 0 -> 18184 bytes .../src/assets/pyconkr2025_hostlogo_small.png | Bin 0 -> 4255 bytes .../mdx_components/mobile_accordion.tsx | 113 ++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 packages/common/src/assets/pyconkr2025_hostlogo_big.png create mode 100644 packages/common/src/assets/pyconkr2025_hostlogo_small.png create mode 100644 packages/common/src/components/mdx_components/mobile_accordion.tsx diff --git a/apps/pyconkr/src/components/layout/Footer/index.tsx b/apps/pyconkr/src/components/layout/Footer/index.tsx index 4c59f084..991c3d38 100644 --- a/apps/pyconkr/src/components/layout/Footer/index.tsx +++ b/apps/pyconkr/src/components/layout/Footer/index.tsx @@ -5,9 +5,9 @@ import { Button, useMediaQuery, useTheme } from "@mui/material"; import * as React from "react"; import FlickrIcon from "@apps/pyconkr/assets/thirdparty/flickr.svg?react"; +import { MobilePageAccordion } from "../../../../../../packages/common/src/components/mdx_components/mobile_accordion"; import { useAppContext } from "../../../contexts/app_context"; -import MobileFooter from "./Mobile/MobileFooter"; interface IconItem { icon: React.FC<{ width?: number; height?: number }>; @@ -90,7 +90,8 @@ export default function Footer() { console.log("isMobile " + isMobile); if (isMobile) { - return ; + return ; + // return ; } else { return ( diff --git a/packages/common/src/assets/pyconkr2025_hostlogo_big.png b/packages/common/src/assets/pyconkr2025_hostlogo_big.png new file mode 100644 index 0000000000000000000000000000000000000000..b010685a01440b7435d3829a7852cc44f84f9429 GIT binary patch literal 18184 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 BF=GG# literal 0 HcmV?d00001 diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/packages/common/src/components/mdx_components/mobile_accordion.tsx new file mode 100644 index 00000000..6c102d0f --- /dev/null +++ b/packages/common/src/components/mdx_components/mobile_accordion.tsx @@ -0,0 +1,113 @@ +import styled from "@emotion/styled"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { AccordionDetails, AccordionSummary, Accordion as MuiAccordion, Stack, Typography } from "@mui/material"; +import * as React from "react"; +import PyCon2025HostLogoBig from "../../assets/pyconkr2025_hostlogo_big.png"; +import PyCon2025HostLogoSmall from "../../assets/pyconkr2025_hostlogo_small.png"; + +const AccordionExpandedStyle: React.CSSProperties = { + backgroundColor: "white", + overflow: "hidden", +}; + +export const MobilePageAccordion: React.FC = () => { + const [expanded, setExpanded] = React.useState(false); + + return ( + + setExpanded((prev) => !prev)}> + }> + {expanded ? null : ( + + {"AUG 15 - 17"} + + + )} + + + {expanded ? ( + + PyCon 2025 Host Logo + + ) : null} + + + + ); +}; + +const AccordionWrapper = styled.div` + display: flex; + flex-direction: column; + border-top: 1px solid ${({ theme }) => theme.palette.primary.dark}; + border-bottom: 1px solid ${({ theme }) => theme.palette.primary.dark}; +`; + +const Divider = styled.div` + height: 1px; + background-color: ${({ theme }) => theme.palette.primary.light}; + margin: 0; +`; + +const StyledAccordion = styled(MuiAccordion)` + box-shadow: none; + border-radius: 0; + + &:before { + display: none; + } + + &.MuiAccordion-root { + margin: 0; + + &:first-of-type { + border-top: none; + } + + &:last-of-type { + border-bottom: none; + } + } + + .MuiAccordionSummary-root { + padding: 10px 35px; + min-height: 60px; + max-height: 60px; + + .MuiAccordionSummary-content { + display: flex; + align-items: center; + margin: 0; + } + + &.Mui-expanded { + min-height: 60px; + max-height: 60px; + } + } +`; + +const Number = styled.span` + font-size: 18px; + font-weight: 400; +`; + +const Question = styled.span` + font-size: 18px; + font-weight: 400; + margin-left: 60px; +`; + +const StyledAccordionDetails = styled(AccordionDetails)` + background-color: white; + color: ${({ theme }) => theme.palette.primary.dark}; + font-size: 14px; + font-weight: 400; + width: 100%; + height: 100%; + padding: 20px 0 20px calc(35px + 18px + 60px); // top right bottom left +`; From 35a13348f8c36d9387ca90b777eea736236b4eaa Mon Sep 17 00:00:00 2001 From: earthyoung Date: Wed, 6 Aug 2025 19:31:06 +0900 Subject: [PATCH 094/324] =?UTF-8?q?feat:=20accordion=20=EC=9B=80=EC=A7=81?= =?UTF-8?q?=EC=9E=84=20marquee=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + .../mdx_components/mobile_accordion.tsx | 41 +++++++++++-------- pnpm-lock.yaml | 14 +++++++ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 1e84e4f7..854ab543 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "react": "^19.1.0", "react-confetti": "^6.4.0", "react-dom": "^19.1.0", + "react-fast-marquee": "^1.6.5", "react-hook-form": "^7.58.0", "react-lottie": "^1.2.10", "react-router-dom": "^7.6.0", diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/packages/common/src/components/mdx_components/mobile_accordion.tsx index 6c102d0f..dc3bbae7 100644 --- a/packages/common/src/components/mdx_components/mobile_accordion.tsx +++ b/packages/common/src/components/mdx_components/mobile_accordion.tsx @@ -2,6 +2,7 @@ import styled from "@emotion/styled"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { AccordionDetails, AccordionSummary, Accordion as MuiAccordion, Stack, Typography } from "@mui/material"; import * as React from "react"; +import Marquee from "react-fast-marquee"; import PyCon2025HostLogoBig from "../../assets/pyconkr2025_hostlogo_big.png"; import PyCon2025HostLogoSmall from "../../assets/pyconkr2025_hostlogo_small.png"; @@ -10,19 +11,31 @@ const AccordionExpandedStyle: React.CSSProperties = { overflow: "hidden", }; +const MarqueeAccordion: React.FC = () => { + return ( + + + + {"AUG 15 - 17"} + + + + {"AUG 15 - 17"} + + + + + ); +}; + export const MobilePageAccordion: React.FC = () => { const [expanded, setExpanded] = React.useState(false); return ( setExpanded((prev) => !prev)}> - }> - {expanded ? null : ( - - {"AUG 15 - 17"} - - - )} + }> + {expanded ? null : } {expanded ? ( @@ -91,15 +104,11 @@ const StyledAccordion = styled(MuiAccordion)` } `; -const Number = styled.span` - font-size: 18px; - font-weight: 400; -`; - -const Question = styled.span` - font-size: 18px; - font-weight: 400; - margin-left: 60px; +const StyledTypography = styled(Typography)` + font-weight: 600; + font-size: 1rem; + color: #938a85; + text-align: center; `; const StyledAccordionDetails = styled(AccordionDetails)` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0d89904..81b455ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + react-fast-marquee: + specifier: ^1.6.5 + version: 1.6.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-hook-form: specifier: ^7.58.0 version: 7.58.0(react@19.1.0) @@ -3253,6 +3256,12 @@ packages: peerDependencies: react: ^19.1.0 + react-fast-marquee@1.6.5: + resolution: {integrity: sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg==} + peerDependencies: + react: '>= 16.8.0 || ^18.0.0' + react-dom: '>= 16.8.0 || ^18.0.0' + react-hook-form@7.58.0: resolution: {integrity: sha512-zGijmEed35oNfOfy7ub99jfjkiLhHwA3dl5AgyKdWC6QQzhnc7tkWewSa+T+A2EpLrc6wo5DUoZctS9kufWJjA==} engines: {node: '>=18.0.0'} @@ -7383,6 +7392,11 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-fast-marquee@1.6.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-hook-form@7.58.0(react@19.1.0): dependencies: react: 19.1.0 From 015b3e28ad10619ea8b3a24e32bdd9585baadc51 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Thu, 7 Aug 2025 01:45:26 +0900 Subject: [PATCH 095/324] =?UTF-8?q?feat:=20mobile=20accordion=20=EC=A4=91?= =?UTF-8?q?=EA=B0=84=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mdx_components/mobile_accordion.tsx | 113 ++++++++++++------ 1 file changed, 79 insertions(+), 34 deletions(-) diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/packages/common/src/components/mdx_components/mobile_accordion.tsx index dc3bbae7..631a3b4f 100644 --- a/packages/common/src/components/mdx_components/mobile_accordion.tsx +++ b/packages/common/src/components/mdx_components/mobile_accordion.tsx @@ -3,48 +3,85 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { AccordionDetails, AccordionSummary, Accordion as MuiAccordion, Stack, Typography } from "@mui/material"; import * as React from "react"; import Marquee from "react-fast-marquee"; +import { useAppContext } from "../../../../../apps/pyconkr/src/contexts/app_context"; import PyCon2025HostLogoBig from "../../assets/pyconkr2025_hostlogo_big.png"; import PyCon2025HostLogoSmall from "../../assets/pyconkr2025_hostlogo_small.png"; -const AccordionExpandedStyle: React.CSSProperties = { - backgroundColor: "white", - overflow: "hidden", -}; - const MarqueeAccordion: React.FC = () => { - return ( - - - - {"AUG 15 - 17"} - - - + const [marqueeKey, setMarqueeKey] = React.useState(0); + + const items = React.useMemo(() => { + return Array.from({ length: 100 }, (_, i) => { + return ( + {"AUG 15 - 17"} - + ); + }); + }, []); + + const onMarqueeCycleComplete = () => { + if (marqueeKey === 0) { + setMarqueeKey(1); + } else { + setMarqueeKey(0); + } + }; + + return ( + + {items} ); }; export const MobilePageAccordion: React.FC = () => { + const { language } = useAppContext(); const [expanded, setExpanded] = React.useState(false); + const venue = language === "ko" ? "서울특별시 중구 필동로 1길 30 동국대학교 신공학관" : "New Engineering Building, Dongguk University"; + + const a = "Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"; return ( setExpanded((prev) => !prev)}> - }> - {expanded ? null : } - + {!expanded && ( + + } + > + {expanded ? null : } + + )} {expanded ? ( - - PyCon 2025 Host Logo + + PyCon 2025 Host Logo + {language === "ko" ? ( + + + {"서울특별시 중구 필동로 1길 30 동국대학교 신공학관"} + + + ) : ( + + + {"New Engineering Building, Dongguk University"} + + + {"Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"} + + + )} ) : null} @@ -56,14 +93,7 @@ export const MobilePageAccordion: React.FC = () => { const AccordionWrapper = styled.div` display: flex; flex-direction: column; - border-top: 1px solid ${({ theme }) => theme.palette.primary.dark}; - border-bottom: 1px solid ${({ theme }) => theme.palette.primary.dark}; -`; - -const Divider = styled.div` - height: 1px; - background-color: ${({ theme }) => theme.palette.primary.light}; - margin: 0; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); `; const StyledAccordion = styled(MuiAccordion)` @@ -100,8 +130,22 @@ const StyledAccordion = styled(MuiAccordion)` &.Mui-expanded { min-height: 60px; max-height: 60px; + object-fit: contain; } } + + '& .MuiAccordionSummary-expandIconWrapper': { + position: 'absolute', + left: 8, + top: '50%', + transform: 'translateY(-50%)' + }, + + width: 100%; + max-width: 100vw; + box-sizing: border-box; + overflow: hidden; + position: relative; `; const StyledTypography = styled(Typography)` @@ -109,14 +153,15 @@ const StyledTypography = styled(Typography)` font-size: 1rem; color: #938a85; text-align: center; + padding: 0 10px; `; const StyledAccordionDetails = styled(AccordionDetails)` - background-color: white; - color: ${({ theme }) => theme.palette.primary.dark}; + border-radius: 16px; font-size: 14px; font-weight: 400; width: 100%; height: 100%; - padding: 20px 0 20px calc(35px + 18px + 60px); // top right bottom left + margin: 0; + padding: 20; `; From 6bb3fc6673e8ae60f8f51fc3f99c1dfaa57e90f6 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Thu, 7 Aug 2025 12:54:51 +0900 Subject: [PATCH 096/324] =?UTF-8?q?feat:=20mobile=20accordion=20=EC=A4=91?= =?UTF-8?q?=EA=B0=84=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mdx_components/mobile_accordion.tsx | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/packages/common/src/components/mdx_components/mobile_accordion.tsx index 631a3b4f..d0c9cbed 100644 --- a/packages/common/src/components/mdx_components/mobile_accordion.tsx +++ b/packages/common/src/components/mdx_components/mobile_accordion.tsx @@ -64,8 +64,10 @@ export const MobilePageAccordion: React.FC = () => { )} {expanded ? ( - - PyCon 2025 Host Logo + <> + + PyCon 2025 Host Logo + {language === "ko" ? ( @@ -73,16 +75,16 @@ export const MobilePageAccordion: React.FC = () => { ) : ( - - + + {"New Engineering Building, Dongguk University"} - + {"Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"} )} - + ) : null} @@ -94,11 +96,13 @@ const AccordionWrapper = styled.div` display: flex; flex-direction: column; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); + margin: 0; + padding: 0; `; const StyledAccordion = styled(MuiAccordion)` box-shadow: none; - border-radius: 0; + border-radius: 16px; &:before { display: none; @@ -144,16 +148,17 @@ const StyledAccordion = styled(MuiAccordion)` width: 100%; max-width: 100vw; box-sizing: border-box; - overflow: hidden; position: relative; + margin: 0; + padding: 0; `; const StyledTypography = styled(Typography)` - font-weight: 600; - font-size: 1rem; + font-weight: 500; + font-size: 20px; color: #938a85; text-align: center; - padding: 0 10px; + padding: 0 20px; `; const StyledAccordionDetails = styled(AccordionDetails)` @@ -163,5 +168,5 @@ const StyledAccordionDetails = styled(AccordionDetails)` width: 100%; height: 100%; margin: 0; - padding: 20; + padding: 0; `; From b82efca865614585e901a03c3e13daa68835556f Mon Sep 17 00:00:00 2001 From: earthyoung Date: Fri, 8 Aug 2025 00:18:34 +0900 Subject: [PATCH 097/324] =?UTF-8?q?feat:=20mobile=20accordion=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mdx_components/mobile_accordion.tsx | 130 +++++++++--------- 1 file changed, 62 insertions(+), 68 deletions(-) diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/packages/common/src/components/mdx_components/mobile_accordion.tsx index d0c9cbed..255f1c13 100644 --- a/packages/common/src/components/mdx_components/mobile_accordion.tsx +++ b/packages/common/src/components/mdx_components/mobile_accordion.tsx @@ -8,8 +8,8 @@ import PyCon2025HostLogoBig from "../../assets/pyconkr2025_hostlogo_big.png"; import PyCon2025HostLogoSmall from "../../assets/pyconkr2025_hostlogo_small.png"; const MarqueeAccordion: React.FC = () => { - const [marqueeKey, setMarqueeKey] = React.useState(0); - + const marqueeWidth = window.innerWidth * 0.9; + const marqueeGradientWidth = window.innerWidth * 0.1; const items = React.useMemo(() => { return Array.from({ length: 100 }, (_, i) => { return ( @@ -21,71 +21,60 @@ const MarqueeAccordion: React.FC = () => { }); }, []); - const onMarqueeCycleComplete = () => { - if (marqueeKey === 0) { - setMarqueeKey(1); - } else { - setMarqueeKey(0); - } - }; - return ( - + // + {items} + // ); }; -export const MobilePageAccordion: React.FC = () => { +export const MobileAccordion: React.FC = () => { const { language } = useAppContext(); const [expanded, setExpanded] = React.useState(false); - const venue = language === "ko" ? "서울특별시 중구 필동로 1길 30 동국대학교 신공학관" : "New Engineering Building, Dongguk University"; - - const a = "Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"; return ( - setExpanded((prev) => !prev)}> - {!expanded && ( - - } - > - {expanded ? null : } - - )} + setExpanded((prev) => !prev)}> + + } + sx={{ margin: 0, padding: 0 }} + > + {expanded ? null : } + - {expanded ? ( - <> - - PyCon 2025 Host Logo + + + PyCon 2025 Host Logo + + {language === "ko" ? ( + + + {"서울특별시 중구 필동로 1길 30 동국대학교 신공학관"} + + + ) : ( + + + {"New Engineering Building, Dongguk University"} + + + {"Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"} + - {language === "ko" ? ( - - - {"서울특별시 중구 필동로 1길 30 동국대학교 신공학관"} - - - ) : ( - - - {"New Engineering Building, Dongguk University"} - - - {"Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"} - - - )} - - ) : null} + )} + @@ -95,14 +84,17 @@ export const MobilePageAccordion: React.FC = () => { const AccordionWrapper = styled.div` display: flex; flex-direction: column; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.1), + 0 -4px 16px rgba(0, 0, 0, 0.1); margin: 0; padding: 0; + border-radius: 16; `; const StyledAccordion = styled(MuiAccordion)` box-shadow: none; - border-radius: 16px; + border-radius: 16; &:before { display: none; @@ -121,7 +113,7 @@ const StyledAccordion = styled(MuiAccordion)` } .MuiAccordionSummary-root { - padding: 10px 35px; + padding: 10px 0px 10px 0px; min-height: 60px; max-height: 60px; @@ -132,25 +124,28 @@ const StyledAccordion = styled(MuiAccordion)` } &.Mui-expanded { - min-height: 60px; - max-height: 60px; + min-height: 0px; + max-height: 0px; object-fit: contain; } } - '& .MuiAccordionSummary-expandIconWrapper': { - position: 'absolute', - left: 8, - top: '50%', - transform: 'translateY(-50%)' - }, + "& .muiaccordionsummary-expandiconwrapper": { + position: 'absolute', + top: 10; + } + + , + .MuiAccordionDetails-root { + margin: 6px 0px 24px 0px; + } width: 100%; max-width: 100vw; box-sizing: border-box; position: relative; - margin: 0; - padding: 0; + marginleft: 0; + paddingleft: 0; `; const StyledTypography = styled(Typography)` @@ -162,7 +157,6 @@ const StyledTypography = styled(Typography)` `; const StyledAccordionDetails = styled(AccordionDetails)` - border-radius: 16px; font-size: 14px; font-weight: 400; width: 100%; From cb7bdcf798cebbc807c921bad93594718d3e2762 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 9 Aug 2025 15:06:20 +0900 Subject: [PATCH 098/324] =?UTF-8?q?fix:=20main=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EB=B9=8C=EB=93=9C=20=EC=8B=A4=ED=8C=A8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layout/Footer/Mobile/MobileFooter.tsx | 32 ------------------- .../src/components/layout/Footer/index.tsx | 7 ++-- .../mdx_components/mobile_accordion.tsx | 23 +++++-------- 3 files changed, 10 insertions(+), 52 deletions(-) diff --git a/apps/pyconkr/src/components/layout/Footer/Mobile/MobileFooter.tsx b/apps/pyconkr/src/components/layout/Footer/Mobile/MobileFooter.tsx index f8d9f87d..957f7aa2 100644 --- a/apps/pyconkr/src/components/layout/Footer/Mobile/MobileFooter.tsx +++ b/apps/pyconkr/src/components/layout/Footer/Mobile/MobileFooter.tsx @@ -44,8 +44,6 @@ const defaultIcons: IconItem[] = [ }, ]; -const Bar: React.FC = () =>
|
; - export default function MobileFooter() { const { sendEmail } = Common.Hooks.Common.useEmail(); const { language } = useAppContext(); @@ -130,36 +128,6 @@ const FooterContent = styled.div` gap: 0.75rem; `; -const FooterText = styled.div` - padding: 0 2rem; - margin: 0.1rem; - - font-size: 9pt; - - a > button { - margin-left: 0.25rem; - padding: 0.05rem 0.25rem; - font-size: 8pt; - color: ${({ theme }) => theme.palette.common.white}; - border-color: ${({ theme }) => theme.palette.common.white}; - - gap: 0.25rem; - - & span { - margin-left: -2px; - margin-right: 0; - - & svg { - font-size: 12pt !important; - } - } - } - - strong { - font-size: 12pt; - } -`; - const FooterBoldText = styled.text` font-weight: 600; `; diff --git a/apps/pyconkr/src/components/layout/Footer/index.tsx b/apps/pyconkr/src/components/layout/Footer/index.tsx index 991c3d38..583fff71 100644 --- a/apps/pyconkr/src/components/layout/Footer/index.tsx +++ b/apps/pyconkr/src/components/layout/Footer/index.tsx @@ -5,8 +5,8 @@ import { Button, useMediaQuery, useTheme } from "@mui/material"; import * as React from "react"; import FlickrIcon from "@apps/pyconkr/assets/thirdparty/flickr.svg?react"; -import { MobilePageAccordion } from "../../../../../../packages/common/src/components/mdx_components/mobile_accordion"; +import MobileFooter from "./Mobile/MobileFooter"; import { useAppContext } from "../../../contexts/app_context"; interface IconItem { @@ -87,11 +87,8 @@ export default function Footer() { }, ]; - console.log("isMobile " + isMobile); - if (isMobile) { - return ; - // return ; + return ; } else { return ( diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/packages/common/src/components/mdx_components/mobile_accordion.tsx index 255f1c13..151023b2 100644 --- a/packages/common/src/components/mdx_components/mobile_accordion.tsx +++ b/packages/common/src/components/mdx_components/mobile_accordion.tsx @@ -3,6 +3,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { AccordionDetails, AccordionSummary, Accordion as MuiAccordion, Stack, Typography } from "@mui/material"; import * as React from "react"; import Marquee from "react-fast-marquee"; + import { useAppContext } from "../../../../../apps/pyconkr/src/contexts/app_context"; import PyCon2025HostLogoBig from "../../assets/pyconkr2025_hostlogo_big.png"; import PyCon2025HostLogoSmall from "../../assets/pyconkr2025_hostlogo_small.png"; @@ -11,23 +12,15 @@ const MarqueeAccordion: React.FC = () => { const marqueeWidth = window.innerWidth * 0.9; const marqueeGradientWidth = window.innerWidth * 0.1; const items = React.useMemo(() => { - return Array.from({ length: 100 }, (_, i) => { - return ( - - {"AUG 15 - 17"} - - - ); - }); + return Array.from({ length: 100 }, () => ( + + AUG 15 - 17 + logo + + )); }, []); - return ( - // - - {items} - - // - ); + return ; }; export const MobileAccordion: React.FC = () => { From ed97dc29feb57afac22680c41b4cd6aea3741e91 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 9 Aug 2025 15:38:17 +0900 Subject: [PATCH 099/324] =?UTF-8?q?feat:=20=EA=B0=9C=EC=9D=B8=ED=9B=84?= =?UTF-8?q?=EC=9B=90=EC=9E=90=20=EB=AA=A9=EB=A1=9D=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/consts/mdx_components.ts | 1 + apps/pyconkr/src/consts/mdx_components.ts | 1 + packages/shop/src/apis/index.ts | 5 +++++ .../shop/src/components/features/index.ts | 2 ++ .../src/components/features/patron_list.tsx | 21 +++++++++++++++++++ packages/shop/src/hooks/index.ts | 7 +++++++ packages/shop/src/schemas/index.ts | 5 +++++ 7 files changed, 42 insertions(+) create mode 100644 packages/shop/src/components/features/patron_list.tsx diff --git a/apps/pyconkr-admin/src/consts/mdx_components.ts b/apps/pyconkr-admin/src/consts/mdx_components.ts index c42243cd..ef656268 100644 --- a/apps/pyconkr-admin/src/consts/mdx_components.ts +++ b/apps/pyconkr-admin/src/consts/mdx_components.ts @@ -154,6 +154,7 @@ const PythonKRShopMDXComponents: MDXComponents = { Shop__Feature__ProductImageCardList: Shop.Components.Features.ProductImageCardList, Shop__Feature__OrderList: Shop.Components.Features.OrderList, Shop__Feature__UserInfo: Shop.Components.Features.UserInfo, + Shop__Feature__PatronList: Shop.Components.Features.PatronList, }; export const PyConKRMDXComponents = { diff --git a/apps/pyconkr/src/consts/mdx_components.ts b/apps/pyconkr/src/consts/mdx_components.ts index c42243cd..ef656268 100644 --- a/apps/pyconkr/src/consts/mdx_components.ts +++ b/apps/pyconkr/src/consts/mdx_components.ts @@ -154,6 +154,7 @@ const PythonKRShopMDXComponents: MDXComponents = { Shop__Feature__ProductImageCardList: Shop.Components.Features.ProductImageCardList, Shop__Feature__OrderList: Shop.Components.Features.OrderList, Shop__Feature__UserInfo: Shop.Components.Features.UserInfo, + Shop__Feature__PatronList: Shop.Components.Features.PatronList, }; export const PyConKRMDXComponents = { diff --git a/packages/shop/src/apis/index.ts b/packages/shop/src/apis/index.ts index 3b9ead95..234b14b1 100644 --- a/packages/shop/src/apis/index.ts +++ b/packages/shop/src/apis/index.ts @@ -133,6 +133,11 @@ namespace ShopAPIs { `v1/orders/${data.order_id}/products/${data.order_product_relation_id}/options/`, data.options ); + + /** + * 후원자 목록을 가져옵니다. + */ + export const listPatrons = (client: ShopAPIClient, year: number) => () => client.get("v1/ext/patron/", { params: { year } }); } export default ShopAPIs; diff --git a/packages/shop/src/components/features/index.ts b/packages/shop/src/components/features/index.ts index 9012b8ce..f592fd8d 100644 --- a/packages/shop/src/components/features/index.ts +++ b/packages/shop/src/components/features/index.ts @@ -1,5 +1,6 @@ import { CartStatus as CartStatus_ } from "./cart"; import { OrderList as OrderList_ } from "./order"; +import { PatronList as PatronList_ } from "./patron_list"; import { ProductImageCardList as ProductImageCardList_, ProductList as ProductList_ } from "./product"; import { UserInfo as UserInfo_ } from "./user_status"; @@ -9,6 +10,7 @@ namespace FeatureComponents { export const ProductList = ProductList_; export const ProductImageCardList = ProductImageCardList_; export const UserInfo = UserInfo_; + export const PatronList = PatronList_; } export default FeatureComponents; diff --git a/packages/shop/src/components/features/patron_list.tsx b/packages/shop/src/components/features/patron_list.tsx new file mode 100644 index 00000000..5fe5ae16 --- /dev/null +++ b/packages/shop/src/components/features/patron_list.tsx @@ -0,0 +1,21 @@ +import { CircularProgress, Stack, Typography } from "@mui/material"; +import { ErrorBoundary, Suspense } from "@suspensive/react"; +import * as React from "react"; + +import ShopHooks from "../../hooks"; + +const InnerPatronList: React.FC<{ year: number }> = ErrorBoundary.with( + { fallback: <>개인후원자 목록을 불러오는 중 문제가 발생했습니다. }, + Suspense.with({ fallback: }, ({ year }) => { + const shopAPIClient = ShopHooks.useShopClient(); + const { data } = ShopHooks.usePatrons(shopAPIClient, year); + return data.map((patron) => ( + + + {patron.contribution_message && } + + )); + }) +); + +export const PatronList: React.FC<{ year: number }> = ({ year }) => } />; diff --git a/packages/shop/src/hooks/index.ts b/packages/shop/src/hooks/index.ts index b117c2e8..02abcd04 100644 --- a/packages/shop/src/hooks/index.ts +++ b/packages/shop/src/hooks/index.ts @@ -12,6 +12,7 @@ const QUERY_KEYS = { PRODUCT_LIST: ["query", "shop", "products"], CART_INFO: ["query", "shop", "cart"], ORDER_LIST: ["query", "shop", "orders"], + PATRONS: ["query", "shop", "patrons"], }; const MUTATION_KEYS = { @@ -135,6 +136,12 @@ namespace ShopHooks { mutationFn: ShopAPIs.patchOrderOptions(client), meta: { invalidates: [QUERY_KEYS.ORDER_LIST] }, }); + + export const usePatrons = (client: ShopAPIClient, year: number) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PATRONS, year], + queryFn: ShopAPIs.listPatrons(client, year), + }); } export default ShopHooks; diff --git a/packages/shop/src/schemas/index.ts b/packages/shop/src/schemas/index.ts index edf3a81e..31d08708 100644 --- a/packages/shop/src/schemas/index.ts +++ b/packages/shop/src/schemas/index.ts @@ -227,6 +227,11 @@ namespace ShopSchemas { }[]; }; + export type Patron = { + name: string; + contribution_message: string; + }; + export const isObjectErrorResponseSchema = (obj?: unknown): obj is ShopSchemas.ErrorResponseSchema => { return ( R.isPlainObject(obj) && From f8c39f29e67d023af11b521c5527d2a9b0a51509 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 9 Aug 2025 15:52:46 +0900 Subject: [PATCH 100/324] =?UTF-8?q?chore:=20=EA=B0=9C=EC=9D=B8=20=ED=9B=84?= =?UTF-8?q?=EC=9B=90=EC=9E=90=20=EB=AA=A9=EB=A1=9D=EC=9D=98=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/shop/src/components/features/patron_list.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shop/src/components/features/patron_list.tsx b/packages/shop/src/components/features/patron_list.tsx index 5fe5ae16..892ffbdd 100644 --- a/packages/shop/src/components/features/patron_list.tsx +++ b/packages/shop/src/components/features/patron_list.tsx @@ -11,8 +11,8 @@ const InnerPatronList: React.FC<{ year: number }> = ErrorBoundary.with( const { data } = ShopHooks.usePatrons(shopAPIClient, year); return data.map((patron) => ( - - {patron.contribution_message && } + ({ fontWeight: 400, color: theme.palette.primary.dark })} children={patron.name} /> + )); }) From d52e65381009ee53420aa71c8eb40f348ca80ce5 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 9 Aug 2025 17:58:33 +0900 Subject: [PATCH 101/324] =?UTF-8?q?feat:=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9D=98=20=EB=A7=81=ED=81=AC=EB=A5=BC=20=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EA=BF=94=EC=A3=BC=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/src/components/auto_text_linking.tsx | 13 +++++++++++++ packages/common/src/components/index.ts | 2 ++ .../shop/src/components/features/patron_list.tsx | 5 ++++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 packages/common/src/components/auto_text_linking.tsx diff --git a/packages/common/src/components/auto_text_linking.tsx b/packages/common/src/components/auto_text_linking.tsx new file mode 100644 index 00000000..aa671f0c --- /dev/null +++ b/packages/common/src/components/auto_text_linking.tsx @@ -0,0 +1,13 @@ +import * as React from "react"; + +import { LinkHandler } from "./link_handler"; + +const urlRegex = /(mailto:[\w.-]+@[\w.-]+\.[a-zA-Z]{2,})|(https?:\/\/[^\s]+)/gi; + +export const AutoTextLinking: React.FC<{ children: string }> = ({ children }) => { + const convertedChildren = children + .split(urlRegex) + .filter((text) => text !== undefined) + .map((text) => (text.match(urlRegex) ? : text)); + return ; +}; diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index 6f6d732f..8b9914d8 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -1,3 +1,4 @@ +import { AutoTextLinking as AutoTextLinkingComponent } from "./auto_text_linking"; import { CenteredPage as CenteredPageComponent } from "./centered_page"; import { CommonContextProvider as CommonContextProviderComponent } from "./common_context"; import { DndFileInput as DndFileInputComponent } from "./dnd_file_input"; @@ -44,6 +45,7 @@ namespace Components { export const ErrorFallback = ErrorFallbackComponent; export const FallbackImage = FallbackImageComponent; export const LinkHandler = LinkHandlerComponent; + export const AutoTextLinking = AutoTextLinkingComponent; export const DndFileInput = DndFileInputComponent; export const Fieldset = FieldsetComponent; diff --git a/packages/shop/src/components/features/patron_list.tsx b/packages/shop/src/components/features/patron_list.tsx index 892ffbdd..e45313d1 100644 --- a/packages/shop/src/components/features/patron_list.tsx +++ b/packages/shop/src/components/features/patron_list.tsx @@ -1,3 +1,4 @@ +import * as Common from "@frontend/common"; import { CircularProgress, Stack, Typography } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import * as React from "react"; @@ -12,7 +13,9 @@ const InnerPatronList: React.FC<{ year: number }> = ErrorBoundary.with( return data.map((patron) => ( ({ fontWeight: 400, color: theme.palette.primary.dark })} children={patron.name} /> - + ({ a: { color: theme.palette.primary.main }, whiteSpace: "pre-wrap" })}> + + )); }) From 93f882345678da732bb0ef225dd03dbfe061a79f Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sun, 10 Aug 2025 21:02:41 +0900 Subject: [PATCH 102/324] =?UTF-8?q?feat:=20=EB=93=B1=EB=A1=9D=20QR=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A7=81=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Header/Mobile/MobileLanguageToggle.tsx | 18 +++--- .../layout/Header/Mobile/MobileNavigation.tsx | 18 +++--- .../src/components/layout/Header/index.tsx | 2 + .../components/layout/SignInButton/index.tsx | 2 +- .../layout/UserScanCodeButton/index.tsx | 56 +++++++++++++++++++ packages/shop/src/apis/index.ts | 6 ++ packages/shop/src/hooks/index.ts | 8 +++ packages/shop/src/schemas/index.ts | 6 ++ 8 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 apps/pyconkr/src/components/layout/UserScanCodeButton/index.tsx diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx index f38217fd..520da036 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileLanguageToggle.tsx @@ -11,26 +11,24 @@ interface MobileLanguageToggleProps { export const MobileLanguageToggle: React.FC = ({ isMainPath = true }) => { const { language, setAppContext } = useAppContext(); - const handleLanguageChange = (newLanguage: "ko" | "en") => { + const toggleLanguage = () => { + const newLanguage = language === "ko" ? "en" : "ko"; localStorage.setItem(LOCAL_STORAGE_LANGUAGE_KEY, newLanguage); setAppContext((ps) => ({ ...ps, language: newLanguage })); }; + return ( - - handleLanguageChange("ko")}> - KO - - handleLanguageChange("en")}> - EN - + + + ); }; const ToggleContainer = styled("div")<{ isMainPath: boolean }>(({ theme, isMainPath }) => ({ display: "flex", - width: 94, - height: 29, + width: "4rem", + height: "1.5rem", border: "1px solid white", borderRadius: 15, padding: 2, diff --git a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx index 2729ed26..8fdd08ed 100644 --- a/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx +++ b/apps/pyconkr/src/components/layout/Header/Mobile/MobileNavigation.tsx @@ -9,6 +9,7 @@ import * as R from "remeda"; import { HamburgerButton } from "./HamburgerButton"; import { MobileLanguageToggle } from "./MobileLanguageToggle"; import { SignInButton } from "../../SignInButton"; +import { ScanCodeButton } from "../../UserScanCodeButton"; type MenuType = BackendAPISchemas.NestedSiteMapSchema; @@ -195,12 +196,15 @@ export const MobileNavigation: React.FC = ({ isOpen, onCl {navState.level === "depth3" && renderDepth3Menu()} - - + + - - + + + + + ); @@ -300,12 +304,6 @@ const MenuChip = styled(Chip)<{ isMainPath?: boolean }>(({ theme, isMainPath = t }, })); -const BottomActions = styled(Stack)<{ isMainPath: boolean }>({ - padding: "20px 23px", - gap: 50, - alignItems: "center", -}); - const HeaderTitle = styled(Typography)<{ isMainPath: boolean }>(({ theme, isMainPath }) => ({ color: isMainPath ? theme.palette.mobileHeader.main.text : theme.palette.mobileHeader.sub.text, fontSize: 18, diff --git a/apps/pyconkr/src/components/layout/Header/index.tsx b/apps/pyconkr/src/components/layout/Header/index.tsx index ef68bcf3..8e660aa7 100644 --- a/apps/pyconkr/src/components/layout/Header/index.tsx +++ b/apps/pyconkr/src/components/layout/Header/index.tsx @@ -11,6 +11,7 @@ import { useAppContext } from "../../../contexts/app_context"; import { CartBadgeButton } from "../CartBadgeButton"; import LanguageSelector from "../LanguageSelector"; import { SignInButton } from "../SignInButton"; +import { ScanCodeIconButton } from "../UserScanCodeButton"; import { MobileHeader } from "./Mobile/MobileHeader"; type MenuType = BackendAPISchemas.NestedSiteMapSchema; @@ -148,6 +149,7 @@ const Header: React.FC = () => { + diff --git a/apps/pyconkr/src/components/layout/SignInButton/index.tsx b/apps/pyconkr/src/components/layout/SignInButton/index.tsx index 490bc1f9..79961295 100644 --- a/apps/pyconkr/src/components/layout/SignInButton/index.tsx +++ b/apps/pyconkr/src/components/layout/SignInButton/index.tsx @@ -49,7 +49,7 @@ const InnerSignInButtonImpl: React.FC = ({ fontWeight: 500, textTransform: "none", minWidth: "auto", - padding: "0 13px", + padding: 0, "&:hover": { backgroundColor: isMainPath ? "rgba(255, 255, 255, 0.1)" : "rgba(18, 109, 127, 0.1)", diff --git a/apps/pyconkr/src/components/layout/UserScanCodeButton/index.tsx b/apps/pyconkr/src/components/layout/UserScanCodeButton/index.tsx new file mode 100644 index 00000000..3a26bf05 --- /dev/null +++ b/apps/pyconkr/src/components/layout/UserScanCodeButton/index.tsx @@ -0,0 +1,56 @@ +import * as Shop from "@frontend/shop"; +import { QrCode2 } from "@mui/icons-material"; +import { Button, IconButton, IconButtonProps } from "@mui/material"; +import { ErrorBoundary, Suspense } from "@suspensive/react"; +import * as React from "react"; + +import { useAppContext } from "../../../contexts/app_context"; + +export const ScanCodeIconButton: React.FC<{ sx?: IconButtonProps["sx"] }> = Suspense.with( + { fallback: }, + ErrorBoundary.with({ fallback: }, ({ sx }) => { + const shopAPIClient = Shop.Hooks.useShopClient(); + const { data } = Shop.Hooks.useUserInfo(shopAPIClient); + + const iconBtnStyle: IconButtonProps["sx"] = (theme) => ({ + color: theme.palette.primary.nonFocus, + "&:hover": { color: theme.palette.primary.dark }, + "&:active": { color: theme.palette.primary.main }, + transition: "color 0.4s ease, background-color 0.4s ease", + }); + + return ( + + } sx={sx ?? iconBtnStyle} /> + + ); + }) +); + +export const ScanCodeButton: React.FC = Suspense.with( + { fallback: null }, + ErrorBoundary.with({ fallback: null }, () => { + const { language } = useAppContext(); + const shopAPIClient = Shop.Hooks.useShopClient(); + const { data } = Shop.Hooks.useUserInfo(shopAPIClient); + + const buttonText = language === "ko" ? "등록 코드" : "Entrance QR Code"; + + return ( + + - - + + {prodRel.scancode_url && ( + + + + + ); return ( diff --git a/packages/shop/src/schemas/index.ts b/packages/shop/src/schemas/index.ts index 6d6b805b..0d135082 100644 --- a/packages/shop/src/schemas/index.ts +++ b/packages/shop/src/schemas/index.ts @@ -148,6 +148,7 @@ namespace ShopSchemas { price: number; donation_price: number; not_refundable_reason: string | null; + scancode_url: string | null; product: { id: string; name: string; From 4571aa8125fca7f1e38beb98ecf1637ffee03a09 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Tue, 12 Aug 2025 13:24:36 +0900 Subject: [PATCH 107/324] =?UTF-8?q?chore:=20=EB=B2=84=ED=8A=BC=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/shop/src/components/features/order.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/shop/src/components/features/order.tsx b/packages/shop/src/components/features/order.tsx index f4ff0898..6cdb4f59 100644 --- a/packages/shop/src/components/features/order.tsx +++ b/packages/shop/src/components/features/order.tsx @@ -90,7 +90,6 @@ const OrderProductRelationItem: React.FC = ({ ? "옵션 수정 중 문제가 발생했습니다,\n잠시 후 다시 시도해주세요." : "An error occurred while modifying the options,\nplease try again later."; - const scanCodeBtnText = language === "ko" ? "등록 QR 코드" : "Registeration QR Code"; const refundBtnDisabled = isPending || !R.isNullish(prodRel.not_refundable_reason); const refundBtnText = R.isNullish(prodRel.not_refundable_reason) ? refundOneProductStr @@ -98,6 +97,9 @@ const OrderProductRelationItem: React.FC = ({ ? refundedStr : prodRel.not_refundable_reason; + const scanCodeDisabled = isPending || prodRel.status === "refunded"; + const scanCodeBtnText = language === "ko" ? "등록 QR 코드" : "Registeration QR Code"; + const refundOneItem = () => oneItemRefundMutation.mutate( { order_id: order.id, order_product_relation_id: prodRel.id }, @@ -138,7 +140,7 @@ const OrderProductRelationItem: React.FC = ({ {prodRel.scancode_url && ( - + + + 비밀번호가 초기화되었습니다 + + + 새로운 비밀번호가 생성되었습니다. 이 비밀번호는 다시 확인할 수 없으니 반드시 복사해 두세요. + + + + + + + ), + }, + }} + /> + + + + + + ); diff --git a/packages/common/src/apis/admin_api.ts b/packages/common/src/apis/admin_api.ts index c27a3e46..b0c94d4a 100644 --- a/packages/common/src/apis/admin_api.ts +++ b/packages/common/src/apis/admin_api.ts @@ -20,7 +20,7 @@ namespace BackendAdminAPIs { client.post("v1/admin-api/user/userext/password/", data); export const resetUserPassword = (client: BackendAPIClient, id: string) => () => - client.delete(`v1/admin-api/user/userext/${id}/password/`); + client.delete(`v1/admin-api/user/userext/${id}/password/`); export const list = (client: BackendAPIClient, app: string, resource: string, params?: Record) => diff --git a/packages/common/src/schemas/backendAdminAPI.ts b/packages/common/src/schemas/backendAdminAPI.ts index 4492c263..f4b2d84f 100644 --- a/packages/common/src/schemas/backendAdminAPI.ts +++ b/packages/common/src/schemas/backendAdminAPI.ts @@ -40,6 +40,10 @@ namespace BackendAdminAPISchemas { new_password_confirm: string; }; + export type UserResetPasswordResponseSchema = { + password: string; + }; + export type PublicFileSchema = { id: string; // UUID file: string; // URL to the public file From 9d7d39454d47abcb1901b1a0e980615a3ede7fa9 Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Sat, 28 Feb 2026 15:31:21 +0900 Subject: [PATCH 114/324] =?UTF-8?q?feat:=20=EA=B3=84=EC=A0=95=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EB=9E=9C=EB=8D=A4=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/layouts/admin_editor.tsx | 28 +++++-- .../src/components/pages/user/editor.tsx | 74 ++++++------------- .../pages/user/password_result_dialog.tsx | 52 +++++++++++++ 3 files changed, 97 insertions(+), 57 deletions(-) create mode 100644 apps/pyconkr-admin/src/components/pages/user/password_result_dialog.tsx diff --git a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx index b211e25f..bd9e981c 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx @@ -45,6 +45,7 @@ type AppResourceIdType = AppResourceType & { id?: string }; type AdminEditorPropsType = React.PropsWithChildren<{ hidingFields?: string[]; context?: Record; + onCreated?: (data: Record) => void; onClose?: () => void; beforeSubmit?: onSubmitType; afterSubmit?: onSubmitType; @@ -253,7 +254,21 @@ const InnerAdminEditor: React.FC = Err { fallback: Common.Components.ErrorFallback }, Suspense.with( { fallback: }, - ({ app, resource, id, hidingFields, context, onClose, beforeSubmit, afterSubmit, extraActions, notModifiable, notDeletable, children }) => { + ({ + app, + resource, + id, + hidingFields, + context, + onCreated, + onClose, + beforeSubmit, + afterSubmit, + extraActions, + notModifiable, + notDeletable, + children, + }) => { const navigate = useNavigate(); const formRef = React.useRef, RJSFSchema, { [k in string]: unknown }> | null>(null); const [editorState, setEditorState] = React.useState({ @@ -297,10 +312,13 @@ const InnerAdminEditor: React.FC = Err beforeSubmit?.(newFormData, event); submitMutation.mutate(newFormData, { onSuccess: (newFormData) => { - addSnackbar(id ? "저장했습니다." : "페이지를 생성했습니다.", "success"); - afterSubmit?.(newFormData, event); - - if (!id && newFormData.id) navigate(`/${app}/${resource}/${newFormData.id}`); + if (!id && onCreated) { + onCreated(newFormData); + } else { + addSnackbar(id ? "저장했습니다." : "페이지를 생성했습니다.", "success"); + afterSubmit?.(newFormData, event); + if (!id && newFormData.id) navigate(`/${app}/${resource}/${newFormData.id}`); + } }, onError: addErrorSnackbar, }); diff --git a/apps/pyconkr-admin/src/components/pages/user/editor.tsx b/apps/pyconkr-admin/src/components/pages/user/editor.tsx index c5b2d9d0..7a3c7523 100644 --- a/apps/pyconkr-admin/src/components/pages/user/editor.tsx +++ b/apps/pyconkr-admin/src/components/pages/user/editor.tsx @@ -1,43 +1,39 @@ import * as Common from "@frontend/common"; -import { ContentCopy, KeyOff } from "@mui/icons-material"; -import { - Button, - ButtonProps, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - IconButton, - InputAdornment, - TextField, -} from "@mui/material"; +import { KeyOff } from "@mui/icons-material"; +import { Button, ButtonProps, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import * as React from "react"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; -import { addErrorSnackbar, addSnackbar } from "../../../utils/snackbar"; +import { PasswordResultDialog } from "./password_result_dialog"; +import { addErrorSnackbar } from "../../../utils/snackbar"; import { AdminEditor } from "../../layouts/admin_editor"; type PageStateType = { isConfirmDialogOpen: boolean; isResultDialogOpen: boolean; newPassword: string | null; + createdUserId: string | null; }; export const AdminUserExtEditor: React.FC = ErrorBoundary.with( { fallback: Common.Components.ErrorFallback }, Suspense.with({ fallback: }, () => { const { id } = useParams<{ id?: string }>(); + const navigate = useNavigate(); const [pageState, setPageState] = React.useState({ isConfirmDialogOpen: false, isResultDialogOpen: false, newPassword: null, + createdUserId: null, }); const openConfirmDialog = () => setPageState((ps) => ({ ...ps, isConfirmDialogOpen: true })); const closeConfirmDialog = () => setPageState((ps) => ({ ...ps, isConfirmDialogOpen: false })); - const closeResultDialog = () => setPageState((ps) => ({ ...ps, isResultDialogOpen: false, newPassword: null })); + const closeResultDialog = () => { + const userId = pageState.createdUserId; + setPageState((ps) => ({ ...ps, isResultDialogOpen: false, newPassword: null, createdUserId: null })); + if (userId) navigate(`/user/userext/${userId}`); + }; const backendAdminClient = Common.Hooks.BackendAdminAPI.useBackendAdminClient(); const useResetPasswordMutation = Common.Hooks.BackendAdminAPI.useResetUserPasswordMutation(backendAdminClient, id || ""); @@ -58,13 +54,13 @@ export const AdminUserExtEditor: React.FC = ErrorBoundary.with( } }; - const copyPasswordToClipboard = () => { - if (pageState.newPassword) { - navigator.clipboard.writeText(pageState.newPassword).then( - () => addSnackbar("비밀번호가 클립보드에 복사되었습니다.", "success"), - () => addSnackbar("클립보드 복사에 실패했습니다.", "error") - ); - } + const onCreated = (data: Record) => { + setPageState((ps) => ({ + ...ps, + isResultDialogOpen: true, + newPassword: data.password, + createdUserId: data.id, + })); }; const resetUserPasswordButton: ButtonProps = { @@ -91,35 +87,9 @@ export const AdminUserExtEditor: React.FC = ErrorBoundary.with( - - 비밀번호가 초기화되었습니다 - - - 새로운 비밀번호가 생성되었습니다. 이 비밀번호는 다시 확인할 수 없으니 반드시 복사해 두세요. - - - - - - - ), - }, - }} - /> - - - - - + - + ); }) diff --git a/apps/pyconkr-admin/src/components/pages/user/password_result_dialog.tsx b/apps/pyconkr-admin/src/components/pages/user/password_result_dialog.tsx new file mode 100644 index 00000000..f25dc3c2 --- /dev/null +++ b/apps/pyconkr-admin/src/components/pages/user/password_result_dialog.tsx @@ -0,0 +1,52 @@ +import { ContentCopy } from "@mui/icons-material"; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton, InputAdornment, TextField } from "@mui/material"; +import * as React from "react"; + +import { addSnackbar } from "../../../utils/snackbar"; + +type PasswordResultDialogProps = { + open: boolean; + password: string | null; + onClose: () => void; +}; + +export const PasswordResultDialog: React.FC = ({ open, password, onClose }) => { + const copyPasswordToClipboard = () => { + if (password) { + navigator.clipboard.writeText(password).then( + () => addSnackbar("비밀번호가 클립보드에 복사되었습니다.", "success"), + () => addSnackbar("클립보드 복사에 실패했습니다.", "error") + ); + } + }; + + return ( + + 비밀번호가 설정되었습니다 + + + 새로운 비밀번호가 생성되었습니다. 이 비밀번호는 다시 확인할 수 없으니 반드시 복사해 두세요. + + + + + + + ), + }, + }} + /> + + + + + + ); +}; From 63a205bc56abb2df1f4aa3f588bb93c8eaa9c86f Mon Sep 17 00:00:00 2001 From: MUsoftware Date: Thu, 2 Apr 2026 21:35:26 +0900 Subject: [PATCH 115/324] =?UTF-8?q?refactor:=20json=20schema=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/elements/admin_list_filter.tsx | 189 ++++++++++++++++++ .../src/components/layouts/admin_list.tsx | 17 +- packages/common/src/apis/admin_api.ts | 3 + packages/common/src/hooks/useAdminAPI.ts | 8 + .../common/src/schemas/backendAdminAPI.ts | 15 ++ packages/common/src/utils/index.ts | 2 + packages/common/src/utils/openapi.ts | 12 ++ 7 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx create mode 100644 packages/common/src/utils/openapi.ts diff --git a/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx b/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx new file mode 100644 index 00000000..fc2b8220 --- /dev/null +++ b/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx @@ -0,0 +1,189 @@ +import { Add, Clear, FilterList, RestartAlt } from "@mui/icons-material"; +import { Box, Button, Chip, FormControl, IconButton, InputLabel, MenuItem, Select, Stack, TextField } from "@mui/material"; +import * as React from "react"; + +import BackendAdminAPISchemas from "../../../../../packages/common/src/schemas/backendAdminAPI"; + +type OpenAPIParameterSchema = BackendAdminAPISchemas.OpenAPIParameterSchema; + +type AdminListFilterProps = { + parameters: OpenAPIParameterSchema[]; + values: Record; + onApply: (values: Record) => void; +}; + +export const AdminListFilter: React.FC = ({ parameters, values, onApply }) => { + const [localValues, setLocalValues] = React.useState>(values); + + React.useEffect(() => { + setLocalValues(values); + }, [values]); + + const handleChange = (name: string, value: string) => { + setLocalValues((prev) => ({ ...prev, [name]: value })); + }; + + const handleApply = () => { + const cleaned = Object.fromEntries(Object.entries(localValues).filter(([, v]) => v !== "")); + onApply(cleaned); + }; + + const handleClear = () => { + setLocalValues({}); + onApply({}); + }; + + if (parameters.length === 0) return null; + + return ( + + + + + 필터 + + + {parameters.map((param) => ( + + ))} + + + + + + + + ); +}; + +type FilterFieldProps = { + param: OpenAPIParameterSchema; + value: string; + onChange: (name: string, value: string) => void; +}; + +const FilterField: React.FC = ({ param, value, onChange }) => { + const { name, schema, description } = param; + + if (schema?.type === "array") return ; + if (schema?.enum) return ; + + const inputType = schema?.type === "integer" || schema?.type === "number" ? "number" : "text"; + const helperText = schema?.format === "uuid" ? "UUID" : description || undefined; + + return ( + onChange(name, e.target.value)} + size="small" + type={inputType} + helperText={helperText} + sx={{ minWidth: 200 }} + /> + ); +}; + +type EnumFilterFieldProps = { + name: string; + options: string[]; + value: string; + onChange: (name: string, value: string) => void; +}; + +const EnumFilterField: React.FC = ({ name, options, value, onChange }) => { + const selectedValues = value ? value.split(",") : []; + + const handleChange = (newValues: string | string[]) => { + const arr = typeof newValues === "string" ? newValues.split(",") : newValues; + onChange(name, arr.filter((v) => v !== "").join(",")); + }; + + return ( + + {name} + + + ); +}; + +type ArrayFilterFieldProps = { + name: string; + items?: { type?: string; enum?: string[] }; + value: string; + onChange: (name: string, value: string) => void; +}; + +const ArrayFilterField: React.FC = ({ name, items, value, onChange }) => { + const values = value ? value.split(",") : []; + + const updateValues = (newValues: string[]) => onChange(name, newValues.filter((v) => v !== "").join(",")); + const handleAdd = () => updateValues([...values, ""]); + const handleRemove = (index: number) => updateValues(values.filter((_, i) => i !== index)); + + const handleItemChange = (index: number, newValue: string) => { + const newValues = [...values]; + newValues[index] = newValue; + updateValues(newValues); + }; + + const inputType = items?.type === "integer" || items?.type === "number" ? "number" : "text"; + + return ( + + + + {name} + + + + + {values.map((v, index) => ( + + {items?.enum ? ( + + + + ) : ( + handleItemChange(index, e.target.value)} size="small" type={inputType} sx={{ minWidth: 150 }} /> + )} + handleRemove(index)}> + + + + ))} + + + ); +}; diff --git a/apps/pyconkr-admin/src/components/layouts/admin_list.tsx b/apps/pyconkr-admin/src/components/layouts/admin_list.tsx index 64ba59ae..8d4afd83 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_list.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_list.tsx @@ -3,8 +3,9 @@ import { Add } from "@mui/icons-material"; import { Box, Button, CircularProgress, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import * as React from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { AdminListFilter } from "../elements/admin_list_filter"; import { BackendAdminSignInGuard } from "../elements/admin_signin_guard"; type AdminListProps = { @@ -26,8 +27,19 @@ const InnerAdminList: React.FC = ErrorBoundary.with( { fallback: Common.Components.ErrorFallback }, Suspense.with({ fallback: }, ({ app, resource, hideCreatedAt, hideUpdatedAt, hideCreateNew }) => { const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const backendAdminClient = Common.Hooks.BackendAdminAPI.useBackendAdminClient(); - const listQuery = Common.Hooks.BackendAdminAPI.useListQuery(backendAdminClient, app, resource); + + const filterParams: Record = Object.fromEntries(searchParams.entries()); + const listQuery = Common.Hooks.BackendAdminAPI.useListQuery(backendAdminClient, app, resource, filterParams); + + const openApiSchemaQuery = Common.Hooks.BackendAdminAPI.useOpenApiSchemaQuery(backendAdminClient); + const queryParameters = React.useMemo( + () => Common.Utils.extractQueryParameters(openApiSchemaQuery.data, app, resource), + [openApiSchemaQuery.data, app, resource] + ); + + const handleFilterApply = (newParams: Record) => setSearchParams(newParams, { replace: true }); return ( @@ -35,6 +47,7 @@ const InnerAdminList: React.FC = ErrorBoundary.with( {app.toUpperCase()} > {resource.toUpperCase()} > 목록
+ {!hideCreateNew && (
- + @@ -81,17 +83,17 @@ type AdminCMSPageEditorStateType = { }; export const AdminCMSPageEditor: React.FC = ErrorBoundary.with( - { fallback: Common.Components.ErrorFallback }, + { fallback: Components.ErrorFallback }, Suspense.with({ fallback: }, () => { const { id } = useParams<{ id?: string }>(); - const { frontendDomain } = Common.Hooks.Common.useCommonContext(); - const backendAdminClient = Common.Hooks.BackendAdminAPI.useBackendAdminClient(); - const { data: initialSections } = Common.Hooks.BackendAdminAPI.useListPageSectionsQuery(backendAdminClient, id || ""); + const { frontendDomain } = useCommonContext(); + const backendAdminClient = useBackendAdminClient(); + const { data: initialSections } = useListPageSectionsQuery(backendAdminClient, id || ""); const [editorState, setEditorState] = React.useState({ sections: initialSections, tab: 0, }); - const bulkUpdateSectionsMutation = Common.Hooks.BackendAdminAPI.useBulkUpdatePageSectionsMutation(backendAdminClient, id || ""); + const bulkUpdateSectionsMutation = useBulkUpdatePageSectionsMutation(backendAdminClient, id || ""); const setTab = (_: React.SyntheticEvent, selectedTab: number) => setEditorState((ps) => ({ ...ps, tab: selectedTab })); diff --git a/apps/pyconkr-admin/src/components/pages/presentation/editor.tsx b/apps/pyconkr-admin/src/components/pages/presentation/editor.tsx index 8af3f857..16daa629 100644 --- a/apps/pyconkr-admin/src/components/pages/presentation/editor.tsx +++ b/apps/pyconkr-admin/src/components/pages/presentation/editor.tsx @@ -1,4 +1,5 @@ -import * as Common from "@frontend/common"; +import { Components } from "@frontend/common"; +import { useBackendAdminClient, useCreateMutation, useListQuery, useRemovePreparedMutation, useSchemaQuery, useUpdatePreparedMutation } from "@frontend/common/src/hooks/useAdminAPI"; import { Autocomplete, Box, Button, Card, CardContent, CircularProgress, Stack, styled, Tab, Tabs, TextField, Typography } from "@mui/material"; import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterLuxon } from "@mui/x-date-pickers/AdapterLuxon"; @@ -143,10 +144,10 @@ const PresentationSpeakerForm: React.FC = ({ di - + - + @@ -263,29 +264,29 @@ type PresentationEditorStateType = { }; export const AdminPresentationEditor: React.FC = ErrorBoundary.with( - { fallback: Common.Components.ErrorFallback }, + { fallback: Components.ErrorFallback }, Suspense.with({ fallback: }, () => { const { id } = useParams<{ id?: string }>(); const addSnackbar = (c: string | React.ReactNode, variant: OptionsObject["variant"]) => enqueueSnackbar(c, { variant, anchorOrigin: { vertical: "bottom", horizontal: "center" } }); - const backendAdminAPIClient = Common.Hooks.BackendAdminAPI.useBackendAdminClient(); + const backendAdminAPIClient = useBackendAdminClient(); const speakerQueryParams = [backendAdminAPIClient, "event", "presentationspeaker"] as const; const presentation = id || DUMMY_UUID; - const speakerCreateMutation = Common.Hooks.BackendAdminAPI.useCreateMutation(...speakerQueryParams); - const speakerUpdateMutation = Common.Hooks.BackendAdminAPI.useUpdatePreparedMutation(...speakerQueryParams); - const speakerDeleteMutation = Common.Hooks.BackendAdminAPI.useRemovePreparedMutation(...speakerQueryParams); - const { data: speakerJsonSchema } = Common.Hooks.BackendAdminAPI.useSchemaQuery(...speakerQueryParams); - const { data: speakerInitialData } = Common.Hooks.BackendAdminAPI.useListQuery(...speakerQueryParams, { presentation }); + const speakerCreateMutation = useCreateMutation(...speakerQueryParams); + const speakerUpdateMutation = useUpdatePreparedMutation(...speakerQueryParams); + const speakerDeleteMutation = useRemovePreparedMutation(...speakerQueryParams); + const { data: speakerJsonSchema } = useSchemaQuery(...speakerQueryParams); + const { data: speakerInitialData } = useListQuery(...speakerQueryParams, { presentation }); const speakers = speakerInitialData.map((s) => ({ ...s, trackId: s.id || Math.random().toString(36).substring(2, 15) })); const scheduleQueryParams = [backendAdminAPIClient, "event", "roomschedule"] as const; - const scheduleCreateMutation = Common.Hooks.BackendAdminAPI.useCreateMutation(...scheduleQueryParams); - const scheduleUpdateMutation = Common.Hooks.BackendAdminAPI.useUpdatePreparedMutation(...scheduleQueryParams); - const scheduleDeleteMutation = Common.Hooks.BackendAdminAPI.useRemovePreparedMutation(...scheduleQueryParams); - const { data: scheduleJsonSchema } = Common.Hooks.BackendAdminAPI.useSchemaQuery(...scheduleQueryParams); - const { data: scheduleInitialData } = Common.Hooks.BackendAdminAPI.useListQuery(...scheduleQueryParams, { presentation }); + const scheduleCreateMutation = useCreateMutation(...scheduleQueryParams); + const scheduleUpdateMutation = useUpdatePreparedMutation(...scheduleQueryParams); + const scheduleDeleteMutation = useRemovePreparedMutation(...scheduleQueryParams); + const { data: scheduleJsonSchema } = useSchemaQuery(...scheduleQueryParams); + const { data: scheduleInitialData } = useListQuery(...scheduleQueryParams, { presentation }); const schedules = scheduleInitialData.map((s) => ({ ...s, trackId: s.id || Math.random().toString(36).substring(2, 15) })); const createEmptySpeaker = (): OnMemoeryPresentationSpeaker => ({ @@ -359,7 +360,7 @@ export const AdminPresentationEditor: React.FC = ErrorBoundary.with( {id ? ( - + 스케줄 정보 {editorState.schedules.map((s) => ( @@ -373,8 +374,8 @@ export const AdminPresentationEditor: React.FC = ErrorBoundary.with( ))} - + ); diff --git a/apps/pyconkr/src/debug/page/mdi_test.tsx b/apps/pyconkr/src/debug/page/mdi_test.tsx index f68d8332..4c55d528 100644 --- a/apps/pyconkr/src/debug/page/mdi_test.tsx +++ b/apps/pyconkr/src/debug/page/mdi_test.tsx @@ -1,4 +1,4 @@ -import * as Common from "@frontend/common"; +import { Components } from "@frontend/common"; import { Box, Stack } from "@mui/material"; import React from "react"; @@ -25,10 +25,10 @@ export const MdiTestPage: React.FC = () => { }} > - + - + ); diff --git a/apps/pyconkr/src/main.tsx b/apps/pyconkr/src/main.tsx index 705368a4..475971f4 100644 --- a/apps/pyconkr/src/main.tsx +++ b/apps/pyconkr/src/main.tsx @@ -1,5 +1,6 @@ import { Global } from "@emotion/react"; -import * as Common from "@frontend/common"; +import { Components } from "@frontend/common"; +import type { ContextOptions } from "@frontend/common/src/contexts"; import * as Shop from "@frontend/shop"; import { CircularProgress, CssBaseline, ThemeProvider } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; @@ -42,7 +43,7 @@ const queryClient = new QueryClient({ }), }); -const CommonOptions: Common.Contexts.ContextOptions = { +const CommonOptions: ContextOptions = { language: "ko", debug: IS_DEBUG_ENV, baseUrl: ".", @@ -60,9 +61,9 @@ const ShopOptions: Shop.Contexts.ContextOptions = { }; const SuspenseFallback = ( - + - + ); const MainApp: React.FC = () => { @@ -83,9 +84,9 @@ const MainApp: React.FC = () => { - + - + @@ -95,7 +96,7 @@ const MainApp: React.FC = () => { - + diff --git a/apps/pyconkr/vite.config.mts b/apps/pyconkr/vite.config.mts index 9765567e..857fbbd6 100644 --- a/apps/pyconkr/vite.config.mts +++ b/apps/pyconkr/vite.config.mts @@ -13,6 +13,7 @@ export default defineConfig({ plugins: [react(), mdx(), mkcert({ hosts: ["local.dev.pycon.kr"] }), svgr()], resolve: { alias: { + "@frontend/common/src": path.resolve(__dirname, "../../packages/common/src"), "@frontend/common": path.resolve(__dirname, "../../packages/common/src/index.ts"), "@frontend/shop": path.resolve(__dirname, "../../packages/shop/src/index.ts"), "@apps/pyconkr": path.resolve(__dirname, "./src"), diff --git a/packages/common/src/apis/admin_api.ts b/packages/common/src/apis/admin_api.ts index b0c94d4a..45251d8c 100644 --- a/packages/common/src/apis/admin_api.ts +++ b/packages/common/src/apis/admin_api.ts @@ -1,99 +1,95 @@ import { BackendAPIClient } from "./client"; -import BackendAdminAPISchemas from "../schemas/backendAdminAPI"; - -namespace BackendAdminAPIs { - export const me = (client: BackendAPIClient) => async () => { - try { - return await client.get("v1/admin-api/user/userext/me/"); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { - return null; - } +import * as BackendAdminAPISchemas from "../schemas/backendAdminAPI"; + +export const me = (client: BackendAPIClient) => async () => { + try { + return await client.get("v1/admin-api/user/userext/me/"); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + return null; + } +}; + +export const signIn = (client: BackendAPIClient) => (data: BackendAdminAPISchemas.UserSignInSchema) => + client.post("v1/admin-api/user/userext/signin/", data); + +export const signOut = (client: BackendAPIClient) => () => client.delete("v1/admin-api/user/userext/signout/"); + +export const changePassword = (client: BackendAPIClient) => (data: BackendAdminAPISchemas.UserChangePasswordSchema) => + client.post("v1/admin-api/user/userext/password/", data); + +export const resetUserPassword = (client: BackendAPIClient, id: string) => () => + client.delete(`v1/admin-api/user/userext/${id}/password/`); + +export const list = + (client: BackendAPIClient, app: string, resource: string, params?: Record) => + () => + client.get(`v1/admin-api/${app}/${resource}/`, { params }); + +export const retrieve = + (client: BackendAPIClient, app: string, resource: string, id: string) => + () => { + if (!id) return Promise.resolve(null); + return client.get(`v1/admin-api/${app}/${resource}/${id}/`); }; - export const signIn = (client: BackendAPIClient) => (data: BackendAdminAPISchemas.UserSignInSchema) => - client.post("v1/admin-api/user/userext/signin/", data); - - export const signOut = (client: BackendAPIClient) => () => client.delete("v1/admin-api/user/userext/signout/"); - - export const changePassword = (client: BackendAPIClient) => (data: BackendAdminAPISchemas.UserChangePasswordSchema) => - client.post("v1/admin-api/user/userext/password/", data); - - export const resetUserPassword = (client: BackendAPIClient, id: string) => () => - client.delete(`v1/admin-api/user/userext/${id}/password/`); - - export const list = - (client: BackendAPIClient, app: string, resource: string, params?: Record) => - () => - client.get(`v1/admin-api/${app}/${resource}/`, { params }); - - export const retrieve = - (client: BackendAPIClient, app: string, resource: string, id: string) => - () => { - if (!id) return Promise.resolve(null); - return client.get(`v1/admin-api/${app}/${resource}/${id}/`); - }; - - export const create = - (client: BackendAPIClient, app: string, resource: string) => - (data: T) => - client.post, T>(`v1/admin-api/${app}/${resource}/`, data); - - export const update = - (client: BackendAPIClient, app: string, resource: string, id: string) => - (data: Omit) => - client.patch>(`v1/admin-api/${app}/${resource}/${id}/`, data); - - export const updatePrepared = - (client: BackendAPIClient, app: string, resource: string) => - (data: T) => - client.patch>(`v1/admin-api/${app}/${resource}/${data.id}/`, data); - - export const remove = (client: BackendAPIClient, app: string, resource: string, id: string) => () => - client.delete(`v1/admin-api/${app}/${resource}/${id}/`); - - export const removePrepared = (client: BackendAPIClient, app: string, resource: string) => (id: string) => - client.delete(`v1/admin-api/${app}/${resource}/${id}/`); - - export const schema = (client: BackendAPIClient, app: string, resource: string) => () => - client.get(`v1/admin-api/${app}/${resource}/json-schema/`); - - export const uploadPublicFile = (client: BackendAPIClient) => (file: File) => { - const formData = new FormData(); - formData.append("file", file); - return client.post(`v1/admin-api/file/publicfile/upload/`, formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); - }; - - export const listSections = (client: BackendAPIClient, pageId: string) => () => { - if (!pageId) return Promise.resolve([]); - return client.get(`v1/admin-api/cms/page/${pageId}/section/`); - }; - - export const bulkUpdateSections = - (client: BackendAPIClient, pageId: string) => (data: { sections: BackendAdminAPISchemas.PageSectionBulkUpdateSchema[] }) => - client.put( - `v1/admin-api/cms/page/${pageId}/section/bulk-update/`, - data - ); - - export const approveModificationAudit = (client: BackendAPIClient, id: string) => (reason?: string | null) => - client.patch( - `v1/admin-api/modification-audit/modification-audit/${id}/approve/`, - { reason: reason ?? null } - ); - - export const rejectModificationAudit = (client: BackendAPIClient, id: string) => (reason?: string | null) => - client.patch( - `v1/admin-api/modification-audit/modification-audit/${id}/reject/`, - { reason: reason ?? null } +export const create = + (client: BackendAPIClient, app: string, resource: string) => + (data: T) => + client.post, T>(`v1/admin-api/${app}/${resource}/`, data); + +export const update = + (client: BackendAPIClient, app: string, resource: string, id: string) => + (data: Omit) => + client.patch>(`v1/admin-api/${app}/${resource}/${id}/`, data); + +export const updatePrepared = + (client: BackendAPIClient, app: string, resource: string) => + (data: T) => + client.patch>(`v1/admin-api/${app}/${resource}/${data.id}/`, data); + +export const remove = (client: BackendAPIClient, app: string, resource: string, id: string) => () => + client.delete(`v1/admin-api/${app}/${resource}/${id}/`); + +export const removePrepared = (client: BackendAPIClient, app: string, resource: string) => (id: string) => + client.delete(`v1/admin-api/${app}/${resource}/${id}/`); + +export const schema = (client: BackendAPIClient, app: string, resource: string) => () => + client.get(`v1/admin-api/${app}/${resource}/json-schema/`); + +export const uploadPublicFile = (client: BackendAPIClient) => (file: File) => { + const formData = new FormData(); + formData.append("file", file); + return client.post(`v1/admin-api/file/publicfile/upload/`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); +}; + +export const listSections = (client: BackendAPIClient, pageId: string) => () => { + if (!pageId) return Promise.resolve([]); + return client.get(`v1/admin-api/cms/page/${pageId}/section/`); +}; + +export const bulkUpdateSections = + (client: BackendAPIClient, pageId: string) => (data: { sections: BackendAdminAPISchemas.PageSectionBulkUpdateSchema[] }) => + client.put( + `v1/admin-api/cms/page/${pageId}/section/bulk-update/`, + data ); - export const previewModificationAudit = - (client: BackendAPIClient, id: string) => - () => - client.get>(`v1/admin-api/modification-audit/modification-audit/${id}/preview/`); -} - -export default BackendAdminAPIs; +export const approveModificationAudit = (client: BackendAPIClient, id: string) => (reason?: string | null) => + client.patch( + `v1/admin-api/modification-audit/modification-audit/${id}/approve/`, + { reason: reason ?? null } + ); + +export const rejectModificationAudit = (client: BackendAPIClient, id: string) => (reason?: string | null) => + client.patch( + `v1/admin-api/modification-audit/modification-audit/${id}/reject/`, + { reason: reason ?? null } + ); + +export const previewModificationAudit = + (client: BackendAPIClient, id: string) => + () => + client.get>(`v1/admin-api/modification-audit/modification-audit/${id}/preview/`); diff --git a/packages/common/src/apis/client.ts b/packages/common/src/apis/client.ts index f8506bdb..958b461c 100644 --- a/packages/common/src/apis/client.ts +++ b/packages/common/src/apis/client.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import * as R from "remeda"; -import BackendAPISchemas from "../schemas/backendAPI"; +import * as BackendAPISchemas from "../schemas/backendAPI"; import { getCookie } from "../utils/cookie"; const DEFAULT_ERROR_MESSAGE = "알 수 없는 문제가 발생했습니다, 잠시 후 다시 시도해주세요."; diff --git a/packages/common/src/apis/index.ts b/packages/common/src/apis/index.ts index 0166366f..664752cd 100644 --- a/packages/common/src/apis/index.ts +++ b/packages/common/src/apis/index.ts @@ -1,17 +1,13 @@ import { BackendAPIClient, BackendAPIClientError as _BackendAPIClientError } from "./client"; -import BackendAPISchemas from "../schemas/backendAPI"; +import * as 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) => client.get(`v1/cms/page/${id}/`); - export const listSponsors = (client: BackendAPIClient) => () => client.get("v1/event/sponsor/"); - export const listSessions = (client: BackendAPIClient, params?: BackendAPISchemas.SessionQueryParameterSchema) => () => - client.get("v1/event/presentation/", { params }); - export const retrieveSession = (client: BackendAPIClient) => (id: string) => { - if (!id) return Promise.resolve(null); - return client.get(`v1/event/presentation/${id}/`); - }; -} - -export default BackendAPIs; +export const BackendAPIClientError = _BackendAPIClientError; +export const listSiteMaps = (client: BackendAPIClient) => () => client.get("v1/cms/sitemap/"); +export const retrievePage = (client: BackendAPIClient) => (id: string) => client.get(`v1/cms/page/${id}/`); +export const listSponsors = (client: BackendAPIClient) => () => client.get("v1/event/sponsor/"); +export const listSessions = (client: BackendAPIClient, params?: BackendAPISchemas.SessionQueryParameterSchema) => () => + client.get("v1/event/presentation/", { params }); +export const retrieveSession = (client: BackendAPIClient) => (id: string) => { + if (!id) return Promise.resolve(null); + return client.get(`v1/event/presentation/${id}/`); +}; diff --git a/packages/common/src/apis/participant_portal_api.ts b/packages/common/src/apis/participant_portal_api.ts index 355b0fd0..b09c0f49 100644 --- a/packages/common/src/apis/participant_portal_api.ts +++ b/packages/common/src/apis/participant_portal_api.ts @@ -1,80 +1,76 @@ import { BackendAPIClient } from "./client"; -import ParticipantPortalAPISchemas from "../schemas/backendParticipantPortalAPI"; - -namespace BackendParticipantPortalAPIs { - export const me = (client: BackendAPIClient) => async () => { - try { - return await client.get("v1/participant-portal/user/me/"); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { - return null; - } - }; - - export const updateMe = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.UserUpdateSchema) => - client.patch("v1/participant-portal/user/me/", data); - - export const previewMeModAudit = (client: BackendAPIClient) => async () => - client.get("v1/participant-portal/user/me/preview/"); - - export const signIn = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.UserSignInSchema) => - client.post("v1/participant-portal/user/signin/", data); - - export const signOut = (client: BackendAPIClient) => () => client.delete("v1/participant-portal/user/signout/"); - - export const changePassword = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.UserChangePasswordSchema) => - client.put("v1/participant-portal/user/password/", data); - - export const listPublicFiles = (client: BackendAPIClient) => () => - client.get("v1/participant-portal/public-file/"); - - export const uploadPublicFile = (client: BackendAPIClient) => (file: File) => { - const formData = new FormData(); - formData.append("file", file); - return client.post(`v1/participant-portal/public-file/upload/`, formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); - }; - - export const listPresentations = (client: BackendAPIClient) => () => - client.get("v1/participant-portal/presentation/"); - - export const retrievePresentation = (client: BackendAPIClient, id: string) => () => { - if (!id) return Promise.resolve(null); - return client.get(`v1/participant-portal/presentation/${id}/`); - }; - - export const previewPresentationModAudit = (client: BackendAPIClient, id: string) => () => { - if (!id) return Promise.resolve(null); - return client.get(`v1/participant-portal/presentation/${id}/preview/`); - }; - - export const patchPresentation = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.PresentationUpdateSchema) => - client.patch( - `v1/participant-portal/presentation/${data.id}/`, - data - ); - - export const listModificationAudits = (client: BackendAPIClient) => () => - client.get("v1/participant-portal/modification-audit/"); - - export const previewModificationAudit = (client: BackendAPIClient, id: string) => () => { - try { - return client.get(`v1/participant-portal/modification-audit/${id}/preview/`); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { - return Promise.resolve(null); - } - }; - - export const retrieveModificationAudit = (client: BackendAPIClient, id: string) => () => - client.get(`v1/participant-portal/modification-audit/${id}`); - - export const cancelModificationAudit = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.ModificationAuditCancelRequestSchema) => - client.patch( - `v1/participant-portal/modification-audit/${data.id}/cancel/`, - data - ); -} - -export default BackendParticipantPortalAPIs; +import * as ParticipantPortalAPISchemas from "../schemas/backendParticipantPortalAPI"; + +export const me = (client: BackendAPIClient) => async () => { + try { + return await client.get("v1/participant-portal/user/me/"); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + return null; + } +}; + +export const updateMe = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.UserUpdateSchema) => + client.patch("v1/participant-portal/user/me/", data); + +export const previewMeModAudit = (client: BackendAPIClient) => async () => + client.get("v1/participant-portal/user/me/preview/"); + +export const signIn = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.UserSignInSchema) => + client.post("v1/participant-portal/user/signin/", data); + +export const signOut = (client: BackendAPIClient) => () => client.delete("v1/participant-portal/user/signout/"); + +export const changePassword = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.UserChangePasswordSchema) => + client.put("v1/participant-portal/user/password/", data); + +export const listPublicFiles = (client: BackendAPIClient) => () => + client.get("v1/participant-portal/public-file/"); + +export const uploadPublicFile = (client: BackendAPIClient) => (file: File) => { + const formData = new FormData(); + formData.append("file", file); + return client.post(`v1/participant-portal/public-file/upload/`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); +}; + +export const listPresentations = (client: BackendAPIClient) => () => + client.get("v1/participant-portal/presentation/"); + +export const retrievePresentation = (client: BackendAPIClient, id: string) => () => { + if (!id) return Promise.resolve(null); + return client.get(`v1/participant-portal/presentation/${id}/`); +}; + +export const previewPresentationModAudit = (client: BackendAPIClient, id: string) => () => { + if (!id) return Promise.resolve(null); + return client.get(`v1/participant-portal/presentation/${id}/preview/`); +}; + +export const patchPresentation = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.PresentationUpdateSchema) => + client.patch( + `v1/participant-portal/presentation/${data.id}/`, + data + ); + +export const listModificationAudits = (client: BackendAPIClient) => () => + client.get("v1/participant-portal/modification-audit/"); + +export const previewModificationAudit = (client: BackendAPIClient, id: string) => () => { + try { + return client.get(`v1/participant-portal/modification-audit/${id}/preview/`); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + return Promise.resolve(null); + } +}; + +export const retrieveModificationAudit = (client: BackendAPIClient, id: string) => () => + client.get(`v1/participant-portal/modification-audit/${id}`); + +export const cancelModificationAudit = (client: BackendAPIClient) => (data: ParticipantPortalAPISchemas.ModificationAuditCancelRequestSchema) => + client.patch( + `v1/participant-portal/modification-audit/${data.id}/cancel/`, + data + ); diff --git a/packages/common/src/apis/session.ts b/packages/common/src/apis/session.ts index 0bfdd2fe..c3987088 100644 --- a/packages/common/src/apis/session.ts +++ b/packages/common/src/apis/session.ts @@ -1,14 +1,10 @@ import { BackendAPIClient } from "./client"; -import BackendSessionAPISchemas from "../schemas/backendSessionAPI"; +import * as BackendSessionAPISchemas from "../schemas/backendSessionAPI"; -namespace SessionAPIs { - export const sessionList = (client: BackendAPIClient) => async () => { - return await client.get("v1/event/presentation/"); - }; +export const sessionList = (client: BackendAPIClient) => async () => { + return await client.get("v1/event/presentation/"); +}; - export const sessionFilteredList = (client: BackendAPIClient, categoryName: string) => async () => { - return await client.get(`v1/event/presentation/?category=${categoryName}`); - }; -} - -export default SessionAPIs; +export const sessionFilteredList = (client: BackendAPIClient, categoryName: string) => async () => { + return await client.get(`v1/event/presentation/?category=${categoryName}`); +}; diff --git a/packages/common/src/components/common_context.tsx b/packages/common/src/components/common_context.tsx index e7981680..a3a2c594 100644 --- a/packages/common/src/components/common_context.tsx +++ b/packages/common/src/components/common_context.tsx @@ -1,12 +1,12 @@ import * as React from "react"; -import GlobalContext from "../contexts"; +import { context, ContextOptions } from "../contexts"; type CommonContextProps = { - options: GlobalContext.ContextOptions; + options: ContextOptions; children: React.ReactNode; }; export const CommonContextProvider: React.FC = (props) => ( - {props.children} + {props.children} ); diff --git a/packages/common/src/components/error_handler.tsx b/packages/common/src/components/error_handler.tsx index 345590b6..643e41fb 100644 --- a/packages/common/src/components/error_handler.tsx +++ b/packages/common/src/components/error_handler.tsx @@ -2,7 +2,7 @@ import { Button, Typography } from "@mui/material"; import { Suspense } from "@suspensive/react"; import * as React from "react"; -import CommonContext from "../hooks/"; +import * as CommonContext from "../hooks"; const DetailedErrorFallback: React.FC<{ error: Error; reset: () => void }> = ({ error, reset }) => { console.error(error); diff --git a/packages/common/src/components/mdx.tsx b/packages/common/src/components/mdx.tsx index 146d9536..0afd1d1c 100644 --- a/packages/common/src/components/mdx.tsx +++ b/packages/common/src/components/mdx.tsx @@ -9,7 +9,7 @@ import * as runtime from "react/jsx-runtime"; import remarkGfm from "remark-gfm"; import * as R from "remeda"; -import Hooks from "../hooks"; +import * as Hooks from "../hooks"; import { ErrorFallback } from "./error_handler"; import { LinkHandler } from "./link_handler"; import { rtrim } from "../utils/string"; diff --git a/packages/common/src/components/mdx_components/session_list.tsx b/packages/common/src/components/mdx_components/session_list.tsx index e905e3f3..97e77c7d 100644 --- a/packages/common/src/components/mdx_components/session_list.tsx +++ b/packages/common/src/components/mdx_components/session_list.tsx @@ -5,8 +5,8 @@ import { Link } from "react-router-dom"; import * as R from "remeda"; import PyCon2025Logo from "../../assets/pyconkr2025_logo.png"; -import Hooks from "../../hooks"; -import BackendAPISchemas from "../../schemas/backendAPI"; +import * as Hooks from "../../hooks"; +import * as BackendAPISchemas from "../../schemas/backendAPI"; import { ErrorFallback } from "../error_handler"; import { FallbackImage } from "../fallback_image"; import { StyledDivider } from "./styled_divider"; diff --git a/packages/common/src/components/mdx_components/session_timetable.tsx b/packages/common/src/components/mdx_components/session_timetable.tsx index 9f02cf58..a646c0a9 100644 --- a/packages/common/src/components/mdx_components/session_timetable.tsx +++ b/packages/common/src/components/mdx_components/session_timetable.tsx @@ -5,8 +5,8 @@ import * as React from "react"; import { Link } from "react-router-dom"; import * as R from "remeda"; -import Hooks from "../../hooks"; -import BackendAPISchemas from "../../schemas/backendAPI"; +import * as Hooks from "../../hooks"; +import * as BackendAPISchemas from "../../schemas/backendAPI"; import { CenteredPage } from "../centered_page"; import { ErrorFallback } from "../error_handler"; import { StyledDivider } from "./styled_divider"; diff --git a/packages/common/src/components/mdx_editor.tsx b/packages/common/src/components/mdx_editor.tsx index f83a0035..2fa500c2 100644 --- a/packages/common/src/components/mdx_editor.tsx +++ b/packages/common/src/components/mdx_editor.tsx @@ -8,7 +8,7 @@ import * as React from "react"; import * as R from "remeda"; // import * as CryptoJS from "crypto-js"; -import Hooks from "../hooks"; +import * as Hooks from "../hooks"; type CustomComponentInfoType = { k: string; // key diff --git a/packages/common/src/contexts/index.ts b/packages/common/src/contexts/index.ts index 69ffc1d2..421d589c 100644 --- a/packages/common/src/contexts/index.ts +++ b/packages/common/src/contexts/index.ts @@ -1,27 +1,23 @@ import { MDXComponents } from "mdx/types"; import * as React from "react"; -namespace GlobalContext { - export type ContextOptions = { - language: "ko" | "en"; - frontendDomain?: string; - baseUrl: string; - debug?: boolean; - backendApiDomain: string; - backendApiTimeout: number; - backendApiCSRFCookieName?: string; - mdxComponents?: MDXComponents; - }; +export type ContextOptions = { + language: "ko" | "en"; + frontendDomain?: string; + baseUrl: string; + debug?: boolean; + backendApiDomain: string; + backendApiTimeout: number; + backendApiCSRFCookieName?: string; + mdxComponents?: MDXComponents; +}; - export const context = React.createContext({ - language: "ko", - frontendDomain: "", - baseUrl: "", - debug: false, - backendApiDomain: "", - backendApiTimeout: 10000, - backendApiCSRFCookieName: "", - }); -} - -export default GlobalContext; +export const context = React.createContext({ + language: "ko", + frontendDomain: "", + baseUrl: "", + debug: false, + backendApiDomain: "", + backendApiTimeout: 10000, + backendApiCSRFCookieName: "", +}); diff --git a/packages/common/src/hooks/index.ts b/packages/common/src/hooks/index.ts index 7dee57f7..cc174e5b 100644 --- a/packages/common/src/hooks/index.ts +++ b/packages/common/src/hooks/index.ts @@ -1,19 +1,7 @@ -import BackendAPIHooks from "./useAPI"; -import BackendAdminAPIHooks from "./useAdminAPI"; -import { useCommonContext as useCommonContextHook } from "./useCommonContext"; -import { useEmail as useEmailHook } from "./useEmail"; -import BackendParticipantPortalAPIHooks from "./useParticipantPortalAPI"; +import { useCommonContext } from "./useCommonContext"; +import { useEmail } from "./useEmail"; -export namespace CommonHooks { - export const useCommonContext = useCommonContextHook; - export const useEmail = useEmailHook; -} - -namespace Hooks { - export const Common = CommonHooks; - export const BackendAPI = BackendAPIHooks; - export const BackendAdminAPI = BackendAdminAPIHooks; - export const BackendParticipantPortalAPI = BackendParticipantPortalAPIHooks; -} - -export default Hooks; +export * as BackendAPI from "./useAPI"; +export * as BackendAdminAPI from "./useAdminAPI"; +export * as BackendParticipantPortalAPI from "./useParticipantPortalAPI"; +export const Common = { useCommonContext, useEmail }; diff --git a/packages/common/src/hooks/useAPI.ts b/packages/common/src/hooks/useAPI.ts index 10922c6e..3cc90d9e 100644 --- a/packages/common/src/hooks/useAPI.ts +++ b/packages/common/src/hooks/useAPI.ts @@ -1,10 +1,10 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import * as React from "react"; -import BackendAPIs from "../apis"; +import * as BackendAPIs from "../apis"; import { BackendAPIClient } from "../apis/client"; -import BackendContext from "../contexts"; -import BackendAPISchemas from "../schemas/backendAPI"; +import { context as backendContext } from "../contexts"; +import * as BackendAPISchemas from "../schemas/backendAPI"; const QUERY_KEYS = { SITEMAP_LIST: ["query", "sitemap", "list"], @@ -13,47 +13,43 @@ const QUERY_KEYS = { SESSION_LIST: ["query", "session", "list"], }; -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; - }; - - export const useBackendClient = () => { - const { language, backendApiDomain, backendApiTimeout } = useBackendContext(); - return new BackendAPIClient(backendApiDomain, backendApiTimeout, "", false, language); - }; - - export const useFlattenSiteMapQuery = (client: BackendAPIClient) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.SITEMAP_LIST, client.language], - queryFn: BackendAPIs.listSiteMaps(client), - }); - - export const usePageQuery = (client: BackendAPIClient, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PAGE, id, client.language], - queryFn: () => BackendAPIs.retrievePage(client)(id), - }); - - export const useSponsorQuery = (client: BackendAPIClient) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.SPONSOR_LIST, client.language], - queryFn: BackendAPIs.listSponsors(client), - }); - - export const useSessionsQuery = (client: BackendAPIClient, params?: BackendAPISchemas.SessionQueryParameterSchema) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.SESSION_LIST, client.language, ...(params ? [JSON.stringify(params)] : [])], - queryFn: BackendAPIs.listSessions(client, params), - }); - - export const useSessionQuery = (client: BackendAPIClient, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.SESSION_LIST, id, client.language], - queryFn: () => BackendAPIs.retrieveSession(client)(id), - }); -} - -export default BackendAPIHooks; +export const useBackendContext = () => { + const ctx = React.useContext(backendContext); + if (!ctx) throw new Error("useBackendContext must be used within a CommonProvider"); + return ctx; +}; + +export const useBackendClient = () => { + const { language, backendApiDomain, backendApiTimeout } = useBackendContext(); + return new BackendAPIClient(backendApiDomain, backendApiTimeout, "", false, language); +}; + +export const useFlattenSiteMapQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.SITEMAP_LIST, client.language], + queryFn: BackendAPIs.listSiteMaps(client), + }); + +export const usePageQuery = (client: BackendAPIClient, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PAGE, id, client.language], + queryFn: () => BackendAPIs.retrievePage(client)(id), + }); + +export const useSponsorQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.SPONSOR_LIST, client.language], + queryFn: BackendAPIs.listSponsors(client), + }); + +export const useSessionsQuery = (client: BackendAPIClient, params?: BackendAPISchemas.SessionQueryParameterSchema) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.SESSION_LIST, client.language, ...(params ? [JSON.stringify(params)] : [])], + queryFn: BackendAPIs.listSessions(client, params), + }); + +export const useSessionQuery = (client: BackendAPIClient, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.SESSION_LIST, id, client.language], + queryFn: () => BackendAPIs.retrieveSession(client)(id), + }); diff --git a/packages/common/src/hooks/useAdminAPI.ts b/packages/common/src/hooks/useAdminAPI.ts index 197ed2e9..8ea31331 100644 --- a/packages/common/src/hooks/useAdminAPI.ts +++ b/packages/common/src/hooks/useAdminAPI.ts @@ -1,9 +1,9 @@ import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; -import BackendAPIHooks from "./useAPI"; -import BackendAdminAPIs from "../apis/admin_api"; +import { useBackendContext } from "./useAPI"; +import * as BackendAdminAPIs from "../apis/admin_api"; import { BackendAPIClient } from "../apis/client"; -import BackendAdminAPISchemas from "../schemas/backendAdminAPI"; +import * as BackendAdminAPISchemas from "../schemas/backendAdminAPI"; const QUERY_KEYS = { ADMIN_ME: ["query", "admin", "me"], @@ -25,131 +25,127 @@ const MUTATION_KEYS = { ADMIN_REJECT_MODIFICATION_AUDIT: ["mutation", "admin", "reject", "modification-audit"], }; -namespace BackendAdminAPIHooks { - export const useBackendAdminClient = () => { - const { backendApiDomain, backendApiTimeout, backendApiCSRFCookieName } = BackendAPIHooks.useBackendContext(); - return new BackendAPIClient(backendApiDomain, backendApiTimeout, backendApiCSRFCookieName, true); - }; - - export const useSignedInUserQuery = (client: BackendAPIClient) => - useSuspenseQuery({ - queryKey: QUERY_KEYS.ADMIN_ME, - queryFn: BackendAdminAPIs.me(client), - }); - - export const useSignInMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_SIGN_IN], - mutationFn: BackendAdminAPIs.signIn(client), - }); - - export const useSignOutMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_SIGN_OUT], - mutationFn: BackendAdminAPIs.signOut(client), - }); - - export const useChangePasswordMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_CHANGE_PASSWORD], - mutationFn: BackendAdminAPIs.changePassword(client), - }); - - export const useResetUserPasswordMutation = (client: BackendAPIClient, id: string) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_RESET_PASSWORD, id], - mutationFn: BackendAdminAPIs.resetUserPassword(client, id), - }); - - export const useSchemaQuery = (client: BackendAPIClient, app: string, resource: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.ADMIN_SCHEMA, app, resource], - queryFn: BackendAdminAPIs.schema(client, app, resource), - }); - - export const useListQuery = (client: BackendAPIClient, app: string, resource: string, params?: Record) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.ADMIN_LIST, app, resource, JSON.stringify(params)], - queryFn: BackendAdminAPIs.list(client, app, resource, params), - }); - - export const useRetrieveQuery = (client: BackendAPIClient, app: string, resource: string, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.ADMIN_RETRIEVE, app, resource, id], - queryFn: BackendAdminAPIs.retrieve(client, app, resource, id), - }); - - export const useCreateMutation = (client: BackendAPIClient, app: string, resource: string) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_CREATE, app, resource], - mutationFn: BackendAdminAPIs.create(client, app, resource), - }); - - export const useUpdateMutation = (client: BackendAPIClient, app: string, resource: string, id: string) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_UPDATE, app, resource, id], - mutationFn: BackendAdminAPIs.update(client, app, resource, id), - }); - - export const useUpdatePreparedMutation = (client: BackendAPIClient, app: string, resource: string) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_UPDATE, app, resource, "prepared"], - mutationFn: BackendAdminAPIs.updatePrepared(client, app, resource), - }); - - export const useRemoveMutation = (client: BackendAPIClient, app: string, resource: string, id: string) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_REMOVE, app, resource, id], - mutationFn: BackendAdminAPIs.remove(client, app, resource, id), - }); - - export const useRemovePreparedMutation = (client: BackendAPIClient, app: string, resource: string) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_REMOVE, app, resource, "prepared"], - mutationFn: BackendAdminAPIs.removePrepared(client, app, resource), - }); - - export const usePublicFileQuery = (client: BackendAPIClient, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.ADMIN_RETRIEVE, "file", "publicfile", id], - queryFn: BackendAdminAPIs.retrieve(client, "file", "publicfile", id), - }); - - export const useUploadPublicFileMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_CREATE, "public-file", "upload"], - mutationFn: BackendAdminAPIs.uploadPublicFile(client), - }); - - export const useListPageSectionsQuery = (client: BackendAPIClient, pageId: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.ADMIN_LIST, "cms", "page", pageId, "section"], - queryFn: BackendAdminAPIs.listSections(client, pageId), - }); - - export const useBulkUpdatePageSectionsMutation = (client: BackendAPIClient, pageId: string) => - useMutation({ - mutationKey: [...MUTATION_KEYS.ADMIN_UPDATE, "cms", "page", pageId, "section"], - mutationFn: BackendAdminAPIs.bulkUpdateSections(client, pageId), - }); - - export const useModificationAuditPreviewQuery = (client: BackendAPIClient, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.ADMIN_PREVIEW_MODIFICATION_AUDIT, id], - queryFn: BackendAdminAPIs.previewModificationAudit(client, id), - }); - - export const useApproveModificationAuditMutation = (client: BackendAPIClient, id: string) => - useMutation({ - mutationKey: MUTATION_KEYS.ADMIN_APPROVE_MODIFICATION_AUDIT, - mutationFn: BackendAdminAPIs.approveModificationAudit(client, id), - }); - - export const useRejectModificationAuditMutation = (client: BackendAPIClient, id: string) => - useMutation({ - mutationKey: MUTATION_KEYS.ADMIN_REJECT_MODIFICATION_AUDIT, - mutationFn: BackendAdminAPIs.rejectModificationAudit(client, id), - }); -} - -export default BackendAdminAPIHooks; +export const useBackendAdminClient = () => { + const { backendApiDomain, backendApiTimeout, backendApiCSRFCookieName } = useBackendContext(); + return new BackendAPIClient(backendApiDomain, backendApiTimeout, backendApiCSRFCookieName, true); +}; + +export const useSignedInUserQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: QUERY_KEYS.ADMIN_ME, + queryFn: BackendAdminAPIs.me(client), + }); + +export const useSignInMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_SIGN_IN], + mutationFn: BackendAdminAPIs.signIn(client), + }); + +export const useSignOutMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_SIGN_OUT], + mutationFn: BackendAdminAPIs.signOut(client), + }); + +export const useChangePasswordMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_CHANGE_PASSWORD], + mutationFn: BackendAdminAPIs.changePassword(client), + }); + +export const useResetUserPasswordMutation = (client: BackendAPIClient, id: string) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_RESET_PASSWORD, id], + mutationFn: BackendAdminAPIs.resetUserPassword(client, id), + }); + +export const useSchemaQuery = (client: BackendAPIClient, app: string, resource: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.ADMIN_SCHEMA, app, resource], + queryFn: BackendAdminAPIs.schema(client, app, resource), + }); + +export const useListQuery = (client: BackendAPIClient, app: string, resource: string, params?: Record) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.ADMIN_LIST, app, resource, JSON.stringify(params)], + queryFn: BackendAdminAPIs.list(client, app, resource, params), + }); + +export const useRetrieveQuery = (client: BackendAPIClient, app: string, resource: string, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.ADMIN_RETRIEVE, app, resource, id], + queryFn: BackendAdminAPIs.retrieve(client, app, resource, id), + }); + +export const useCreateMutation = (client: BackendAPIClient, app: string, resource: string) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_CREATE, app, resource], + mutationFn: BackendAdminAPIs.create(client, app, resource), + }); + +export const useUpdateMutation = (client: BackendAPIClient, app: string, resource: string, id: string) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_UPDATE, app, resource, id], + mutationFn: BackendAdminAPIs.update(client, app, resource, id), + }); + +export const useUpdatePreparedMutation = (client: BackendAPIClient, app: string, resource: string) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_UPDATE, app, resource, "prepared"], + mutationFn: BackendAdminAPIs.updatePrepared(client, app, resource), + }); + +export const useRemoveMutation = (client: BackendAPIClient, app: string, resource: string, id: string) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_REMOVE, app, resource, id], + mutationFn: BackendAdminAPIs.remove(client, app, resource, id), + }); + +export const useRemovePreparedMutation = (client: BackendAPIClient, app: string, resource: string) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_REMOVE, app, resource, "prepared"], + mutationFn: BackendAdminAPIs.removePrepared(client, app, resource), + }); + +export const usePublicFileQuery = (client: BackendAPIClient, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.ADMIN_RETRIEVE, "file", "publicfile", id], + queryFn: BackendAdminAPIs.retrieve(client, "file", "publicfile", id), + }); + +export const useUploadPublicFileMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_CREATE, "public-file", "upload"], + mutationFn: BackendAdminAPIs.uploadPublicFile(client), + }); + +export const useListPageSectionsQuery = (client: BackendAPIClient, pageId: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.ADMIN_LIST, "cms", "page", pageId, "section"], + queryFn: BackendAdminAPIs.listSections(client, pageId), + }); + +export const useBulkUpdatePageSectionsMutation = (client: BackendAPIClient, pageId: string) => + useMutation({ + mutationKey: [...MUTATION_KEYS.ADMIN_UPDATE, "cms", "page", pageId, "section"], + mutationFn: BackendAdminAPIs.bulkUpdateSections(client, pageId), + }); + +export const useModificationAuditPreviewQuery = (client: BackendAPIClient, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.ADMIN_PREVIEW_MODIFICATION_AUDIT, id], + queryFn: BackendAdminAPIs.previewModificationAudit(client, id), + }); + +export const useApproveModificationAuditMutation = (client: BackendAPIClient, id: string) => + useMutation({ + mutationKey: MUTATION_KEYS.ADMIN_APPROVE_MODIFICATION_AUDIT, + mutationFn: BackendAdminAPIs.approveModificationAudit(client, id), + }); + +export const useRejectModificationAuditMutation = (client: BackendAPIClient, id: string) => + useMutation({ + mutationKey: MUTATION_KEYS.ADMIN_REJECT_MODIFICATION_AUDIT, + mutationFn: BackendAdminAPIs.rejectModificationAudit(client, id), + }); diff --git a/packages/common/src/hooks/useCommonContext.ts b/packages/common/src/hooks/useCommonContext.ts index 36ae9cc5..3cb5e569 100644 --- a/packages/common/src/hooks/useCommonContext.ts +++ b/packages/common/src/hooks/useCommonContext.ts @@ -1,11 +1,11 @@ import * as React from "react"; -import GlobalContext from "../contexts"; +import { context } from "../contexts"; export const useCommonContext = () => { - const context = React.useContext(GlobalContext.context); - if (!context) { + const ctx = React.useContext(context); + if (!ctx) { throw new Error("useCommonContext must be used within a CommonProvider"); } - return context; + return ctx; }; diff --git a/packages/common/src/hooks/useParticipantPortalAPI.ts b/packages/common/src/hooks/useParticipantPortalAPI.ts index 266f7590..f5d4d332 100644 --- a/packages/common/src/hooks/useParticipantPortalAPI.ts +++ b/packages/common/src/hooks/useParticipantPortalAPI.ts @@ -1,8 +1,8 @@ import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; -import BackendAPIHooks from "./useAPI"; +import { useBackendContext } from "./useAPI"; import { BackendAPIClient } from "../apis/client"; -import ParticipantPortalAPI from "../apis/participant_portal_api"; +import * as ParticipantPortalAPI from "../apis/participant_portal_api"; const QUERY_KEYS = { PARTICIPANT_ME: ["query", "participant", "me"], @@ -23,107 +23,103 @@ const MUTATION_KEYS = { PARTICIPANT_CANCEL_MODIFICATION_AUDIT: ["mutation", "participant", "cancel", "modification-audit"], }; -namespace BackendParticipantPortalAPIHooks { - export const useParticipantPortalClient = () => { - const { backendApiDomain, backendApiTimeout, backendApiCSRFCookieName, language } = BackendAPIHooks.useBackendContext(); - return new BackendAPIClient(backendApiDomain, backendApiTimeout, backendApiCSRFCookieName, true, language); - }; - - export const useSignedInUserQuery = (client: BackendAPIClient) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_ME, client.language], - queryFn: ParticipantPortalAPI.me(client), - }); - - export const usePreviewMeModAuditQuery = (client: BackendAPIClient) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_ME, "preview", client.language], - queryFn: ParticipantPortalAPI.previewMeModAudit(client), - }); - - export const useUpdateMeMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.PARTICIPANT_UPDATE_ME], - mutationFn: ParticipantPortalAPI.updateMe(client), - }); - - export const useSignInMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.PARTICIPANT_SIGN_IN], - mutationFn: ParticipantPortalAPI.signIn(client), - }); - - export const useSignOutMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.PARTICIPANT_SIGN_OUT], - mutationFn: ParticipantPortalAPI.signOut(client), - }); - - export const useChangePasswordMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.PARTICIPANT_CHANGE_PASSWORD], - mutationFn: ParticipantPortalAPI.changePassword(client), - }); - - export const usePublicFilesQuery = (client: BackendAPIClient) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_PUBLIC_FILES, client.language], - queryFn: ParticipantPortalAPI.listPublicFiles(client), - }); - - export const useUploadPublicFileMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.PARTICIPANT_UPLOAD_PUBLIC_FILE, "upload"], - mutationFn: ParticipantPortalAPI.uploadPublicFile(client), - }); - - export const useListPresentationsQuery = (client: BackendAPIClient) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_LIST_PRESENTATION, client.language], - queryFn: ParticipantPortalAPI.listPresentations(client), - }); - - export const useRetrievePresentationQuery = (client: BackendAPIClient, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_RETRIEVE_PRESENTATION, id, client.language], - queryFn: ParticipantPortalAPI.retrievePresentation(client, id), - }); - - export const useUpdatePresentationMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.PARTICIPANT_UPDATE_PRESENTATION], - mutationFn: ParticipantPortalAPI.patchPresentation(client), - }); - - export const usePreviewPresentationModAuditQuery = (client: BackendAPIClient, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_RETRIEVE_PRESENTATION, id, "preview", client.language], - queryFn: ParticipantPortalAPI.previewPresentationModAudit(client, id), - }); - - export const useModificationAuditsQuery = (client: BackendAPIClient) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_LIST_MODIFICATION_AUDIT, client.language], - queryFn: ParticipantPortalAPI.listModificationAudits(client), - }); - - export const useModificationAuditPreviewQuery = (client: BackendAPIClient, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_LIST_MODIFICATION_AUDIT, "preview", id, client.language], - queryFn: ParticipantPortalAPI.previewModificationAudit(client, id), - }); - - export const useRetrieveModificationAuditQuery = (client: BackendAPIClient, id: string) => - useSuspenseQuery({ - queryKey: [...QUERY_KEYS.PARTICIPANT_RETRIEVE_MODIFICATION_AUDIT, id, client.language], - queryFn: ParticipantPortalAPI.retrieveModificationAudit(client, id), - }); - - export const useCancelModificationAuditMutation = (client: BackendAPIClient) => - useMutation({ - mutationKey: [...MUTATION_KEYS.PARTICIPANT_CANCEL_MODIFICATION_AUDIT], - mutationFn: ParticipantPortalAPI.cancelModificationAudit(client), - }); -} - -export default BackendParticipantPortalAPIHooks; +export const useParticipantPortalClient = () => { + const { backendApiDomain, backendApiTimeout, backendApiCSRFCookieName, language } = useBackendContext(); + return new BackendAPIClient(backendApiDomain, backendApiTimeout, backendApiCSRFCookieName, true, language); +}; + +export const useSignedInUserQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_ME, client.language], + queryFn: ParticipantPortalAPI.me(client), + }); + +export const usePreviewMeModAuditQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_ME, "preview", client.language], + queryFn: ParticipantPortalAPI.previewMeModAudit(client), + }); + +export const useUpdateMeMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.PARTICIPANT_UPDATE_ME], + mutationFn: ParticipantPortalAPI.updateMe(client), + }); + +export const useSignInMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.PARTICIPANT_SIGN_IN], + mutationFn: ParticipantPortalAPI.signIn(client), + }); + +export const useSignOutMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.PARTICIPANT_SIGN_OUT], + mutationFn: ParticipantPortalAPI.signOut(client), + }); + +export const useChangePasswordMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.PARTICIPANT_CHANGE_PASSWORD], + mutationFn: ParticipantPortalAPI.changePassword(client), + }); + +export const usePublicFilesQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_PUBLIC_FILES, client.language], + queryFn: ParticipantPortalAPI.listPublicFiles(client), + }); + +export const useUploadPublicFileMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.PARTICIPANT_UPLOAD_PUBLIC_FILE, "upload"], + mutationFn: ParticipantPortalAPI.uploadPublicFile(client), + }); + +export const useListPresentationsQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_LIST_PRESENTATION, client.language], + queryFn: ParticipantPortalAPI.listPresentations(client), + }); + +export const useRetrievePresentationQuery = (client: BackendAPIClient, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_RETRIEVE_PRESENTATION, id, client.language], + queryFn: ParticipantPortalAPI.retrievePresentation(client, id), + }); + +export const useUpdatePresentationMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.PARTICIPANT_UPDATE_PRESENTATION], + mutationFn: ParticipantPortalAPI.patchPresentation(client), + }); + +export const usePreviewPresentationModAuditQuery = (client: BackendAPIClient, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_RETRIEVE_PRESENTATION, id, "preview", client.language], + queryFn: ParticipantPortalAPI.previewPresentationModAudit(client, id), + }); + +export const useModificationAuditsQuery = (client: BackendAPIClient) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_LIST_MODIFICATION_AUDIT, client.language], + queryFn: ParticipantPortalAPI.listModificationAudits(client), + }); + +export const useModificationAuditPreviewQuery = (client: BackendAPIClient, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_LIST_MODIFICATION_AUDIT, "preview", id, client.language], + queryFn: ParticipantPortalAPI.previewModificationAudit(client, id), + }); + +export const useRetrieveModificationAuditQuery = (client: BackendAPIClient, id: string) => + useSuspenseQuery({ + queryKey: [...QUERY_KEYS.PARTICIPANT_RETRIEVE_MODIFICATION_AUDIT, id, client.language], + queryFn: ParticipantPortalAPI.retrieveModificationAudit(client, id), + }); + +export const useCancelModificationAuditMutation = (client: BackendAPIClient) => + useMutation({ + mutationKey: [...MUTATION_KEYS.PARTICIPANT_CANCEL_MODIFICATION_AUDIT], + mutationFn: ParticipantPortalAPI.cancelModificationAudit(client), + }); diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 3517381a..15451f63 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,8 +1,8 @@ -export { default as BackendAdminAPIs } from "./apis/admin_api"; -export { default as BackendAPIs } from "./apis/index"; -export { default as BackendParticipantPortalAPIs } from "./apis/participant_portal_api"; +export * as BackendAdminAPIs from "./apis/admin_api"; +export * as BackendAPIs from "./apis/index"; +export * as BackendParticipantPortalAPIs from "./apis/participant_portal_api"; 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"; +export * as Contexts from "./contexts/index"; +export * as Hooks from "./hooks/index"; +export * as Schemas from "./schemas/index"; +export * as Utils from "./utils/index"; diff --git a/packages/common/src/schemas/backendAPI.ts b/packages/common/src/schemas/backendAPI.ts index be05f0b2..5b7ba581 100644 --- a/packages/common/src/schemas/backendAPI.ts +++ b/packages/common/src/schemas/backendAPI.ts @@ -1,116 +1,112 @@ import * as R from "remeda"; -namespace BackendAPISchemas { - export type EmptyObject = Record; +export type EmptyObject = Record; - export type DetailedErrorSchema = { - code: string; - detail: string; - attr: string | null; - }; +export type DetailedErrorSchema = { + code: string; + detail: string; + attr: string | null; +}; - export type ErrorResponseSchema = { - type: string; - errors: DetailedErrorSchema[]; - }; +export type ErrorResponseSchema = { + type: string; + errors: DetailedErrorSchema[]; +}; - export type FlattenedSiteMapSchema = { - id: string; - route_code: string; - name: string; - order: number; - parent_sitemap: string | null; - hide: boolean; - page: string | null; - external_link: string | null; - }; +export type FlattenedSiteMapSchema = { + id: string; + route_code: string; + name: string; + order: number; + parent_sitemap: string | null; + hide: boolean; + page: string | null; + external_link: string | null; +}; - export type NestedSiteMapSchema = { - id: string; - route_code: string; - name: string; - order: number; - hide: boolean; - parent_sitemap: string | null; - children: NestedSiteMapSchema[]; - page: string | null; - external_link: string | null; - }; +export type NestedSiteMapSchema = { + id: string; + route_code: string; + name: string; + order: number; + hide: boolean; + parent_sitemap: string | null; + children: NestedSiteMapSchema[]; + page: string | null; + external_link: string | null; +}; - export type SectionSchema = { - id: string; - css: string; +export type SectionSchema = { + id: string; + css: string; - order: number; - body: string; - }; + order: number; + body: string; +}; - export type PageSchema = { - id: string; - css: string; - title: string; - subtitle: string; +export type PageSchema = { + id: string; + css: string; + title: string; + subtitle: string; - show_top_title_banner: boolean; - show_bottom_sponsor_banner: boolean; + show_top_title_banner: boolean; + show_bottom_sponsor_banner: boolean; - sections: SectionSchema[]; - }; + sections: SectionSchema[]; +}; - export type SponsorTierSchema = { +export type SponsorTierSchema = { + id: string; + name: string; + order: number; + sponsors: { id: string; name: string; - order: number; - sponsors: { - id: string; - name: string; - logo: string; - description: string; - tags: string[]; - }[]; - }; + logo: string; + description: string; + tags: string[]; + }[]; +}; - export type SessionQueryParameterSchema = { - event?: string; - types?: string; - }; +export type SessionQueryParameterSchema = { + event?: string; + types?: string; +}; - export type SessionSchema = { +export type SessionSchema = { + id: string; + title: string; + summary: string | null; + description: string; + slideshow_url: string | null; + public_slideshow_file: string | null; + image: string | null; + categories: { id: string; - title: string; - summary: string | null; - description: string; - slideshow_url: string | null; - public_slideshow_file: string | null; + name: string; + }[]; + speakers: { + id: string; + nickname: string; + biography: string; image: string | null; - categories: { - id: string; - name: string; - }[]; - speakers: { - id: string; - nickname: string; - biography: string; - image: string | null; - }[]; - room_schedules: { - id: string; - room_name: string; - start_at: string; - end_at: string; - }[]; - }; - - 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)); - }) - ); - }; -} + }[]; + room_schedules: { + id: string; + room_name: string; + start_at: string; + end_at: string; + }[]; +}; -export default BackendAPISchemas; +export const isObjectErrorResponseSchema = (obj?: unknown): obj is 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)); + }) + ); +}; diff --git a/packages/common/src/schemas/backendAdminAPI.ts b/packages/common/src/schemas/backendAdminAPI.ts index f4b2d84f..645aa53e 100644 --- a/packages/common/src/schemas/backendAdminAPI.ts +++ b/packages/common/src/schemas/backendAdminAPI.ts @@ -1,138 +1,134 @@ import { RJSFSchema, UiSchema } from "@rjsf/utils"; -namespace BackendAdminAPISchemas { - export type DetailedErrorSchema = { - code: string; - detail: string; - attr: string | null; - }; - - export type ErrorResponseSchema = { - type: string; - errors: DetailedErrorSchema[]; - }; - - export type AdminSchemaDefinition = { - schema: RJSFSchema; - ui_schema: UiSchema; - translation_fields: string[]; - }; - - export type UserSchema = { - id: number; - username: string; - email: string; - first_name: string; - last_name: string; - is_staff: boolean; - is_active: boolean; - date_joined: string; // ISO 8601 format - }; - - export type UserSignInSchema = { - identity: string; // username or email - password: string; - }; - - export type UserChangePasswordSchema = { - old_password: string; - new_password: string; - new_password_confirm: string; - }; - - export type UserResetPasswordResponseSchema = { - password: string; - }; - - export type PublicFileSchema = { - id: string; // UUID - file: string; // URL to the public file - mimetype: string | null; // MIME type of the file - hash: string; // Hash of the file for integrity check - size: number; // Size of the file in bytes - }; - - export type PageSectionSchema = { - id?: string; - order: number; - css: string; - body_ko: string | null; - body_en: string | null; - }; - - export type FlattenedSiteMapSchema = { - id: string; - route_code: string; - name_ko: string; - name_en: string; - order: number; - parent_sitemap: string | null; - hide: boolean; - page: string | null; - external_link: string | null; - }; - - export type NestedSiteMapSchema = { - id: string; - route_code: string; - name_ko: string; - name_en: string; - order: number; - parent_sitemap: string | null; - hide: boolean; - children: NestedSiteMapSchema[]; - page: string | null; - external_link: string | null; - }; - - export type PageSectionBulkUpdateSchema = PageSectionSchema | Omit; - - export type PresentationSchema = { - id: string; // UUID - type: string; // UUID of the presentation type - categories: string[]; // Array of category UUIDs - title_ko: string; - title_en: string; - summary_ko: string; - summary_en: string; - description_ko: string; - description_en: string; - slideshow_url: string | null; - image: string | null; - }; - - export type ModificationAuditSchema = { - id: string; // UUID - status: "requested" | "approved" | "rejected" | "cancelled"; // Status of the modification request +export type DetailedErrorSchema = { + code: string; + detail: string; + attr: string | null; +}; + +export type ErrorResponseSchema = { + type: string; + errors: DetailedErrorSchema[]; +}; + +export type AdminSchemaDefinition = { + schema: RJSFSchema; + ui_schema: UiSchema; + translation_fields: string[]; +}; + +export type UserSchema = { + id: number; + username: string; + email: string; + first_name: string; + last_name: string; + is_staff: boolean; + is_active: boolean; + date_joined: string; // ISO 8601 format +}; + +export type UserSignInSchema = { + identity: string; // username or email + password: string; +}; + +export type UserChangePasswordSchema = { + old_password: string; + new_password: string; + new_password_confirm: string; +}; + +export type UserResetPasswordResponseSchema = { + password: string; +}; + +export type PublicFileSchema = { + id: string; // UUID + file: string; // URL to the public file + mimetype: string | null; // MIME type of the file + hash: string; // Hash of the file for integrity check + size: number; // Size of the file in bytes +}; + +export type PageSectionSchema = { + id?: string; + order: number; + css: string; + body_ko: string | null; + body_en: string | null; +}; + +export type FlattenedSiteMapSchema = { + id: string; + route_code: string; + name_ko: string; + name_en: string; + order: number; + parent_sitemap: string | null; + hide: boolean; + page: string | null; + external_link: string | null; +}; + +export type NestedSiteMapSchema = { + id: string; + route_code: string; + name_ko: string; + name_en: string; + order: number; + parent_sitemap: string | null; + hide: boolean; + children: NestedSiteMapSchema[]; + page: string | null; + external_link: string | null; +}; + +export type PageSectionBulkUpdateSchema = PageSectionSchema | Omit; + +export type PresentationSchema = { + id: string; // UUID + type: string; // UUID of the presentation type + categories: string[]; // Array of category UUIDs + title_ko: string; + title_en: string; + summary_ko: string; + summary_en: string; + description_ko: string; + description_en: string; + slideshow_url: string | null; + image: string | null; +}; + +export type ModificationAuditSchema = { + id: string; // UUID + status: "requested" | "approved" | "rejected" | "cancelled"; // Status of the modification request + created_at: string; // ISO 8601 timestamp + updated_at: string; // ISO 8601 timestamp + created_by: string; + updated_by: string | null; // User ID of the person who last updated the audit + modification_data: string; // JSON string containing the modification data + str_repr: string; // String representation of the modification audit, e.g., "Presentation Title - Status" + comments: { + id: string; // UUID of the comment + content: string; // Content of the comment created_at: string; // ISO 8601 timestamp - updated_at: string; // ISO 8601 timestamp - created_by: string; - updated_by: string | null; // User ID of the person who last updated the audit - modification_data: string; // JSON string containing the modification data - str_repr: string; // String representation of the modification audit, e.g., "Presentation Title - Status" - comments: { - id: string; // UUID of the comment - content: string; // Content of the comment - created_at: string; // ISO 8601 timestamp - created_by: { - id: number; // User ID of the commenter - nickname: string; // Nickname of the commenter - is_superuser: boolean; // Whether the commenter is a staff member - }; - updated_at: string; // ISO 8601 timestamp - }[]; - instance: { - app: string; - model: string; - id: string; // UUID of the instance being modified, e.g., presentation ID + created_by: { + id: number; // User ID of the commenter + nickname: string; // Nickname of the commenter + is_superuser: boolean; // Whether the commenter is a staff member }; + updated_at: string; // ISO 8601 timestamp + }[]; + instance: { + app: string; + model: string; + id: string; // UUID of the instance being modified, e.g., presentation ID }; +}; - export type ModificationAuditPreviewSchema = { - modification_audit: ModificationAuditSchema; - original: T; - modified: T; - }; -} - -export default BackendAdminAPISchemas; +export type ModificationAuditPreviewSchema = { + modification_audit: ModificationAuditSchema; + original: T; + modified: T; +}; diff --git a/packages/common/src/schemas/backendParticipantPortalAPI.ts b/packages/common/src/schemas/backendParticipantPortalAPI.ts index f10a1e0a..b35339f9 100644 --- a/packages/common/src/schemas/backendParticipantPortalAPI.ts +++ b/packages/common/src/schemas/backendParticipantPortalAPI.ts @@ -1,143 +1,139 @@ -namespace BackendParticipantPortalAPISchemas { - export type EmptyObject = Record; - - export type DetailedErrorSchema = { - code: string; - detail: string; - attr: string | null; - }; - - export type ErrorResponseSchema = { - type: string; - errors: DetailedErrorSchema[]; - }; - - export type UserSchema = { - id: number; - email: string; - username: string; - nickname: string | null; - nickname_ko: string | null; - nickname_en: string | null; - image: string | null; // PK of the user's profile image - profile_image: string | null; // URL to the user's profile image - - has_requested_modification_audit: boolean; - requested_modification_audit_id: string | null; - }; - - export type UserUpdateSchema = { - nickname_ko: string | null; - nickname_en: string | null; - image?: string | null; // PK of the user's profile image - }; - - export type UserSignInSchema = { - identity: string; // email - password: string; - }; - - export type UserChangePasswordSchema = { - old_password: string; - new_password: string; - new_password_confirm: string; - }; - - export type PublicFileSchema = { - id: string; // UUID - file: string; // URL to the public file - name: string; // Name of the public file - }; - - export type PresentationRetrieveSchema = { - id: string; // UUID - title: string; // Title of the presentation, translated to the current language - title_ko: string; // Title in Korean - title_en: string; // Title in English - summary: string; // Summary of the presentation, translated to the current language - summary_ko: string; // Summary in Korean - summary_en: string; // Summary in English - description: string; // Description of the presentation, translated to the current language - description_ko: string; // Description in Korean - description_en: string; // Description in English - slideshow_url: string | null; // URL to the presentation's slideshow, if available - image: string | null; // PK of the presentation's image - speakers: { - id: string; // UUID of the speaker - biography_ko: string; // Biography in Korean - biography_en: string; // Biography in English - image: string | null; // PK of the speaker's image - user: { - id: number; // User ID of the speaker - email: string; // Email of the speaker - nickname_ko: string | null; // Nickname in Korean - nickname_en: string | null; // Nickname in English - }; - }[]; - - has_requested_modification_audit: boolean; - requested_modification_audit_id: string | null; - }; - - export type PresentationUpdateSchema = { - id: string; - title_ko: string; - title_en: string; - summary_ko: string; - summary_en: string; - description_ko: string; - description_en: string; - image: string | null; - speakers: { - id: string; // UUID of the speaker - biography_ko: string; // Biography in Korean - biography_en: string; // Biography in English - image: string | null; // PK of the speaker's image - }[]; - }; - - export type ModificationAuditSchema = { - id: string; // UUID - str_repr: string; // String representation of the modification audit, e.g., "Presentation Title - Status" - status: "requested" | "approved" | "rejected" | "cancelled"; // Status of the modification request +export type EmptyObject = Record; + +export type DetailedErrorSchema = { + code: string; + detail: string; + attr: string | null; +}; + +export type ErrorResponseSchema = { + type: string; + errors: DetailedErrorSchema[]; +}; + +export type UserSchema = { + id: number; + email: string; + username: string; + nickname: string | null; + nickname_ko: string | null; + nickname_en: string | null; + image: string | null; // PK of the user's profile image + profile_image: string | null; // URL to the user's profile image + + has_requested_modification_audit: boolean; + requested_modification_audit_id: string | null; +}; + +export type UserUpdateSchema = { + nickname_ko: string | null; + nickname_en: string | null; + image?: string | null; // PK of the user's profile image +}; + +export type UserSignInSchema = { + identity: string; // email + password: string; +}; + +export type UserChangePasswordSchema = { + old_password: string; + new_password: string; + new_password_confirm: string; +}; + +export type PublicFileSchema = { + id: string; // UUID + file: string; // URL to the public file + name: string; // Name of the public file +}; + +export type PresentationRetrieveSchema = { + id: string; // UUID + title: string; // Title of the presentation, translated to the current language + title_ko: string; // Title in Korean + title_en: string; // Title in English + summary: string; // Summary of the presentation, translated to the current language + summary_ko: string; // Summary in Korean + summary_en: string; // Summary in English + description: string; // Description of the presentation, translated to the current language + description_ko: string; // Description in Korean + description_en: string; // Description in English + slideshow_url: string | null; // URL to the presentation's slideshow, if available + image: string | null; // PK of the presentation's image + speakers: { + id: string; // UUID of the speaker + biography_ko: string; // Biography in Korean + biography_en: string; // Biography in English + image: string | null; // PK of the speaker's image + user: { + id: number; // User ID of the speaker + email: string; // Email of the speaker + nickname_ko: string | null; // Nickname in Korean + nickname_en: string | null; // Nickname in English + }; + }[]; + + has_requested_modification_audit: boolean; + requested_modification_audit_id: string | null; +}; + +export type PresentationUpdateSchema = { + id: string; + title_ko: string; + title_en: string; + summary_ko: string; + summary_en: string; + description_ko: string; + description_en: string; + image: string | null; + speakers: { + id: string; // UUID of the speaker + biography_ko: string; // Biography in Korean + biography_en: string; // Biography in English + image: string | null; // PK of the speaker's image + }[]; +}; + +export type ModificationAuditSchema = { + id: string; // UUID + str_repr: string; // String representation of the modification audit, e.g., "Presentation Title - Status" + status: "requested" | "approved" | "rejected" | "cancelled"; // Status of the modification request + created_at: string; // ISO 8601 timestamp + updated_at: string; // ISO 8601 timestamp + + instance_type: T; // Type of the instance being modified (e.g., "presentation") + instance_id: string; // UUID of the instance being modified (e.g., presentation ID) + modification_data: string; // JSON string containing the modification data + + comments: { + id: string; // UUID of the comment + content: string; // Content of the comment created_at: string; // ISO 8601 timestamp + created_by: { + id: number; // User ID of the commenter + nickname: string; // Nickname of the commenter + is_superuser: boolean; // Whether the commenter is a staff member + }; updated_at: string; // ISO 8601 timestamp - - instance_type: T; // Type of the instance being modified (e.g., "presentation") - instance_id: string; // UUID of the instance being modified (e.g., presentation ID) - modification_data: string; // JSON string containing the modification data - - comments: { - id: string; // UUID of the comment - content: string; // Content of the comment - created_at: string; // ISO 8601 timestamp - created_by: { - id: number; // User ID of the commenter - nickname: string; // Nickname of the commenter - is_superuser: boolean; // Whether the commenter is a staff member - }; - updated_at: string; // ISO 8601 timestamp - }[]; - }; - - type ModificationAuditPresentationPreviewSchema = { - modification_audit: ModificationAuditSchema<"presentation">; - original: PresentationRetrieveSchema; - modified: PresentationRetrieveSchema; - }; - - type ModificationAuditUserPreviewSchema = { - modification_audit: ModificationAuditSchema<"userext">; - original: UserSchema; - modified: UserSchema; - }; - - export type ModificationAuditPreviewSchema = ModificationAuditPresentationPreviewSchema | ModificationAuditUserPreviewSchema; - - export type ModificationAuditCancelRequestSchema = { - id: string; // UUID of the modification audit - reason: string | null; // Reason for cancelling the modification request - }; -} - -export default BackendParticipantPortalAPISchemas; + }[]; +}; + +type ModificationAuditPresentationPreviewSchema = { + modification_audit: ModificationAuditSchema<"presentation">; + original: PresentationRetrieveSchema; + modified: PresentationRetrieveSchema; +}; + +type ModificationAuditUserPreviewSchema = { + modification_audit: ModificationAuditSchema<"userext">; + original: UserSchema; + modified: UserSchema; +}; + +export type ModificationAuditPreviewSchema = ModificationAuditPresentationPreviewSchema | ModificationAuditUserPreviewSchema; + +export type ModificationAuditCancelRequestSchema = { + id: string; // UUID of the modification audit + reason: string | null; // Reason for cancelling the modification request +}; diff --git a/packages/common/src/schemas/backendSessionAPI.ts b/packages/common/src/schemas/backendSessionAPI.ts index a9e2cc14..91aac876 100644 --- a/packages/common/src/schemas/backendSessionAPI.ts +++ b/packages/common/src/schemas/backendSessionAPI.ts @@ -1,33 +1,29 @@ -namespace BackendSessionAPISchemas { - export type SessionTypeSchema = { - id: string; - event: string; - name: string; - }; +export type SessionTypeSchema = { + id: string; + event: string; + name: string; +}; - export type SessionCategorySchema = { - id: string; - presentationType: string; - name: string; - }; +export type SessionCategorySchema = { + id: string; + presentationType: string; + name: string; +}; - export type SessionSpeakerSchema = { - id: string; - presentation: string; - user: string; - name: string; - biography: string; - image: string; // DB 반영 필요 - }; +export type SessionSpeakerSchema = { + id: string; + presentation: string; + user: string; + name: string; + biography: string; + image: string; // DB 반영 필요 +}; - export type SessionSchema = { - id: string; - name: string; // DB 반영 필요 - doNotRecord: boolean; // DB 반영 필요 - presentationType: SessionTypeSchema; - presentationCategories: SessionCategorySchema[]; - presentationSpeaker: SessionSpeakerSchema[]; - }; -} - -export default BackendSessionAPISchemas; +export type SessionSchema = { + id: string; + name: string; // DB 반영 필요 + doNotRecord: boolean; // DB 반영 필요 + presentationType: SessionTypeSchema; + presentationCategories: SessionCategorySchema[]; + presentationSpeaker: SessionSpeakerSchema[]; +}; diff --git a/packages/common/src/schemas/index.ts b/packages/common/src/schemas/index.ts index 96569e3a..66cfe361 100644 --- a/packages/common/src/schemas/index.ts +++ b/packages/common/src/schemas/index.ts @@ -1,9 +1,3 @@ -import * as _BackendAPISchemas from "./backendAPI"; -import * as _BackendAdminAPISchemas from "./backendAdminAPI"; - -namespace CommonSchemas { - export const BackendAPI = _BackendAPISchemas; - export const BackendAdminAPI = _BackendAdminAPISchemas; -} - -export default CommonSchemas; +export * as BackendAPI from "./backendAPI"; +export * as BackendAdminAPI from "./backendAdminAPI"; +export * as ParticipantPortalAPI from "./backendParticipantPortalAPI"; diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 2c443624..1ccc6492 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -1,26 +1,5 @@ -import { buildFlatSiteMap as _buildFlatSiteMap, buildNestedSiteMap as _buildNestedSiteMap, parseCss as _parseCss } from "./api"; -import { getCookie as _getCookie } from "./cookie"; -import { getFormValue as _getFormValue, isFormValid as _isFormValid } from "./form"; -import { - filterPropertiesByLanguageInJsonSchema as _filterPropertiesByLanguageInJsonSchema, - filterReadOnlyPropertiesInJsonSchema as _filterReadOnlyPropertiesInJsonSchema, - filterWritablePropertiesInJsonSchema as _filterWritablePropertiesInJsonSchema, -} from "./json_schema"; -import { isFilledString as _isFilledString, isValidHttpUrl as _isValidHttpUrl, rtrim as _rtrim } from "./string"; - -namespace Utils { - export const buildFlatSiteMap = _buildFlatSiteMap; - export const buildNestedSiteMap = _buildNestedSiteMap; - export const parseCss = _parseCss; - export const getCookie = _getCookie; - export const isFormValid = _isFormValid; - export const getFormValue = _getFormValue; - export const isFilledString = _isFilledString; - export const isValidHttpUrl = _isValidHttpUrl; - export const rtrim = _rtrim; - export const filterWritablePropertiesInJsonSchema = _filterWritablePropertiesInJsonSchema; - export const filterReadOnlyPropertiesInJsonSchema = _filterReadOnlyPropertiesInJsonSchema; - export const filterPropertiesByLanguageInJsonSchema = _filterPropertiesByLanguageInJsonSchema; -} - -export default Utils; +export * from "./api"; +export * from "./cookie"; +export * from "./form"; +export * from "./json_schema"; +export * from "./string"; From 13f350a1ac0167fccbd3b49220e19e023d2854c4 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 7 Apr 2026 21:33:46 +0900 Subject: [PATCH 118/324] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20with=20claude?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/elements/admin_list_filter.tsx | 5 +---- apps/pyconkr-admin/src/components/pages/page/editor.tsx | 7 ++----- apps/pyconkr-admin/src/components/pages/sitemap/list.tsx | 6 +++--- apps/pyconkr/src/components/layout/BreadCrumb/index.tsx | 4 ++-- apps/pyconkr/src/components/layout/Header/index.tsx | 4 ++-- apps/pyconkr/src/components/pages/sponsor_detail.tsx | 4 ++-- apps/pyconkr/src/contexts/app_context.tsx | 8 ++++---- 7 files changed, 16 insertions(+), 22 deletions(-) diff --git a/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx b/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx index e38a55e6..142a0da7 100644 --- a/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx +++ b/apps/pyconkr-admin/src/components/elements/admin_list_filter.tsx @@ -2,10 +2,7 @@ import { Add, Clear, FilterList, RestartAlt } from "@mui/icons-material"; import { Box, Button, Chip, FormControl, IconButton, InputLabel, MenuItem, Select, Stack, TextField } from "@mui/material"; import * as React from "react"; -import BackendAdminAPISchemas from "../../../../../packages/common/src/schemas/backendAdminAPI"; - -type OpenAPIParameterSchema = BackendAdminAPISchemas.OpenAPIParameterSchema; -type ChoicesResponse = BackendAdminAPISchemas.ChoicesResponse; +import { ChoicesResponse, OpenAPIParameterSchema } from "../../../../../packages/common/src/schemas/backendAdminAPI"; type AdminListFilterProps = { parameters: OpenAPIParameterSchema[]; diff --git a/apps/pyconkr-admin/src/components/pages/page/editor.tsx b/apps/pyconkr-admin/src/components/pages/page/editor.tsx index e445b1d3..c67d2d76 100644 --- a/apps/pyconkr-admin/src/components/pages/page/editor.tsx +++ b/apps/pyconkr-admin/src/components/pages/page/editor.tsx @@ -8,15 +8,12 @@ import { commands } from "@uiw/react-md-editor"; import * as React from "react"; import { useParams } from "react-router-dom"; -// I don't know why, I don't want to know why, I shouldn't have to wonder why, -// but for whatever reason this stupid namespace won't import on Common.Schemas.BackendAdminAPI.PageSectionSchema. -// TODO: FIXME: Remove this hack when the issue is resolved. This is dumb. -import BackendAdminAPISchemas from "../../../../../../packages/common/src/schemas/backendAdminAPI"; +import { PageSectionSchema } from "../../../../../../packages/common/src/schemas/backendAdminAPI"; import { muiTheme } from "../../../styles/globalStyles"; import { addErrorSnackbar } from "../../../utils/snackbar"; import { AdminEditor } from "../../layouts/admin_editor"; -type SectionType = BackendAdminAPISchemas.PageSectionSchema; +type SectionType = PageSectionSchema; type CommonSectionEditorPropType = { disabled?: boolean; diff --git a/apps/pyconkr-admin/src/components/pages/sitemap/list.tsx b/apps/pyconkr-admin/src/components/pages/sitemap/list.tsx index a6cc6f56..002bb816 100644 --- a/apps/pyconkr-admin/src/components/pages/sitemap/list.tsx +++ b/apps/pyconkr-admin/src/components/pages/sitemap/list.tsx @@ -21,13 +21,13 @@ import { enqueueSnackbar, OptionsObject } from "notistack"; import * as React from "react"; import { GroupOptions, ReactSortable, SortableEvent, SortableOptions } from "react-sortablejs"; -import BackendAdminAPISchemas from "../../../../../../packages/common/src/schemas/backendAdminAPI"; +import { FlattenedSiteMapSchema, NestedSiteMapSchema } from "../../../../../../packages/common/src/schemas/backendAdminAPI"; import { BackendAdminSignInGuard } from "../../elements/admin_signin_guard"; import { AdminEditor } from "../../layouts/admin_editor"; -type FlatSiteMap = BackendAdminAPISchemas.FlattenedSiteMapSchema; +type FlatSiteMap = FlattenedSiteMapSchema; type FlatSiteMapObj = Record; -type NestedSiteMap = BackendAdminAPISchemas.NestedSiteMapSchema; +type NestedSiteMap = NestedSiteMapSchema; type FlatNestedSiteMap = Record; const DepthColorMap: React.CSSProperties["backgroundColor"][] = [ diff --git a/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx b/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx index 464b7af9..483b786d 100644 --- a/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx +++ b/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx @@ -3,11 +3,11 @@ import * as React from "react"; import { Link } from "react-router-dom"; import * as R from "remeda"; -import BackendAPISchemas from "../../../../../../packages/common/src/schemas/backendAPI"; +import { NestedSiteMapSchema } from "../../../../../../packages/common/src/schemas/backendAPI"; type BreadCrumbPropType = { title: string; - parentSiteMaps: (BackendAPISchemas.NestedSiteMapSchema | undefined)[]; + parentSiteMaps: (NestedSiteMapSchema | undefined)[]; }; export const BreadCrumb: React.FC = ({ title, parentSiteMaps }) => { diff --git a/apps/pyconkr/src/components/layout/Header/index.tsx b/apps/pyconkr/src/components/layout/Header/index.tsx index dcdb843d..0ecbe1b1 100644 --- a/apps/pyconkr/src/components/layout/Header/index.tsx +++ b/apps/pyconkr/src/components/layout/Header/index.tsx @@ -6,7 +6,7 @@ import * as React from "react"; import { Link } from "react-router-dom"; import * as R from "remeda"; -import BackendAPISchemas from "../../../../../../packages/common/src/schemas/backendAPI"; +import { NestedSiteMapSchema } from "../../../../../../packages/common/src/schemas/backendAPI"; import { useAppContext } from "../../../contexts/app_context"; import { CartBadgeButton } from "../CartBadgeButton"; import LanguageSelector from "../LanguageSelector"; @@ -14,7 +14,7 @@ import { SignInButton } from "../SignInButton"; // import { ScanCodeIconButton } from "../UserScanCodeButton"; import { MobileHeader } from "./Mobile/MobileHeader"; -type MenuType = BackendAPISchemas.NestedSiteMapSchema; +type MenuType = NestedSiteMapSchema; type MenuOrUndefinedType = MenuType | undefined; type NavigationStateType = { diff --git a/apps/pyconkr/src/components/pages/sponsor_detail.tsx b/apps/pyconkr/src/components/pages/sponsor_detail.tsx index c9e16323..064aa22f 100644 --- a/apps/pyconkr/src/components/pages/sponsor_detail.tsx +++ b/apps/pyconkr/src/components/pages/sponsor_detail.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import { useParams } from "react-router-dom"; import * as R from "remeda"; -import BackendAPISchemas from "../../../../../packages/common/src/schemas/backendAPI"; +import { SponsorTierSchema } from "../../../../../packages/common/src/schemas/backendAPI"; import { useAppContext } from "../../contexts/app_context"; import { PageLayout } from "../layout/PageLayout"; @@ -54,7 +54,7 @@ export const SponsorDetailPage: React.FC = ErrorBoundary.with( Suspense.with({ fallback: }, () => { const { id } = useParams(); const { language, sponsorTiers, setAppContext } = useAppContext(); - const sponsors = sponsorTiers?.reduce((acc, tier) => [...acc, ...tier.sponsors], [] as BackendAPISchemas.SponsorTierSchema["sponsors"]); + const sponsors = sponsorTiers?.reduce((acc, tier) => [...acc, ...tier.sponsors], [] as SponsorTierSchema["sponsors"]); const sponsor = sponsors?.find((s) => s.id === id); const title = language === "ko" ? "후원사" : "Sponsor"; diff --git a/apps/pyconkr/src/contexts/app_context.tsx b/apps/pyconkr/src/contexts/app_context.tsx index 875a772e..3c56bb8b 100644 --- a/apps/pyconkr/src/contexts/app_context.tsx +++ b/apps/pyconkr/src/contexts/app_context.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import BackendAPISchemas from "../../../../packages/common/src/schemas/backendAPI"; +import { NestedSiteMapSchema, SponsorTierSchema } from "../../../../packages/common/src/schemas/backendAPI"; type LanguageType = "ko" | "en"; @@ -9,10 +9,10 @@ export type AppContextType = { shouldShowTitleBanner: boolean; shouldShowSponsorBanner: boolean; - siteMapNode?: BackendAPISchemas.NestedSiteMapSchema; - sponsorTiers?: BackendAPISchemas.SponsorTierSchema[]; + siteMapNode?: NestedSiteMapSchema; + sponsorTiers?: SponsorTierSchema[]; title: string; - currentSiteMapDepth: (BackendAPISchemas.NestedSiteMapSchema | undefined)[]; + currentSiteMapDepth: (NestedSiteMapSchema | undefined)[]; setAppContext: React.Dispatch>>; }; From 8da04798a7445c3eb5e0b8d566810e960471bc2c Mon Sep 17 00:00:00 2001 From: earthyoung Date: Fri, 10 Apr 2026 23:13:08 +0900 Subject: [PATCH 119/324] =?UTF-8?q?fix:=20session=20timetable,=20session?= =?UTF-8?q?=20list=202025=EB=85=84=EB=8F=84=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/consts/mdx_components.ts | 36 +++++- .../mdx_components/session_list.tsx | 108 +++++++++--------- .../mdx_components/session_timetable.tsx | 19 ++- 3 files changed, 98 insertions(+), 65 deletions(-) diff --git a/apps/pyconkr/src/consts/mdx_components.ts b/apps/pyconkr/src/consts/mdx_components.ts index 8b2b7670..e572bd61 100644 --- a/apps/pyconkr/src/consts/mdx_components.ts +++ b/apps/pyconkr/src/consts/mdx_components.ts @@ -1,8 +1,11 @@ // 후대의 개발자님께 : 컴포넌트 맨 첫글자가 대문자로 시작하지 않으면 JSX 컴포넌트가 아니라 일반 HTML 태그로 인식합니다. 제발 대문자로 시작해주세요. -import { Components } from "@frontend/common"; +import { Components, Schemas } from "@frontend/common"; import * as Shop from "@frontend/shop"; import * as mui from "@mui/material"; import type { MDXComponents } from "mdx/types.js"; +import * as React from "react"; + +import PyCon2025Logo from "../assets/pyconkr2025_logo.png"; const MUIMDXComponents: MDXComponents = { Mui__material__Accordion: mui.Accordion, @@ -130,6 +133,33 @@ const MUIMDXComponents: MDXComponents = { Mui__material__Zoom: mui.Zoom, }; +const getPyConKR2025SessionUrl = (session: Schemas.BackendAPI.SessionSchema): string => { + const urlSafeTitle = session.title + .replace(/ /g, "-") + .replace(/([.])/g, "_") + .replace(/(?![.0-9A-Za-zㄱ-ㅣ가-힣-])./g, ""); + return `/presentations/${session.id}#${urlSafeTitle}`; +}; + +const PyConKR2025FallbackImage = React.createElement("img", { + src: PyCon2025Logo, + alt: "PyCon 2025 Logo", + style: { width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" }, +}); + +const PyConKR2025SessionList: React.FC> = (props) => + React.createElement(Components.MDX.SessionList, { + ...props, + fallbackImage: PyConKR2025FallbackImage, + getSessionUrl: getPyConKR2025SessionUrl, + }); + +const PyConKR2025SessionTimeTable: React.FC> = (props) => + React.createElement(Components.MDX.SessionTimeTable, { + ...props, + getSessionUrl: getPyConKR2025SessionUrl, + }); + const PyConKRCommonMDXComponents: MDXComponents = { Common__Components__Lottie: Components.LottiePlayer, Common__Components__NetworkLottie: Components.NetworkLottiePlayer, @@ -139,8 +169,8 @@ const PyConKRCommonMDXComponents: MDXComponents = { Common__Components__MDX__Map: Components.MDX.Map, Common__Components__MDX__FAQAccordion: Components.MDX.FAQAccordion, Common__Components__MDX__FullWidthStyledButton: Components.MDX.StyledFullWidthButton, - Common__Components__Session__List: Components.MDX.SessionList, - Common__Components__Session__TimeTable: Components.MDX.SessionTimeTable, + Common__Components__Session__List: PyConKR2025SessionList, + Common__Components__Session__TimeTable: PyConKR2025SessionTimeTable, }; const PythonKRShopMDXComponents: MDXComponents = { diff --git a/packages/common/src/components/mdx_components/session_list.tsx b/packages/common/src/components/mdx_components/session_list.tsx index 97e77c7d..1408f9bc 100644 --- a/packages/common/src/components/mdx_components/session_list.tsx +++ b/packages/common/src/components/mdx_components/session_list.tsx @@ -4,7 +4,6 @@ import * as React from "react"; import { Link } from "react-router-dom"; import * as R from "remeda"; -import PyCon2025Logo from "../../assets/pyconkr2025_logo.png"; import * as Hooks from "../../hooks"; import * as BackendAPISchemas from "../../schemas/backendAPI"; import { ErrorFallback } from "../error_handler"; @@ -13,65 +12,72 @@ import { StyledDivider } from "./styled_divider"; const EXCLUDE_CATEGORIES = ["후원사", "Sponsor"]; -const SessionItem: React.FC<{ session: BackendAPISchemas.SessionSchema; enableLink?: boolean }> = Suspense.with( - { fallback: }, - ({ session, enableLink }) => { - const sessionTitle = session.title.replace("\\n", "\n"); - - let speakerImgSrc = session.image || ""; - if (!speakerImgSrc && R.isArray(session.speakers) && !R.isEmpty(session.speakers)) { - for (const speaker of session.speakers) { - if (speaker.image) { - speakerImgSrc = speaker.image; - break; - } +const SessionItem: React.FC<{ + session: BackendAPISchemas.SessionSchema; + enableLink?: boolean; + fallbackImage?: React.ReactNode; + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; +}> = Suspense.with({ fallback: }, ({ session, enableLink, fallbackImage, getSessionUrl }) => { + const sessionTitle = session.title.replace("\\n", "\n"); + + let speakerImgSrc = session.image || ""; + if (!speakerImgSrc && R.isArray(session.speakers) && !R.isEmpty(session.speakers)) { + for (const speaker of session.speakers) { + if (speaker.image) { + speakerImgSrc = speaker.image; + break; } } + } - const urlSafeTitle = session.title - .replace(/ /g, "-") - .replace(/([.])/g, "_") - .replace(/(?![0-9A-Za-zㄱ-ㅣ가-힣-_])./g, ""); - const sessionDetailedUrl = `/presentations/${session.id}#${urlSafeTitle}`; - const result = ( - - } />} - /> - - - {session.summary && } - - {session.speakers.map((speaker) => ( - - ))} - - - {session.categories.map((tag) => ( - - ))} - + const sessionDetailedUrl = getSessionUrl ? getSessionUrl(session) : undefined; + const result = ( + + {fallbackImage}} + /> + } + /> + + + {session.summary && } + + {session.speakers.map((speaker) => ( + + ))} - - ); - return ( - <> - {enableLink ? : result} - - - ); - } -); + + {session.categories.map((tag) => ( + + ))} + + + + ); + return ( + <> + {enableLink && sessionDetailedUrl ? : result} + + + ); +}); type SessionListPropType = { event?: string; types?: string | string[]; enableLink?: boolean; + fallbackImage?: React.ReactNode; + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; }; export const SessionList: React.FC = ErrorBoundary.with( { fallback: ErrorFallback }, - Suspense.with({ fallback: }, ({ event, types, enableLink }) => { + Suspense.with({ fallback: }, ({ event, types, enableLink, fallbackImage, getSessionUrl }) => { const { language } = Hooks.Common.useCommonContext(); const backendAPIClient = Hooks.BackendAPI.useBackendClient(); const params = { ...(event && { event }), ...(types && { types: R.isString(types) ? types : types.join(",") }) }; @@ -122,7 +128,7 @@ export const SessionList: React.FC = ErrorBoundary.with( )} {filteredSessions.map((s) => ( - + ))} ); @@ -194,10 +200,8 @@ const SessionImageErrorFallbackBox = styled(Box)(({ theme }) => ({ justifyContent: "center", })); -const SessionImageErrorFallback: React.FC = () => ( - - PyCon 2025 Logo - +const SessionImageErrorFallback: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( + {children} ); const SessionTitle = styled(Typography)({ diff --git a/packages/common/src/components/mdx_components/session_timetable.tsx b/packages/common/src/components/mdx_components/session_timetable.tsx index a646c0a9..1453fd5c 100644 --- a/packages/common/src/components/mdx_components/session_timetable.tsx +++ b/packages/common/src/components/mdx_components/session_timetable.tsx @@ -107,18 +107,16 @@ const SessionColumn: React.FC<{ rowSpan: number; colSpan?: number; session: BackendAPISchemas.SessionSchema; -}> = ({ rowSpan, colSpan, session }) => { - const clickable = R.isArray(session.speakers) && !R.isEmpty(session.speakers); + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; +}> = ({ rowSpan, colSpan, session, getSessionUrl }) => { + const sessionUrl = getSessionUrl ? getSessionUrl(session) : undefined; + const clickable = R.isArray(session.speakers) && !R.isEmpty(session.speakers) && !!sessionUrl; // Firefox는 rowSpan된 td의 height를 계산할 때 rowSpan을 고려하지 않습니다. 따라서 직접 계산하여 height를 설정합니다. const sessionBoxHeight = `${TD_HEIGHT * rowSpan}rem`; - const urlSafeTitle = session.title - .replace(/ /g, "-") - .replace(/([.])/g, "_") - .replace(/(?![.0-9A-Za-zㄱ-ㅣ가-힣-])./g, ""); return ( {clickable ? ( - + = ({ lang type SessionTimeTablePropType = { event?: string; types?: string | string[]; + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; }; export const SessionTimeTable: React.FC = ErrorBoundary.with( { fallback: ErrorFallback }, - Suspense.with({ fallback: } /> }, ({ event, types }) => { + Suspense.with({ fallback: } /> }, ({ event, types, getSessionUrl }) => { const [confDate, setConfDate] = React.useState(""); const { language } = Hooks.Common.useCommonContext(); @@ -274,7 +273,7 @@ export const SessionTimeTable: React.FC = ErrorBoundar return ( - + ); } @@ -293,7 +292,7 @@ export const SessionTimeTable: React.FC = ErrorBoundar } // 세션이 여러 줄에 걸쳐있는 경우, n-1 줄만큼 해당 room에 column을 생성하지 않도록 합니다. if (roomDatum.rowSpan > 1) rooms[room] = roomDatum.rowSpan - 1; - return ; + return ; })} ); From 57eb9223752691ffe9c1cf19e72e7c7a21e48a0b Mon Sep 17 00:00:00 2001 From: earthyoung Date: Fri, 10 Apr 2026 23:22:38 +0900 Subject: [PATCH 120/324] =?UTF-8?q?fix:=20mobile=20accordion,=20mobile=20c?= =?UTF-8?q?over=202025=EB=85=84=EB=8F=84=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/pyconkr/src/consts/mdx_components.ts | 21 ++++++++++ packages/common/src/components/index.ts | 4 ++ .../mdx_components/mobile_accordion.tsx | 41 +++++++++++-------- .../mdx_components/mobile_cover.tsx | 27 ++++++++---- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/apps/pyconkr/src/consts/mdx_components.ts b/apps/pyconkr/src/consts/mdx_components.ts index e572bd61..32769812 100644 --- a/apps/pyconkr/src/consts/mdx_components.ts +++ b/apps/pyconkr/src/consts/mdx_components.ts @@ -5,6 +5,10 @@ import * as mui from "@mui/material"; import type { MDXComponents } from "mdx/types.js"; import * as React from "react"; +import PyCon2025HostLogoBig from "../../../../packages/common/src/assets/pyconkr2025_hostlogo_big.png"; +import PyCon2025HostLogoSmall from "../../../../packages/common/src/assets/pyconkr2025_hostlogo_small.png"; +import PyCon2025MobileLogoImage from "../../../../packages/common/src/assets/pyconkr2025_main_cover_image.png"; +import PyCon2025MobileLogoTitle from "../../../../packages/common/src/assets/pyconkr2025_main_cover_title.png"; import PyCon2025Logo from "../assets/pyconkr2025_logo.png"; const MUIMDXComponents: MDXComponents = { @@ -160,6 +164,21 @@ const PyConKR2025SessionTimeTable: React.FC = () => + React.createElement(Components.MDX.MobileAccordion, { + marqueeText: "AUG 15 - 17", + marqueeLogoSrc: PyCon2025HostLogoSmall, + hostLogoBigSrc: PyCon2025HostLogoBig, + venueKo: "서울특별시 중구 필동로 1길 30 동국대학교 신공학관", + venueEnLines: ["New Engineering Building, Dongguk University", "Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"], + }); + +const PyConKR2025MobileCover: React.FC = () => + React.createElement(Components.MDX.MobileCover, { + coverImageSrc: PyCon2025MobileLogoImage, + coverTitleSrc: PyCon2025MobileLogoTitle, + }); + const PyConKRCommonMDXComponents: MDXComponents = { Common__Components__Lottie: Components.LottiePlayer, Common__Components__NetworkLottie: Components.NetworkLottiePlayer, @@ -171,6 +190,8 @@ const PyConKRCommonMDXComponents: MDXComponents = { Common__Components__MDX__FullWidthStyledButton: Components.MDX.StyledFullWidthButton, Common__Components__Session__List: PyConKR2025SessionList, Common__Components__Session__TimeTable: PyConKR2025SessionTimeTable, + Common__Components__MDX__MobileAccordion: PyConKR2025MobileAccordion, + Common__Components__MDX__MobileCover: PyConKR2025MobileCover, }; const PythonKRShopMDXComponents: MDXComponents = { diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index 8b9914d8..24751bcc 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -21,6 +21,8 @@ import { } from "./mdx_components/faq_accordion"; import type { MapPropType as MapComponentPropType } from "./mdx_components/map"; import { Map as MapComponent } from "./mdx_components/map"; +import { MobileAccordion as MobileAccordionComponent } from "./mdx_components/mobile_accordion"; +import { MobileCover as MobileCoverComponent } from "./mdx_components/mobile_cover"; import { OneDetailsOpener as OneDetailsOpenerComponent } from "./mdx_components/one_details_opener"; import { SessionList as SessionListComponent } from "./mdx_components/session_list"; import { SessionTimeTable as SessionTimeTableComponent } from "./mdx_components/session_timetable"; @@ -51,6 +53,8 @@ namespace Components { export namespace MDX { export const Confetti = ConfettiComponent; + export const MobileAccordion = MobileAccordionComponent; + export const MobileCover = MobileCoverComponent; export const StyledFullWidthButton = StyledFullWidthButtonComponent; export const PrimaryStyledDetails = PrimaryStyledDetailsComponent; export const SecondaryStyledDetails = SecondaryStyledDetailsComponent; diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/packages/common/src/components/mdx_components/mobile_accordion.tsx index 151023b2..9940a567 100644 --- a/packages/common/src/components/mdx_components/mobile_accordion.tsx +++ b/packages/common/src/components/mdx_components/mobile_accordion.tsx @@ -4,27 +4,33 @@ import { AccordionDetails, AccordionSummary, Accordion as MuiAccordion, Stack, T import * as React from "react"; import Marquee from "react-fast-marquee"; -import { useAppContext } from "../../../../../apps/pyconkr/src/contexts/app_context"; -import PyCon2025HostLogoBig from "../../assets/pyconkr2025_hostlogo_big.png"; -import PyCon2025HostLogoSmall from "../../assets/pyconkr2025_hostlogo_small.png"; +import * as Hooks from "../../hooks"; -const MarqueeAccordion: React.FC = () => { +const MarqueeAccordion: React.FC<{ marqueeText: string; marqueeLogoSrc: string }> = ({ marqueeText, marqueeLogoSrc }) => { const marqueeWidth = window.innerWidth * 0.9; const marqueeGradientWidth = window.innerWidth * 0.1; const items = React.useMemo(() => { return Array.from({ length: 100 }, () => ( - AUG 15 - 17 - logo + {marqueeText} + logo )); - }, []); + }, [marqueeText, marqueeLogoSrc]); return ; }; -export const MobileAccordion: React.FC = () => { - const { language } = useAppContext(); +type MobileAccordionProps = { + marqueeText: string; + marqueeLogoSrc: string; + hostLogoBigSrc: string; + venueKo: string; + venueEnLines: string[]; +}; + +export const MobileAccordion: React.FC = ({ marqueeText, marqueeLogoSrc, hostLogoBigSrc, venueKo, venueEnLines }) => { + const { language } = Hooks.Common.useCommonContext(); const [expanded, setExpanded] = React.useState(false); return ( @@ -44,27 +50,26 @@ export const MobileAccordion: React.FC = () => { } sx={{ margin: 0, padding: 0 }} > - {expanded ? null : } + {expanded ? null : } - PyCon 2025 Host Logo + Host Logo {language === "ko" ? ( - {"서울특별시 중구 필동로 1길 30 동국대학교 신공학관"} + {venueKo} ) : ( - - {"New Engineering Building, Dongguk University"} - - - {"Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"} - + {venueEnLines.map((line, i) => ( + + {line} + + ))} )} diff --git a/packages/common/src/components/mdx_components/mobile_cover.tsx b/packages/common/src/components/mdx_components/mobile_cover.tsx index b2878c8d..cb37c6db 100644 --- a/packages/common/src/components/mdx_components/mobile_cover.tsx +++ b/packages/common/src/components/mdx_components/mobile_cover.tsx @@ -1,21 +1,32 @@ import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import { ButtonBase, Stack, Typography } from "@mui/material"; import * as React from "react"; -import { useAppContext } from "../../../../../apps/pyconkr/src/contexts/app_context"; -import PyCon2025MobileLogoImage from "../../assets/pyconkr2025_main_cover_image.png"; -import PyCon2025MobileLogoTitle from "../../assets/pyconkr2025_main_cover_title.png"; -export const MobileCover: React.FC = () => { - const { language } = useAppContext(); - const buttonTitle = language === "ko" ? "티켓 구매하기" : "Buy Ticket"; +import * as Hooks from "../../hooks"; + +type MobileCoverProps = { + coverImageSrc: string; + coverTitleSrc: string; + buttonTextKo?: string; + buttonTextEn?: string; +}; + +export const MobileCover: React.FC = ({ + coverImageSrc, + coverTitleSrc, + buttonTextKo = "티켓 구매하기", + buttonTextEn = "Buy Ticket", +}) => { + const { language } = Hooks.Common.useCommonContext(); + const buttonTitle = language === "ko" ? buttonTextKo : buttonTextEn; return ( - Pycon 2025 Mobile Image + Mobile Cover Image - Pycon 2025 Mobile Title + Mobile Cover Title Date: Sat, 11 Apr 2026 12:03:31 +0900 Subject: [PATCH 121/324] =?UTF-8?q?refactor:=20packages/common/src/compone?= =?UTF-8?q?nts=20=EC=9D=98=20hook=20=EC=9D=98=EC=A1=B4=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(props=20=EC=A3=BC=EC=9E=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/elements/error_fallback.tsx | 8 ++++++++ .../src/components/layouts/admin_editor.tsx | 11 +++++++---- .../src/components/layouts/admin_list.tsx | 4 ++-- .../src/components/pages/account/account.tsx | 5 +++-- .../pages/modification_audit/components.tsx | 18 +++++++++++++++--- .../pages/modification_audit/list.tsx | 4 ++-- .../pages/modification_audit/pages.tsx | 4 ++-- .../src/components/pages/page/editor.tsx | 6 ++++-- .../components/pages/presentation/editor.tsx | 16 +++++++++++++--- .../src/components/pages/sitemap/list.tsx | 4 ++-- .../src/components/pages/user/editor.tsx | 4 ++-- .../components/dialogs/public_file_upload.tsx | 2 +- .../components/elements/multilang_field.tsx | 4 +++- .../src/components/pages/dynamic_route.tsx | 4 +++- .../components/pages/presentation_detail.tsx | 14 ++++++++++++-- .../src/components/pages/sponsor_detail.tsx | 4 +++- apps/pyconkr/src/debug/page/mdi_test.tsx | 4 +++- .../common/src/components/dnd_file_input.tsx | 6 ++---- .../common/src/components/error_handler.tsx | 11 ++--------- packages/common/src/components/mdx.tsx | 6 +++--- .../shop/src/components/features/product.tsx | 3 ++- 21 files changed, 94 insertions(+), 48 deletions(-) create mode 100644 apps/pyconkr-admin/src/components/elements/error_fallback.tsx diff --git a/apps/pyconkr-admin/src/components/elements/error_fallback.tsx b/apps/pyconkr-admin/src/components/elements/error_fallback.tsx new file mode 100644 index 00000000..50b6dda0 --- /dev/null +++ b/apps/pyconkr-admin/src/components/elements/error_fallback.tsx @@ -0,0 +1,8 @@ +import { Components } from "@frontend/common"; +import { useCommonContext } from "@frontend/common/src/hooks/useCommonContext"; +import * as React from "react"; + +export const ErrorFallback: React.FC<{ error: Error; reset: () => void }> = ({ error, reset }) => { + const { debug } = useCommonContext(); + return ; +}; diff --git a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx index 5c0cbc67..c7d0b037 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx @@ -8,6 +8,7 @@ import { useSchemaQuery, useUpdateMutation, } from "@frontend/common/src/hooks/useAdminAPI"; +import { useCommonContext } from "@frontend/common/src/hooks/useCommonContext"; import { filterPropertiesByLanguageInJsonSchema, filterReadOnlyPropertiesInJsonSchema, @@ -50,6 +51,7 @@ import * as R from "remeda"; import { addErrorSnackbar, addSnackbar } from "../../utils/snackbar"; import { BackendAdminSignInGuard } from "../elements/admin_signin_guard"; +import { ErrorFallback } from "../elements/error_fallback"; type EditorFormDataEventType = IChangeEvent, RJSFSchema, { [k in string]: unknown }>; type onSubmitType = (data: Record, event: React.FormEvent) => void; @@ -146,7 +148,7 @@ const fieldPropsToSelectedProps = (props: FieldProps): OutlinedSelectProps & { d }; const M2MSelect: Field = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with({ fallback: }, (props) => { const selectable = (props.schema.items as JSONSchema7).oneOf as DescriptedEnum[]; const selectableListObj: DescriptedEnumObject = selectable.reduce((a, i) => ({ ...a, [i.const]: i }), {} as DescriptedEnumObject); @@ -181,7 +183,8 @@ const MDRendererContainer = styled(Box)(({ theme }) => ({ }, })); -const MDEditorField: Field = ErrorBoundary.with({ fallback: Components.ErrorFallback }, ({ disabled, formData, name, onChange: rawOnChange }) => { +const MDEditorField: Field = ErrorBoundary.with({ fallback: ErrorFallback }, ({ disabled, formData, name, onChange: rawOnChange }) => { + const { baseUrl, mdxComponents } = useCommonContext(); const [valueState, setValueState] = React.useState(formData?.toString() || ""); const onChange = (value?: string) => { setValueState(value); @@ -195,7 +198,7 @@ const MDEditorField: Field = ErrorBoundary.with({ fallback: Components.ErrorFall - + @@ -262,7 +265,7 @@ type InnerAdminEditorStateType = { }; const InnerAdminEditor: React.FC = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with( { fallback: }, ({ diff --git a/apps/pyconkr-admin/src/components/layouts/admin_list.tsx b/apps/pyconkr-admin/src/components/layouts/admin_list.tsx index de63ebad..ee1f208b 100644 --- a/apps/pyconkr-admin/src/components/layouts/admin_list.tsx +++ b/apps/pyconkr-admin/src/components/layouts/admin_list.tsx @@ -1,4 +1,3 @@ -import { Components } from "@frontend/common"; import { useBackendAdminClient, useChoicesQuery, useListQuery, useOpenApiSchemaQuery } from "@frontend/common/src/hooks/useAdminAPI"; import { extractQueryParameters } from "@frontend/common/src/utils"; import { Add } from "@mui/icons-material"; @@ -9,6 +8,7 @@ import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { AdminListFilter } from "../elements/admin_list_filter"; import { BackendAdminSignInGuard } from "../elements/admin_signin_guard"; +import { ErrorFallback } from "../elements/error_fallback"; type AdminListProps = { app: string; @@ -26,7 +26,7 @@ type ListRowType = { }; const InnerAdminList: React.FC = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with({ fallback: }, ({ app, resource, hideCreatedAt, hideUpdatedAt, hideCreateNew }) => { const navigate = useNavigate(); diff --git a/apps/pyconkr-admin/src/components/pages/account/account.tsx b/apps/pyconkr-admin/src/components/pages/account/account.tsx index 7249f46a..ca6db0f1 100644 --- a/apps/pyconkr-admin/src/components/pages/account/account.tsx +++ b/apps/pyconkr-admin/src/components/pages/account/account.tsx @@ -1,12 +1,13 @@ -import { Components } from "@frontend/common"; import { useBackendAdminClient, useSignedInUserQuery } from "@frontend/common/src/hooks/useAdminAPI"; import { CircularProgress } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import * as React from "react"; import { Navigate } from "react-router-dom"; +import { ErrorFallback } from "../../elements/error_fallback"; + export const AccountRedirectPage: React.FC = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with({ fallback: }, () => { const backendAdminAPIClient = useBackendAdminClient(); const { data } = useSignedInUserQuery(backendAdminAPIClient); diff --git a/apps/pyconkr-admin/src/components/pages/modification_audit/components.tsx b/apps/pyconkr-admin/src/components/pages/modification_audit/components.tsx index 5d696588..3fbfaa1c 100644 --- a/apps/pyconkr-admin/src/components/pages/modification_audit/components.tsx +++ b/apps/pyconkr-admin/src/components/pages/modification_audit/components.tsx @@ -1,5 +1,6 @@ import { Components } from "@frontend/common"; import { useBackendAdminClient, usePublicFileQuery } from "@frontend/common/src/hooks/useAdminAPI"; +import { useCommonContext } from "@frontend/common/src/hooks/useCommonContext"; import { Accordion, AccordionDetails, @@ -78,10 +79,11 @@ export const PreviewTextField: React.FC = ({ originalDataset, }; export const PreviewMarkdownField: React.FC = ({ originalDataset, previewDataset, name, label }) => { + const { baseUrl, mdxComponents } = useCommonContext(); return originalDataset[name] === previewDataset[name] ? ( - + ) : ( @@ -91,7 +93,12 @@ export const PreviewMarkdownField: React.FC = ({ origin - + 기존 값을 보려면 여기를 클릭해주세요. @@ -100,7 +107,12 @@ export const PreviewMarkdownField: React.FC = ({ origin - + diff --git a/apps/pyconkr-admin/src/components/pages/modification_audit/list.tsx b/apps/pyconkr-admin/src/components/pages/modification_audit/list.tsx index a9bfd471..f17a7b8c 100644 --- a/apps/pyconkr-admin/src/components/pages/modification_audit/list.tsx +++ b/apps/pyconkr-admin/src/components/pages/modification_audit/list.tsx @@ -1,4 +1,3 @@ -import { Components } from "@frontend/common"; import { useBackendAdminClient, useListQuery } from "@frontend/common/src/hooks/useAdminAPI"; import { CircularProgress, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; @@ -6,6 +5,7 @@ import * as React from "react"; import { Link } from "react-router-dom"; import { BackendAdminSignInGuard } from "../../elements/admin_signin_guard"; +import { ErrorFallback } from "../../elements/error_fallback"; type ListRowType = { id: string; @@ -16,7 +16,7 @@ type ListRowType = { }; const InnerAdminModificationAuditList: React.FC = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with({ fallback: }, () => { const backendAdminClient = useBackendAdminClient(); const listQuery = useListQuery(backendAdminClient, "modification-audit", "modification-audit"); diff --git a/apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx b/apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx index 732f7938..b892a1f7 100644 --- a/apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx +++ b/apps/pyconkr-admin/src/components/pages/modification_audit/pages.tsx @@ -1,4 +1,3 @@ -import { Components } from "@frontend/common"; import { useBackendAdminClient, useModificationAuditPreviewQuery } from "@frontend/common/src/hooks/useAdminAPI"; import { Box, Button, CircularProgress, Divider, Stack, Typography } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; @@ -8,6 +7,7 @@ import { Navigate, useParams } from "react-router-dom"; import { ModificationAuditProperties } from "./components"; import { ApproveSubmitConfirmDialog, RejectSubmitConfirmDialog } from "./dialogs"; import { SubModificationAuditPage } from "./sub_pages"; +import { ErrorFallback } from "../../elements/error_fallback"; import { BackendAdminSignInGuard } from "../../elements/admin_signin_guard"; type EditorStateType = { actionStatus?: "approve" | "reject" }; @@ -67,7 +67,7 @@ const InnerAdminModificationAuditEditor: React.FC = () => { export const AdminModificationAuditEditor: React.FC = () => { return ( - + }> diff --git a/apps/pyconkr-admin/src/components/pages/page/editor.tsx b/apps/pyconkr-admin/src/components/pages/page/editor.tsx index c67d2d76..59b7c7d0 100644 --- a/apps/pyconkr-admin/src/components/pages/page/editor.tsx +++ b/apps/pyconkr-admin/src/components/pages/page/editor.tsx @@ -11,6 +11,7 @@ import { useParams } from "react-router-dom"; import { PageSectionSchema } from "../../../../../../packages/common/src/schemas/backendAdminAPI"; import { muiTheme } from "../../../styles/globalStyles"; import { addErrorSnackbar } from "../../../utils/snackbar"; +import { ErrorFallback } from "../../elements/error_fallback"; import { AdminEditor } from "../../layouts/admin_editor"; type SectionType = PageSectionSchema; @@ -33,6 +34,7 @@ type SectionEditorPropType = CommonSectionEditorPropType & { }; const SectionTextEditor: React.FC = ({ disabled, defaultValue, onInsertNewSection, onChange, onDelete }) => { + const { baseUrl, mdxComponents } = useCommonContext(); const deleteActionButton = commands.group([], { name: "delete", groupName: "delete", @@ -51,7 +53,7 @@ const SectionTextEditor: React.FC = ({ disabled, defa - + @@ -80,7 +82,7 @@ type AdminCMSPageEditorStateType = { }; export const AdminCMSPageEditor: React.FC = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with({ fallback: }, () => { const { id } = useParams<{ id?: string }>(); const { frontendDomain } = useCommonContext(); diff --git a/apps/pyconkr-admin/src/components/pages/presentation/editor.tsx b/apps/pyconkr-admin/src/components/pages/presentation/editor.tsx index 16daa629..93311038 100644 --- a/apps/pyconkr-admin/src/components/pages/presentation/editor.tsx +++ b/apps/pyconkr-admin/src/components/pages/presentation/editor.tsx @@ -1,5 +1,13 @@ import { Components } from "@frontend/common"; -import { useBackendAdminClient, useCreateMutation, useListQuery, useRemovePreparedMutation, useSchemaQuery, useUpdatePreparedMutation } from "@frontend/common/src/hooks/useAdminAPI"; +import { + useBackendAdminClient, + useCreateMutation, + useListQuery, + useRemovePreparedMutation, + useSchemaQuery, + useUpdatePreparedMutation, +} from "@frontend/common/src/hooks/useAdminAPI"; +import { useCommonContext } from "@frontend/common/src/hooks/useCommonContext"; import { Autocomplete, Box, Button, Card, CardContent, CircularProgress, Stack, styled, Tab, Tabs, TextField, Typography } from "@mui/material"; import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterLuxon } from "@mui/x-date-pickers/AdapterLuxon"; @@ -10,6 +18,7 @@ import { enqueueSnackbar, OptionsObject } from "notistack"; import * as React from "react"; import { useParams } from "react-router-dom"; +import { ErrorFallback } from "../../elements/error_fallback"; import { AdminEditor } from "../../layouts/admin_editor"; const DUMMY_UUID = "00000000-0000-4000-8000-000000000000"; @@ -87,6 +96,7 @@ type AutoCompleteType = { }; const PresentationSpeakerForm: React.FC = ({ disabled, schema, speaker, onChange, onRemove }) => { + const { baseUrl, mdxComponents } = useCommonContext(); const [formState, setFormState] = React.useState({ tab: "ko" }); const setLanguage = (_: React.SyntheticEvent, tab: "ko" | "en") => setFormState((ps) => ({ ...ps, tab })); @@ -147,7 +157,7 @@ const PresentationSpeakerForm: React.FC = ({ di - + @@ -264,7 +274,7 @@ type PresentationEditorStateType = { }; export const AdminPresentationEditor: React.FC = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with({ fallback: }, () => { const { id } = useParams<{ id?: string }>(); diff --git a/apps/pyconkr-admin/src/components/pages/sitemap/list.tsx b/apps/pyconkr-admin/src/components/pages/sitemap/list.tsx index 002bb816..67d5c36f 100644 --- a/apps/pyconkr-admin/src/components/pages/sitemap/list.tsx +++ b/apps/pyconkr-admin/src/components/pages/sitemap/list.tsx @@ -1,4 +1,3 @@ -import { Components } from "@frontend/common"; import { useBackendAdminClient, useListQuery, useRemovePreparedMutation, useUpdatePreparedMutation } from "@frontend/common/src/hooks/useAdminAPI"; import { buildFlatSiteMap, buildNestedSiteMap } from "@frontend/common/src/utils"; import { Add, Delete, Edit, Save } from "@mui/icons-material"; @@ -23,6 +22,7 @@ import { GroupOptions, ReactSortable, SortableEvent, SortableOptions } from "rea import { FlattenedSiteMapSchema, NestedSiteMapSchema } from "../../../../../../packages/common/src/schemas/backendAdminAPI"; import { BackendAdminSignInGuard } from "../../elements/admin_signin_guard"; +import { ErrorFallback } from "../../elements/error_fallback"; import { AdminEditor } from "../../layouts/admin_editor"; type FlatSiteMap = FlattenedSiteMapSchema; @@ -95,7 +95,7 @@ type InnerSiteMapStateType = { const ModifyDetectionFields: (keyof FlatSiteMap)[] = ["order", "parent_sitemap"]; const InnerSiteMapList: React.FC = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with({ fallback: }, () => { const backendAdminAPIClient = useBackendAdminClient(); const { data } = useListQuery(backendAdminAPIClient, "cms", "sitemap"); diff --git a/apps/pyconkr-admin/src/components/pages/user/editor.tsx b/apps/pyconkr-admin/src/components/pages/user/editor.tsx index 7bc16c92..614eea47 100644 --- a/apps/pyconkr-admin/src/components/pages/user/editor.tsx +++ b/apps/pyconkr-admin/src/components/pages/user/editor.tsx @@ -1,4 +1,3 @@ -import { Components } from "@frontend/common"; import { useBackendAdminClient, useResetUserPasswordMutation } from "@frontend/common/src/hooks/useAdminAPI"; import { KeyOff } from "@mui/icons-material"; import { Button, ButtonProps, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; @@ -8,6 +7,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { PasswordResultDialog } from "./password_result_dialog"; import { addErrorSnackbar } from "../../../utils/snackbar"; +import { ErrorFallback } from "../../elements/error_fallback"; import { AdminEditor } from "../../layouts/admin_editor"; type PageStateType = { @@ -18,7 +18,7 @@ type PageStateType = { }; export const AdminUserExtEditor: React.FC = ErrorBoundary.with( - { fallback: Components.ErrorFallback }, + { fallback: ErrorFallback }, Suspense.with({ fallback: }, () => { const { id } = useParams<{ id?: string }>(); const navigate = useNavigate(); diff --git a/apps/pyconkr-participant-portal/src/components/dialogs/public_file_upload.tsx b/apps/pyconkr-participant-portal/src/components/dialogs/public_file_upload.tsx index 7c245f6f..9794dc88 100644 --- a/apps/pyconkr-participant-portal/src/components/dialogs/public_file_upload.tsx +++ b/apps/pyconkr-participant-portal/src/components/dialogs/public_file_upload.tsx @@ -108,7 +108,7 @@ export const PublicFileUploadDialog: React.FC = ({ - + + +
+ {corpMailOrderSalesRegistrationNumberStr} + + {hostingProviderStr} + + {contractEmailStr} + pyconkr@pycon.kr + + + {links.map((link, index) => ( + + + {link.text} + + {index < links.length - 1 && |} + + ))} + + + + + {defaultIcons.map((icon) => ( + + + ))} + + {copyrightStr} + + + ); +} + +const FooterContainer = styled.footer` + background-color: ${({ theme }) => theme.palette.primary.main}; + color: ${({ theme }) => theme.palette.common.white}; + font-size: 0.75rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + max-height: 16rem; + padding: 1rem 0; +`; + +const FooterContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; +`; + +const FooterText = styled.div` + padding: 0 2rem; + margin: 0.1rem; + + font-size: 9pt; + + a > button { + margin-left: 0.25rem; + padding: 0.05rem 0.25rem; + font-size: 8pt; + color: ${({ theme }) => theme.palette.common.white}; + border-color: ${({ theme }) => theme.palette.common.white}; + + gap: 0.25rem; + + & span { + margin-left: -2px; + margin-right: 0; + + & svg { + font-size: 12pt !important; + } + } + } + + strong { + font-size: 12pt; + } +`; + +const FooterSlogan = styled.div` + text-align: center; +`; + +const FooterLinks = styled.div` + display: flex; + align-items: center; + gap: 0.625rem; +`; + +const FooterIcons = styled.div` + display: flex; + align-items: center; + gap: 9px; +`; + +const Link = styled.a` + color: ${({ theme }) => theme.palette.common.white}; + text-decoration: none; + &:hover { + text-decoration: underline; + } +`; + +const Separator = styled.span` + color: ${({ theme }) => theme.palette.common.white}; + opacity: 0.5; +`; + +const IconLink = styled.a` + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; + + &:hover { + opacity: 0.8; + } + + img { + width: 20px; + height: 20px; + } +`; diff --git a/apps/pyconkr-2026/src/components/layout/Header/index.tsx b/apps/pyconkr-2026/src/components/layout/Header/index.tsx new file mode 100644 index 00000000..38e8fd75 --- /dev/null +++ b/apps/pyconkr-2026/src/components/layout/Header/index.tsx @@ -0,0 +1,307 @@ +import { Components } from "@frontend/common"; +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 { NestedSiteMapSchema } from "../../../../../../packages/common/src/schemas/backendAPI"; +import { useAppContext } from "../../../contexts/app_context"; +import { CartBadgeButton } from "../CartBadgeButton"; +import LanguageSelector from "../LanguageSelector"; +import { SignInButton } from "../SignInButton"; +// import { ScanCodeIconButton } from "../UserScanCodeButton"; + +type MenuType = 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 { 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 ( + + + + + + + + + + {siteMapNode ? ( + <> + + {Object.values(siteMapNode.children) + .filter((s) => !s.hide) + .map((r) => ( + + + + ))} + + + {navState.depth1 && ( + + + + {navState.depth1.name} + + + + + {Object.values(navState.depth1.children) + .filter((s) => !s.hide) + .map((r) => ( + setDepth2(r)} + // 하위 depth가 있는 경우, 하위 depth를 선택할 수 있도록 유지하기 위해 depth2도 유지합니다. + onMouseLeave={() => R.isEmpty(navState.depth2?.children ?? {}) && setDepth2(undefined)} + target={R.isString(r.external_link) ? "_blank" : undefined} + rel={R.isString(r.external_link) ? "noopener noreferrer" : undefined} + to={r.external_link || getDepth2Route(r.route_code)} + /> + ))} + + + {navState.depth2 && !R.isEmpty(navState.depth2.children) && ( + <> + {!R.isEmpty(navState.depth2.children) && } + + + {Object.values(navState.depth2.children) + .filter((s) => !s.hide) + .map((r) => ( + setDepth3(r)} + onMouseLeave={() => setDepth3(undefined)} + target={R.isString(r.external_link) ? "_blank" : undefined} + rel={R.isString(r.external_link) ? "noopener noreferrer" : undefined} + to={r.external_link || getDepth3Route(r?.route_code)} + /> + ))} + + + )} + + + + )} + + ) : ( + + )} + + + + + {/* */} + + + + + {shouldShowTitleBanner && ( + <> + + + {breadCrumbArray + .filter((routeInfo) => R.isNonNullish(routeInfo)) + .map(({ route_code, name }, index) => { + breadCrumbRoute += `${route_code}/`; + return ( + + {index > 0 && } + + + ); + })} + + + {title} + + + {/* Spacer for fixed header */} + + + )} + + ); +}; + +const ResponsivePaddingDefinition = ({ theme }: MUIStyledCommonProps) => ({ + paddingRight: theme!.spacing(16), + paddingLeft: theme!.spacing(16), + + [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 HeaderContainer = styled("header")(({ theme }) => ({ + position: "fixed", + + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + + width: "100%", + minWidth: "100%", + maxWidth: "100%", + height: HeaderHeight, + + backgroundColor: theme.palette.primary.light, + color: theme.palette.primary.dark, + + fontWeight: 500, + + zIndex: theme.zIndex.appBar, + transition: "background-color 0.3s ease-in-out", + + ...ResponsivePaddingDefinition({ theme }), +})); + +const NavOuterContainer = styled(Stack)(({ theme }) => ({ + width: "100vw", + + position: "fixed", + left: 0, + top: HeaderHeight, + + zIndex: theme.zIndex.appBar + 1, + + backgroundColor: "rgba(255, 255, 255, 0.7)", + boxShadow: "0 5px 5px 0px rgba(0, 0, 0, 0.1)", + backdropFilter: "blur(10px)", + + fontSize: "0.875rem", + color: theme.palette.primary.dark, +})); + +const NavInnerContainer = styled(Stack)(({ theme }) => ({ + width: "100%", + minHeight: "10rem", + overflowY: "auto", + gap: "1rem", + + backgroundColor: "rgba(182, 216, 215, 0.05)", + + paddingTop: "1.5rem", + paddingBottom: "2rem", + + ...ResponsivePaddingDefinition({ theme }), +})); + +const NavSideElementContainer = styled(Stack)({ + flexGrow: 1, + flexBasis: 0, +}); + +const Depth1to2Divider = styled(Divider)(({ theme }) => ({ + width: "3.375rem", + borderBottom: `4px solid ${theme.palette.highlight.main}`, +})); + +const Depth2Item = styled(Link)(({ theme }) => ({ + fontWeight: 300, + textDecoration: "none", + width: "fit-content", + borderBottom: "2px solid transparent", + + "&.active": { + fontWeight: 700, + borderBottom: `2px solid ${theme.palette.primary.dark}`, + }, +})); + +const Depth2to3Divider = styled(Divider)(({ theme }) => ({ borderColor: theme.palette.primary.light })); + +const Depth3Item = styled(Depth2Item)({ fontSize: "0.75rem" }); + +const BreadCrumbContainer = styled(Stack)(({ theme }) => ({ + position: "fixed", + + top: HeaderHeight, + width: "100%", + height: BreadCrumbHeight, + 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, + + ...ResponsivePaddingDefinition({ theme }), + + "& a": { + color: "#000000", + fontWeight: 300, + fontSize: "0.75rem", + textDecoration: "none", + + "&:hover": { + textDecoration: "underline", + }, + }, + "& svg": { + color: "rgba(0, 0, 0, 0.5)", + fontSize: "0.75rem", + }, +})); + +export default Header; diff --git a/apps/pyconkr-2026/src/components/layout/LanguageSelector/index.tsx b/apps/pyconkr-2026/src/components/layout/LanguageSelector/index.tsx new file mode 100644 index 00000000..5535802d --- /dev/null +++ b/apps/pyconkr-2026/src/components/layout/LanguageSelector/index.tsx @@ -0,0 +1,33 @@ +import { Language } from "@mui/icons-material"; +import { Button, Stack, styled } from "@mui/material"; + +import { LOCAL_STORAGE_LANGUAGE_KEY } from "../../../consts/local_stroage"; +import { useAppContext } from "../../../contexts/app_context"; + +export default function LanguageSelector() { + const { language, setAppContext } = useAppContext(); + const toggleLanguage = () => { + const newLanguage = language === "ko" ? "en" : "ko"; + localStorage.setItem(LOCAL_STORAGE_LANGUAGE_KEY, newLanguage); + setAppContext((ps) => ({ ...ps, language: newLanguage })); + }; + + return ( + + theme.palette.primary.nonFocus, w: "1.5rem", h: "1.5rem" }} /> + + KO + + + EN + + + ); +} + +const LanguageItem = styled(Button)<{ selected: boolean }>(({ selected, theme }) => ({ + color: selected ? theme.palette.primary.dark : theme.palette.primary.nonFocus, + minWidth: 0, + padding: "0.375rem 0.25rem", + transition: "color 0.2s ease", +})); diff --git a/apps/pyconkr-2026/src/components/layout/PageLayout/index.tsx b/apps/pyconkr-2026/src/components/layout/PageLayout/index.tsx new file mode 100644 index 00000000..322bb1c9 --- /dev/null +++ b/apps/pyconkr-2026/src/components/layout/PageLayout/index.tsx @@ -0,0 +1,23 @@ +import { Stack, styled } from "@mui/material"; + +export const PageLayout = styled(Stack)(({ theme }) => ({ + height: "75%", + width: "100%", + maxWidth: "1200px", + + justifyContent: "flex-start", + alignItems: "center", + + paddingTop: theme.spacing(8), + paddingBottom: theme.spacing(8), + + paddingRight: theme.spacing(16), + paddingLeft: theme.spacing(16), + + [theme.breakpoints.down("lg")]: { + padding: theme.spacing(4), + }, + [theme.breakpoints.down("sm")]: { + padding: theme.spacing(2), + }, +})); diff --git a/apps/pyconkr-2026/src/components/layout/SignInButton/index.tsx b/apps/pyconkr-2026/src/components/layout/SignInButton/index.tsx new file mode 100644 index 00000000..79961295 --- /dev/null +++ b/apps/pyconkr-2026/src/components/layout/SignInButton/index.tsx @@ -0,0 +1,105 @@ +import * as Shop from "@frontend/shop"; +import { Login, Logout } from "@mui/icons-material"; +import { Button, Stack } from "@mui/material"; +import { ErrorBoundary, Suspense } from "@suspensive/react"; +import { useNavigate } from "react-router-dom"; + +import { useAppContext } from "../../../contexts/app_context"; + +type InnerSignInButtonImplPropType = { + loading?: boolean; + signedIn?: boolean; + onSignOut?: () => void; + isMobile?: boolean; + isMainPath?: boolean; + onClose?: () => void; +}; + +const InnerSignInButtonImpl: React.FC = ({ + loading, + signedIn, + onSignOut, + isMobile = false, + isMainPath = true, + onClose, +}) => { + const navigate = useNavigate(); + const { language } = useAppContext(); + + const signInBtnStr = language === "ko" ? "로그인" : "Sign In"; + const signOutBtnStr = language === "ko" ? "로그아웃" : "Sign Out"; + + const handleClick = () => { + if (signedIn) { + onSignOut?.(); + } else { + onClose?.(); + navigate("/account/sign-in"); + } + }; + + if (isMobile) { + return ( + + ); + } + + return ( +