Skip to content

Commit 80f34ea

Browse files
committed
chore: modification-audit preview 페이지를 위한 리팩토링
1 parent e9efee4 commit 80f34ea

File tree

3 files changed

+196
-143
lines changed

3 files changed

+196
-143
lines changed

apps/pyconkr-participant-portal/src/components/pages/profile_editor.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,52 @@ export const ProfileEditor: React.FC = ErrorBoundary.with(
150150
{ fallback: ErrorPage },
151151
Suspense.with({ fallback: <LoadingPage /> }, () => <SignInGuard children={<InnerProfileEditor />} />)
152152
);
153+
154+
type ProfileEditorFormProps = {
155+
disabled?: boolean;
156+
showSubmitButton?: boolean;
157+
onSubmit?: (profile: ProfileType) => void;
158+
language: "ko" | "en";
159+
defaultValue: ProfileType;
160+
};
161+
162+
// TODO: FIXME: 언젠간 위의 ProfileEditor를 아래 ProfileEditorForm에 마이그레이션해야 함
163+
export const ProfileEditorForm: React.FC<ProfileEditorFormProps> = ({ disabled, language, defaultValue, showSubmitButton, onSubmit }) => {
164+
const [formState, setFormState] = React.useState<ProfileType>(defaultValue);
165+
const setImageId = (image: string | null) => setFormState((ps) => ({ ...ps, image }));
166+
const onImageSelectChange = (e: SelectChangeEvent<string | null>) => setImageId(e.target.value);
167+
const setNickname = (value: string | undefined, lang: "ko" | "en") => setFormState((ps) => ({ ...ps, [`nickname_${lang}`]: value }));
168+
169+
const onSubmitClick = () => onSubmit?.(formState);
170+
171+
const titleStr = language === "ko" ? "프로필 정보 수정" : "Edit Profile Information";
172+
const submitStr = language === "ko" ? "제출" : "Submit";
173+
const speakerImageStr = language === "ko" ? "프로필 이미지" : "Profile Image";
174+
175+
return (
176+
<>
177+
<PrimaryTitle variant="h4" children={titleStr} />
178+
<Stack spacing={2} sx={{ width: "100%", flexGrow: 1 }}>
179+
<PublicFileSelector label={speakerImageStr} value={formState.image} onChange={onImageSelectChange} />
180+
<MultiLanguageField
181+
label={{ ko: "닉네임", en: "Nickname" }}
182+
value={{
183+
ko: formState.nickname_ko || "",
184+
en: formState.nickname_en || "",
185+
}}
186+
disabled={disabled}
187+
onChange={setNickname}
188+
description={{
189+
ko: "닉네임은 발표자 소개에 사용됩니다. 청중이 기억하기 쉬운 이름을 입력해주세요.",
190+
en: "The nickname will be used in the speaker biography. Please enter a name that is easy for the audience to remember.",
191+
}}
192+
name="nickname"
193+
fullWidth
194+
/>
195+
{showSubmitButton && (
196+
<Button variant="contained" fullWidth startIcon={<SendAndArchive />} onClick={onSubmitClick} children={submitStr} disabled={disabled} />
197+
)}
198+
</Stack>
199+
</>
200+
);
201+
};

apps/pyconkr-participant-portal/src/components/pages/session_editor.tsx

Lines changed: 141 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { ErrorBoundary, Suspense } from "@suspensive/react";
55
import { enqueueSnackbar, OptionsObject } from "notistack";
66
import * as React from "react";
77
import { Link, Navigate, useParams } from "react-router-dom";
8-
import * as R from "remeda";
98

109
import { useAppContext } from "../../contexts/app_context";
1110
import { SubmitConfirmDialog } from "../dialogs/submit_confirm";
@@ -19,6 +18,7 @@ import { PrimaryTitle, SecondaryTitle } from "../elements/titles";
1918
import { Page } from "../page";
2019

2120
type SessionUpdateSchema = {
21+
id: string;
2222
title_ko: string;
2323
title_en: string;
2424
summary_ko: string;
@@ -35,42 +35,46 @@ type SessionUpdateSchema = {
3535
}[];
3636
};
3737

