diff --git a/apps/time/src/shared/ui/Modal.tsx b/apps/time/src/shared/ui/Modal.tsx index 11751cf5..f279dc60 100644 --- a/apps/time/src/shared/ui/Modal.tsx +++ b/apps/time/src/shared/ui/Modal.tsx @@ -37,11 +37,11 @@ interface ModalDropdownProps extends PropsWithChildren { interface ModalDropdownItemProps extends PropsWithChildren { onClick: () => void; selected: boolean; + closeOnClick?: boolean; } -interface ModalItemProps { +interface ModalItemProps extends PropsWithChildren { title: string; - value: string; } interface ModalDropdownContextType { @@ -59,7 +59,7 @@ const ModalDropdownContext = createContext({ function ModalFilter({ title, children }: ModalFilterProps) { return (
-

{title}

+

{title}

@@ -92,7 +92,7 @@ function ModalDropdown({ title, value, children }: ModalDropdownProps) { const [open, setOpen] = useState(false); const closeAction = useCallback(() => { - setOpen(false); + setOpen(() => false); }, [setOpen]); const dropdownRef = useOutsideClick({ callback: closeAction }); @@ -109,23 +109,16 @@ function ModalDropdown({ title, value, children }: ModalDropdownProps) {
setOpen((prev) => !prev)}>
-
- {value} -
+
{value}
{open && ( -
+
{children}
)} @@ -139,6 +132,7 @@ function ModalDropdownItem({ selected, onClick, children, + closeOnClick = false, }: ModalDropdownItemProps) { const { action } = useContext(ModalDropdownContext); @@ -149,9 +143,12 @@ function ModalDropdownItem({ selected ? 'text-blue-400' : 'text-black', )} type="button" - onClick={() => { + onClick={(event) => { + event.stopPropagation(); onClick(); - action.closeAction(); + if (closeOnClick) { + action.closeAction(); + } }} > {children} @@ -159,17 +156,17 @@ function ModalDropdownItem({ ); } -function ModalItem({ title, value }: ModalItemProps) { +function ModalItem({ title, children }: ModalItemProps) { return (
-

{title}

-

{value}

+

{title}

+
{children}
); } function ModalContent({ children }: PropsWithChildren) { - return
{children}
; + return
{children}
; } export default function Modal({ title, close, children }: ModalProps) { @@ -195,7 +192,7 @@ export default function Modal({ title, close, children }: ModalProps) {

{title ?? ''}

diff --git a/apps/time/src/widgets/time-table/model/constants/grade.ts b/apps/time/src/widgets/time-table/model/constants/grade.ts index b73d6a3f..26a0c752 100644 --- a/apps/time/src/widgets/time-table/model/constants/grade.ts +++ b/apps/time/src/widgets/time-table/model/constants/grade.ts @@ -1,3 +1,8 @@ -const GRADE = ['1학년', '2학년', '3학년', '4학년', '5학년', '6학년'] as const; - -export default GRADE; +export const GRADE = [ + '1학년', + '2학년', + '3학년', + '4학년', + '5학년', + '6학년', +] as const; diff --git a/apps/time/src/widgets/time-table/model/constants/index.ts b/apps/time/src/widgets/time-table/model/constants/index.ts index f3f31e7d..18f12058 100644 --- a/apps/time/src/widgets/time-table/model/constants/index.ts +++ b/apps/time/src/widgets/time-table/model/constants/index.ts @@ -1,2 +1,4 @@ export * from './period'; -export { default as GRADE } from './grade'; +export * from './grade'; +export * from './region'; +export * from './lecture'; diff --git a/apps/time/src/widgets/time-table/model/constants/lecture.ts b/apps/time/src/widgets/time-table/model/constants/lecture.ts new file mode 100644 index 00000000..c0c9a0f7 --- /dev/null +++ b/apps/time/src/widgets/time-table/model/constants/lecture.ts @@ -0,0 +1,14 @@ +import { LectureKey, LectureValue } from '@/widgets/time-table'; + +export const LECTURE = { + CULTURE: '교양', + MAJOR: '전공', + TEACHING: '교직이수', + ROTC: 'ROTC', + LINKEDFUSION: '연계융합', +} as const; + +export const LECTURE_ARRAY = Object.entries(LECTURE) as [ + LectureKey, + LectureValue, +][]; diff --git a/apps/time/src/widgets/time-table/model/constants/period.ts b/apps/time/src/widgets/time-table/model/constants/period.ts index 4b0dbc27..ac924d11 100644 --- a/apps/time/src/widgets/time-table/model/constants/period.ts +++ b/apps/time/src/widgets/time-table/model/constants/period.ts @@ -147,9 +147,12 @@ const DAY_STATUS = { night: '야간', } as const; +const SPECIAL_PERIOD = ['이러닝', '교외수업', '사회봉사'] as const; + export { DAY_PERIOD, NIGHT_PERIOD, + SPECIAL_PERIOD, DAY_PERIOD_ARRAY, NIGHT_PERIOD_ARRAY, DAY_STATUS, diff --git a/apps/time/src/widgets/time-table/model/constants/region.ts b/apps/time/src/widgets/time-table/model/constants/region.ts new file mode 100644 index 00000000..26ba0cc5 --- /dev/null +++ b/apps/time/src/widgets/time-table/model/constants/region.ts @@ -0,0 +1,6 @@ +export const REGION = { + campus1: '수원', + campus2: '서울', +} as const; + +export const REGION_VALUE_ARRAY = Object.values(REGION); diff --git a/apps/time/src/widgets/time-table/types/grade.ts b/apps/time/src/widgets/time-table/types/grade.ts index b8a36806..5314bcad 100644 --- a/apps/time/src/widgets/time-table/types/grade.ts +++ b/apps/time/src/widgets/time-table/types/grade.ts @@ -1,4 +1,4 @@ -import GRADE from '@/widgets/time-table/model/constants/grade'; +import { GRADE } from '@/widgets/time-table'; type Grade = (typeof GRADE)[number]; diff --git a/apps/time/src/widgets/time-table/types/index.ts b/apps/time/src/widgets/time-table/types/index.ts index 151e1863..38d55cdd 100644 --- a/apps/time/src/widgets/time-table/types/index.ts +++ b/apps/time/src/widgets/time-table/types/index.ts @@ -1,3 +1,5 @@ export * from './day-status'; export * from './grade'; export * from './period'; +export * from './lecture'; +export * from './region'; diff --git a/apps/time/src/widgets/time-table/types/lecture.ts b/apps/time/src/widgets/time-table/types/lecture.ts new file mode 100644 index 00000000..009f44ed --- /dev/null +++ b/apps/time/src/widgets/time-table/types/lecture.ts @@ -0,0 +1,5 @@ +import { LECTURE } from '@/widgets/time-table'; + +export type LectureKey = keyof typeof LECTURE; + +export type LectureValue = (typeof LECTURE)[keyof typeof LECTURE]; diff --git a/apps/time/src/widgets/time-table/types/period.ts b/apps/time/src/widgets/time-table/types/period.ts index 3263a695..90a55610 100644 --- a/apps/time/src/widgets/time-table/types/period.ts +++ b/apps/time/src/widgets/time-table/types/period.ts @@ -1,7 +1,7 @@ -import { DAY_PERIOD, NIGHT_PERIOD } from '@/widgets/time-table'; +import { DAY_PERIOD, NIGHT_PERIOD, SPECIAL_PERIOD } from '@/widgets/time-table'; -type DayPeriod = keyof typeof DAY_PERIOD; +export type SpecialPeriod = (typeof SPECIAL_PERIOD)[number]; -type NightPeriod = keyof typeof NIGHT_PERIOD; +export type DayPeriod = keyof typeof DAY_PERIOD | SpecialPeriod; -export type { DayPeriod, NightPeriod }; +export type NightPeriod = keyof typeof NIGHT_PERIOD | SpecialPeriod; diff --git a/apps/time/src/widgets/time-table/types/region.ts b/apps/time/src/widgets/time-table/types/region.ts new file mode 100644 index 00000000..a8afe443 --- /dev/null +++ b/apps/time/src/widgets/time-table/types/region.ts @@ -0,0 +1,3 @@ +import { REGION } from '@/widgets/time-table'; + +export type Region = (typeof REGION)[keyof typeof REGION]; diff --git a/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx b/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx index c7d36a3e..4d9d6d32 100644 --- a/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx +++ b/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx @@ -2,49 +2,91 @@ import { Dispatch, + HTMLAttributes, SetStateAction, + memo, useCallback, useEffect, useState, } from 'react'; +import { Input } from '@clab-platforms/design-system'; import { CloseOutline } from '@clab-platforms/icon'; import { DAY_VALUE_ARRAY, MODAL_KEY } from '@/shared/constants'; import { useModalAction, useModalState } from '@/shared/hooks'; import type { DayKor } from '@/shared/types'; import { Modal } from '@/shared/ui'; +import { + DayPeriod, + DayStatus, + Grade, + LectureKey, + NightPeriod, + Region, + SPECIAL_PERIOD, + SpecialPeriod, +} from '@/widgets/time-table'; import { DAY_PERIOD_ARRAY, - DAY_STATUS, - type DayPeriod, - type DayStatus, GRADE, - type Grade, + LECTURE, + LECTURE_ARRAY, NIGHT_PERIOD_ARRAY, - type NightPeriod, + REGION, + REGION_VALUE_ARRAY, } from '@/widgets/time-table'; interface TimeTableModalFilterProps { title: string; list: T[]; - origin: T; + origin: T[]; handleFilterItem: (listVal: T) => void; parseFunc?: () => boolean; } -interface TimeTableModalPeriodDropdownProps { +interface TimeTableModalPeriodDropdownProps { dayStatus: DayStatus; - selectedPeriod: DayPeriod[]; - dropdownItemHandler: (period: DayPeriod) => void; + selectedPeriod: T[]; + handlePeriodDropdownItem: (period: T) => void; +} + +interface TimeTableModalLectureTypeDropdownProps { + selectedLectureType: LectureKey[]; + handleLectureTypeDropdownItem: (lecture: LectureKey) => void; +} + +interface TimeTableModalDropdownButtonProps + extends HTMLAttributes { + onClick: (event: React.MouseEvent) => void; + value: string; } -interface TimeTableModalProps { +interface TimeTableLectureTableProps { + lectureList: unknown[]; +} + +interface TimeTableModalProps { dayStatus: DayStatus; day: DayKor; - period: DayPeriod | NightPeriod; + period: T; } +const LECTURE_TABLE_ROW_HEADER = [ + '과목코드', + '캠퍼스', + '카테고리', + '학점', + '학년', + '전공', + '수업명', + '담당교수', + '학기', + '시간', + '수업구분', + '초과여부', +] as const; + function TimeTableModalFilter({ title, list, @@ -52,12 +94,14 @@ function TimeTableModalFilter({ handleFilterItem, parseFunc, }: TimeTableModalFilterProps) { + const originSet = new Set(origin); + return ( {list.map((listVal) => ( handleFilterItem(listVal)} > {listVal} @@ -67,70 +111,201 @@ function TimeTableModalFilter({ ); } -function TimeTableModalPeriodDropdown({ +const TimeTableModalPeriodDropdown = memo(function TimeTableModalPeriodDropdown< + T extends DayPeriod | NightPeriod, +>({ dayStatus, selectedPeriod, - dropdownItemHandler, -}: TimeTableModalPeriodDropdownProps) { + handlePeriodDropdownItem, +}: TimeTableModalPeriodDropdownProps) { const periodList = dayStatus === 'day' ? DAY_PERIOD_ARRAY : NIGHT_PERIOD_ARRAY; const selectedValue = ( -
- {selectedPeriod.map((period) => ( - - ))} -
+ <> + {selectedPeriod.length ? ( +
+ {selectedPeriod.map((period) => ( + { + event.stopPropagation(); + handlePeriodDropdownItem(period); + }} + value={period} + /> + ))} +
+ ) : ( +

시간을 선택하세요

+ )} + ); return ( - + {periodList.map(([period, obj]) => ( dropdownItemHandler(period as DayPeriod | NightPeriod)} + key={`dropdown-${period}`} + selected={selectedPeriod.includes(period as T)} + onClick={() => handlePeriodDropdownItem(period as T)} >{`${period} (${obj.string})`} ))} + {SPECIAL_PERIOD.map((period) => ( + handlePeriodDropdownItem(period as T)} + > + {period} + + ))} ); -} +}); -export default function TimeTableModal({ - dayStatus, - day, - period, -}: TimeTableModalProps) { +const TimeTableModalLectureTypeDropdown = memo( + function TimeTableModalLectureTypeDropdown({ + selectedLectureType, + handleLectureTypeDropdownItem, + }: TimeTableModalLectureTypeDropdownProps) { + const selectedValue = ( + <> + {selectedLectureType.length ? ( +
+ {selectedLectureType.map((lectureType) => ( + { + event.stopPropagation(); + handleLectureTypeDropdownItem(lectureType); + }} + value={LECTURE[lectureType]} + /> + ))} +
+ ) : ( +

강의 구분을 선택하세요

+ )} + + ); + + return ( + + {LECTURE_ARRAY.map(([lectureKey, lectureValue]) => ( + handleLectureTypeDropdownItem(lectureKey)} + > + {lectureValue} + + ))} + + ); + }, +); + +const TimeTableModalDropdownButton = memo( + function TimeTableModalDropdownButton({ + onClick, + value, + }: TimeTableModalDropdownButtonProps) { + return ( + + ); + }, +); + +const TimeTableModalInput = memo(function TimeTableModalInput() { + return ( + + + + ); +}); + +const TimeTableLectureTable = memo(function TimeTableLectureTable({ + lectureList, +}: TimeTableLectureTableProps) { + return ( +
+ + + + {LECTURE_TABLE_ROW_HEADER.map((header) => ( + + ))} + + + + + + + +
+ {header} +
+ {lectureList.length ? ( +

검색 결과가 없습니다.

+ ) : ( +

검색 결과가 없습니다.

+ )} +
+
+ ); +}); + +export default function TimeTableModal< + T extends DayPeriod | NightPeriod | SpecialPeriod, +>({ dayStatus, day, period }: TimeTableModalProps) { const { close } = useModalAction({ key: MODAL_KEY.timeTable }); const visible = useModalState({ key: MODAL_KEY.timeTable }).visible; - const [selectedGrade, setSelectedGrade] = useState(''); - const [selectedDay, setSelectedDay] = useState(day); + const [selectedRegion, setSelectedRegion] = useState([ + REGION.campus1, + REGION.campus2, + ]); + const [selectedGrade, setSelectedGrade] = useState([]); + const [selectedDay, setSelectedDay] = useState([day]); const [selectedPeriod, setSelectedPeriod] = useState< DayPeriod[] | NightPeriod[] >([period]); + const [selectedLectureType, setSelectedLectureType] = useState( + [], + ); useEffect(() => { - setSelectedGrade(''); - setSelectedDay(day as DayKor); + setSelectedGrade([]); + setSelectedRegion([REGION.campus1, REGION.campus2]); + setSelectedDay([day]); setSelectedPeriod([period]); + setSelectedLectureType([]); }, [day, period]); const handleFilterItem = useCallback( - (targetValue: T, targetAction: Dispatch>) => { - targetAction(targetValue); + ( + targetValue: T, + targetList: T[], + targetAction: Dispatch>, + ) => { + const targetSet = new Set(targetList); + + if (targetSet.has(targetValue)) { + targetSet.delete(targetValue); + } else { + targetSet.add(targetValue); + } + + targetAction(() => Array.from(targetSet)); }, [], ); @@ -157,26 +332,47 @@ export default function TimeTableModal({ {visible && ( - + str)} + origin={selectedRegion} + handleFilterItem={(region: Region) => + handleFilterItem(region, selectedRegion, setSelectedRegion) + } + /> - handleFilterItem(grade, setSelectedGrade) + handleFilterItem={(grade: Grade) => + handleFilterItem(grade, selectedGrade, setSelectedGrade) } /> handleFilterItem(day, setSelectedDay)} + handleFilterItem={(day) => + handleFilterItem(day, selectedDay, setSelectedDay) + } /> + + handleFilterItem( + lecture, + selectedLectureType, + setSelectedLectureType, + ) + } /> + + )}