diff --git a/apps/pyconkr-2026/src/components/layout/UserMenuButton/index.tsx b/apps/pyconkr-2026/src/components/layout/UserMenuButton/index.tsx index b05099e7..81862665 100644 --- a/apps/pyconkr-2026/src/components/layout/UserMenuButton/index.tsx +++ b/apps/pyconkr-2026/src/components/layout/UserMenuButton/index.tsx @@ -2,7 +2,7 @@ import { useCommonContext } from "@frontend/common/hooks/useCommonContext"; import { UserSignInAccount, UserSignInMethod } from "@frontend/shop/components/common"; import { useShopClient, useSignOutMutation, useUserStatus } from "@frontend/shop/hooks"; import { UserSignedInStatus } from "@frontend/shop/schemas"; -import { AccountCircle, Login, Logout, ManageAccounts, Receipt } from "@mui/icons-material"; +import { AccountCircle, CalendarMonth, Login, Logout, ManageAccounts, Receipt } from "@mui/icons-material"; import { Button, Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, styled, Typography } from "@mui/material"; import { ErrorBoundary, Suspense } from "@suspensive/react"; import { FC, MouseEvent, useState } from "react"; @@ -52,6 +52,7 @@ const InnerUserMenuButton: FC = ({ loading, user, o const handleMenuClose = () => setAnchorEl(null); const signInLabel = language === "ko" ? "로그인" : "Sign In"; + const myTimetableLabel = language === "ko" ? "나의 세션 시간표 보기" : "View My Session Schedule"; const orderHistoryLabel = language === "ko" ? "결제 내역" : "Order History"; const manageAccountLabel = language === "ko" ? "계정 관리" : "Manage Account"; const signOutLabel = language === "ko" ? "로그아웃" : "Sign Out"; @@ -109,6 +110,12 @@ const InnerUserMenuButton: FC = ({ loading, user, o , , + + + + + {myTimetableLabel} + , diff --git a/apps/pyconkr-2026/src/components/pages/my-timetable.tsx b/apps/pyconkr-2026/src/components/pages/my-timetable.tsx new file mode 100644 index 00000000..3c16a043 --- /dev/null +++ b/apps/pyconkr-2026/src/components/pages/my-timetable.tsx @@ -0,0 +1,76 @@ +import { useBackendClient, useSessionsQuery } from "@frontend/common/hooks/useAPI"; +import { getSessionDetailUrl } from "@frontend/common/utils"; +import { useShopClient, useUserStatus } from "@frontend/shop/hooks"; +import { Button, CircularProgress, Stack, Typography } from "@mui/material"; +import { FC, useEffect, useMemo } from "react"; +import { Link as RouterLink } from "react-router-dom"; + +import { PageLayout } from "@apps/pyconkr-2026/components/layout/PageLayout"; +import { EVENT_NAME } from "@apps/pyconkr-2026/consts"; +import { useAppContext } from "@apps/pyconkr-2026/contexts/app_context"; +import { MyTimetableGrid, TimetablePlacement, TRACKS } from "@apps/pyconkr-2026/features/schedule/my_timetable_grid"; +import { useMyScheduleQuery } from "@apps/pyconkr-2026/features/schedule/use_my_schedule"; + +export const MyTimetablePage: FC = () => { + const { language, setAppContext } = useAppContext(); + const isKo = language === "ko"; + const backendClient = useBackendClient(); + const { data: user } = useUserStatus(useShopClient()); + const isAuthenticated = user?.meta.is_authenticated === true; + const { data: sessions } = useSessionsQuery(backendClient, { event: EVENT_NAME, types: "세션" }); + const eventId = sessions[0]?.presentation_type.event.id; + const { data: bookmarks, isLoading } = useMyScheduleQuery(backendClient, eventId, isAuthenticated); + const placements = useMemo(() => { + const selectedIds = new Set(bookmarks?.presentation_ids ?? []); + return sessions.flatMap((session) => + selectedIds.has(session.id) + ? (session.room_schedules.length === TRACKS.length ? session.room_schedules.slice(0, 1) : session.room_schedules).flatMap( + (schedule): TimetablePlacement[] => { + const track = TRACKS.find(({ roomOrder }) => roomOrder === schedule.room_order); + const startMs = Date.parse(schedule.start_at); + const endMs = Date.parse(schedule.end_at); + if (!track || !Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) return []; + return [ + { + key: `${session.id}:${schedule.id}`, + trackKey: track.key, + startMs, + endMs, + title: session.title, + speakers: session.speakers.map(({ nickname }) => nickname).join(", ") || undefined, + href: getSessionDetailUrl(session), + trackSpan: session.room_schedules.length === TRACKS.length ? TRACKS.length : undefined, + }, + ]; + } + ) + : [] + ); + }, [bookmarks, sessions]); + + useEffect(() => { + setAppContext((prev) => ({ + ...prev, + title: isKo ? "내 시간표" : "My Schedule", + shouldShowTitleBanner: true, + shouldShowSponsorBanner: false, + })); + }, [isKo, setAppContext]); + + if (!isAuthenticated) { + return ( + + + + +