38-
type SessionEditorState = SessionUpdateSchema & {
39-
openSubmitConfirmDialog: boolean;
38+
type SessionSchema = SessionUpdateSchema & {
39+
speakers: (SessionUpdateSchema["speakers"][number] & {
40+
user: {
41+
id: number; // UUID of the user
42+
email: string; // Email of the user
43+
nickname_ko: string | null; // Nickname in Korean
44+
nickname_en: string | null; // Nickname in English
45+
};
46+
})[];
4047
};
4148

42-
const DummySessionInfo: SessionUpdateSchema = {
43-
title_ko: "",
44-
title_en: "",
45-
summary_ko: "",
46-
summary_en: "",
47-
description_ko: "",
48-
description_en: "",
49-
image: null,
50-
51-
speakers: [],
49+
type SessionEditorFormProps = {
50+
disabled?: boolean;
51+
showSubmitButton?: boolean;
52+
onSubmit?: (session: SessionUpdateSchema) => void;
53+
language: "ko" | "en";
54+
defaultValue: SessionSchema;
5255
};
5356

54-
const InnerSessionEditor: React.FC = () => {
55-
const { sessionId } = useParams<{ sessionId?: string }>();
56-
const { language } = useAppContext();
57-
const participantPortalClient = Common.Hooks.BackendParticipantPortalAPI.useParticipantPortalClient();
58-
const updateSessionMutation = Common.Hooks.BackendParticipantPortalAPI.useUpdatePresentationMutation(participantPortalClient);
59-
const { data: session } = Common.Hooks.BackendParticipantPortalAPI.useRetrievePresentationQuery(participantPortalClient, sessionId || "");
60-
const { data: profile } = Common.Hooks.BackendParticipantPortalAPI.useSignedInUserQuery(participantPortalClient);
61-
const [editorState, setEditorState] = React.useState<SessionEditorState>({
62-
openSubmitConfirmDialog: false,
63-
...(session || DummySessionInfo),
64-
});
57+
type SessionEditorFormState = SessionSchema;
6558

66-
if (!sessionId || !session || !profile || !(R.isArray(editorState.speakers) && !R.isEmpty(editorState.speakers)))
67-
return <Navigate to="/" replace />;
59+
export const SessionEditorForm: React.FC<SessionEditorFormProps> = ({ disabled, language, defaultValue, showSubmitButton, onSubmit }) => {
60+
const [formState, setFormState] = React.useState<SessionEditorFormState>(defaultValue);
61+
62+
const setTitle = (value: string | undefined, lang: "ko" | "en") => setFormState((ps) => ({ ...ps, [`title_${lang}`]: value }));
63+
const setSummary = (value: string | undefined, lang: "ko" | "en") => setFormState((ps) => ({ ...ps, [`summary_${lang}`]: value }));
64+
const setDescription = (value: string | undefined, lang: "ko" | "en") => setFormState((ps) => ({ ...ps, [`description_${lang}`]: value }));
65+
const setImage = (image: string | null) => setFormState((ps) => ({ ...ps, image }));
66+
const setSpeakerImage = (image: string | null) => setFormState((ps) => ({ ...ps, speakers: [{ ...speaker, image }] }));
67+
const setSpeakerBiography = (value: string | undefined, lang: "ko" | "en") =>
68+
setFormState((ps) => ({ ...ps, speakers: [{ ...speaker, [`biography_${lang}`]: value }] }));
69+
const onImageSelectChange = (e: SelectChangeEvent<string | null>) => setImage(e.target.value);
70+
const onSpeakerImageSelectChange = (e: SelectChangeEvent<string | null>) => setSpeakerImage(e.target.value);
71+
72+
const onSubmitButtonClick = () => onSubmit?.(formState);
6873

6974
// 유저는 하나의 세션에 발표자가 한번만 가능하고, 백엔드에서 본 유저의 세션 발표자 정보만 제공하므로, 첫 번째 발표자 정보를 사용해도 안전합니다.
70-
const speaker = editorState.speakers[0];
75+
const speaker = formState.speakers[0];
7176

7277
const titleStr = language === "ko" ? "발표 정보 수정" : "Edit Session Information";
73-
const submitStr = language === "ko" ? "제출" : "Submit";
7478
const sessionEditDescription =
7579
language === "ko" ? (
7680
<Typography variant="body2" color="textSecondary">
@@ -106,6 +110,99 @@ const InnerSessionEditor: React.FC = () => {
106110
);
107111
const sessionImageStr = language === "ko" ? "발표 이미지" : "Session Image";
108112
const speakerImageStr = language === "ko" ? "발표자 이미지" : "Speaker Image";
113+
const submitStr = language === "ko" ? "제출" : "Submit";
114+
115+
return (
116+
<>
117+
<PrimaryTitle variant="h4" children={titleStr} />
118+
<Box sx={{ width: "100%", mb: 2, textAlign: "start" }} children={sessionEditDescription} />
119+
<Stack spacing={2} sx={{ width: "100%" }}>
120+
<MultiLanguageField
121+
label={{ ko: "발표 제목", en: "Session Title" }}
122+
value={{ ko: formState.title_ko, en: formState.title_en }}
123+
onChange={setTitle}
124+
disabled={disabled}
125+
fullWidth
126+
/>
127+
<MultiLanguageField
128+
label={{ ko: "발표 요약", en: "Session Summary" }}
129+
description={{ ko: "발표를 짧게 요약해주세요.", en: "Please enter the short session summary." }}
130+
value={{ ko: formState.summary_ko, en: formState.summary_en }}
131+
onChange={setSummary}
132+
disabled={disabled}
133+
multiline
134+
rows={4}
135+
fullWidth
136+
/>
137+
<MultiLanguageMarkdownField
138+
label={{ ko: "발표 내용", en: "Session Description" }}
139+
description={{
140+
ko: "발표의 상세 내용을 입력해주세요.\n상세 설명은 마크다운 문법을 지원합니다.",
141+
en: "Please enter the description of the session.\nDetailed descriptions support Markdown syntax.",
142+
}}
143+
value={{ ko: formState.description_ko, en: formState.description_en }}
144+
disabled={disabled}
145+
onChange={setDescription}
146+
/>
147+
<PublicFileSelector label={sessionImageStr} value={formState.image} disabled={disabled} onChange={onImageSelectChange} />
148+
<Divider />
149+
150+
<SecondaryTitle variant="h5" children={titleStrForSpeaker} />
151+
<Box sx={{ width: "100%", mb: 2, textAlign: "start" }} children={speakerEditDescription} />
152+
<MultiLanguageField
153+
label={{ ko: "발표자 별칭", en: "Speaker Nickname" }}
154+
description={{
155+
ko: (
156+
<Stack spacing={1}>
157+
<Typography variant="body2" color="textSecondary" children="발표자 별칭은 프로필 편집에서 변경할 수 있어요." />
158+
<Link to="/user" children={<Button size="small" variant="contained" children="프로필 수정 페이지로 이동" />} />
159+
</Stack>
160+
),
161+
en: (
162+
<Stack spacing={1}>
163+
<Typography variant="body2" color="textSecondary" children="You can change speaker nickname in the profile editor." />
164+
<Link to="/user" children={<Button size="small" variant="contained" children="Go to Profile Editor" />} />
165+
</Stack>
166+
),
167+
}}
168+
// value={{ ko: speaker.user.nickname_ko || "", en: speaker.user.nickname_en || "" }}
169+
disabled
170+
fullWidth
171+
/>
172+
<MultiLanguageMarkdownField
173+
label={{ ko: "발표자 소개", en: "Speaker Biography" }}
174+
value={{ ko: speaker.biography_ko || "", en: speaker.biography_en || "" }}
175+
onChange={setSpeakerBiography}
176+
disabled={disabled}
177+
description={{
178+
ko: "본인의 소개를 입력해주세요.\n본인 소개는 마크다운 문법을 지원합니다.",
179+
en: "Please enter your biography.\nBiographies support Markdown syntax.",
180+
}}
181+
/>
182+
<PublicFileSelector label={speakerImageStr} value={speaker.image} disabled={disabled} onChange={onSpeakerImageSelectChange} />
183+
{showSubmitButton && (
184+
<Button variant="contained" startIcon={<SendAndArchive />} onClick={onSubmitButtonClick} disabled={disabled} children={submitStr} />
185+
)}
186+
</Stack>
187+
</>
188+
);
189+
};
190+
191+
type SessionEditorState = {
192+
openSubmitConfirmDialog: boolean;
193+
formData?: SessionUpdateSchema;
194+
};
195+
196+
const InnerSessionEditor: React.FC = () => {
197+
const { sessionId } = useParams<{ sessionId?: string }>();
198+
const { language } = useAppContext();
199+
const [editorState, setEditorState] = React.useState<SessionEditorState>({ openSubmitConfirmDialog: false });
200+
const participantPortalClient = Common.Hooks.BackendParticipantPortalAPI.useParticipantPortalClient();
201+
const updateSessionMutation = Common.Hooks.BackendParticipantPortalAPI.useUpdatePresentationMutation(participantPortalClient);
202+
const { data: session } = Common.Hooks.BackendParticipantPortalAPI.useRetrievePresentationQuery(participantPortalClient, sessionId || "");
203+
204+
if (!sessionId || !session) return <Navigate to="/" replace />;
205+
109206
const submitSucceedStr =
110207
language === "ko"
111208
? "발표 정보 수정을 요청했어요. 검토 후 반영될 예정이에요."
@@ -114,48 +211,26 @@ const InnerSessionEditor: React.FC = () => {
114211
const addSnackbar = (c: string | React.ReactNode, variant: OptionsObject["variant"]) =>
115212
enqueueSnackbar(c, { variant, anchorOrigin: { vertical: "bottom", horizontal: "center" } });
116213

117-
const openSubmitConfirmDialog = () => setEditorState((ps) => ({ ...ps, openSubmitConfirmDialog: true }));
214+
const openSubmitConfirmDialog = (formData: SessionUpdateSchema) => setEditorState((ps) => ({ ...ps, openSubmitConfirmDialog: true, formData }));
118215
const closeSubmitConfirmDialog = () => setEditorState((ps) => ({ ...ps, openSubmitConfirmDialog: false }));
119216

120-
const setTitle = (value: string | undefined, lang: "ko" | "en") => setEditorState((ps) => ({ ...ps, [`title_${lang}`]: value }));
121-
const setSummary = (value: string | undefined, lang: "ko" | "en") => setEditorState((ps) => ({ ...ps, [`summary_${lang}`]: value }));
122-
const setDescription = (value: string | undefined, lang: "ko" | "en") => setEditorState((ps) => ({ ...ps, [`description_${lang}`]: value }));
123-
const setImage = (image: string | null) => setEditorState((ps) => ({ ...ps, image }));
124-
const setSpeakerImage = (image: string | null) => setEditorState((ps) => ({ ...ps, speakers: [{ ...speaker, image }] }));
125-
const setSpeakerBiography = (value: string | undefined, lang: "ko" | "en") =>
126-
setEditorState((ps) => ({ ...ps, speakers: [{ ...speaker, [`biography_${lang}`]: value }] }));
217+
const updateSession = () => {
218+
if (!editorState.formData) return;
127219

128-
const onImageSelectChange = (e: SelectChangeEvent<string | null>) => setImage(e.target.value);
129-
const onSpeakerImageSelectChange = (e: SelectChangeEvent<string | null>) => setSpeakerImage(e.target.value);
220+
updateSessionMutation.mutate(editorState.formData, {
221+
onSuccess: () => {
222+
addSnackbar(submitSucceedStr, "success");
223+
closeSubmitConfirmDialog();
224+
},
225+
onError: (error) => {
226+
console.error("Updating session failed:", error);
130227

131-
const updateSession = () => {
132-
updateSessionMutation.mutate(
133-
{
134-
id: sessionId,
135-
title_ko: editorState.title_ko,
136-
title_en: editorState.title_en,
137-
summary_ko: editorState.summary_ko,
138-
summary_en: editorState.summary_en,
139-
description_ko: editorState.description_ko,
140-
description_en: editorState.description_en,
141-
image: editorState.image || null,
142-
speakers: editorState.speakers,
228+
let errorMessage = error instanceof Error ? error.message : "An unknown error occurred.";
229+
if (error instanceof Common.BackendAPIs.BackendAPIClientError) errorMessage = error.message;
230+
231+
addSnackbar(errorMessage, "error");
143232
},
144-
{
145-
onSuccess: () => {
146-
addSnackbar(submitSucceedStr, "success");
147-
closeSubmitConfirmDialog();
148-
},
149-
onError: (error) => {
150-
console.error("Updating session failed:", error);
151-
152-
let errorMessage = error instanceof Error ? error.message : "An unknown error occurred.";
153-
if (error instanceof Common.BackendAPIs.BackendAPIClientError) errorMessage = error.message;
154-
155-
addSnackbar(errorMessage, "error");
156-
},
157-
}
158-
);
233+
});
159234
};
160235

161236
const modificationAuditId = session.requested_modification_audit_id || "";
@@ -166,84 +241,7 @@ const InnerSessionEditor: React.FC = () => {
166241
<SubmitConfirmDialog open={editorState.openSubmitConfirmDialog} onClose={closeSubmitConfirmDialog} onSubmit={updateSession} />
167242
<Page>
168243
{session.has_requested_modification_audit && <CurrentlyModAuditInProgress language={language} modificationAuditId={modificationAuditId} />}
169-
<PrimaryTitle variant="h4" children={titleStr} />
170-
<Box sx={{ width: "100%", mb: 2, textAlign: "start" }} children={sessionEditDescription} />
171-
<Stack spacing={2} sx={{ width: "100%" }}>
172-
<MultiLanguageField
173-
label={{ ko: "발표 제목", en: "Session Title" }}
174-
value={{ ko: editorState.title_ko, en: editorState.title_en }}
175-
onChange={setTitle}
176-
disabled={formDisabled}
177-
fullWidth
178-
/>
179-
<MultiLanguageField
180-
label={{ ko: "발표 요약", en: "Session Summary" }}
181-
description={{ ko: "발표를 짧게 요약해주세요.", en: "Please enter the short session summary." }}
182-
value={{ ko: editorState.summary_ko, en: editorState.summary_en }}
183-
onChange={setSummary}
184-
disabled={formDisabled}
185-
multiline
186-
rows={4}
187-
fullWidth
188-
/>
189-
<MultiLanguageMarkdownField
190-
label={{ ko: "발표 내용", en: "Session Description" }}
191-
description={{
192-
ko: "발표의 상세 내용을 입력해주세요.\n상세 설명은 마크다운 문법을 지원합니다.",
193-
en: "Please enter the description of the session.\nDetailed descriptions support Markdown syntax.",
194-
}}
195-
value={{ ko: editorState.description_ko, en: editorState.description_en }}
196-
disabled={formDisabled}
197-
onChange={setDescription}
198-
/>
199-
<PublicFileSelector label={sessionImageStr} value={editorState.image} disabled={formDisabled} onChange={onImageSelectChange} />
200-
<Divider />
201-
202-
<SecondaryTitle variant="h5" children={titleStrForSpeaker} />
203-
<Box sx={{ width: "100%", mb: 2, textAlign: "start" }} children={speakerEditDescription} />
204-
<MultiLanguageField
205-
label={{ ko: "발표자 별칭", en: "Speaker Nickname" }}
206-
description={{
207-
ko: (
208-
<Stack spacing={1}>
209-
<Typography variant="body2" color="textSecondary" children="발표자 별칭은 프로필 편집에서 변경할 수 있어요." />
210-
<Link to="/user" children={<Button size="small" variant="contained" children="프로필 수정 페이지로 이동" />} />
211-
</Stack>
212-
),
213-
en: (
214-
<Stack spacing={1}>
215-
<Typography variant="body2" color="textSecondary" children="You can change speaker nickname in the profile editor." />
216-
<Link to="/user" children={<Button size="small" variant="contained" children="Go to Profile Editor" />} />
217-
</Stack>
218-
),
219-
}}
220-
value={{ ko: profile.nickname_ko || "", en: profile.nickname_en || "" }}
221-
disabled
222-
fullWidth
223-
/>
224-
<MultiLanguageMarkdownField
225-
label={{ ko: "발표자 소개", en: "Speaker Biography" }}
226-
value={{ ko: speaker.biography_ko || "", en: speaker.biography_en || "" }}
227-
onChange={setSpeakerBiography}
228-
disabled={formDisabled}
229-
description={{
230-
ko: "본인의 소개를 입력해주세요.\n본인 소개는 마크다운 문법을 지원합니다.",
231-
en: "Please enter your biography.\nBiographies support Markdown syntax.",
232-
}}
233-
/>
234-
<PublicFileSelector label={speakerImageStr} value={speaker.image} disabled={formDisabled} onChange={onSpeakerImageSelectChange} />
235-
236-
<Stack>
237-
<Button
238-
variant="contained"
239-
startIcon={<SendAndArchive />}
240-
onClick={openSubmitConfirmDialog}
241-
loading={updateSessionMutation.isPending}
242-
disabled={formDisabled}
243-
children={submitStr}
244-
/>
245-
</Stack>
246-
</Stack>
244+
<SessionEditorForm disabled={formDisabled} language={language} defaultValue={session} onSubmit={openSubmitConfirmDialog} showSubmitButton />
247245
</Page>
248246
</>
249247
);

0 commit comments

Comments
 (0)