Skip to content

Commit da32559

Browse files
committed
feat: 시간표의 좌우 스크롤 인디케이터가 클릭 가능하도록
1 parent 8ade824 commit da32559

4 files changed

Lines changed: 86 additions & 14 deletions

File tree

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getSessionDetailUrl } from "@frontend/common/utils";
1515
import { getRoomOrders, getRooms, getTimeTableData, TIME_COL_WIDTH, useHorizontalOverflow } from "./session_timetable_data";
1616
import {
1717
BreakTime,
18+
HorizontalScrollNotice,
1819
ScrollHintEdge,
1920
SessionBox,
2021
SessionDateItemContainer,
@@ -88,7 +89,7 @@ export const SessionTimeTable: FC<SessionTimeTablePropType> = ErrorBoundary.with
8889
Suspense.with({ fallback: <CenteredPage children={<CircularProgress />} /> }, ({ event, types, rowHeight = TD_HEIGHT }) => {
8990
const location = useLocation();
9091
const tdHeight = Number(rowHeight) || TD_HEIGHT; // MDX에서 문자열로 들어와도 안전하게 처리
91-
const { scrollRef, canScrollLeft, canScrollRight } = useHorizontalOverflow();
92+
const { scrollRef, canScrollLeft, canScrollRight, scrollByViewport } = useHorizontalOverflow();
9293

9394
const [confDate, setConfDate] = useState<string>(location.state?.selectedDate ?? "");
9495

@@ -117,7 +118,10 @@ export const SessionTimeTable: FC<SessionTimeTablePropType> = ErrorBoundary.with
117118

118119
return (
119120
<Stack direction="column" sx={{ width: "100%" }}>
120-
<Typography variant="body2" sx={{ width: "100%", textAlign: "right", my: 0.5, fontSize: "0.6rem" }} children={warningMessage} />
121+
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: "100%", my: 0.5, gap: 1 }}>
122+
<HorizontalScrollNotice visible={canScrollLeft || canScrollRight} language={language} />
123+
<Typography variant="body2" sx={{ textAlign: "right", fontSize: "0.6rem" }} children={warningMessage} />
124+
</Stack>
121125
<StyledDivider />
122126
{dates.length > 1 && (
123127
<>
@@ -268,11 +272,25 @@ export const SessionTimeTable: FC<SessionTimeTablePropType> = ErrorBoundary.with
268272
</SessionTableBody>
269273
</SessionTable>
270274
</SessionTableScroll>
271-
{/* 표가 화면보다 넓을 때만 좌우 스크롤 가능 방향을 페이드+화살표로 안내한다. */}
272-
<ScrollHintEdge className="left" data-visible={canScrollLeft || undefined} aria-hidden>
275+
{/* 표가 화면보다 넓을 때만 좌우 스크롤 가능 방향을 페이드+화살표로 안내하고, 누르면 한 화면씩 스크롤한다. */}
276+
<ScrollHintEdge
277+
type="button"
278+
className="left"
279+
data-visible={canScrollLeft || undefined}
280+
disabled={!canScrollLeft}
281+
onClick={() => scrollByViewport("left")}
282+
aria-label={language === "ko" ? "이전 화면으로 스크롤" : "Scroll left"}
283+
>
273284
<KeyboardArrowLeft fontSize="small" />
274285
</ScrollHintEdge>
275-
<ScrollHintEdge className="right" data-visible={canScrollRight || undefined} aria-hidden>
286+
<ScrollHintEdge
287+
type="button"
288+
className="right"
289+
data-visible={canScrollRight || undefined}
290+
disabled={!canScrollRight}
291+
onClick={() => scrollByViewport("right")}
292+
aria-label={language === "ko" ? "다음 화면으로 스크롤" : "Scroll right"}
293+
>
276294
<KeyboardArrowRight fontSize="small" />
277295
</ScrollHintEdge>
278296
</SessionTableScrollWrapper>

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,14 @@ export const useHorizontalOverflow = () => {
5151
};
5252
}, [update]);
5353

54-
return { scrollRef, canScrollLeft: left, canScrollRight: right };
54+
// 스크롤 안내 화살표 클릭 시 현재 보이는 폭(clientWidth)만큼 좌/우로 한 화면씩 이동한다.
55+
const scrollByViewport = useCallback((direction: "left" | "right") => {
56+
const el = scrollRef.current;
57+
if (!el) return;
58+
el.scrollBy({ left: direction === "left" ? -el.clientWidth : el.clientWidth, behavior: "smooth" });
59+
}, []);
60+
61+
return { scrollRef, canScrollLeft: left, canScrollRight: right, scrollByViewport };
5562
};
5663

5764
const getPaddedTime = (time: DateTime) => `${time.hour}:${time.minute.toString().padStart(2, "0")}`;

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SwapHoriz } from "@mui/icons-material";
12
import { keyframes, Stack, styled, TableRow, Typography } from "@mui/material";
23
import { FC } from "react";
34

