1
1
import * as Common from "@frontend/common" ;
2
- import { Box , Chip , CircularProgress , Divider , Stack , styled , Typography } from "@mui/material" ;
2
+ import { Box , Chip , CircularProgress , Divider , Stack , styled , Table , TableBody , TableCell , TableRow , Typography } from "@mui/material" ;
3
3
import { ErrorBoundary , Suspense } from "@suspensive/react" ;
4
+ import { DateTime } from "luxon" ;
4
5
import * as React from "react" ;
5
6
import { Navigate , useParams } from "react-router-dom" ;
6
7
@@ -148,6 +149,31 @@ export const PresentationDetailPage: React.FC = ErrorBoundary.with(
148
149
const speakersStr = language === "ko" ? "발표자" : "Speakers" ;
149
150
// const slideShowStr = language === "ko" ? "발표 슬라이드" : "Presentation Slideshow";
150
151
152
+ const datetimeLabel = language === "ko" ? "발표 시각" : "Presentation Time" ;
153
+ const datetimeSeparator = language === "ko" ? " ~ " : " - " ;
154
+ const minText = language === "ko" ? "분" : "min." ;
155
+
156
+ // 동일 시간별로 모아서 보여줌. 단, 방은 콤마(,)로 join해서 보여줌
157
+ const scheduleMap : Record < string , string [ ] > = presentation . room_schedules . reduce (
158
+ ( acc , schedule ) => {
159
+ const startAt = DateTime . fromISO ( schedule . start_at ) . setLocale ( language ) ;
160
+ const endAt = DateTime . fromISO ( schedule . end_at ) . setLocale ( language ) ;
161
+ if ( ! startAt . isValid || ! endAt . isValid ) return acc ; // 유효하지 않은 날짜는 무시
162
+
163
+ const duration = Number . parseInt ( endAt . diff ( startAt , [ "minutes" ] ) . minutes . toString ( ) ) ;
164
+ const startAtFormatted = startAt . toLocaleString ( DateTime . DATETIME_MED ) ;
165
+ // 동일 일자인 경우, 시간만 표시
166
+ const endAtFormatted = endAt . toLocaleString ( startAt . hasSame ( endAt , "day" ) ? DateTime . TIME_SIMPLE : DateTime . DATETIME_MED ) ;
167
+
168
+ const key = `${ startAtFormatted } ${ datetimeSeparator } ${ endAtFormatted } (${ duration } ${ minText } )` ;
169
+ const roomText = schedule . room_name . replace ( "\\n" , "\n" ) ;
170
+ if ( ! acc [ key ] ) acc [ key ] = [ roomText ] ;
171
+ else acc [ key ] . push ( roomText ) ;
172
+ return acc ;
173
+ } ,
174
+ { } as Record < string , string [ ] >
175
+ ) ;
176
+
151
177
React . useEffect ( ( ) => {
152
178
setAppContext ( ( prev ) => ( {
153
179
...prev ,
@@ -159,28 +185,55 @@ export const PresentationDetailPage: React.FC = ErrorBoundary.with(
159
185
160
186
return (
161
187
< PageLayout >
162
- < Typography variant = "h4" fontWeight = "700" textAlign = "start" sx = { { width : "100%" , px : 2 , pt : 0 , pb : 1 } } children = { presentation . title } />
188
+ < Typography
189
+ variant = "h4"
190
+ sx = { { width : "100%" , px : 2 , pt : 0 , pb : 1 , fontWeight : "700" , whiteSpace : "pre-wrap" } }
191
+ children = { presentation . title . replace ( "\\n" , "\n" ) }
192
+ />
163
193
{ presentation . summary && (
164
194
< Typography
165
195
variant = "subtitle1"
166
196
sx = { { width : "100%" , px : 2 , pt : 1 , pb : 3 , fontWeight : "600" , whiteSpace : "pre-wrap" } }
167
197
children = { presentation . summary }
168
198
/>
169
199
) }
170
- < Divider flexItem />
171
- { presentation . categories . length ? (
172
- < >
173
- < Stack direction = "row" alignItems = "center" justifyContent = "flex-start" sx = { { width : "100%" , gap : 1 , p : 2 } } >
174
- < Typography variant = "subtitle1" fontWeight = "bold" children = { categoriesStr } />
175
- < Stack direction = "row" spacing = { 1 } sx = { { width : "100%" } } >
176
- { presentation . categories . map ( ( c ) => (
177
- < Chip key = { c . id } size = "small" variant = "outlined" color = "primary" label = { c . name } />
178
- ) ) }
179
- </ Stack >
180
- </ Stack >
181
- < Divider flexItem />
182
- </ >
183
- ) : null }
200
+ < Table sx = { { tableLayout : "auto" } } >
201
+ < TableBody >
202
+ { presentation . room_schedules . length
203
+ ? Object . entries ( scheduleMap ) . map ( ( [ datetime , rooms ] , index ) => (
204
+ < TableRow key = { datetime } >
205
+ { index === 0 && (
206
+ < TableCell rowSpan = { presentation . room_schedules . length } sx = { { width : "1%" , whiteSpace : "nowrap" } } >
207
+ < Typography variant = "subtitle1" fontWeight = "bold" children = { datetimeLabel } />
208
+ </ TableCell >
209
+ ) }
210
+ < TableCell >
211
+ < Stack direction = "row" justifyContent = "flex-start" alignItems = "center" sx = { { width : "100%" , flexWrap : "wrap" , gap : 1 } } >
212
+ < Typography variant = "subtitle1" children = { datetime } />
213
+ < Stack direction = "row" sx = { { flexGrow : 0 , gap : 1 , flexWrap : "wrap" } } >
214
+ { rooms . map ( ( room , index ) => (
215
+ < Chip key = { index } sx = { { whiteSpace : "pre-wrap" } } label = { room . replace ( "\\n" , "\n" ) } />
216
+ ) ) }
217
+ </ Stack >
218
+ </ Stack >
219
+ </ TableCell >
220
+ </ TableRow >
221
+ ) )
222
+ : null }
223
+ { presentation . categories . length ? (
224
+ < TableRow >
225
+ < TableCell children = { < Typography variant = "subtitle1" fontWeight = "bold" children = { categoriesStr } /> } />
226
+ < TableCell >
227
+ < Stack direction = "row" spacing = { 1 } sx = { { width : "100%" } } >
228
+ { presentation . categories . map ( ( c ) => (
229
+ < Chip key = { c . id } size = "small" variant = "outlined" color = "primary" label = { c . name } />
230
+ ) ) }
231
+ </ Stack >
232
+ </ TableCell >
233
+ </ TableRow >
234
+ ) : null }
235
+ </ TableBody >
236
+ </ Table >
184
237
{ /* {presentation.slideshow_url && (
185
238
<>
186
239
<Typography variant="subtitle1" fontWeight="bold" sx={{ width: "100%", p: 2, a: { color: "blue" } }}>
0 commit comments