Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/api/timetable/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type FrameListQueryParams = {
const canUseStudentTimetableQuery = (token: string, userType?: TimetableUserType) =>
Boolean(token) && (!userType || userType === 'STUDENT');

export const isValidTimetableFrameId = (timetableFrameId: number | null | undefined): timetableFrameId is number =>
typeof timetableFrameId === 'number' && Number.isInteger(timetableFrameId) && timetableFrameId > 0;

export const createDefaultTimetableFrameList = (): TimetableFrameListResponse => [
{
id: null,
Expand Down Expand Up @@ -92,7 +95,10 @@ export const timetableQueries = {
lectureInfo: (authorization: string, timetableFrameId: number) =>
queryOptions({
queryKey: timetableQueryKeys.lectureInfo(timetableFrameId),
queryFn: () => (authorization ? getTimetableLectureInfo(authorization, timetableFrameId) : null),
queryFn: () =>
authorization && isValidTimetableFrameId(timetableFrameId)
? getTimetableLectureInfo(authorization, timetableFrameId)
: null,
}),

allLectures: (token: string) =>
Expand Down
29 changes: 15 additions & 14 deletions src/components/IndexComponents/IndexTimetable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useEffect } from 'react';
import Link from 'next/link';
import { isValidTimetableFrameId } from 'api/timetable/queries';
import ErrorBoundary from 'components/boundary/ErrorBoundary';
import Timetable from 'components/TimetablePage/components/Timetable';
import TimetableGridPlaceholder from 'components/TimetablePage/components/TimetableGridPlaceholder';
import useSemesterOptionList from 'components/TimetablePage/hooks/useSemesterOptionList';
import useTimetableFrameList from 'components/TimetablePage/hooks/useTimetableFrameList';
import ROUTES from 'static/routes';
Expand All @@ -19,34 +21,33 @@ export default function IndexTimeTable() {
const token = useTokenState();
const { data: timetableFrameList } = useTimetableFrameList(token, semester);

const currentFrameId = timetableFrameList?.find((frame) => frame.is_main)?.id ?? 0;
const currentFrameId = timetableFrameList?.find((frame) => frame.is_main)?.id;
const hasValidCurrentFrameId = isValidTimetableFrameId(currentFrameId);
const isClient = useMount();

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

const renderTimetable = (
<Timetable
timetableFrameId={currentFrameId}
const renderPlaceholder = (
<TimetableGridPlaceholder
columnWidth={44}
firstColumnWidth={29}
rowHeight={17.3}
totalHeight={369}
/>
);

const renderPlaceholder = (
<div
aria-hidden
style={{
height: 369,
width: '100%',
borderRadius: 12,
backgroundColor: '#f7f8fa',
border: '1px solid #e4e8ee',
}}
const renderTimetable = hasValidCurrentFrameId ? (
<Timetable
timetableFrameId={currentFrameId}
columnWidth={44}
firstColumnWidth={29}
rowHeight={17.3}
totalHeight={369}
/>
) : (
renderPlaceholder
);

return (
Expand Down
174 changes: 129 additions & 45 deletions src/components/TimetablePage/components/MainTimetable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import React from 'react';
import { useRouter } from 'next/router';
import { useSuspenseQuery } from '@tanstack/react-query';
import { deptQueries } from 'api/dept/queries';
import { Lecture, MyLectureInfo } from 'api/timetable/entity';
import { isValidTimetableFrameId } from 'api/timetable/queries';
import DownloadIcon from 'assets/svg/download-icon.svg';
import GraduationIcon from 'assets/svg/graduation-icon.svg';
import EditIcon from 'assets/svg/pen-icon.svg';
import Curriculum from 'components/TimetablePage/components/Curriculum';
import Timetable from 'components/TimetablePage/components/Timetable';
import TimetableGridPlaceholder from 'components/TimetablePage/components/TimetableGridPlaceholder';
import TotalGrades from 'components/TimetablePage/components/TotalGrades';
import useMyLectures from 'components/TimetablePage/hooks/useMyLectures';
import useSemesterCheck from 'components/TimetablePage/hooks/useMySemester';
Expand All @@ -20,7 +23,105 @@ import { useSemester } from 'utils/zustand/semester';
import DownloadTimetableModal from './DownloadTimetableModal';
import styles from './MyLectureTimetable.module.scss';

function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
interface MainTimetableLayoutProps {
curriculum: React.ReactNode;
myLectures: Lecture[] | MyLectureInfo[] | undefined;
onClickDownloadImage: (e: React.MouseEvent<HTMLButtonElement>) => void;
onClickEdit: () => void;
onClickGraduation: () => void;
timetableContent: React.ReactNode;
footer?: React.ReactNode;
}

function MainTimetableLayout({
curriculum,
myLectures,
onClickDownloadImage,
onClickEdit,
onClickGraduation,
timetableContent,
footer,
}: MainTimetableLayoutProps) {
return (
<div className={styles['page__timetable-wrap']}>
<div className={styles.page__filter}>
<div className={styles['page__total-grades']}>
<TotalGrades myLectureList={myLectures} />
</div>
<button type="button" className={styles.page__button} onClick={onClickGraduation}>
<GraduationIcon />
졸업학점 계산기
</button>
{curriculum}
<button type="button" className={styles.page__button} onClick={onClickDownloadImage}>
<DownloadIcon />
이미지 저장
</button>
<button type="button" className={styles.page__button} onClick={onClickEdit}>
<div className={styles['page__edit-icon']}>
<EditIcon />
</div>
시간표 수정
</button>
</div>
<div className={styles.page__timetable}>{timetableContent}</div>
<div>{footer}</div>
</div>
);
}

function InvalidMainTimetable() {
const token = useTokenState();
const semester = useSemester();
const logger = useLogger();
const router = useRouter();
const { data: timeTableFrameList } = useTimetableFrameList(token, semester);
const { data: deptList } = useSuspenseQuery(deptQueries.list());
const { data: mySemester } = useSemesterCheck(token);

const isSemesterAndTimetableExist = () => {
if (mySemester?.semesters.length === 0) {
toast.error('학기가 존재하지 않습니다. 학기를 추가해주세요.');
return false;
}

if (!timeTableFrameList.some((frame) => isValidTimetableFrameId(frame.id))) {
toast.error('시간표가 존재하지 않습니다. 시간표를 추가해주세요.');
return false;
}

return true;
};

const onClickDownloadImage = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
isSemesterAndTimetableExist();
};

const onClickEdit = () => {
isSemesterAndTimetableExist();
};

return (
<MainTimetableLayout
curriculum={<Curriculum list={deptList} />}
myLectures={[]}
onClickDownloadImage={onClickDownloadImage}
onClickEdit={onClickEdit}
onClickGraduation={() => {
router.push(ROUTES.GraduationCalculator());
logger.actionEventClick({
team: 'USER',
event_label: 'graduation_calculator',
value: '졸업학점 계산기',
});
}}
timetableContent={<TimetableGridPlaceholder columnWidth={140} firstColumnWidth={70} rowHeight={33} totalHeight={700} />}
/>
);
}

function ValidMainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
const [isModalOpen, openModal, closeModal] = useBooleanState(false);
const token = useTokenState();
const semester = useSemester();
Expand All @@ -37,7 +138,7 @@ function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
return false;
}

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

return (
<div className={styles['page__timetable-wrap']}>
<div className={styles.page__filter}>
<div className={styles['page__total-grades']}>
<TotalGrades myLectureList={myLectures} />
</div>
<button
type="button"
className={styles.page__button}
onClick={() => {
router.push(ROUTES.GraduationCalculator());
logger.actionEventClick({
team: 'USER',
event_label: 'graduation_calculator',
value: '졸업학점 계산기',
});
}}
>
<GraduationIcon />
졸업학점 계산기
</button>
<Curriculum list={deptList} />
<button type="button" className={styles.page__button} onClick={onClickDownloadImage}>
<DownloadIcon />
이미지 저장
</button>
<button type="button" className={styles.page__button} onClick={onClickEdit}>
<div className={styles['page__edit-icon']}>
<EditIcon />
</div>
시간표 수정
</button>
</div>
<div className={styles.page__timetable}>
<Timetable
timetableFrameId={timetableFrameId}
columnWidth={140}
firstColumnWidth={70}
rowHeight={33}
totalHeight={700}
/>
</div>
<div>{isModalOpen && <DownloadTimetableModal onClose={closeModal} timetableFrameId={timetableFrameId} />}</div>
</div>
<MainTimetableLayout
curriculum={<Curriculum list={deptList} />}
myLectures={myLectures}
onClickDownloadImage={onClickDownloadImage}
onClickEdit={onClickEdit}
onClickGraduation={() => {
router.push(ROUTES.GraduationCalculator());
logger.actionEventClick({
team: 'USER',
event_label: 'graduation_calculator',
value: '졸업학점 계산기',
});
}}
timetableContent={
<Timetable timetableFrameId={timetableFrameId} columnWidth={140} firstColumnWidth={70} rowHeight={33} totalHeight={700} />
}
footer={isModalOpen && <DownloadTimetableModal onClose={closeModal} timetableFrameId={timetableFrameId} />}
/>
);
}

function MainTimetable({ timetableFrameId }: { timetableFrameId: number }) {
if (!isValidTimetableFrameId(timetableFrameId)) {
return <InvalidMainTimetable />;
}

return <ValidMainTimetable timetableFrameId={timetableFrameId} />;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export default MainTimetable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { cn } from '@bcsdlab/utils';
import { DAYS_STRING } from 'static/timetable';
import styles from 'components/TimetablePage/components/Timetable/Timetable.module.scss';

const DEFAULT_TIME_STRING = ['9', '10', '11', '12', '13', '14', '15', '16', '17', '18'].flatMap((time) => [
time,
'',
]);

interface TimetableGridPlaceholderProps {
firstColumnWidth: number;
columnWidth: number;
rowHeight: number;
totalHeight: number;
}

export default function TimetableGridPlaceholder({
firstColumnWidth,
columnWidth,
rowHeight,
totalHeight,
}: TimetableGridPlaceholderProps) {
const columnHeight = DEFAULT_TIME_STRING.length * rowHeight;

return (
<div className={styles.timetable} style={{ height: `${totalHeight}px`, fontSize: `${rowHeight / 2}px` }} aria-hidden>
<div className={styles.timetable__head} style={{ height: `${rowHeight + 5}px` }}>
<div
className={cn({
[styles.timetable__col]: true,
[styles['timetable__col--head']]: true,
})}
style={{ width: `${firstColumnWidth}px` }}
/>
{DAYS_STRING.map((day) => (
<div
className={cn({
[styles.timetable__col]: true,
[styles['timetable__col--head']]: true,
})}
style={{ width: `${columnWidth}px` }}
key={day}
>
{day}
</div>
))}
</div>
<div className={styles.timetable__content} style={{ height: `${20 * rowHeight}px` }}>
<div className={styles['timetable__row-container']} aria-hidden="true">
{DEFAULT_TIME_STRING.map((value, index) => (
<div
className={styles['timetable__row-line']}
style={{ height: `${rowHeight + 1}px` }}
key={`placeholder-row-${value}-${index}`}
/>
))}
</div>
<div
className={styles.timetable__col}
style={{
width: `${firstColumnWidth}px`,
fontSize: `${rowHeight / 2}px`,
height: `${columnHeight}px`,
}}
aria-hidden="true"
>
{DEFAULT_TIME_STRING.map((value, index) => (
<div
style={{ height: `${rowHeight}px` }}
key={`placeholder-time-${value}-${index}`}
className={
columnWidth > 50 ? styles['timetable__content--time'] : styles['timetable__content--time-main']
}
>
{value}
</div>
))}
</div>

{DAYS_STRING.map((day) => (
<div
className={styles.timetable__col}
style={{
width: `${columnWidth}px`,
height: `${columnHeight}px`,
}}
key={`placeholder-day-${day}`}
/>
))}
</div>
</div>
);
}
Loading
Loading