Skip to content

Commit d7bf3a7

Browse files
committed
fix: avoid invalid timetable placeholder hooks (refs #1224)
1 parent 33950c2 commit d7bf3a7

3 files changed

Lines changed: 222 additions & 46 deletions

File tree

src/components/IndexComponents/IndexTimetable/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Link from 'next/link';
33
import { isValidTimetableFrameId } from 'api/timetable/queries';
44
import ErrorBoundary from 'components/boundary/ErrorBoundary';
55
import Timetable from 'components/TimetablePage/components/Timetable';
6+
import TimetableGridPlaceholder from 'components/TimetablePage/components/TimetableGridPlaceholder';
67
import useSemesterOptionList from 'components/TimetablePage/hooks/useSemesterOptionList';
78
import useTimetableFrameList from 'components/TimetablePage/hooks/useTimetableFrameList';
89
import ROUTES from 'static/routes';
@@ -29,8 +30,7 @@ export default function IndexTimeTable() {
2930
}, [semesterOptionList, updateSemester]);
3031

3132
const renderPlaceholder = (
32-
<Timetable
33-
timetableFrameId={0}
33+
<TimetableGridPlaceholder
3434
columnWidth={44}
3535
firstColumnWidth={29}
3636
rowHeight={17.3}

src/components/TimetablePage/components/MainTimetable/index.tsx

Lines changed: 127 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import React from 'react';
22
import { useRouter } from 'next/router';
33
import { useSuspenseQuery } from '@tanstack/react-query';
44
import { deptQueries } from 'api/dept/queries';
5+
import { Lecture, MyLectureInfo } from 'api/timetable/entity';
56
import { isValidTimetableFrameId } from 'api/timetable/queries';
67
import DownloadIcon from 'assets/svg/download-icon.svg';
78
import GraduationIcon from 'assets/svg/graduation-icon.svg';
89
import EditIcon from 'assets/svg/pen-icon.svg';
910
import Curriculum from 'components/TimetablePage/components/Curriculum';
1011
import Timetable from 'components/TimetablePage/components/Timetable';
12+
import TimetableGridPlaceholder from 'components/TimetablePage/components/TimetableGridPlaceholder';
1113
import TotalGrades from 'components/TimetablePage/components/TotalGrades';
1214
import useMyLectures from 'components/TimetablePage/hooks/useMyLectures';
1315
import useSemesterCheck from 'components/TimetablePage/hooks/useMySemester';
@@ -21,7 +23,105 @@ import { useSemester } from 'utils/zustand/semester';
2123
import DownloadTimetableModal from './DownloadTimetableModal';
2224
import styles from './MyLectureTimetable.module.scss';
2325

24-
function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
26+
interface MainTimetableLayoutProps {
27+
curriculum: React.ReactNode;
28+
myLectures: Lecture[] | MyLectureInfo[] | undefined;
29+
onClickDownloadImage: (e: React.MouseEvent<HTMLButtonElement>) => void;
30+
onClickEdit: () => void;
31+
onClickGraduation: () => void;
32+
timetableContent: React.ReactNode;
33+
footer?: React.ReactNode;
34+
}
35+
36+
function MainTimetableLayout({
37+
curriculum,
38+
myLectures,
39+
onClickDownloadImage,
40+
onClickEdit,
41+
onClickGraduation,
42+
timetableContent,
43+
footer,
44+
}: MainTimetableLayoutProps) {
45+
return (
46+
<div className={styles['page__timetable-wrap']}>
47+
<div className={styles.page__filter}>
48+
<div className={styles['page__total-grades']}>
49+
<TotalGrades myLectureList={myLectures} />
50+
</div>
51+
<button type="button" className={styles.page__button} onClick={onClickGraduation}>
52+
<GraduationIcon />
53+
졸업학점 계산기
54+
</button>
55+
{curriculum}
56+
<button type="button" className={styles.page__button} onClick={onClickDownloadImage}>
57+
<DownloadIcon />
58+
이미지 저장
59+
</button>
60+
<button type="button" className={styles.page__button} onClick={onClickEdit}>
61+
<div className={styles['page__edit-icon']}>
62+
<EditIcon />
63+
</div>
64+
시간표 수정
65+
</button>
66+
</div>
67+
<div className={styles.page__timetable}>{timetableContent}</div>
68+
<div>{footer}</div>
69+
</div>
70+
);
71+
}
72+
73+
function InvalidMainTimetable() {
74+
const token = useTokenState();
75+
const semester = useSemester();
76+
const logger = useLogger();
77+
const router = useRouter();
78+
const { data: timeTableFrameList } = useTimetableFrameList(token, semester);
79+
const { data: deptList } = useSuspenseQuery(deptQueries.list());
80+
const { data: mySemester } = useSemesterCheck(token);
81+
82+
const isSemesterAndTimetableExist = () => {
83+
if (mySemester?.semesters.length === 0) {
84+
toast.error('학기가 존재하지 않습니다. 학기를 추가해주세요.');
85+
return false;
86+
}
87+
88+
if (!timeTableFrameList.some((frame) => isValidTimetableFrameId(frame.id))) {
89+
toast.error('시간표가 존재하지 않습니다. 시간표를 추가해주세요.');
90+
return false;
91+
}
92+
93+
return true;
94+
};
95+
96+
const onClickDownloadImage = (e: React.MouseEvent<HTMLButtonElement>) => {
97+
e.stopPropagation();
98+
isSemesterAndTimetableExist();
99+
};
100+
101+
const onClickEdit = () => {
102+
isSemesterAndTimetableExist();
103+
};
104+
105+
return (
106+
<MainTimetableLayout
107+
curriculum={<Curriculum list={deptList} />}
108+
myLectures={[]}
109+
onClickDownloadImage={onClickDownloadImage}
110+
onClickEdit={onClickEdit}
111+
onClickGraduation={() => {
112+
router.push(ROUTES.GraduationCalculator());
113+
logger.actionEventClick({
114+
team: 'USER',
115+
event_label: 'graduation_calculator',
116+
value: '졸업학점 계산기',
117+
});
118+
}}
119+
timetableContent={<TimetableGridPlaceholder columnWidth={140} firstColumnWidth={70} rowHeight={33} totalHeight={700} />}
120+
/>
121+
);
122+
}
123+
124+
function ValidMainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
25125
const [isModalOpen, openModal, closeModal] = useBooleanState(false);
26126
const token = useTokenState();
27127
const semester = useSemester();
@@ -69,50 +169,33 @@ function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
69169
};
70170