@@ -11,6 +12,30 @@ export const BreakTime: FC<{ language: "ko" | "en"; duration: number }> = ({ lan
1112
return <Typography variant="subtitle2" fontWeight="500" children={text} />;
1213
};
1314

15+
// 표가 화면 폭을 넘겨 좌우 스크롤이 가능할 때만 상단에 노출하는 작은 안내 문구.
16+
// visible 이 false 여도 자리를 유지해 나타날 때 레이아웃이 흔들리지 않도록 opacity 로만 감춘다.
17+
export const HorizontalScrollNotice: FC<{ visible: boolean; language: "ko" | "en" }> = ({ visible, language }) => (
18+
<Stack
19+
direction="row"
20+
alignItems="center"
21+
gap={0.25}
22+
aria-hidden={!visible}
23+
sx={{
24+
flexShrink: 0,
25+
color: (t) => t.palette.primary.main,
26+
fontSize: "0.6rem",
27+
whiteSpace: "nowrap",
28+
userSelect: "none",
29+
pointerEvents: "none",
30+
opacity: visible ? 1 : 0,
31+
transition: "opacity 0.25s ease",
32+
}}
33+
>
34+
<SwapHoriz sx={{ fontSize: "0.9rem" }} />
35+
<span>{language === "ko" ? "좌우로 스크롤할 수 있습니다" : "Scroll horizontally to see more"}</span>
36+
</Stack>
37+
);
38+
1439
export const SessionDateItemContainer = styled(Stack)({
1540
alignItems: "center",
1641
justifyContent: "center",
@@ -67,8 +92,8 @@ export const SessionTableScrollWrapper = styled("div")({
6792
});
6893

6994
// 표가 화면 폭을 넘칠 때 해당 방향으로 더 스크롤할 수 있음을 알리는 페이드+화살표.
70-
// data-visible 이 있을 때만 나타나며, 스크롤을 막지 않도록 pointer-events는 비활성화한다.
71-
export const ScrollHintEdge = styled("div")(({ theme }) => ({
95+
// data-visible 이 있을 때만 나타나고 클릭 가능해지며, 이때 눌러 한 화면씩 스크롤할 수 있다.
96+
export const ScrollHintEdge = styled("button")(({ theme }) => ({
7297
position: "absolute",
7398
top: 0,
7499
bottom: 0,
@@ -77,11 +102,18 @@ export const ScrollHintEdge = styled("div")(({ theme }) => ({
77102
alignItems: "flex-start",
78103
paddingTop: "0.75rem",
79104
color: theme.palette.primary.main,
105+
// button 기본 스타일 초기화
106+
border: "none",
107+
margin: 0,
108+
font: "inherit",
109+
appearance: "none",
110+
WebkitAppearance: "none",
111+
// 숨겨진(스크롤 불가) 동안엔 아래 셀 클릭을 막지 않도록 pointer-events 를 끈다.
80112
pointerEvents: "none",
81113
opacity: 0,
82114
transition: "opacity 0.25s ease",
83115
zIndex: 3, // sticky 시간 열(zIndex 2) 위에 표시
84-
"&[data-visible]": { opacity: 1 },
116+
"&[data-visible]": { opacity: 1, pointerEvents: "auto", cursor: "pointer" },
85117

86118
"&.right": {
87119
right: 0,

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getSessionDetailUrl } from "@frontend/common/utils";
1515
import { getRoomOrders, getRooms, getTimeTableData, TimeTableData, useHorizontalOverflow } from "./session_timetable_data";
1616
import {
1717
BreakTime,
18+
HorizontalScrollNotice,
1819
ScrollHintEdge,
1920
SessionBox,
2021
SessionDateItemContainer,
@@ -192,7 +193,7 @@ export const SessionTimeTableTransposed: FC<SessionTimeTableTransposedPropType>
192193
const location = useLocation();
193194
const rowH = Number(rowHeight) || ROW_HEIGHT; // MDX 에서 문자열로 들어와도 안전하게 처리
194195
const slotW = Number(slotWidth) || SLOT_WIDTH;
195-
const { scrollRef, canScrollLeft, canScrollRight } = useHorizontalOverflow();
196+
const { scrollRef, canScrollLeft, canScrollRight, scrollByViewport } = useHorizontalOverflow();
196197

197198
const [confDate, setConfDate] = useState<string>(location.state?.selectedDate ?? "");
198199

@@ -233,7 +234,10 @@ export const SessionTimeTableTransposed: FC<SessionTimeTableTransposedPropType>
233234

234235
return (
235236
<Stack direction="column" sx={{ width: "100%" }}>
236-
<Typography variant="body2" sx={{ width: "100%", textAlign: "right", my: 0.5, fontSize: "0.6rem" }} children={warningMessage} />
237+
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: "100%", my: 0.5, gap: 1 }}>
238+
<HorizontalScrollNotice visible={canScrollLeft || canScrollRight} language={language} />
239+
<Typography variant="body2" sx={{ textAlign: "right", fontSize: "0.6rem" }} children={warningMessage} />
240+
</Stack>
237241
<StyledDivider />
238242
{dates.length > 1 && (
239243
<>
@@ -329,16 +333,27 @@ export const SessionTimeTableTransposed: FC<SessionTimeTableTransposedPropType>
329333
</TableBody>
330334
</TransposedTable>
331335
</SessionTableScroll>
332-
{/* 표가 화면보다 넓을 때만 좌우 스크롤 가능 방향을 페이드+화살표로 안내한다. */}
336+
{/* 표가 화면보다 넓을 때만 좌우 스크롤 가능 방향을 페이드+화살표로 안내하고, 누르면 한 화면씩 스크롤한다. */}
333337
<ScrollHintEdge
338+
type="button"
334339
className="left"
335340
data-visible={canScrollLeft || undefined}
336-
aria-hidden
341+
disabled={!canScrollLeft}
342+
onClick={() => scrollByViewport("left")}
343+
aria-label={language === "ko" ? "이전 화면으로 스크롤" : "Scroll left"}
337344
sx={{ "--hint-left": ROOM_COL_WIDTH, paddingTop: SCROLL_HINT_TOP }}
338345
>
339346
<KeyboardArrowLeft fontSize="small" />
340347
</ScrollHintEdge>
341-
<ScrollHintEdge className="right" data-visible={canScrollRight || undefined} aria-hidden sx={{ paddingTop: SCROLL_HINT_TOP }}>
348+
<ScrollHintEdge
349+
type="button"
350+
className="right"
351+
data-visible={canScrollRight || undefined}
352+
disabled={!canScrollRight}
353+
onClick={() => scrollByViewport("right")}
354+
aria-label={language === "ko" ? "다음 화면으로 스크롤" : "Scroll right"}
355+
sx={{ paddingTop: SCROLL_HINT_TOP }}
356+
>
342357
<KeyboardArrowRight fontSize="small" />
343358
</ScrollHintEdge>
344359
</SessionTableScrollWrapper>

0 commit comments

Comments
 (0)