11import * as Common from "@frontend/common" ;
22import { Autocomplete , Box , Button , Card , CardContent , CircularProgress , Stack , styled , Tab , Tabs , TextField , Typography } from "@mui/material" ;
3+ import { DateTimePicker , LocalizationProvider } from "@mui/x-date-pickers" ;
4+ import { AdapterLuxon } from "@mui/x-date-pickers/AdapterLuxon" ;
5+ import { PickerValue } from "@mui/x-date-pickers/internals" ;
36import { ErrorBoundary , Suspense } from "@suspensive/react" ;
7+ import { DateTime } from "luxon" ;
48import { enqueueSnackbar , OptionsObject } from "notistack" ;
59import * as React from "react" ;
610import { useParams } from "react-router-dom" ;
@@ -155,8 +159,107 @@ const PresentationSpeakerForm: React.FC<PresentationSpeakerFormPropType> = ({ di
155159 ) ;
156160} ;
157161
162+ type ScheduleSchemaType = {
163+ schema : {
164+ type : "object" ;
165+ properties : {
166+ room : { type : "string" ; oneOf : enumItemType [ ] } ;
167+ presentation : { type : "string" ; oneOf : enumItemType [ ] } ;
168+ start_at : { type : "string" ; format : "date-time" } ;
169+ end_at : { type : "string" ; format : "date-time" } ;
170+ } ;
171+ required ?: string [ ] ;
172+ $schema ?: string ;
173+ } ;
174+ ui_schema ?: Record < string , { "ui:widget" ?: string ; "ui:field" ?: string } > ;
175+ translation_fields ?: string [ ] ;
176+ } ;
177+
178+ type OnMemorySchedule = {
179+ id ?: string ;
180+ trackId : string ; // Unique identifier for the schedule item, used for local state management
181+ room : string ;
182+ presentation : string ;
183+ start_at : string ; // ISO 8601 date-time string
184+ end_at : string ; // ISO 8601 date-time string
185+ } ;
186+
187+ type Schedule = Omit < OnMemorySchedule , "trackId" > & { id : string } ;
188+
189+ type ScheduleFormPropType = {
190+ schema : ScheduleSchemaType ;
191+ disabled ?: boolean ;
192+ schedule : OnMemorySchedule ;
193+ onChange : ( schedule : OnMemorySchedule ) => void ;
194+ onRemove : ( schedule : OnMemorySchedule ) => void ;
195+ } ;
196+
197+ const PresentationScheduleForm : React . FC < ScheduleFormPropType > = ( { schema, disabled, schedule, onChange, onRemove } ) => {
198+ const roomOptions : AutoCompleteType [ ] = schema . schema . properties . room . oneOf . map ( ( item ) => ( {
199+ name : "room" ,
200+ value : item . const || "" ,
201+ label : item . title ,
202+ } ) ) ;
203+ const currentSelectedRoom = roomOptions . find ( ( r ) => r . value === schedule . room ?. toString ( ) ) ;
204+
205+ const onSelectChange = ( fieldName : string ) => ( _ : React . SyntheticEvent , selected : AutoCompleteType | null ) => {
206+ onChange ( { ...schedule , [ fieldName ] : selected ?. value || "" } ) ;
207+ } ;
208+
209+ const onScheduleTimeChange = ( fieldName : string ) => ( value : PickerValue ) => {
210+ if ( ! value || ! DateTime . isDateTime ( value ) ) {
211+ console . warn ( `Invalid date-time value for ${ fieldName } :` , value ) ;
212+ return ;
213+ }
214+ onChange ( { ...schedule , [ fieldName ] : value . toISO ( { includeOffset : false } ) } ) ;
215+ } ;
216+ const onScheduleRemove = ( ) => window . confirm ( "스케줄을 삭제하시겠습니까?" ) && onRemove ( schedule ) ;
217+
218+ return (
219+ < Card >
220+ < CardContent >
221+ < Stack spacing = { 2 } >
222+ < Autocomplete
223+ fullWidth
224+ defaultValue = { currentSelectedRoom }
225+ value = { currentSelectedRoom }
226+ onChange = { onSelectChange ( "room" ) }
227+ options = { roomOptions }
228+ disabled = { disabled }
229+ renderInput = { ( params ) => < TextField { ...params } label = "발표 장소" /> }
230+ />
231+ < LocalizationProvider dateAdapter = { AdapterLuxon } adapterLocale = "ko-kr" >
232+ < DateTimePicker
233+ disabled = { disabled }
234+ name = "start_at"
235+ label = "시작 시각"
236+ value = { DateTime . fromISO ( schedule . start_at ) }
237+ onChange = { onScheduleTimeChange ( "start_at" ) }
238+ minDateTime = { DateTime . local ( 2000 , 1 , 1 , 0 , 0 , 0 ) }
239+ maxDateTime = { DateTime . local ( 2100 , 12 , 31 , 23 , 59 , 59 ) }
240+ disablePast
241+ />
242+ < DateTimePicker
243+ disabled = { disabled }
244+ name = "end_at"
245+ label = "종료 시각"
246+ value = { DateTime . fromISO ( schedule . end_at ) }
247+ onChange = { onScheduleTimeChange ( "end_at" ) }
248+ minDateTime = { DateTime . local ( 2000 , 1 , 1 , 0 , 0 , 0 ) }
249+ maxDateTime = { DateTime . local ( 2100 , 12 , 31 , 23 , 59 , 59 ) }
250+ disablePast
251+ />
252+ </ LocalizationProvider >
253+ < Button variant = "outlined" color = "error" onClick = { onScheduleRemove } children = "스케줄 삭제" />
254+ </ Stack >
255+ </ CardContent >
256+ </ Card >
257+ ) ;
258+ } ;
259+
158260type PresentationEditorStateType = {
159261 speakers : OnMemoeryPresentationSpeaker [ ] ;
262+ schedules : OnMemorySchedule [ ] ;
160263} ;
161264
162265export const AdminPresentationEditor : React . FC = ErrorBoundary . with (
@@ -177,6 +280,14 @@ export const AdminPresentationEditor: React.FC = ErrorBoundary.with(
177280 const { data : speakerInitialData } = Common . Hooks . BackendAdminAPI . useListQuery < PresentationSpeaker > ( ...speakerQueryParams , { presentation } ) ;
178281 const speakers = speakerInitialData . map ( ( s ) => ( { ...s , trackId : s . id || Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) } ) ) ;
179282
283+ const scheduleQueryParams = [ backendAdminAPIClient , "event" , "roomschedule" ] as const ;
284+ const scheduleCreateMutation = Common . Hooks . BackendAdminAPI . useCreateMutation < OnMemorySchedule > ( ...scheduleQueryParams ) ;
285+ const scheduleUpdateMutation = Common . Hooks . BackendAdminAPI . useUpdatePreparedMutation < Schedule > ( ...scheduleQueryParams ) ;
286+ const scheduleDeleteMutation = Common . Hooks . BackendAdminAPI . useRemovePreparedMutation ( ...scheduleQueryParams ) ;
287+ const { data : scheduleJsonSchema } = Common . Hooks . BackendAdminAPI . useSchemaQuery ( ...scheduleQueryParams ) ;
288+ const { data : scheduleInitialData } = Common . Hooks . BackendAdminAPI . useListQuery < Schedule > ( ...scheduleQueryParams , { presentation } ) ;
289+ const schedules = scheduleInitialData . map ( ( s ) => ( { ...s , trackId : s . id || Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) } ) ) ;
290+
180291 const createEmptySpeaker = ( ) : OnMemoeryPresentationSpeaker => ( {
181292 trackId : Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ,
182293 presentation,
@@ -186,13 +297,30 @@ export const AdminPresentationEditor: React.FC = ErrorBoundary.with(
186297 biography_en : "" ,
187298 } ) ;
188299
189- const [ editorState , setEditorState ] = React . useState < PresentationEditorStateType > ( { speakers } ) ;
300+ const createEmptySchedule = ( ) : OnMemorySchedule => ( {
301+ trackId : Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ,
302+ room : "" ,
303+ presentation,
304+ start_at : DateTime . now ( ) . toISO ( { includeOffset : false } ) ,
305+ end_at : DateTime . now ( ) . plus ( { hours : 1 } ) . toISO ( { includeOffset : false } ) ,
306+ } ) ;
307+
308+ const [ editorState , setEditorState ] = React . useState < PresentationEditorStateType > ( { speakers, schedules } ) ;
190309 const onSpeakerCreate = ( ) => setEditorState ( ( ps ) => ( { ...ps , speakers : [ ...ps . speakers , createEmptySpeaker ( ) ] } ) ) ;
191310 const onSpeakerRemove = ( oldSpeaker : OnMemoeryPresentationSpeaker ) =>
192311 setEditorState ( ( ps ) => ( { ...ps , speakers : ps . speakers . filter ( ( s ) => s . trackId !== oldSpeaker . trackId ) } ) ) ;
193312 const onSpeakerChange = ( newSpeaker : OnMemoeryPresentationSpeaker ) =>
194313 setEditorState ( ( ps ) => ( { ...ps , speakers : ps . speakers . map ( ( s ) => ( s . trackId === newSpeaker . trackId ? newSpeaker : s ) ) } ) ) ;
195314
315+ const onScheduleCreate = ( ) => setEditorState ( ( ps ) => ( { ...ps , schedules : [ ...ps . schedules , createEmptySchedule ( ) ] } ) ) ;
316+ const onScheduleRemove = ( oldSchedule : OnMemorySchedule ) =>
317+ setEditorState ( ( ps ) => ( { ...ps , schedules : ps . schedules . filter ( ( s ) => s . trackId !== oldSchedule . trackId ) } ) ) ;
318+ const onScheduleChange = ( newSchedule : OnMemorySchedule ) =>
319+ setEditorState ( ( ps ) => ( {
320+ ...ps ,
321+ schedules : ps . schedules . map ( ( s ) => ( s . trackId === newSchedule . trackId ? newSchedule : s ) ) ,
322+ } ) ) ;
323+
196324 const onSpeakerSubmit = ( ) => {
197325 if ( ! id ) return ;
198326
@@ -204,31 +332,68 @@ export const AdminPresentationEditor: React.FC = ErrorBoundary.with(
204332 const deleteMut = deletedSpeakerIds . map ( ( id ) => speakerDeleteMutation . mutateAsync ( id ) ) ;
205333 const createMut = newSpeakers . filter ( ( s ) => s . id === undefined ) . map ( ( s ) => speakerCreateMutation . mutateAsync ( s ) ) ;
206334 const updateMut = newSpeakers . filter ( ( s ) => s . id !== undefined ) . map ( ( s ) => speakerUpdateMutation . mutateAsync ( s as PresentationSpeaker ) ) ;
207- Promise . all ( [ ...deleteMut , ...createMut , ...updateMut ] ) . then ( ( ) => addSnackbar ( "발표자 정보가 저장되었습니다." , "success" ) ) ;
335+ return Promise . all ( [ ...deleteMut , ...createMut , ...updateMut ] ) . then ( ( ) => addSnackbar ( "발표자 정보가 저장되었습니다." , "success" ) ) ;
336+ } ;
337+
338+ const onScheduleSubmit = ( ) => {
339+ if ( ! id ) return ;
340+
341+ addSnackbar ( "스케줄 정보를 저장하는 중입니다..." , "info" ) ;
342+ const newSchedules = editorState . schedules ;
343+ const editorScheduleIds = newSchedules . filter ( ( s ) => s . id ) . map ( ( s ) => s . id ! ) ;
344+ const deletedScheduleIds = scheduleInitialData . filter ( ( s ) => ! editorScheduleIds . includes ( s . id ) ) . map ( ( s ) => s . id ! ) ;
345+
346+ const deleteMut = deletedScheduleIds . map ( ( id ) => scheduleDeleteMutation . mutateAsync ( id ) ) ;
347+ const createMut = newSchedules . filter ( ( s ) => s . id === undefined ) . map ( ( s ) => scheduleCreateMutation . mutateAsync ( s ) ) ;
348+ const updateMut = newSchedules . filter ( ( s ) => s . id !== undefined ) . map ( ( s ) => scheduleUpdateMutation . mutateAsync ( s as Schedule ) ) ;
349+ return Promise . all ( [ ...deleteMut , ...createMut , ...updateMut ] ) . then ( ( ) => addSnackbar ( "스케줄 정보가 저장되었습니다." , "success" ) ) ;
350+ } ;
351+
352+ const onPresentationSubmit = ( ) => {
353+ if ( ! id ) return ;
354+ addSnackbar ( "발표 정보를 저장하는 중입니다..." , "info" ) ;
355+ return Promise . all ( [ onSpeakerSubmit ( ) , onScheduleSubmit ( ) ] ) . then ( ( ) => addSnackbar ( "발표 정보가 저장되었습니다." , "success" ) ) ;
208356 } ;
209357
210358 return (
211- < AdminEditor app = "event" resource = "presentation" id = { id } afterSubmit = { onSpeakerSubmit } >
359+ < AdminEditor app = "event" resource = "presentation" id = { id } afterSubmit = { onPresentationSubmit } >
212360 { id ? (
213361 < Stack sx = { { mb : 2 } } spacing = { 2 } >
214- < Typography variant = "h6" > 발표자 정보</ Typography >
215- < Stack spacing = { 2 } >
216- { editorState . speakers . map ( ( s ) => (
217- < PresentationSpeakerForm
218- key = { s . id }
219- schema = { speakerJsonSchema as SpeakerSchemaType }
220- speaker = { s }
221- onChange = { onSpeakerChange }
222- onRemove = { onSpeakerRemove }
223- />
224- ) ) }
225- < Button variant = "outlined" onClick = { onSpeakerCreate } children = "발표자 추가" />
226- </ Stack >
362+ < Common . Components . Fieldset legend = "스케줄 정보" >
363+ < Typography variant = "h6" > 스케줄 정보</ Typography >
364+ < Stack spacing = { 2 } >
365+ { editorState . schedules . map ( ( s ) => (
366+ < PresentationScheduleForm
367+ key = { s . trackId }
368+ schema = { scheduleJsonSchema as ScheduleSchemaType }
369+ schedule = { s }
370+ onChange = { onScheduleChange }
371+ onRemove = { onScheduleRemove }
372+ />
373+ ) ) }
374+ < Button variant = "outlined" onClick = { onScheduleCreate } children = "스케줄 추가" />
375+ </ Stack >
376+ </ Common . Components . Fieldset >
377+ < Common . Components . Fieldset legend = "발표자 정보" >
378+ < Typography variant = "h6" > 발표자 정보</ Typography >
379+ < Stack spacing = { 2 } >
380+ { editorState . speakers . map ( ( s ) => (
381+ < PresentationSpeakerForm
382+ key = { s . id }
383+ schema = { speakerJsonSchema as SpeakerSchemaType }
384+ speaker = { s }
385+ onChange = { onSpeakerChange }
386+ onRemove = { onSpeakerRemove }
387+ />
388+ ) ) }
389+ < Button variant = "outlined" onClick = { onSpeakerCreate } children = "발표자 추가" />
390+ </ Stack >
391+ </ Common . Components . Fieldset >
227392 </ Stack >
228393 ) : (
229394 < Stack >
230- < Typography variant = "h6" > 발표자 정보</ Typography >
231- < Typography variant = "body1" > 발표자를 추가하려면 발표를 먼저 저장하세요.</ Typography >
395+ < Typography variant = "h6" > 발표자 & 스케줄 정보 </ Typography >
396+ < Typography variant = "body1" > 발표자나 스케줄 정보를 추가하려면 발표를 먼저 저장하세요.</ Typography >
232397 </ Stack >
233398 ) }
234399 </ AdminEditor >
0 commit comments