71171
return (
72-
<div className={styles['page__timetable-wrap']}>
73-
<div className={styles.page__filter}>
74-
<div className={styles['page__total-grades']}>
75-
<TotalGrades myLectureList={myLectures} />
76-
</div>
77-
<button
78-
type="button"
79-
className={styles.page__button}
80-
onClick={() => {
81-
router.push(ROUTES.GraduationCalculator());
82-
logger.actionEventClick({
83-
team: 'USER',
84-
event_label: 'graduation_calculator',
85-
value: '졸업학점 계산기',
86-
});
87-
}}
88-
>
89-
<GraduationIcon />
90-
졸업학점 계산기
91-
</button>
92-
<Curriculum list={deptList} />
93-
<button type="button" className={styles.page__button} onClick={onClickDownloadImage}>
94-
<DownloadIcon />
95-
이미지 저장
96-
</button>
97-
<button type="button" className={styles.page__button} onClick={onClickEdit}>
98-
<div className={styles['page__edit-icon']}>
99-
<EditIcon />
100-
</div>
101-
시간표 수정
102-
</button>
103-
</div>
104-
<div className={styles.page__timetable}>
105-
<Timetable
106-
timetableFrameId={timetableFrameId}
107-
columnWidth={140}
108-
firstColumnWidth={70}
109-
rowHeight={33}
110-
totalHeight={700}
111-
/>
112-
</div>
113-
<div>{isModalOpen && <DownloadTimetableModal onClose={closeModal} timetableFrameId={timetableFrameId} />}</div>
114-
</div>
172+
<MainTimetableLayout
173+
curriculum={<Curriculum list={deptList} />}
174+
myLectures={myLectures}
175+
onClickDownloadImage={onClickDownloadImage}
176+
onClickEdit={onClickEdit}
177+
onClickGraduation={() => {
178+
router.push(ROUTES.GraduationCalculator());
179+
logger.actionEventClick({
180+
team: 'USER',
181+
event_label: 'graduation_calculator',
182+
value: '졸업학점 계산기',
183+
});
184+
}}
185+
timetableContent={
186+
<Timetable timetableFrameId={timetableFrameId} columnWidth={140} firstColumnWidth={70} rowHeight={33} totalHeight={700} />
187+
}
188+
footer={isModalOpen && <DownloadTimetableModal onClose={closeModal} timetableFrameId={timetableFrameId} />}
189+
/>
115190
);
116191
}
117192

