Skip to content

Commit 0d13698

Browse files
committed
fix: 세션 상세에서 각 연도의 로고 노출 및 전반적인 리펙토링
1 parent e4eff8b commit 0d13698

14 files changed

Lines changed: 85 additions & 108 deletions

File tree

apps/pyconkr-2025/src/components/pages/presentation_detail.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import { useCommonContext } from "@frontend/common/hooks/useCommonContext";
44
import { Box, Chip, CircularProgress, Divider, Stack, styled, Table, TableBody, TableCell, TableRow, Typography } from "@mui/material";
55
import { ErrorBoundary, Suspense } from "@suspensive/react";
66
import { DateTime } from "luxon";
7-
import { CSSProperties, FC, useEffect } from "react";
7+
import { CSSProperties, FC, ReactNode, useEffect } from "react";
88
import { Navigate, useParams } from "react-router-dom";
99
import { isString } from "remeda";
1010

11-
import PyCon2025Logo from "@apps/pyconkr-2025/assets/pyconkr2025_logo.png";
1211
import { PageLayout } from "@apps/pyconkr-2025/components/layout/PageLayout";
1312
import { useAppContext } from "@apps/pyconkr-2025/contexts/app_context";
1413

1514
const PROFILE_IMAGE_SIZE = "7rem";
15+
const PROFILE_FALLBACK_IMAGE_STYLE: CSSProperties = { width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" };
1616

1717
type SimplifiedSpeakerSchema = {
1818
id: string;
@@ -94,19 +94,23 @@ const ProfileImageStyle: CSSProperties = {
9494

9595
const ProfileImage = styled(FallbackImage)(ProfileImageStyle);
9696

97-
const ProfileImageErrorFallback: FC = () => (
97+
const ProfileImageErrorFallback: FC<{ children?: ReactNode }> = ({ children }) => (
9898
<Stack alignItems="center" justifyContent="center" sx={{ ...ProfileImageStyle }}>
99-
<img src={PyCon2025Logo} alt="PyCon 2025 Logo" style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" }} />
99+
{children}
100100
</Stack>
101101
);
102102

103-
const PresentationSpeakerItem: FC<{ speaker: SimplifiedSpeakerSchema }> = ({ speaker }) => {
103+
const PresentationSpeakerItem: FC<{ speaker: SimplifiedSpeakerSchema; fallbackImage?: ReactNode }> = ({ speaker, fallbackImage }) => {
104104
const { baseUrl, mdxComponents } = useCommonContext();
105105
return (
106106
<>
107107
<Stack direction="row" spacing={4} sx={{ px: 2, py: 1 }}>
108108
<ProfileImageContainer sx={{ flexGrow: 0 }}>
109-
<ProfileImage alt="Speaker Image" src={speaker.image || ""} errorFallback={<ProfileImageErrorFallback />} />
109+
<ProfileImage
110+
alt="Speaker Image"
111+
src={speaker.image || ""}
112+
errorFallback={<ProfileImageErrorFallback>{fallbackImage}</ProfileImageErrorFallback>}
113+
/>
110114
</ProfileImageContainer>
111115
<Stack alignItems="flex-start" justifyContent="center" sx={{ flexGrow: 1 }}>
112116
<Typography variant="h4" fontWeight="700" fontSize="2rem" children={speaker.nickname} />
@@ -155,6 +159,11 @@ export const PresentationDetailPage: FC = ErrorBoundary.with(
155159

156160
if (!id || !presentation) return <Navigate to="/" replace />;
157161

162+
const presentationEvent = presentation.presentation_type.event;
163+
const speakerFallbackImage = presentationEvent.logo ? (
164+
<img src={presentationEvent.logo} alt={presentationEvent.name} style={PROFILE_FALLBACK_IMAGE_STYLE} />
165+
) : undefined;
166+
158167
const descriptionFallback = language === "ko" ? "해당 발표의 설명은 준비 중이에요!" : "Description of the presentation is under preparation!";
159168
const categoriesStr = language === "ko" ? "카테고리" : "Categories";
160169
const speakersStr = language === "ko" ? "발표자" : "Speakers";
@@ -270,7 +279,7 @@ export const PresentationDetailPage: FC = ErrorBoundary.with(
270279
<Typography variant="h5" fontWeight="bold" sx={{ width: "100%", px: 2, py: 4 }} children={speakersStr} />
271280
<Stack spacing={2} sx={{ width: "100%", px: 3 }}>
272281
{presentation.speakers.map((speaker) => (
273-
<PresentationSpeakerItem key={speaker.id} speaker={speaker as SimplifiedSpeakerSchema} />
282+
<PresentationSpeakerItem key={speaker.id} speaker={speaker as SimplifiedSpeakerSchema} fallbackImage={speakerFallbackImage} />
274283
))}
275284
</Stack>
276285
</>

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

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
SessionTimeTable,
1717
StyledFullWidthButton,
1818
} from "@frontend/common/components/mdx_components";
19-
import type { SessionSchema } from "@frontend/common/schemas/backendAPI";
2019
import { PriceDisplay, ShopContextProvider, SignInGuard, UserSignInAccount, UserSignInMethod } from "@frontend/shop/components/common";
2120
import { CartStatus, OrderList, PatronList, ProductImageCardList, ProductList, UserInfo } from "@frontend/shop/components/features";
2221
import {
@@ -145,9 +144,7 @@ import {
145144
Zoom,
146145
} from "@mui/material";
147146
import type { MDXComponents } from "mdx/types.js";
148-
import { ComponentProps, FC, createElement } from "react";
149-
150-
import PyCon2025Logo from "@apps/pyconkr-2025/assets/pyconkr2025_logo.png";
147+
import { FC, createElement } from "react";
151148

152149
const MUIMDXComponents: MDXComponents = {
153150
Mui__material__Accordion: Accordion,
@@ -275,33 +272,6 @@ const MUIMDXComponents: MDXComponents = {
275272
Mui__material__Zoom: Zoom,
276273
};
277274

278-
const getPyConKR2025SessionUrl = (session: SessionSchema): string => {
279-
const urlSafeTitle = session.title
280-
.replace(/ /g, "-")
281-
.replace(/([.])/g, "_")
282-
.replace(/(?![.0-9A-Za-z---])./g, "");
283-
return `/presentations/${session.id}#${urlSafeTitle}`;
284-
};
285-
286-
const PyConKR2025FallbackImage = createElement("img", {
287-
src: PyCon2025Logo,
288-
alt: "PyCon 2025 Logo",
289-
style: { width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" },
290-
});
291-
292-
const PyConKR2025SessionList: FC<ComponentProps<typeof SessionList>> = (props) =>
293-
createElement(SessionList, {
294-
...props,
295-
fallbackImage: PyConKR2025FallbackImage,
296-
getSessionUrl: getPyConKR2025SessionUrl,
297-
});
298-
299-
const PyConKR2025SessionTimeTable: FC<ComponentProps<typeof SessionTimeTable>> = (props) =>
300-
createElement(SessionTimeTable, {
301-
...props,
302-
getSessionUrl: getPyConKR2025SessionUrl,
303-
});
304-
305275
const PyConKR2025MobileAccordion: FC<object> = () =>
306276
createElement(MobileAccordion, {
307277
marqueeText: "AUG 15 - 17",
@@ -326,8 +296,8 @@ const PyConKRCommonMDXComponents: MDXComponents = {
326296
Common__Components__MDX__Map: MDXMap,
327297
Common__Components__MDX__FAQAccordion: FAQAccordion,
328298
Common__Components__MDX__FullWidthStyledButton: StyledFullWidthButton,
329-
Common__Components__Session__List: PyConKR2025SessionList,
330-
Common__Components__Session__TimeTable: PyConKR2025SessionTimeTable,
299+
Common__Components__Session__List: SessionList,
300+
Common__Components__Session__TimeTable: SessionTimeTable,
331301
Common__Components__MDX__MobileAccordion: PyConKR2025MobileAccordion,
332302
Common__Components__MDX__MobileCover: PyConKR2025MobileCover,
333303
};

apps/pyconkr-2025/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const queryClient = new QueryClient({
4949
const backendApiDomain = import.meta.env.DEV ? "" : import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN;
5050

5151
const CommonOptions: ContextOptions = {
52+
appType: "main",
5253
language: "ko",
5354
debug: IS_DEBUG_ENV,
5455
baseUrl: ".",

apps/pyconkr-2026/src/components/pages/presentation_detail.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import PyCon2025Logo from "@frontend/common/assets/pyconkr2025_logo.png";
21
import { CenteredPage, ErrorFallback, FallbackImage, LinkHandler, MDXRenderer } from "@frontend/common/components";
32
import { useBackendClient, useSessionQuery } from "@frontend/common/hooks/useAPI";
43
import { useCommonContext } from "@frontend/common/hooks/useCommonContext";
54
import { Box, Chip, CircularProgress, Divider, Stack, styled, Table, TableBody, TableCell, TableRow, Typography } from "@mui/material";
65
import { ErrorBoundary, Suspense } from "@suspensive/react";
76
import { DateTime } from "luxon";
8-
import { CSSProperties, FC, useEffect } from "react";
7+
import { CSSProperties, FC, ReactNode, useEffect } from "react";
98
import { Navigate, useParams } from "react-router-dom";
109
import { isString } from "remeda";
1110

1211
import { PageLayout } from "@apps/pyconkr-2026/components/layout/PageLayout";
1312
import { useAppContext } from "@apps/pyconkr-2026/contexts/app_context";
1413

1514
const PROFILE_IMAGE_SIZE = "7rem";
15+
const PROFILE_FALLBACK_IMAGE_STYLE: CSSProperties = { width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" };
1616

1717
type SimplifiedSpeakerSchema = {
1818
id: string;
@@ -94,19 +94,23 @@ const ProfileImageStyle: CSSProperties = {
9494

9595
const ProfileImage = styled(FallbackImage)(ProfileImageStyle);
9696

97-
const ProfileImageErrorFallback: FC = () => (
97+
const ProfileImageErrorFallback: FC<{ children?: ReactNode }> = ({ children }) => (
9898
<Stack alignItems="center" justifyContent="center" sx={{ ...ProfileImageStyle }}>
99-
<img src={PyCon2025Logo} alt="PyCon 2025 Logo" style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" }} />
99+
{children}
100100
</Stack>
101101
);
102102

103-
const PresentationSpeakerItem: FC<{ speaker: SimplifiedSpeakerSchema }> = ({ speaker }) => {
103+
const PresentationSpeakerItem: FC<{ speaker: SimplifiedSpeakerSchema; fallbackImage?: ReactNode }> = ({ speaker, fallbackImage }) => {
104104
const { baseUrl, mdxComponents } = useCommonContext();
105105
return (
106106
<>
107107
<Stack direction="row" spacing={4} sx={{ px: 2, py: 1 }}>
108108
<ProfileImageContainer sx={{ flexGrow: 0 }}>
109-
<ProfileImage alt="Speaker Image" src={speaker.image || ""} errorFallback={<ProfileImageErrorFallback />} />
109+
<ProfileImage
110+
alt="Speaker Image"
111+
src={speaker.image || ""}
112+
errorFallback={<ProfileImageErrorFallback>{fallbackImage}</ProfileImageErrorFallback>}
113+
/>
110114
</ProfileImageContainer>
111115
<Stack alignItems="flex-start" justifyContent="center" sx={{ flexGrow: 1 }}>
112116
<Typography variant="h4" fontWeight="700" fontSize="2rem" children={speaker.nickname} />
@@ -155,6 +159,11 @@ export const PresentationDetailPage: FC = ErrorBoundary.with(
155159

156160
if (!id || !presentation) return <Navigate to="/" replace />;
157161

162+
const presentationEvent = presentation.presentation_type.event;
163+
const speakerFallbackImage = presentationEvent.logo ? (
164+
<img src={presentationEvent.logo} alt={presentationEvent.name} style={PROFILE_FALLBACK_IMAGE_STYLE} />
165+
) : undefined;
166+
158167
const descriptionFallback = language === "ko" ? "해당 발표의 설명은 준비 중이에요!" : "Description of the presentation is under preparation!";
159168
const categoriesStr = language === "ko" ? "카테고리" : "Categories";
160169
const speakersStr = language === "ko" ? "발표자" : "Speakers";
@@ -270,7 +279,7 @@ export const PresentationDetailPage: FC = ErrorBoundary.with(
270279
<Typography variant="h5" fontWeight="bold" sx={{ width: "100%", px: 2, py: 4 }} children={speakersStr} />
271280
<Stack spacing={2} sx={{ width: "100%", px: 3 }}>
272281
{presentation.speakers.map((speaker) => (
273-
<PresentationSpeakerItem key={speaker.id} speaker={speaker as SimplifiedSpeakerSchema} />
282+
<PresentationSpeakerItem key={speaker.id} speaker={speaker as SimplifiedSpeakerSchema} fallbackImage={speakerFallbackImage} />
274283
))}
275284
</Stack>
276285
</>

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

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
SessionTimeTable,
1818
StyledFullWidthButton,
1919
} from "@frontend/common/components/mdx_components";
20-
import type { SessionSchema } from "@frontend/common/schemas/backendAPI";
2120
import { PriceDisplay, ShopContextProvider, SignInGuard, UserSignInAccount, UserSignInMethod } from "@frontend/shop/components/common";
2221
import { CartStatus, OrderList, PatronList, ProductImageCardList, ProductList, UserInfo } from "@frontend/shop/components/features";
2322
import {
@@ -146,7 +145,7 @@ import {
146145
Zoom,
147146
} from "@mui/material";
148147
import type { MDXComponents } from "mdx/types.js";
149-
import { ComponentProps, FC, createElement } from "react";
148+
import { FC, createElement } from "react";
150149
const MUIMDXComponents: MDXComponents = {
151150
Mui__material__Accordion: Accordion,
152151
Mui__material__AccordionActions: AccordionActions,
@@ -273,26 +272,6 @@ const MUIMDXComponents: MDXComponents = {
273272
Mui__material__Zoom: Zoom,
274273
};
275274

276-
const getPyConKR2025SessionUrl = (session: SessionSchema): string => {
277-
const urlSafeTitle = session.title
278-
.replace(/ /g, "-")
279-
.replace(/([.])/g, "_")
280-
.replace(/(?![.0-9A-Za-z---])./g, "");
281-
return `/presentations/${session.id}#${urlSafeTitle}`;
282-
};
283-
284-
const PyConKR2025SessionList: FC<ComponentProps<typeof SessionList>> = (props) =>
285-
createElement(SessionList, {
286-
...props,
287-
getSessionUrl: getPyConKR2025SessionUrl,
288-
});
289-
290-
const PyConKR2025SessionTimeTable: FC<ComponentProps<typeof SessionTimeTable>> = (props) =>
291-
createElement(SessionTimeTable, {
292-
...props,
293-
getSessionUrl: getPyConKR2025SessionUrl,
294-
});
295-
296275
const PyConKR2025MobileAccordion: FC<object> = () =>
297276
createElement(MobileAccordion, {
298277
marqueeText: "AUG 15 - 17",
@@ -317,8 +296,8 @@ const PyConKRCommonMDXComponents: MDXComponents = {
317296
Common__Components__MDX__Map: MDXMap,
318297
Common__Components__MDX__FAQAccordion: FAQAccordion,
319298
Common__Components__MDX__FullWidthStyledButton: StyledFullWidthButton,
320-
Common__Components__Session__List: PyConKR2025SessionList,
321-
Common__Components__Session__TimeTable: PyConKR2025SessionTimeTable,
299+
Common__Components__Session__List: SessionList,
300+
Common__Components__Session__TimeTable: SessionTimeTable,
322301
Common__Components__MDX__MobileAccordion: PyConKR2025MobileAccordion,
323302
Common__Components__MDX__MobileCover: PyConKR2025MobileCover,
324303
Common__Components__MDX__PyConKR2026MainCover: PyConKR2026MainCover,

apps/pyconkr-2026/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const queryClient = new QueryClient({
5050
const backendApiDomain = import.meta.env.DEV ? "" : import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN;
5151

5252
const CommonOptions: ContextOptions = {
53+
appType: "main",
5354
language: "ko",
5455
debug: IS_DEBUG_ENV,
5556
baseUrl: ".",

apps/pyconkr-admin/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const queryClient = new QueryClient({
4141
const backendApiDomain = import.meta.env.DEV ? "" : import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN;
4242

4343
const CommonOptions: ContextOptions = {
44+
appType: "admin",
4445
debug: true,
4546
language: "ko",
4647
baseUrl: ".",

apps/pyconkr-participant-portal/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const muiTheme = createTheme();
3939
const backendApiDomain = import.meta.env.DEV ? "" : import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN;
4040

4141
const CommonOptions: ContextOptions = {
42+
appType: "participant_portal",
4243
language: "ko",
4344
debug: IS_DEBUG_ENV,
4445
baseUrl: ".",

packages/common/src/components/mdx_components/session_list.tsx

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ErrorFallback } from "@frontend/common/components/error_handler";
88
import { FallbackImage } from "@frontend/common/components/fallback_image";
99
import { BackendAPI, Common } from "@frontend/common/hooks";
1010
import { SessionSchema } from "@frontend/common/schemas/backendAPI";
11+
import { getSessionDetailUrl } from "@frontend/common/utils";
1112

1213
import { StyledDivider } from "./styled_divider";
1314

@@ -17,9 +18,8 @@ const SESSION_FALLBACK_IMAGE_STYLE: CSSProperties = { width: "100%", height: "10
1718
const SessionItem: FC<{
1819
session: SessionSchema;
1920
enableLink?: boolean;
20-
fallbackImage?: ReactNode;
21-
getSessionUrl?: (session: SessionSchema) => string;
22-
}> = Suspense.with({ fallback: <CircularProgress /> }, ({ session, enableLink, fallbackImage, getSessionUrl }) => {
21+
linkable?: boolean;
22+
}> = Suspense.with({ fallback: <CircularProgress /> }, ({ session, enableLink, linkable }) => {
2323
const sessionTitle = session.title.replace("\\n", "\n");
2424

2525
let speakerImgSrc = session.image || "";
@@ -32,7 +32,12 @@ const SessionItem: FC<{
3232
}
3333
}
3434

35-
const sessionDetailedUrl = getSessionUrl ? getSessionUrl(session) : undefined;
35+
const sessionEvent = session.presentation_type.event;
36+
const resolvedFallbackImage = sessionEvent.logo ? (
37+
<img src={sessionEvent.logo} alt={sessionEvent.name} style={SESSION_FALLBACK_IMAGE_STYLE} />
38+
) : undefined;
39+
40+
const sessionDetailedUrl = linkable ? getSessionDetailUrl(session) : undefined;
3641
const result = (
3742
<SessionItemContainer direction="row">
3843
<SessionImageContainer
@@ -41,7 +46,7 @@ const SessionItem: FC<{
4146
src={speakerImgSrc}
4247
alt="Session Image"
4348
loading="lazy"
44-
errorFallback={<SessionImageErrorFallback>{fallbackImage}</SessionImageErrorFallback>}
49+
errorFallback={<SessionImageErrorFallback>{resolvedFallbackImage}</SessionImageErrorFallback>}
4550
/>
4651
}
4752
/>
@@ -76,10 +81,6 @@ type SessionListPropType = {
7681
types?: string | string[];
7782
/** `true`면 각 세션을 상세 페이지 링크로 감싼다. */
7883
enableLink?: boolean;
79-
/** 세션 이미지가 없을 때 표시할 기본 대체 이미지 노드. */
80-
fallbackImage?: ReactNode;
81-
/** 세션 객체로부터 상세 페이지 URL 을 만드는 함수. */
82-
getSessionUrl?: (session: SessionSchema) => string;
8384
};
8485

8586
/**
@@ -89,27 +90,12 @@ type SessionListPropType = {
8990
*/
9091
export const SessionList: FC<SessionListPropType> = ErrorBoundary.with(
9192
{ fallback: ErrorFallback },
92-
Suspense.with({ fallback: <CircularProgress /> }, ({ event, types, enableLink, fallbackImage, getSessionUrl }) => {
93-
const { language } = Common.useCommonContext();
93+
Suspense.with({ fallback: <CircularProgress /> }, ({ event, types, enableLink }) => {
94+
const { language, appType } = Common.useCommonContext();
95+
const linkable = appType === "main";
9496
const backendAPIClient = BackendAPI.useBackendClient();
9597
const params = { ...(event && { event }), ...(types && { types: isString(types) ? types : types.join(",") }) };
9698
const { data: sessions } = BackendAPI.useSessionsQuery(backendAPIClient, params);
97-
const { data: events } = BackendAPI.useEventsQuery(backendAPIClient);
98-
99-
const resolvedFallbackImageByEvent = useMemo<Record<string, ReactNode>>(() => {
100-
const map: Record<string, ReactNode> = {};
101-
for (const ev of events ?? []) {
102-
if (!ev.logo) continue;
103-
const year = ev.name.match(/\d{4}/)?.[0];
104-
if (year) map[year] = <img src={ev.logo} alt={ev.name} style={SESSION_FALLBACK_IMAGE_STYLE} />;
105-
}
106-
return map;
107-
}, [events]);
108-
109-
const resolvedFallbackImage =
110-
(event
111-
? (resolvedFallbackImageByEvent[event] ?? Object.entries(resolvedFallbackImageByEvent).find(([key]) => event.includes(key))?.[1])
112-
: undefined) ?? fallbackImage;
11399

114100
const warningMessage =
115101
language === "ko"
@@ -156,7 +142,7 @@ export const SessionList: FC<SessionListPropType> = ErrorBoundary.with(
156142
)}
157143
</Box>
158144
{filteredSessions.map((s) => (
159-
<SessionItem key={s.id} session={s} enableLink={enableLink} fallbackImage={resolvedFallbackImage} getSessionUrl={getSessionUrl} />
145+
<SessionItem key={s.id} session={s} enableLink={enableLink} linkable={linkable} />
160146
))}
161147
</Box>
162148
);

0 commit comments

Comments
 (0)