Skip to content

Commit e141d7c

Browse files
committed
fix: 시간표를 한국 표준시로 고정
1 parent d84ceae commit e141d7c

9 files changed

Lines changed: 71 additions & 35 deletions

File tree

apps/pyconkr-2026/src/components/pages/my-timetable.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useBackendClient, useSessionsQuery } from "@frontend/common/hooks/useAPI";
2-
import { getSessionDetailUrl } from "@frontend/common/utils";
2+
import { getSessionDetailUrl, KOREA_TIME_ZONE } from "@frontend/common/utils";
33
import { useShopClient, useUserStatus } from "@frontend/shop/hooks";
44
import { Button, CircularProgress, Stack, Typography } from "@mui/material";
5+
import { DateTime } from "luxon";
56
import { FC, useEffect, useMemo } from "react";
67
import { Link as RouterLink } from "react-router-dom";
78

@@ -27,8 +28,8 @@ export const MyTimetablePage: FC = () => {
2728
? (session.room_schedules.length === TRACKS.length ? session.room_schedules.slice(0, 1) : session.room_schedules).flatMap(
2829
(schedule): TimetablePlacement[] => {
2930
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);
31+
const startMs = DateTime.fromISO(schedule.start_at, { zone: KOREA_TIME_ZONE }).toMillis();
32+
const endMs = DateTime.fromISO(schedule.end_at, { zone: KOREA_TIME_ZONE }).toMillis();
3233
if (!track || !Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) return [];
3334
return [
3435
{

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CenteredPage, ErrorFallback, FallbackImage, LinkHandler, MDXRenderer } from "@frontend/common/components";
22
import { useBackendClient, useSessionQuery } from "@frontend/common/hooks/useAPI";
33
import { useCommonContext } from "@frontend/common/hooks/useCommonContext";
4+
import { KOREA_TIME_ZONE } from "@frontend/common/utils";
45
import { Box, Chip, CircularProgress, Divider, Stack, styled, Table, TableBody, TableCell, TableRow, Typography } from "@mui/material";
56
import { ErrorBoundary, Suspense } from "@suspensive/react";
67
import { DateTime } from "luxon";
@@ -174,15 +175,15 @@ export const PresentationDetailPage: FC = ErrorBoundary.with(
174175
const slideShowStr = language === "ko" ? "발표 슬라이드" : "Presentation Slideshow";
175176
const slideShowLinkStr = language === "ko" ? "링크" : "Link";
176177

177-
const datetimeLabel = language === "ko" ? "발표 시각" : "Presentation Time";
178+
const datetimeLabel = language === "ko" ? "발표 시각 (KST)" : "Presentation Time (KST)";
178179
const datetimeSeparator = language === "ko" ? " ~ " : " - ";
179180
const minText = language === "ko" ? "분" : "min.";
180181

181182
// 동일 시간별로 모아서 보여줌. 단, 방은 콤마(,)로 join해서 보여줌
182183
const scheduleMap: Record<string, string[]> = presentation.room_schedules.reduce(
183184
(acc, schedule) => {
184-
const startAt = DateTime.fromISO(schedule.start_at).setLocale(language);
185-
const endAt = DateTime.fromISO(schedule.end_at).setLocale(language);
185+
const startAt = DateTime.fromISO(schedule.start_at, { zone: KOREA_TIME_ZONE }).setLocale(language);
186+
const endAt = DateTime.fromISO(schedule.end_at, { zone: KOREA_TIME_ZONE }).setLocale(language);
186187
if (!startAt.isValid || !endAt.isValid) return acc; // 유효하지 않은 날짜는 무시
187188

188189
const duration = Number.parseInt(endAt.diff(startAt, ["minutes"]).minutes.toString());

apps/pyconkr-2026/src/features/schedule/my_timetable_grid.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { KOREA_TIME_ZONE } from "@frontend/common/utils";
12
import NorthEastRoundedIcon from "@mui/icons-material/NorthEastRounded";
23
import { Box, Button, Stack, Typography } from "@mui/material";
34
import { DateTime } from "luxon";
@@ -105,7 +106,8 @@ const SessionCard: FC<{ placement: TimetablePlacement; color: string }> = ({ pla
105106
whiteSpace: "nowrap",
106107
}}
107108
>
108-
{DateTime.fromMillis(placement.startMs).toFormat("HH:mm")}{DateTime.fromMillis(placement.endMs).toFormat("HH:mm")}
109+
{DateTime.fromMillis(placement.startMs, { zone: KOREA_TIME_ZONE }).toFormat("HH:mm")}
110+
{DateTime.fromMillis(placement.endMs, { zone: KOREA_TIME_ZONE }).toFormat("HH:mm")}
109111
</Typography>
110112
<Typography
111113
title={placement.title}
@@ -149,7 +151,7 @@ export const MyTimetableGrid: FC<{ placements: TimetablePlacement[] }> = ({ plac
149151
const placementsByDay = useMemo(() => {
150152
const grouped = new Map<string, TimetablePlacement[]>();
151153
placements.forEach((placement) => {
152-
const day = DateTime.fromMillis(placement.startMs).toISODate() ?? "";
154+
const day = DateTime.fromMillis(placement.startMs, { zone: KOREA_TIME_ZONE }).toISODate() ?? "";
153155
grouped.set(day, [...(grouped.get(day) ?? []), placement]);
154156
});
155157
return grouped;
@@ -161,10 +163,10 @@ export const MyTimetableGrid: FC<{ placements: TimetablePlacement[] }> = ({ plac
161163
const dayPlacements = placementsByDay.get(activeDay) ?? [];
162164
const morningPrograms = FIXED_MORNING[activeDay];
163165
const afternoonPrograms = FIXED_AFTERNOON[activeDay];
164-
const dayStartMs = DateTime.fromISO(activeDay)
166+
const dayStartMs = DateTime.fromISO(activeDay, { zone: KOREA_TIME_ZONE })
165167
.set({ hour: AFTERNOON_START_HOUR, minute: AFTERNOON_START_MINUTE, second: 0, millisecond: 0 })
166168
.toMillis();
167-
const dayEndMs = DateTime.fromISO(`${activeDay}T${afternoonPrograms.at(-1)?.end}:00`).toMillis();
169+
const dayEndMs = DateTime.fromISO(`${activeDay}T${afternoonPrograms.at(-1)?.end}:00`, { zone: KOREA_TIME_ZONE }).toMillis();
168170
const timeSlots = Array.from({ length: (dayEndMs - dayStartMs) / SLOT_MS }, (_, index) => dayStartMs + index * SLOT_MS);
169171

170172
const selectDay = (day: string) =>
@@ -188,6 +190,10 @@ export const MyTimetableGrid: FC<{ placements: TimetablePlacement[] }> = ({ plac
188190
sx={{ alignSelf: "flex-end", fontWeight: 700, whiteSpace: "nowrap" }}
189191
children={isKo ? "발표 추가" : "Add session"}
190192
/>
193+
<Typography
194+
sx={{ alignSelf: "flex-end", color: "text.secondary", fontSize: { xs: "0.68rem", sm: "0.75rem" }, textAlign: "right" }}
195+
children={isKo ? "모든 시간은 한국 표준시(KST) 기준입니다." : "All times are shown in Korea Standard Time (KST)."}
196+
/>
191197

192198
<Box sx={{ width: "100%", minWidth: 0, overflow: "hidden", border: "1px solid", borderColor: "divider", borderRadius: "0.75rem" }}>
193199
<Stack direction="row" spacing={0} sx={{ borderBottom: "1px solid", borderColor: "divider" }}>
@@ -212,7 +218,7 @@ export const MyTimetableGrid: FC<{ placements: TimetablePlacement[] }> = ({ plac
212218
whiteSpace: "nowrap",
213219
"&:hover": { backgroundColor: day === activeDay ? "primary.main" : "action.hover" },
214220
}}
215-
children={`Day ${index + 1} · ${DateTime.fromISO(day).toFormat("M.d")}`}
221+
children={`Day ${index + 1} · ${DateTime.fromISO(day, { zone: KOREA_TIME_ZONE }).toFormat("M.d")}`}
216222
/>
217223
))}
218224
</Stack>
@@ -331,7 +337,7 @@ export const MyTimetableGrid: FC<{ placements: TimetablePlacement[] }> = ({ plac
331337
}}
332338
>
333339
<Typography sx={{ color: "text.secondary", fontSize: { xs: "0.55rem", sm: "0.68rem" }, fontWeight: 700, lineHeight: 1 }}>
334-
{DateTime.fromMillis(time).toFormat("H:mm")}
340+
{DateTime.fromMillis(time, { zone: KOREA_TIME_ZONE }).toFormat("H:mm")}
335341
</Typography>
336342
</Stack>
337343
))}
@@ -353,8 +359,12 @@ export const MyTimetableGrid: FC<{ placements: TimetablePlacement[] }> = ({ plac
353359
))}
354360

355361
{afternoonPrograms.map((program) => {
356-
const startSlot = Math.round((DateTime.fromISO(`${activeDay}T${program.start}:00`).toMillis() - dayStartMs) / SLOT_MS);
357-
const endSlot = Math.round((DateTime.fromISO(`${activeDay}T${program.end}:00`).toMillis() - dayStartMs) / SLOT_MS);
362+
const startSlot = Math.round(
363+
(DateTime.fromISO(`${activeDay}T${program.start}:00`, { zone: KOREA_TIME_ZONE }).toMillis() - dayStartMs) / SLOT_MS
364+
);
365+
const endSlot = Math.round(
366+
(DateTime.fromISO(`${activeDay}T${program.end}:00`, { zone: KOREA_TIME_ZONE }).toMillis() - dayStartMs) / SLOT_MS
367+
);
358368
return (
359369
<Stack
360370
key={`${program.start}-${program.end}`}

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { CenteredPage } from "@frontend/common/components/centered_page";
1010
import { ErrorFallback } from "@frontend/common/components/error_handler";
1111
import { BackendAPI, Common } from "@frontend/common/hooks";
1212
import { SessionSchema } from "@frontend/common/schemas/backendAPI";
13-
import { getSessionDetailUrl } from "@frontend/common/utils";
13+
import { getSessionDetailUrl, KOREA_TIME_ZONE } from "@frontend/common/utils";
1414

1515
import { getRoomOrders, getRooms, getTimeTableData, TIME_COL_WIDTH, useHorizontalOverflow } from "./session_timetable_data";
1616
import {
@@ -26,6 +26,7 @@ import {
2626
SessionTableScroll,
2727
SessionTableScrollWrapper,
2828
SessionTitle,
29+
TimetableNotices,
2930
} from "./session_timetable_shared";
3031
import { StyledDivider } from "./styled_divider";
3132

@@ -116,23 +117,20 @@ export const SessionTimeTable: FC<SessionTimeTablePropType> = ErrorBoundary.with
116117

117118
let breakCount = 0;
118119

119-
const warningMessage =
120-
language === "ko"
121-
? "* 발표 목록은 발표자 사정에 따라 변동될 수 있습니다."
122-
: "* The list of sessions may change due to the speaker's circumstances.";
123-
124120
return (
125121
<Stack direction="column" sx={{ width: "100%" }}>
126122
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: "100%", my: 0.5, gap: 1 }}>
127123
<HorizontalScrollNotice visible={canScrollLeft || canScrollRight} language={language} />
128-
<Typography variant="body2" sx={{ textAlign: "right", fontSize: "0.6rem" }} children={warningMessage} />
124+
<TimetableNotices language={language} />
129125
</Stack>
130126
<StyledDivider />
131127
{dates.length > 1 && (
132128
<>
133129
<Stack spacing={2} direction="row" justifyContent="center" alignItems="center">
134130
{dates.map((date, i) => {
135-
const dateStr = DateTime.fromISO(date).setLocale(language).toLocaleString({ weekday: "long", month: "long", day: "numeric" });
131+
const dateStr = DateTime.fromISO(date, { zone: KOREA_TIME_ZONE })
132+
.setLocale(language)
133+
.toLocaleString({ weekday: "long", month: "long", day: "numeric" });
136134
return (
137135
<Button
138136
variant="text"

packages/common/src/components/mdx_components/session_timetable_data.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DateTime } from "luxon";
22
import { useCallback, useEffect, useRef, useState } from "react";
33

44
import { SessionSchema } from "@frontend/common/schemas/backendAPI";
5+
import { KOREA_TIME_ZONE } from "@frontend/common/utils";
56

67
// 세로형 SessionTimeTable / 가로형 SessionTimeTableTransposed 가 공유하는 데이터 헬퍼·훅.
78

@@ -80,8 +81,14 @@ export const getRoomOrders = (data: SessionSchema[]): { [room: string]: number }
8081
const getConfStartEndTimePerDay: (data: SessionSchema[]) => {
8182
[date: string]: { start: DateTime; end: DateTime };
8283
} = (data) => {
83-
const startTimes = data.reduce((acc, s) => [...acc, ...s.room_schedules.map((r) => DateTime.fromISO(r.start_at))], [] as DateTime[]);
84-
const endTimes = data.reduce((acc, s) => [...acc, ...s.room_schedules.map((r) => DateTime.fromISO(r.end_at))], [] as DateTime[]);
84+
const startTimes = data.reduce(
85+
(acc, s) => [...acc, ...s.room_schedules.map((r) => DateTime.fromISO(r.start_at, { zone: KOREA_TIME_ZONE }))],
86+
[] as DateTime[]
87+
);
88+
const endTimes = data.reduce(
89+
(acc, s) => [...acc, ...s.room_schedules.map((r) => DateTime.fromISO(r.end_at, { zone: KOREA_TIME_ZONE }))],
90+
[] as DateTime[]
91+
);
8592
const allTimes = [...startTimes, ...endTimes];
8693

8794
const timesPerDay = allTimes.reduce(
@@ -130,8 +137,8 @@ export const getTimeTableData: (data: SessionSchema[]) => TimeTableData = (data)
130137
// Fill timeTableData with session data
131138
data.forEach((session) => {
132139
session.room_schedules.forEach((schedule) => {
133-
const start = DateTime.fromISO(schedule.start_at);
134-
const end = DateTime.fromISO(schedule.end_at);
140+
const start = DateTime.fromISO(schedule.start_at, { zone: KOREA_TIME_ZONE });
141+
const end = DateTime.fromISO(schedule.end_at, { zone: KOREA_TIME_ZONE });
135142

136143
if (!start.isValid || !end.isValid) {
137144
console.warn(`Invalid start or end time for session ${session.id} in room ${schedule.room_name}`);

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ export const HorizontalScrollNotice: FC<{ visible: boolean; language: "ko" | "en
3636
</Stack>
3737
);
3838

39+
export const TimetableNotices: FC<{ language: "ko" | "en" }> = ({ language }) => (
40+
<Stack alignItems="flex-end">
41+
<Typography
42+
variant="body2"
43+
sx={{ textAlign: "right", fontSize: "0.6rem" }}
44+
children={language === "ko" ? "모든 시간은 한국 표준시(KST) 기준입니다." : "All times are shown in Korea Standard Time (KST)."}
45+
/>
46+
<Typography
47+
variant="body2"
48+
sx={{ textAlign: "right", fontSize: "0.6rem" }}
49+
children={
50+
language === "ko"
51+
? "* 발표 목록은 발표자 사정에 따라 변동될 수 있습니다."
52+
: "* The list of sessions may change due to the speaker's circumstances."
53+
}
54+
/>
55+
</Stack>
56+
);
57+
3958
export const SessionDateItemContainer = styled(Stack)({
4059
alignItems: "center",
4160
justifyContent: "center",

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { CenteredPage } from "@frontend/common/components/centered_page";
1010
import { ErrorFallback } from "@frontend/common/components/error_handler";
1111
import { BackendAPI, Common } from "@frontend/common/hooks";
1212
import { SessionSchema } from "@frontend/common/schemas/backendAPI";
13-
import { getSessionDetailUrl } from "@frontend/common/utils";
13+
import { getSessionDetailUrl, KOREA_TIME_ZONE } from "@frontend/common/utils";
1414

1515
import { getRoomOrders, getRooms, getTimeTableData, TimeTableData, useHorizontalOverflow } from "./session_timetable_data";
1616
import {
@@ -26,6 +26,7 @@ import {
2626
SessionTableScroll,
2727
SessionTableScrollWrapper,
2828
SessionTitle,
29+
TimetableNotices,
2930
} from "./session_timetable_shared";
3031
import { StyledDivider } from "./styled_divider";
3132

@@ -228,23 +229,20 @@ export const SessionTimeTableTransposed: FC<SessionTimeTableTransposedPropType>
228229

229230
const totalWidth = columns.reduce((acc, col) => acc + (col.kind === "break" ? breakColWidth(col.slotCount, slotW) : slotW), 0);
230231

231-
const warningMessage =
232-
language === "ko"
233-
? "* 발표 목록은 발표자 사정에 따라 변동될 수 있습니다."
234-
: "* The list of sessions may change due to the speaker's circumstances.";
235-
236232
return (
237233
<Stack direction="column" sx={{ width: "100%" }}>
238234
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: "100%", my: 0.5, gap: 1 }}>
239235
<HorizontalScrollNotice visible={canScrollLeft || canScrollRight} language={language} />
240-
<Typography variant="body2" sx={{ textAlign: "right", fontSize: "0.6rem" }} children={warningMessage} />
236+
<TimetableNotices language={language} />
241237
</Stack>
242238
<StyledDivider />
243239
{dates.length > 1 && (
244240
<>
245241
<Stack spacing={2} direction="row" justifyContent="center" alignItems="center">
246242
{dates.map((date, i) => {
247-
const dateStr = DateTime.fromISO(date).setLocale(language).toLocaleString({ weekday: "long", month: "long", day: "numeric" });
243+
const dateStr = DateTime.fromISO(date, { zone: KOREA_TIME_ZONE })
244+
.setLocale(language)
245+
.toLocaleString({ weekday: "long", month: "long", day: "numeric" });
248246
return (
249247
<Button
250248
variant="text"

packages/common/src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export { clamp, snapToStep, stepsBetween } from "./math";
1212
export { extractQueryParameters } from "./openapi";
1313
export { getSessionDetailUrl } from "./session";
1414
export { isFilledString, isHexColor, isValidHttpUrl, rtrim } from "./string";
15-
export { dayBoundsMs, dayTabLabel, eachDayISO, floorToMinute, formatMs, isoDateOf, toMs, toNaiveISO } from "./time";
15+
export { dayBoundsMs, dayTabLabel, eachDayISO, floorToMinute, formatMs, isoDateOf, KOREA_TIME_ZONE, toMs, toNaiveISO } from "./time";

packages/common/src/utils/time.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { DateTime } from "luxon";
22

3+
export const KOREA_TIME_ZONE = "Asia/Seoul";
4+
35
export const toMs = (iso: string): number => DateTime.fromISO(iso).toMillis();
46
export const toNaiveISO = (ms: number): string => DateTime.fromMillis(ms).toISO({ includeOffset: false })!;
57
export const isoDateOf = (iso: string): string => DateTime.fromISO(iso).toISODate()!;

0 commit comments

Comments
 (0)