193+
function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
194+
if (!isValidTimetableFrameId(timetableFrameId)) {
195+
return <InvalidMainTimetable />;
196+
}
197+
198+
return <ValidMainTimetable timetableFrameId={timetableFrameId} />;
199+
}
200+
118201
export default MainTimetable;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { cn } from '@bcsdlab/utils';
2+
import { DAYS_STRING } from 'static/timetable';
3+
import styles from 'components/TimetablePage/components/Timetable/Timetable.module.scss';
4+
5+
const DEFAULT_TIME_STRING = ['9', '10', '11', '12', '13', '14', '15', '16', '17', '18'].flatMap((time) => [
6+
time,
7+
'',
8+
]);
9+
10+
interface TimetableGridPlaceholderProps {
11+
firstColumnWidth: number;
12+
columnWidth: number;
13+
rowHeight: number;
14+
totalHeight: number;
15+
}
16+
17+
export default function TimetableGridPlaceholder({
18+
firstColumnWidth,
19+
columnWidth,
20+
rowHeight,
21+
totalHeight,
22+
}: TimetableGridPlaceholderProps) {
23+
const columnHeight = DEFAULT_TIME_STRING.length * rowHeight;
24+
25+
return (
26+
<div className={styles.timetable} style={{ height: `${totalHeight}px`, fontSize: `${rowHeight / 2}px` }} aria-hidden>
27+
<div className={styles.timetable__head} style={{ height: `${rowHeight + 5}px` }}>
28+
<div
29+
className={cn({
30+
[styles.timetable__col]: true,
31+
[styles['timetable__col--head']]: true,
32+
})}
33+
style={{ width: `${firstColumnWidth}px` }}
34+
/>
35+
{DAYS_STRING.map((day) => (
36+
<div
37+
className={cn({
38+
[styles.timetable__col]: true,
39+
[styles['timetable__col--head']]: true,
40+
})}
41+
style={{ width: `${columnWidth}px` }}
42+
key={day}
43+
>
44+
{day}
45+
</div>
46+
))}
47+
</div>
48+
<div className={styles.timetable__content} style={{ height: `${20 * rowHeight}px` }}>
49+
<div className={styles['timetable__row-container']} aria-hidden="true">
50+
{DEFAULT_TIME_STRING.map((value, index) => (
51+
<div
52+
className={styles['timetable__row-line']}
53+
style={{ height: `${rowHeight + 1}px` }}
54+
key={`placeholder-row-${value}-${index}`}
55+
/>
56+
))}
57+
</div>
58+
<div
59+
className={styles.timetable__col}
60+
style={{
61+
width: `${firstColumnWidth}px`,
62+
fontSize: `${rowHeight / 2}px`,
63+
height: `${columnHeight}px`,
64+
}}
65+
aria-hidden="true"
66+
>
67+
{DEFAULT_TIME_STRING.map((value, index) => (
68+
<div
69+
style={{ height: `${rowHeight}px` }}
70+
key={`placeholder-time-${value}-${index}`}
71+
className={
72+
columnWidth > 50 ? styles['timetable__content--time'] : styles['timetable__content--time-main']
73+
}
74+
>
75+
{value}
76+
</div>
77+
))}
78+
</div>
79+
80+
{DAYS_STRING.map((day) => (
81+
<div
82+
className={styles.timetable__col}
83+
style={{
84+
width: `${columnWidth}px`,
85+
height: `${columnHeight}px`,
86+
}}
87+
key={`placeholder-day-${day}`}
88+
/>
89+
))}
90+
</div>
91+
</div>
92+
);
93+
}

0 commit comments

Comments
 (0)