diff --git a/apps/pyconkr-2026/index.html b/apps/pyconkr-2026/index.html
index 6d1a4d2..b921dcb 100644
--- a/apps/pyconkr-2026/index.html
+++ b/apps/pyconkr-2026/index.html
@@ -1,18 +1,18 @@
-
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
@@ -22,7 +22,9 @@
-
+ shrink-to-fit=no"
+ />
diff --git a/apps/pyconkr-2026/package.json b/apps/pyconkr-2026/package.json
index bbec73a..0351ea9 100644
--- a/apps/pyconkr-2026/package.json
+++ b/apps/pyconkr-2026/package.json
@@ -1,13 +1,13 @@
{
- "name": "@apps/pyconkr-2026",
- "dependencies": {
- "@frontend/common": "workspace:*",
- "@frontend/shop": "workspace:*"
- },
- "devDependencies": {
- "vite": "^6.3.5",
- "vite-plugin-mdx": "^3.6.1",
- "vite-plugin-mkcert": "^1.17.8",
- "vite-plugin-svgr": "^4.3.0"
- }
+ "name": "@apps/pyconkr-2026",
+ "dependencies": {
+ "@frontend/common": "workspace:*",
+ "@frontend/shop": "workspace:*"
+ },
+ "devDependencies": {
+ "vite": "^6.3.5",
+ "vite-plugin-mdx": "^3.6.1",
+ "vite-plugin-mkcert": "^1.17.8",
+ "vite-plugin-svgr": "^4.3.0"
+ }
}
diff --git a/apps/pyconkr-2026/src/App.tsx b/apps/pyconkr-2026/src/App.tsx
index de9e00b..051811e 100644
--- a/apps/pyconkr-2026/src/App.tsx
+++ b/apps/pyconkr-2026/src/App.tsx
@@ -1,10 +1,10 @@
import { useBackendClient, useFlattenSiteMapQuery, useSponsorQuery } from "@frontend/common/src/hooks/useAPI";
+import * as BackendAPISchemas from "@frontend/common/src/schemas/backendAPI";
import { buildNestedSiteMap } from "@frontend/common/src/utils";
import * as React from "react";
import { Route, Routes, useLocation } from "react-router-dom";
import * as R from "remeda";
-import * as BackendAPISchemas from "@frontend/common/src/schemas/backendAPI";
import MainLayout from "./components/layout/index.tsx";
import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx";
import { PresentationDetailPage } from "./components/pages/presentation_detail.tsx";
diff --git a/apps/pyconkr-2026/src/assets/pyconkr2026_main_cover_image.png b/apps/pyconkr-2026/src/assets/pyconkr2026_main_cover_image.png
new file mode 100644
index 0000000..97f37df
Binary files /dev/null and b/apps/pyconkr-2026/src/assets/pyconkr2026_main_cover_image.png differ
diff --git a/apps/pyconkr-2026/src/components/layout/Footer/Mobile/MobileFooter.tsx b/apps/pyconkr-2026/src/components/layout/Footer/Mobile/MobileFooter.tsx
new file mode 100644
index 0000000..2eb9368
--- /dev/null
+++ b/apps/pyconkr-2026/src/components/layout/Footer/Mobile/MobileFooter.tsx
@@ -0,0 +1,167 @@
+import styled from "@emotion/styled";
+import { useEmail } from "@frontend/common/src/hooks/useEmail";
+import { Article, Email, Facebook, GitHub, Instagram, LinkedIn, X, YouTube } from "@mui/icons-material";
+import * as React from "react";
+
+import FlickrIcon from "@apps/pyconkr-2026/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/" },
+];
+
+export default function MobileFooter() {
+ const { sendEmail } = useEmail();
+ const { language } = useAppContext();
+
+ const title = language === "ko" ? "파이콘 한국 2026" : "PyCon Korea 2026";
+ const committeeTitle =
+ language === "ko"
+ ? "파이콘 한국 2026은 파이콘 한국 준비위원회가 만들고 있습니다"
+ : "PyCon Korea 2026 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,
+ ${({ theme }) => theme.palette.background.default} 0%,
+ ${({ theme }) => theme.palette.background.paper} 25%,
+ ${({ theme }) => theme.palette.primary.dark} 75%,
+ ${({ theme }) => theme.palette.primary.main} 100%
+ );
+ color: ${({ theme }) => theme.palette.text.primary};
+ 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 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.text.primary};
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
+const Separator = styled.span`
+ color: ${({ theme }) => theme.palette.text.primary};
+ opacity: 0.5;
+ margin: 0.05rem 0;
+`;
+
+const IconLink = styled.a`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: ${({ theme }) => theme.palette.text.primary};
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ img {
+ width: 20px;
+ height: 20px;
+ }
+`;
diff --git a/apps/pyconkr-2026/src/components/layout/Footer/index.tsx b/apps/pyconkr-2026/src/components/layout/Footer/index.tsx
index 7f947fb..a010177 100644
--- a/apps/pyconkr-2026/src/components/layout/Footer/index.tsx
+++ b/apps/pyconkr-2026/src/components/layout/Footer/index.tsx
@@ -1,12 +1,13 @@
import styled from "@emotion/styled";
import { useEmail } from "@frontend/common/src/hooks/useEmail";
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-2026/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,9 +50,13 @@ const Bar: React.FC = () => ;
+
const corpPasamoStr = language === "ko" ? "사단법인 파이썬사용자모임" : "Python Korea";
const corpAddressStr =
language === "ko" ? "서울특별시 강남구 강남대로84길 24-4" : "24-4, Gangnam-daero 84-gil, Gangnam-gu, Seoul, Republic of Korea";
@@ -137,8 +142,14 @@ export default function Footer() {
}
const FooterContainer = styled.footer`
- background-color: ${({ theme }) => theme.palette.primary.main};
- color: ${({ theme }) => theme.palette.common.white};
+ background: linear-gradient(
+ to bottom,
+ ${({ theme }) => theme.palette.background.default} 0%,
+ ${({ theme }) => theme.palette.background.paper} 25%,
+ ${({ theme }) => theme.palette.primary.dark} 75%,
+ ${({ theme }) => theme.palette.primary.main} 100%
+ );
+ color: ${({ theme }) => theme.palette.text.primary};
font-size: 0.75rem;
display: flex;
flex-direction: column;
diff --git a/apps/pyconkr-2026/src/components/layout/Header/Mobile/HamburgerButton.tsx b/apps/pyconkr-2026/src/components/layout/Header/Mobile/HamburgerButton.tsx
new file mode 100644
index 0000000..18f90b5
--- /dev/null
+++ b/apps/pyconkr-2026/src/components/layout/Header/Mobile/HamburgerButton.tsx
@@ -0,0 +1,45 @@
+import { IconButton, styled } from "@mui/material";
+import * as React from "react";
+
+interface HamburgerButtonProps {
+ isOpen: boolean;
+ onClick: () => void;
+}
+
+export const HamburgerButton: React.FC = ({ isOpen, onClick }) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const StyledIconButton = styled(IconButton)({
+ padding: 0,
+ width: 26,
+ height: 18,
+ color: "#ededde",
+});
+
+const HamburgerIcon = styled("div")<{ isOpen: boolean }>(({ isOpen }) => ({
+ 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: "#ededde",
+ borderRadius: 1,
+ transition: "height 0.3s ease",
+ },
+}));
diff --git a/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileHeader.tsx b/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileHeader.tsx
new file mode 100644
index 0000000..98c5f1c
--- /dev/null
+++ b/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileHeader.tsx
@@ -0,0 +1,58 @@
+import { Components } from "@frontend/common";
+import { Box, Stack, styled, Typography } from "@mui/material";
+import * as React from "react";
+import { Link } from "react-router-dom";
+
+import { HamburgerButton } from "./HamburgerButton";
+import { MobileNavigation } from "./MobileNavigation";
+import LanguageSelector from "../../LanguageSelector";
+import { useAppContext } from "../../../../contexts/app_context";
+
+export const MobileHeader: React.FC = () => {
+ const { siteMapNode } = useAppContext();
+ const [isOpen, setIsOpen] = React.useState(false);
+
+ return (
+ <>
+
+
+ setIsOpen(!isOpen)} />
+
+
+
+
+ 파이콘 한국 2026
+
+
+
+
+
+
+
+ setIsOpen(false)} siteMapNode={siteMapNode} />
+ >
+ );
+};
+
+const MobileHeaderContainer = styled("header")<{ isOpen: boolean }>(({ theme, isOpen }) => ({
+ display: isOpen ? "none" : "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+
+ position: "sticky",
+ top: 0,
+ left: 0,
+ right: 0,
+ width: "100%",
+ height: 60,
+ padding: "15px 20px",
+
+ backgroundColor: "rgba(18, 9, 30, 0.9)",
+ backdropFilter: "blur(10px)",
+ WebkitBackdropFilter: "blur(10px)",
+ borderBottom: "1px solid rgba(237, 94, 189, 0.2)",
+
+ zIndex: theme.zIndex.appBar,
+}));
+
+const LeftContent = styled(Box)({ display: "flex", alignItems: "center", gap: 17 });
diff --git a/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileNavigation.tsx
new file mode 100644
index 0000000..7848de4
--- /dev/null
+++ b/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileNavigation.tsx
@@ -0,0 +1,264 @@
+import { Components } from "@frontend/common";
+import * as BackendAPISchemas from "@frontend/common/src/schemas/backendAPI";
+import { ArrowBack, ArrowForward } from "@mui/icons-material";
+import { Box, Button, Chip, Drawer, IconButton, Stack, styled } from "@mui/material";
+import * as React from "react";
+import { Link } from "react-router-dom";
+import * as R from "remeda";
+
+import { HamburgerButton } from "./HamburgerButton";
+import LanguageSelector from "../../LanguageSelector";
+
+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 [navState, setNavState] = React.useState({ level: "depth1", breadcrumbs: [] });
+
+ 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 renderDepth1Menu = () => {
+ if (!siteMapNode) return null;
+ return (
+
+ {Object.values(siteMapNode.children)
+ .filter((s) => !s.hide)
+ .map((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) && Object.values(menu.children).some((c) => !c.hide) && (
+ 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 (
+
+
+
+
+
+
+
+ 파이콘 한국 2026
+
+
+
+
+
+ {navState.level === "depth1" && renderDepth1Menu()}
+ {navState.level === "depth2" && renderDepth2Menu()}
+ {navState.level === "depth3" && renderDepth3Menu()}
+
+
+
+
+
+
+
+ );
+};
+
+const StyledDrawer = styled(Drawer)({
+ "& .MuiDrawer-paper": {
+ width: "70vw",
+ background: "rgba(18, 9, 30, 0.97)",
+ backdropFilter: "blur(12px)",
+ WebkitBackdropFilter: "blur(12px)",
+ color: "#ededde",
+ borderTopRightRadius: 15,
+ borderBottomRightRadius: 15,
+ borderRight: "1px solid rgba(237, 94, 189, 0.2)",
+ },
+});
+
+const DrawerContent = styled(Box)({ height: "100%", display: "flex", flexDirection: "column" });
+
+const NavigationHeader = styled(Box)({
+ display: "flex",
+ alignItems: "center",
+ padding: "23px 23px 10px 23px",
+ gap: 17,
+});
+
+const HeaderTitle = styled("span")({
+ color: "#ededde",
+ fontSize: "16px",
+ fontWeight: 600,
+});
+
+const NavigationContent = styled(Box)({ flex: 1, overflow: "auto" });
+
+const MenuContainer = styled(Stack)({ padding: "20px 0", gap: "25px" });
+
+const MenuItem = styled(Box)({ display: "flex", alignItems: "center", padding: "0 23px", gap: 23 });
+
+const MenuLink = styled(Link)({
+ color: "#ededde",
+ textDecoration: "none",
+ fontSize: "20px",
+ fontWeight: 600,
+});
+
+const MenuButton = styled(Button)({
+ color: "#ededde",
+ textTransform: "none",
+ fontSize: "20px",
+ fontWeight: 600,
+ padding: 0,
+ minWidth: "auto",
+ justifyContent: "flex-start",
+});
+
+const MenuArrowButton = styled(IconButton)({ color: "#ededde", padding: 8 });
+
+const NavigationMenuSection = styled(Box)({ padding: "20px 0" });
+
+const DepthHeader = styled(Box)({ display: "flex", alignItems: "center", padding: "0 23px 12px", gap: 8 });
+
+const BackButton = styled(Button)({
+ color: "#ed5ebd",
+ textTransform: "none",
+ padding: "0 15px 0 0",
+ minWidth: "auto",
+});
+
+const DepthTitle = styled("span")({ color: "#ededde", fontSize: "18px", fontWeight: 700 });
+
+const DepthDivider = styled("div")({
+ margin: "0 23px 16px",
+ height: 3,
+ width: "3rem",
+ borderRadius: 2,
+ backgroundColor: "#f5c73d",
+});
+
+const DepthMenuList = styled(Stack)({ padding: "0 23px", gap: "12px" });
+
+const Depth2MenuItem = styled(Box)({ display: "flex", alignItems: "center", gap: 8 });
+
+const DepthMenuGrid = styled(Box)({ padding: "0 23px", display: "flex", flexWrap: "wrap", gap: "10px" });
+
+const MenuChip = styled(Chip)({
+ backgroundColor: "rgba(237, 94, 189, 0.15)",
+ color: "#ededde",
+ height: 40,
+ borderRadius: 15,
+ padding: "10px 4px",
+ fontSize: "15px",
+ fontWeight: 600,
+ border: "1px solid rgba(237, 94, 189, 0.3)",
+ "&:hover": { backgroundColor: "rgba(237, 94, 189, 0.3)" },
+ "& .MuiChip-label": { padding: "0 10px" },
+});
diff --git a/apps/pyconkr-2026/src/components/layout/Header/index.tsx b/apps/pyconkr-2026/src/components/layout/Header/index.tsx
index 38e8fd7..b9da072 100644
--- a/apps/pyconkr-2026/src/components/layout/Header/index.tsx
+++ b/apps/pyconkr-2026/src/components/layout/Header/index.tsx
@@ -1,6 +1,6 @@
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 { 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";
@@ -8,10 +8,8 @@ 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";
+import { MobileHeader } from "./Mobile/MobileHeader";
type MenuType = NestedSiteMapSchema;
type MenuOrUndefinedType = MenuType | undefined;
@@ -25,8 +23,10 @@ type NavigationStateType = {
const HeaderHeight: React.CSSProperties["height"] = "3.625rem";
const BreadCrumbHeight: React.CSSProperties["height"] = "4.5rem";
-const Header: React.FC = () => {
+export default function Header() {
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({});
@@ -39,114 +39,110 @@ 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);
- const headerContainerStyle: SxProps = shouldShowTitleBanner
- ? {}
- : {
- backgroundColor: "transparent",
- [":hover"]: { backgroundColor: (theme) => theme.palette.primary.light },
- };
+ const headerStyle: SxProps = shouldShowTitleBanner ? {} : { backgroundColor: "transparent" };
return (
-
+
-
-
-
+
+
+
+
+ PyCon Korea 2026
+
- {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)}
- />
- ))}
-
- >
- )}
-
-
-
- )}
- >
+ {siteMapNode ? (
+
+ {Object.values(siteMapNode.children)
+ .filter((s) => !s.hide)
+ .map((r) => (
+
+ setDepth1(r)} isActive={navState.depth1?.id === r.id}>
+ {r.name}
+
+
+ ))}
+
) : (
-
+
)}
-
-
-
-
- {/* */}
-
-
+
+
+
+
+ {navState.depth1 && (
+
+
+
+ {navState.depth1.name}
+
+
+
+
+ {Object.values(navState.depth1.children)
+ .filter((s) => !s.hide)
+ .map((r) => (
+ setDepth2(r)}
+ 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)}
+ >
+ {r.name}
+
+ ))}
+
+
+ {navState.depth2 && !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)}
+ >
+ {r.name}
+
+ ))}
+
+ >
+ )}
+
+
+
+ )}
+
{shouldShowTitleBanner && (
<>
@@ -157,151 +153,133 @@ const Header: React.FC = () => {
breadCrumbRoute += `${route_code}/`;
return (
- {index > 0 && }
+ {index > 0 && }
);
})}
-
+
{title}
- {/* Spacer for fixed header */}
>
)}
);
-};
+}
-const ResponsivePaddingDefinition = ({ theme }: MUIStyledCommonProps) => ({
+const ResponsivePadding = ({ 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),
- },
+ [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,
-
+ backgroundColor: "rgba(18, 9, 30, 0.85)",
+ backdropFilter: "blur(12px)",
+ WebkitBackdropFilter: "blur(12px)",
+ borderBottom: "1px solid rgba(237, 94, 189, 0.2)",
+ color: "#ededde",
fontWeight: 500,
-
zIndex: theme.zIndex.appBar,
transition: "background-color 0.3s ease-in-out",
+ "& .header-title-text": {
+ opacity: 0,
+ transition: "opacity 0.2s ease",
+ },
+ "&:hover .header-title-text": {
+ opacity: 1,
+ },
+ ...ResponsivePadding({ theme }),
+}));
- ...ResponsivePaddingDefinition({ theme }),
+const NavButton = styled(Button)<{ isActive?: boolean }>(({ isActive }) => ({
+ color: isActive ? "#ed5ebd" : "#ededde",
+ minWidth: 0,
+ textTransform: "none",
+ fontSize: "0.9rem",
+ fontWeight: isActive ? 700 : 400,
+ transition: "color 0.2s ease",
+ "&:hover": { color: "#ed5ebd", backgroundColor: "transparent" },
}));
-const NavOuterContainer = styled(Stack)(({ theme }) => ({
- width: "100vw",
+const NavSideElementContainer = styled(Stack)({ flexGrow: 1, flexBasis: 0 });
+const NavDropdownOuter = 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,
+ backgroundColor: "rgba(18, 9, 30, 0.95)",
+ boxShadow: "0 5px 20px rgba(0, 0, 0, 0.4)",
+ backdropFilter: "blur(12px)",
+ WebkitBackdropFilter: "blur(12px)",
+ borderBottom: "1px solid rgba(237, 94, 189, 0.2)",
}));
-const NavInnerContainer = styled(Stack)(({ theme }) => ({
+const NavDropdownInner = 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 }),
+ color: "#ededde",
+ ...ResponsivePadding({ theme }),
}));
-const NavSideElementContainer = styled(Stack)({
- flexGrow: 1,
- flexBasis: 0,
+const HighlightDivider = styled(Divider)({
+ width: "3rem",
+ borderBottom: "4px solid #f5c73d",
+ borderColor: "#f5c73d",
});
-const Depth1to2Divider = styled(Divider)(({ theme }) => ({
- width: "3.375rem",
- borderBottom: `4px solid ${theme.palette.highlight.main}`,
-}));
-
-const Depth2Item = styled(Link)(({ theme }) => ({
+const Depth2Item = styled(Link)({
+ color: "#ededde",
fontWeight: 300,
textDecoration: "none",
width: "fit-content",
borderBottom: "2px solid transparent",
+ transition: "color 0.15s ease",
+ "&.active": { fontWeight: 700, borderBottom: "2px solid #ed5ebd", color: "#ed5ebd" },
+ "&:hover": { color: "#ed5ebd" },
+});
- "&.active": {
- fontWeight: 700,
- borderBottom: `2px solid ${theme.palette.primary.dark}`,
- },
-}));
-
-const Depth2to3Divider = styled(Divider)(({ theme }) => ({ borderColor: theme.palette.primary.light }));
+const Depth2to3Divider = styled(Divider)({ borderColor: "rgba(237, 94, 189, 0.3)" });
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)",
+ background: "rgba(18, 9, 30, 0.8)",
+ boxShadow: "0 1px 10px rgba(0, 0, 0, 0.3)",
backdropFilter: "blur(10px)",
-
+ WebkitBackdropFilter: "blur(10px)",
+ borderBottom: "1px solid rgba(237, 94, 189, 0.15)",
gap: "0.25rem",
justifyContent: "center",
alignItems: "flex-start",
-
zIndex: theme.zIndex.appBar - 1,
-
- ...ResponsivePaddingDefinition({ theme }),
-
+ ...ResponsivePadding({ theme }),
"& a": {
- color: "#000000",
+ color: "#f5c73d",
fontWeight: 300,
fontSize: "0.75rem",
textDecoration: "none",
-
- "&:hover": {
- textDecoration: "underline",
- },
- },
- "& svg": {
- color: "rgba(0, 0, 0, 0.5)",
- fontSize: "0.75rem",
+ "&:hover": { textDecoration: "underline" },
},
}));
-
-export default Header;
diff --git a/apps/pyconkr-2026/src/components/layout/index.tsx b/apps/pyconkr-2026/src/components/layout/index.tsx
index ee0da09..f86462c 100644
--- a/apps/pyconkr-2026/src/components/layout/index.tsx
+++ b/apps/pyconkr-2026/src/components/layout/index.tsx
@@ -2,10 +2,10 @@ import styled from "@emotion/styled";
import { Stack } from "@mui/material";
import { Outlet } from "react-router-dom";
+import { useAppContext } from "../../contexts/app_context";
import Footer from "./Footer";
import Header from "./Header";
import { Sponsor } from "./Sponsor";
-import { useAppContext } from "../../contexts/app_context";
export default function MainLayout() {
const { shouldShowSponsorBanner } = useAppContext();
diff --git a/apps/pyconkr-2026/src/consts/mdx_components.ts b/apps/pyconkr-2026/src/consts/mdx_components.ts
index 403ea8d..974feee 100644
--- a/apps/pyconkr-2026/src/consts/mdx_components.ts
+++ b/apps/pyconkr-2026/src/consts/mdx_components.ts
@@ -7,9 +7,9 @@ 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 PyCon2025Logo from "../../../../packages/common/src/assets/pyconkr2025_logo.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 "../../../../packages/common/src/assets/pyconkr2025_logo.png";
const MUIMDXComponents: MDXComponents = {
Mui__material__Accordion: mui.Accordion,
diff --git a/apps/pyconkr-2026/src/styles/globalStyles.ts b/apps/pyconkr-2026/src/styles/globalStyles.ts
index 0f60b97..e21d4c9 100644
--- a/apps/pyconkr-2026/src/styles/globalStyles.ts
+++ b/apps/pyconkr-2026/src/styles/globalStyles.ts
@@ -7,108 +7,109 @@ export const muiTheme = createTheme({
'Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif',
},
palette: {
+ mode: "dark",
primary: {
- main: "#259299",
- light: "#B6D8D7",
- dark: "#126D7F",
- nonFocus: "#7AB2B3",
+ main: "#ed5ebd",
+ light: "#f5a0dc",
+ dark: "#c93da0",
+ nonFocus: "#c97baa",
},
secondary: {
- main: "#259299",
- light: "#B6D8D7",
- dark: "#126D7F",
+ main: "#ed5ebd",
+ light: "#f5a0dc",
+ dark: "#c93da0",
},
highlight: {
- main: "#E17101",
- light: "#EE8D74",
- dark: "#C66900",
- contrastText: "#FFFFFF",
+ main: "#f5c73d",
+ light: "#f9d86d",
+ dark: "#d4a818",
+ contrastText: "#12091e",
},
mobileHeader: {
main: {
- background: "rgba(182, 216, 215, 0.1)",
- text: "#FFFFFF",
- activeLanguage: "#888888",
+ background: "rgba(237, 94, 189, 0.1)",
+ text: "#ededde",
+ activeLanguage: "#888880",
},
sub: {
- background: "#B6D8D7",
- text: "rgba(18, 109, 127, 0.6)",
- activeLanguage: "#126D7F",
+ background: "#1e1230",
+ text: "rgba(237, 94, 189, 0.6)",
+ activeLanguage: "#ed5ebd",
},
},
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",
+ "linear-gradient(0deg, rgba(18, 9, 30, 0.85), rgba(18, 9, 30, 0.85)), linear-gradient(0deg, rgba(237, 94, 189, 0.08), rgba(237, 94, 189, 0.08))",
+ text: "#ededde",
chip: {
- background: "rgba(212, 212, 212, 0.5)",
- hover: "rgba(212, 212, 212, 0.7)",
+ background: "rgba(237, 94, 189, 0.15)",
+ hover: "rgba(237, 94, 189, 0.25)",
},
- divider: "rgba(255, 255, 255, 0.3)",
+ divider: "rgba(237, 94, 189, 0.2)",
languageToggle: {
background: "transparent",
active: {
- background: "rgba(255, 255, 255, 0.7)",
- hover: "rgba(255, 255, 255, 0.8)",
+ background: "rgba(237, 94, 189, 0.3)",
+ hover: "rgba(237, 94, 189, 0.4)",
},
inactive: {
- hover: "rgba(255, 255, 255, 0.1)",
+ hover: "rgba(237, 94, 189, 0.1)",
},
},
},
sub: {
- background: "#B6D8D7",
- text: "rgba(18, 109, 127, 0.9)",
+ background: "#1e1230",
+ text: "rgba(237, 94, 189, 0.9)",
chip: {
- background: "rgba(18, 109, 127, 0.2)",
- hover: "rgba(18, 109, 127, 0.3)",
+ background: "rgba(237, 94, 189, 0.15)",
+ hover: "rgba(237, 94, 189, 0.25)",
},
- divider: "rgba(18, 109, 127, 0.3)",
+ divider: "rgba(237, 94, 189, 0.2)",
languageToggle: {
- background: "rgba(255, 255, 255, 0.1)",
+ background: "rgba(237, 94, 189, 0.05)",
active: {
- background: "rgba(255, 255, 255, 0.9)",
- hover: "rgba(255, 255, 255, 1)",
+ background: "rgba(237, 94, 189, 0.3)",
+ hover: "rgba(237, 94, 189, 0.4)",
},
inactive: {
- hover: "rgba(255, 255, 255, 0.3)",
+ hover: "rgba(237, 94, 189, 0.1)",
},
},
},
},
text: {
- primary: "#000000",
- secondary: "#666666",
- disabled: "#999999",
+ primary: "#ededde",
+ secondary: "#b8b8a8",
+ disabled: "#888880",
},
background: {
- default: "#FFFFFF",
- paper: "#FFFFFF",
+ default: "#12091e",
+ paper: "#1e1230",
},
common: {
black: "#000000",
- white: "#FFFFFF",
+ white: "#ffffff",
},
error: {
- main: "#d32f2f",
- light: "#ef5350",
- dark: "#c62828",
+ main: "#f44336",
+ light: "#e57373",
+ dark: "#d32f2f",
},
warning: {
- main: "#ed6c02",
- light: "#ff9800",
- dark: "#e65100",
+ main: "#f5c73d",
+ light: "#f9d86d",
+ dark: "#d4a818",
},
info: {
- main: "#0288d1",
- light: "#03a9f4",
- dark: "#01579b",
+ main: "#29b6f6",
+ light: "#4fc3f7",
+ dark: "#0288d1",
},
success: {
- main: "#2e7d32",
- light: "#4caf50",
- dark: "#1b5e20",
+ main: "#66bb6a",
+ light: "#81c784",
+ dark: "#388e3c",
},
},
});
diff --git a/apps/pyconkr-2026/tsconfig.json b/apps/pyconkr-2026/tsconfig.json
index 06ac591..e9694fd 100644
--- a/apps/pyconkr-2026/tsconfig.json
+++ b/apps/pyconkr-2026/tsconfig.json
@@ -25,8 +25,8 @@
/* Paths */
"baseUrl": ".",
"paths": {
- "@apps/pyconkr-2026/*": ["apps/pyconkr-2026/src/*"],
+ "@apps/pyconkr-2026/*": ["apps/pyconkr-2026/src/*"]
}
},
- "include": ["src", "vite.config.mts", "vite-env.d.ts", "../../types"],
+ "include": ["src", "vite.config.mts", "vite-env.d.ts", "../../types"]
}
diff --git a/packages/common/src/components/mdx_components/mobile_cover.tsx b/packages/common/src/components/mdx_components/mobile_cover.tsx
index cb37c6d..badd579 100644
--- a/packages/common/src/components/mdx_components/mobile_cover.tsx
+++ b/packages/common/src/components/mdx_components/mobile_cover.tsx
@@ -7,6 +7,7 @@ import * as Hooks from "../../hooks";
type MobileCoverProps = {
coverImageSrc: string;
coverTitleSrc: string;
+ coverImageObjectFit?: React.CSSProperties["objectFit"];
buttonTextKo?: string;
buttonTextEn?: string;
};
@@ -14,6 +15,7 @@ type MobileCoverProps = {
export const MobileCover: React.FC = ({
coverImageSrc,
coverTitleSrc,
+ coverImageObjectFit = "cover",
buttonTextKo = "티켓 구매하기",
buttonTextEn = "Buy Ticket",
}) => {
@@ -22,8 +24,8 @@ export const MobileCover: React.FC = ({
return (
-
-
+
+