Skip to content

Commit 194d0c2

Browse files
authored
[유저] 유효하지 않은 시간표 frame id로 인한 버그 수정 (#1225)
1 parent 0196787 commit 194d0c2

5 files changed

Lines changed: 262 additions & 73 deletions

File tree

src/api/timetable/queries.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ type FrameListQueryParams = {
3131
const canUseStudentTimetableQuery = (token: string, userType?: TimetableUserType) =>
3232
Boolean(token) && (!userType || userType === 'STUDENT');
3333

34+
export const isValidTimetableFrameId = (timetableFrameId: number | null | undefined): timetableFrameId is number =>
35+
typeof timetableFrameId === 'number' && Number.isInteger(timetableFrameId) && timetableFrameId > 0;
36+
3437
export const createDefaultTimetableFrameList = (): TimetableFrameListResponse => [
3538
{
3639
id: null,
@@ -92,7 +95,10 @@ export const timetableQueries = {
9295
lectureInfo: (authorization: string, timetableFrameId: number) =>
9396
queryOptions({
9497
queryKey: timetableQueryKeys.lectureInfo(timetableFrameId),
95-
queryFn: () => (authorization ? getTimetableLectureInfo(authorization, timetableFrameId) : null),
98+
queryFn: () =>
99+
authorization && isValidTimetableFrameId(timetableFrameId)
100+
? getTimetableLectureInfo(authorization, timetableFrameId)
101+
: null,
96102
}),
97103

98104
allLectures: (token: string) =>

src/components/IndexComponents/IndexTimetable/index.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useEffect } from 'react';
22
import Link from 'next/link';
3+
import { isValidTimetableFrameId } from 'api/timetable/queries';
34
import ErrorBoundary from 'components/boundary/ErrorBoundary';
45
import Timetable from 'components/TimetablePage/components/Timetable';
6+
import TimetableGridPlaceholder from 'components/TimetablePage/components/TimetableGridPlaceholder';
57
import useSemesterOptionList from 'components/TimetablePage/hooks/useSemesterOptionList';
68
import useTimetableFrameList from 'components/TimetablePage/hooks/useTimetableFrameList';
79
import ROUTES from 'static/routes';
@@ -19,34 +21,33 @@ export default function IndexTimeTable() {
1921
const token = useTokenState();
2022
const { data: timetableFrameList } = useTimetableFrameList(token, semester);
2123

22-
const currentFrameId = timetableFrameList?.find((frame) => frame.is_main)?.id ?? 0;
24+
const currentFrameId = timetableFrameList?.find((frame) => frame.is_main)?.id;
25+
const hasValidCurrentFrameId = isValidTimetableFrameId(currentFrameId);
2326
const isClient = useMount();
2427

2528
useEffect(() => {
2629
if (semesterOptionList.length > 0) updateSemester(semesterOptionList[0].value);
2730
}, [semesterOptionList, updateSemester]);
2831

29-
const renderTimetable = (
30-
<Timetable
31-
timetableFrameId={currentFrameId}
32+
const renderPlaceholder = (
33+
<TimetableGridPlaceholder
3234
columnWidth={44}
3335
firstColumnWidth={29}
3436
rowHeight={17.3}
3537
totalHeight={369}
3638
/>
3739
);
3840

39-
const renderPlaceholder = (
40-
<div
41-
aria-hidden
42-
style={{
43-
height: 369,
44-
width: '100%',
45-
borderRadius: 12,
46-
backgroundColor: '#f7f8fa',
47-
border: '1px solid #e4e8ee',
48-
}}
41+
const renderTimetable = hasValidCurrentFrameId ? (
42+
<Timetable
43+
timetableFrameId={currentFrameId}
44+
columnWidth={44}
45+
firstColumnWidth={29}
46+
rowHeight={17.3}
47+
totalHeight={369}
4948
/>
49+
) : (
50+
renderPlaceholder
5051
);
5152

5253
return (

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

Lines changed: 129 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +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';
6+
import { isValidTimetableFrameId } from 'api/timetable/queries';
57
import DownloadIcon from 'assets/svg/download-icon.svg';
68
import GraduationIcon from 'assets/svg/graduation-icon.svg';
79
import EditIcon from 'assets/svg/pen-icon.svg';
810
import Curriculum from 'components/TimetablePage/components/Curriculum';
911
import Timetable from 'components/TimetablePage/components/Timetable';
12+
import TimetableGridPlaceholder from 'components/TimetablePage/components/TimetableGridPlaceholder';
1013
import TotalGrades from 'components/TimetablePage/components/TotalGrades';
1114
import useMyLectures from 'components/TimetablePage/hooks/useMyLectures';
1215
import useSemesterCheck from 'components/TimetablePage/hooks/useMySemester';
@@ -20,7 +23,105 @@ import { useSemester } from 'utils/zustand/semester';
2023
import DownloadTimetableModal from './DownloadTimetableModal';
2124
import styles from './MyLectureTimetable.module.scss';
2225

23-
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 }) {
24125
const [isModalOpen, openModal, closeModal] = useBooleanState(false);
25126
const token = useTokenState();
26127
const semester = useSemester();
@@ -37,7 +138,7 @@ function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
37138
return false;
38139
}
39140

40-
if (timeTableFrameList.length === 0) {
141+
if (!timeTableFrameList.some((frame) => isValidTimetableFrameId(frame.id))) {
41142
toast.error('시간표가 존재하지 않습니다. 시간표를 추가해주세요.');
42143
return false;
43144
}
@@ -68,50 +169,33 @@ function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
68169
};
69170

70171
return (
71-
<div className={styles['page__timetable-wrap']}>
72-
<div className={styles.page__filter}>
73-
<div className={styles['page__total-grades']}>
74-
<TotalGrades myLectureList={myLectures} />
75-
</div>
76-
<button
77-
type="button"
78-
className={styles.page__button}
79-
onClick={() => {
80-
router.push(ROUTES.GraduationCalculator());
81-
logger.actionEventClick({
82-
team: 'USER',
83-
event_label: 'graduation_calculator',
84-
value: '졸업학점 계산기',
85-
});
86-
}}
87-
>
88-
<GraduationIcon />
89-
졸업학점 계산기
90-
</button>
91-
<Curriculum list={deptList} />
92-
<button type="button" className={styles.page__button} onClick={onClickDownloadImage}>
93-
<DownloadIcon />
94-
이미지 저장
95-
</button>
96-
<button type="button" className={styles.page__button} onClick={onClickEdit}>
97-
<div className={styles['page__edit-icon']}>
98-
<EditIcon />
99-
</div>
100-
시간표 수정
101-
</button>
102-
</div>
103-
<div className={styles.page__timetable}>
104-
<Timetable
105-
timetableFrameId={timetableFrameId}
106-
columnWidth={140}
107-
firstColumnWidth={70}
108-
rowHeight={33}
109-
totalHeight={700}
110-
/>
111-
</div>
112-
<div>{isModalOpen && <DownloadTimetableModal onClose={closeModal} timetableFrameId={timetableFrameId} />}</div>
113-
</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+
/>
114190
);
115191
}
116192

193+
function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
194+
if (!isValidTimetableFrameId(timetableFrameId)) {
195+
return <InvalidMainTimetable />;
196+
}
197+
198+
return <ValidMainTimetable timetableFrameId={timetableFrameId} />;
199+
}
200+
117201
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)