From ea84f178917148e0ddf328a7e5f4a57da7c5cc17 Mon Sep 17 00:00:00 2001 From: y00eunji Date: Tue, 14 Jul 2026 13:43:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=84=B8=EC=85=98=20=EC=B0=9C=ED=95=98?= =?UTF-8?q?=EA=B8=B0=EC=99=80=20=EB=82=B4=20=EC=8B=9C=EA=B0=84=ED=91=9C=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 --- .../layout/UserMenuButton/index.tsx | 9 +- .../src/components/pages/my-timetable.tsx | 76 ++++ .../pyconkr-2026/src/consts/mdx_components.ts | 13 +- .../features/schedule/my_timetable_grid.tsx | 413 ++++++++++++++++++ .../schedule/toggle_schedule_button.tsx | 107 +++++ .../src/features/schedule/use_my_schedule.ts | 58 +++ apps/pyconkr-2026/src/router.tsx | 2 + .../mdx_components/session_list.tsx | 23 +- .../mdx_components/session_timetable.tsx | 2 +- 9 files changed, 695 insertions(+), 8 deletions(-) create mode 100644 apps/pyconkr-2026/src/components/pages/my-timetable.tsx create mode 100644 apps/pyconkr-2026/src/features/schedule/my_timetable_grid.tsx create mode 100644 apps/pyconkr-2026/src/features/schedule/toggle_schedule_button.tsx create mode 100644 apps/pyconkr-2026/src/features/schedule/use_my_schedule.ts 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 ( + + + + +