Skip to content

Commit ffde6b8

Browse files
authored
Merge pull request #70 from pythonkr/feature/timetable
feat: 세션 찜하기와 내 시간표 추가
2 parents 17c6423 + 1f1b597 commit ffde6b8

10 files changed

Lines changed: 755 additions & 8 deletions

File tree

apps/pyconkr-2026/src/components/layout/UserMenuButton/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useCommonContext } from "@frontend/common/hooks/useCommonContext";
22
import { UserSignInAccount, UserSignInMethod } from "@frontend/shop/components/common";
33
import { useShopClient, useSignOutMutation, useUserStatus } from "@frontend/shop/hooks";
44
import { UserSignedInStatus } from "@frontend/shop/schemas";
5-
import { AccountCircle, Login, Logout, ManageAccounts, Receipt } from "@mui/icons-material";
5+
import { AccountCircle, CalendarMonth, Login, Logout, ManageAccounts, Receipt } from "@mui/icons-material";
66
import { Button, Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, styled, Typography } from "@mui/material";
77
import { ErrorBoundary, Suspense } from "@suspensive/react";
88
import { FC, MouseEvent, useState } from "react";
@@ -52,6 +52,7 @@ const InnerUserMenuButton: FC<InnerUserMenuButtonPropType> = ({ loading, user, o
5252
const handleMenuClose = () => setAnchorEl(null);
5353

5454
const signInLabel = language === "ko" ? "로그인" : "Sign In";
55+
const myTimetableLabel = language === "ko" ? "나의 세션 시간표 보기" : "View My Session Schedule";
5556
const orderHistoryLabel = language === "ko" ? "결제 내역" : "Order History";
5657
const manageAccountLabel = language === "ko" ? "계정 관리" : "Manage Account";
5758
const signOutLabel = language === "ko" ? "로그아웃" : "Sign Out";
@@ -109,6 +110,12 @@ const InnerUserMenuButton: FC<InnerUserMenuButtonPropType> = ({ loading, user, o
109110
</Typography>
110111
</UserNameItem>,
111112
<Divider key="divider" sx={{ my: 0.5 }} />,
113+
<MenuItem key="my-timetable" component={RouterLink} to="/my-timetable" onClick={closeAll}>
114+
<ListItemIcon>
115+
<CalendarMonth fontSize="small" />
116+
</ListItemIcon>
117+
<ListItemText>{myTimetableLabel}</ListItemText>
118+
</MenuItem>,
112119
<MenuItem key="orders" component={RouterLink} to="/store/order-histories" onClick={closeAll}>
113120
<ListItemIcon>
114121
<Receipt fontSize="small" />
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useBackendClient, useSessionsQuery } from "@frontend/common/hooks/useAPI";
2+
import { getSessionDetailUrl } from "@frontend/common/utils";
3+
import { useShopClient, useUserStatus } from "@frontend/shop/hooks";
4+
import { Button, CircularProgress, Stack, Typography } from "@mui/material";
5+
import { FC, useEffect, useMemo } from "react";
6+
import { Link as RouterLink } from "react-router-dom";
7+
8+
import { PageLayout } from "@apps/pyconkr-2026/components/layout/PageLayout";
9+
import { EVENT_NAME } from "@apps/pyconkr-2026/consts";
10+
import { useAppContext } from "@apps/pyconkr-2026/contexts/app_context";
11+
import { MyTimetableGrid, TimetablePlacement, TRACKS } from "@apps/pyconkr-2026/features/schedule/my_timetable_grid";
12+
import { useMyScheduleQuery } from "@apps/pyconkr-2026/features/schedule/use_my_schedule";
13+
14+
export const MyTimetablePage: FC = () => {
15+
const { language, setAppContext } = useAppContext();
16+
const isKo = language === "ko";
17+
const backendClient = useBackendClient();
18+
const { data: user } = useUserStatus(useShopClient());
19+
const isAuthenticated = user?.meta.is_authenticated === true;
20+
const { data: sessions } = useSessionsQuery(backendClient, { event: EVENT_NAME, types: "세션" });
21+
const eventId = sessions[0]?.presentation_type.event.id;
22+
const { data: bookmarks, isLoading } = useMyScheduleQuery(backendClient, eventId, isAuthenticated);
23+
const placements = useMemo(() => {
24+
const selectedIds = new Set(bookmarks?.presentation_ids ?? []);
25+
return sessions.flatMap((session) =>
26+
selectedIds.has(session.id)
27+
? (session.room_schedules.length === TRACKS.length ? session.room_schedules.slice(0, 1) : session.room_schedules).flatMap(
28+
(schedule): TimetablePlacement[] => {
29+
const track = TRACKS.find(({ roomOrder }) => roomOrder === schedule.room_order);
30+
const startMs = Date.parse(schedule.start_at);
31+
const endMs = Date.parse(schedule.end_at);
32+
if (!track || !Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) return [];
33+
return [
34+
{
35+
key: `${session.id}:${schedule.id}`,
36+
trackKey: track.key,
37+
startMs,
38+
endMs,
39+
title: session.title,
40+
speakers: session.speakers.map(({ nickname }) => nickname).join(", ") || undefined,
41+
href: getSessionDetailUrl(session),
42+
trackSpan: session.room_schedules.length === TRACKS.length ? TRACKS.length : undefined,
43+
},
44+
];
45+
}
46+
)
47+
: []
48+
);
49+
}, [bookmarks, sessions]);
50+
51+
useEffect(() => {
52+
setAppContext((prev) => ({
53+
...prev,
54+
title: isKo ? "내 시간표" : "My Schedule",
55+
shouldShowTitleBanner: true,
56+
shouldShowSponsorBanner: false,
57+
}));
58+
}, [isKo, setAppContext]);
59+
60+
if (!isAuthenticated) {
61+
return (
62+
<PageLayout>
63+
<Stack alignItems="center" spacing={2} sx={{ py: 6, textAlign: "center" }}>
64+
<Typography variant="h5" fontWeight={800} children={isKo ? "로그인이 필요해요" : "Sign in required"} />
65+
<Typography
66+
color="text.secondary"
67+
children={isKo ? "나의 세션 시간표를 보려면 로그인해 주세요." : "Sign in to view your session schedule."}
68+
/>
69+
<Button component={RouterLink} to="/account/sign-in" variant="contained" children={isKo ? "로그인하기" : "Sign in"} />
70+
</Stack>
71+
</PageLayout>
72+
);
73+
}
74+
75+
if (isLoading) {
76+
return (
77+
<PageLayout>
78+
<Stack alignItems="center" sx={{ py: 6 }}>
79+
<CircularProgress />
80+
</Stack>
81+
</PageLayout>
82+
);
83+
}
84+
85+
return (
86+
<PageLayout>
87+
<MyTimetableGrid placements={placements} />
88+
</PageLayout>
89+
);
90+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
11
export const LOCAL_STORAGE_LANGUAGE_KEY = "language";
2+
3+
/** 로그인 후 복귀할 위치를 잠시 보관하는 sessionStorage 키. */
4+
export const PENDING_REDIRECT_KEY = "pyconkr-2026:pending-redirect";
5+
6+
export const SCHEDULE_LS_PREFIX = "pyconkr-2026:my-schedule";
7+
export const scheduleStorageKey = (username: string | null): string =>
8+
username ? `${SCHEDULE_LS_PREFIX}:${username}` : `${SCHEDULE_LS_PREFIX}:__anon__`;

apps/pyconkr-2026/src/consts/mdx_components.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
SessionTimeTableTransposed,
1919
StyledFullWidthButton,
2020
} from "@frontend/common/components/mdx_components";
21+
import type { SessionSchema } from "@frontend/common/schemas/backendAPI";
2122
import { PriceDisplay, ShopContextProvider, SignInGuard, UserSignInAccount, UserSignInMethod } from "@frontend/shop/components/common";
2223
import { CartStatus, OrderList, PatronList, ProductImageCardList, ProductList, UserInfo } from "@frontend/shop/components/features";
2324
import {
@@ -146,7 +147,9 @@ import {
146147
Zoom,
147148
} from "@mui/material";
148149
import type { MDXComponents } from "mdx/types.js";
149-
import { FC, createElement } from "react";
150+
import { ComponentProps, FC, createElement } from "react";
151+
152+
import { SessionScheduleToggleButton } from "@apps/pyconkr-2026/features/schedule/toggle_schedule_button";
150153
const MUIMDXComponents: MDXComponents = {
151154
Mui__material__Accordion: Accordion,
152155
Mui__material__AccordionActions: AccordionActions,
@@ -288,6 +291,12 @@ const PyConKR2025MobileCover: FC<object> = () =>
288291
coverTitleSrc: PyCon2025MobileLogoTitle,
289292
});
290293

294+
const PyConKR2026SessionList: FC<ComponentProps<typeof SessionList>> = (props) =>
295+
createElement(SessionList, {
296+
...props,
297+
renderAction: (session: SessionSchema) => createElement(SessionScheduleToggleButton, { session }),
298+
});
299+
291300
const PyConKRCommonMDXComponents: MDXComponents = {
292301
Common__Components__Lottie: LottiePlayer,
293302
Common__Components__NetworkLottie: NetworkLottiePlayer,
@@ -297,7 +306,7 @@ const PyConKRCommonMDXComponents: MDXComponents = {
297306
Common__Components__MDX__Map: MDXMap,
298307
Common__Components__MDX__FAQAccordion: FAQAccordion,
299308
Common__Components__MDX__FullWidthStyledButton: StyledFullWidthButton,
300-
Common__Components__Session__List: SessionList,
309+
Common__Components__Session__List: PyConKR2026SessionList,
301310
Common__Components__Session__TimeTable: SessionTimeTable,
302311
Common__Components__Session__TimeTableTransposed: SessionTimeTableTransposed,
303312
Common__Components__MDX__MobileAccordion: PyConKR2025MobileAccordion,

0 commit comments

Comments
 (0)