From cc73f245b07dad697dae9c2842592305bbef2fdf Mon Sep 17 00:00:00 2001 From: q1Lim Date: Thu, 11 Sep 2025 01:59:48 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20Promise.all=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95=20-=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20await=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=A7=81?= =?UTF-8?q?=EB=A0=AC=ED=99=94=20=EB=B6=80=EB=B6=84=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?-=20=EC=BA=90=EC=8B=9C=20=ED=99=9C=EC=9A=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SearchDialog.tsx | 47 +++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 593951fc..5bd1024e 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -85,15 +85,40 @@ const PAGE_SIZE = 100; const fetchMajors = () => axios.get('/schedules-majors.json'); const fetchLiberalArts = () => axios.get('/schedules-liberal-arts.json'); -// TODO: 이 코드를 개선해서 API 호출을 최소화 해보세요 + Promise.all이 현재 잘못 사용되고 있습니다. 같이 개선해주세요. -const fetchAllLectures = async () => await Promise.all([ - (console.log('API Call 1', performance.now()), await fetchMajors()), - (console.log('API Call 2', performance.now()), await fetchLiberalArts()), - (console.log('API Call 3', performance.now()), await fetchMajors()), - (console.log('API Call 4', performance.now()), await fetchLiberalArts()), - (console.log('API Call 5', performance.now()), await fetchMajors()), - (console.log('API Call 6', performance.now()), await fetchLiberalArts()), -]); +let majorsCache: Promise | null = null; +let liberalArtsCache: Promise | null = null; + +const fetchAllLectures = async () => { + console.log("[fetchAllLectures] 호출 시작:"); + + if(!majorsCache) { + console.log("[fetchAllLectures] majors API 요청 실행"); + majorsCache = fetchMajors() + .then(response => response.data) + .catch(error => { + majorsCache = null; + throw error; + }) + } else { + console.log("[fetchAllLectures] majors 캐시 사용") + } + + if(!liberalArtsCache) { + console.log("[fetchAllLectures] liberalArts API 요청 실행"); + liberalArtsCache = fetchLiberalArts() + .then(response => response.data) + .catch(error => { + liberalArtsCache = null; + throw error; + }) + } else { + console.log("[fetchAllLectures] liberalArts 캐시 사용") + } + + const [majors, liberalArts] = await Promise.all([majorsCache, liberalArtsCache]); + + return majors.concat(liberalArts); +} // TODO: 이 컴포넌트에서 불필요한 연산이 발생하지 않도록 다양한 방식으로 시도해주세요. const SearchDialog = ({ searchInfo, onClose }: Props) => { @@ -169,11 +194,11 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { useEffect(() => { const start = performance.now(); console.log('API 호출 시작: ', start) - fetchAllLectures().then(results => { + fetchAllLectures().then(lectures => { const end = performance.now(); console.log('모든 API 호출 완료 ', end) console.log('API 호출에 걸린 시간(ms): ', end - start) - setLectures(results.flatMap(result => result.data)); + setLectures(lectures); }) }, []); From df43959c9672313f5597436bfb072243eab9a95c Mon Sep 17 00:00:00 2001 From: q1Lim Date: Thu, 11 Sep 2025 17:01:32 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20useAutoCallback=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useAutoCallback.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/hooks/useAutoCallback.ts diff --git a/src/hooks/useAutoCallback.ts b/src/hooks/useAutoCallback.ts new file mode 100644 index 00000000..64ee2646 --- /dev/null +++ b/src/hooks/useAutoCallback.ts @@ -0,0 +1,14 @@ +import { useCallback, useRef } from "react"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyFunction = (...args: any[]) => any; + +export function useAutoCallback(fn: T): T { + const ref = useRef(fn); + ref.current = fn; + + const autoCallback = useCallback((...args: Parameters) => { + return ref.current(...args); + }, []); + return autoCallback as T; +} \ No newline at end of file From f49b1996cda1a2e9ea96a577daac459971c97a0e Mon Sep 17 00:00:00 2001 From: q1Lim Date: Thu, 11 Sep 2025 18:22:31 +0900 Subject: [PATCH 03/16] =?UTF-8?q?fix:=20Promise.all=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B8=B0=EC=A1=B4=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=ED=95=98=EB=8A=94=20=EA=B2=83=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20=EB=B6=80=EB=B6=84=EB=A7=8C=20=EC=A0=81=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SearchDialog.tsx | 48 +++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 5bd1024e..31f23f1d 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -32,7 +32,7 @@ import { import { useScheduleContext } from "./ScheduleContext.tsx"; import { Lecture } from "./types.ts"; import { parseSchedule } from "./utils.ts"; -import axios from "axios"; +import axios, {AxiosResponse} from "axios"; import { DAY_LABELS } from "./constants.ts"; interface Props { @@ -85,40 +85,28 @@ const PAGE_SIZE = 100; const fetchMajors = () => axios.get('/schedules-majors.json'); const fetchLiberalArts = () => axios.get('/schedules-liberal-arts.json'); -let majorsCache: Promise | null = null; -let liberalArtsCache: Promise | null = null; +let majorsCache: Promise> | null = null; +let liberalArtsCache: Promise> | null = null; const fetchAllLectures = async () => { - console.log("[fetchAllLectures] 호출 시작:"); - - if(!majorsCache) { - console.log("[fetchAllLectures] majors API 요청 실행"); - majorsCache = fetchMajors() - .then(response => response.data) - .catch(error => { - majorsCache = null; - throw error; - }) - } else { - console.log("[fetchAllLectures] majors 캐시 사용") + if (!majorsCache) { + majorsCache = fetchMajors().catch(error => { majorsCache = null; throw error; }); } - - if(!liberalArtsCache) { - console.log("[fetchAllLectures] liberalArts API 요청 실행"); - liberalArtsCache = fetchLiberalArts() - .then(response => response.data) - .catch(error => { - liberalArtsCache = null; - throw error; - }) - } else { - console.log("[fetchAllLectures] liberalArts 캐시 사용") + if (!liberalArtsCache) { + liberalArtsCache = fetchLiberalArts().catch(error => { liberalArtsCache = null; throw error; }); } const [majors, liberalArts] = await Promise.all([majorsCache, liberalArtsCache]); - return majors.concat(liberalArts); -} + return Promise.all([ + (console.log("API Call 1", performance.now()), majors), + (console.log("API Call 2", performance.now()), liberalArts), + (console.log("API Call 3", performance.now()), majors), + (console.log("API Call 4", performance.now()), liberalArts), + (console.log("API Call 5", performance.now()), majors), + (console.log("API Call 6", performance.now()), liberalArts), + ]); +}; // TODO: 이 컴포넌트에서 불필요한 연산이 발생하지 않도록 다양한 방식으로 시도해주세요. const SearchDialog = ({ searchInfo, onClose }: Props) => { @@ -194,11 +182,11 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { useEffect(() => { const start = performance.now(); console.log('API 호출 시작: ', start) - fetchAllLectures().then(lectures => { + fetchAllLectures().then(results => { const end = performance.now(); console.log('모든 API 호출 완료 ', end) console.log('API 호출에 걸린 시간(ms): ', end - start) - setLectures(lectures); + setLectures(results.flatMap(r => r.data)); }) }, []); From b6fe6eea2252fe07f77a5d464504487777df76de Mon Sep 17 00:00:00 2001 From: q1Lim Date: Thu, 11 Sep 2025 18:31:58 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor:=20SearchItem=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SearchDialog.tsx | 20 +++++--------------- src/SearchItem.tsx | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 src/SearchItem.tsx diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 31f23f1d..b9d0302f 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -1,7 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { Box, - Button, Checkbox, CheckboxGroup, FormControl, @@ -21,7 +20,6 @@ import { TagCloseButton, TagLabel, Tbody, - Td, Text, Th, Thead, @@ -34,6 +32,8 @@ import { Lecture } from "./types.ts"; import { parseSchedule } from "./utils.ts"; import axios, {AxiosResponse} from "axios"; import { DAY_LABELS } from "./constants.ts"; +import SearchItem from "./SearchItem.tsx"; +import { useAutoCallback } from "./hooks/useAutoCallback.ts"; interface Props { searchInfo: { @@ -161,7 +161,7 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { loaderWrapperRef.current?.scrollTo(0, 0); }; - const addSchedule = (lecture: Lecture) => { + const addSchedule = useAutoCallback((lecture: Lecture) => { if (!searchInfo) return; const { tableId } = searchInfo; @@ -177,7 +177,7 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { })); onClose(); - }; + }); useEffect(() => { const start = performance.now(); @@ -364,17 +364,7 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { {visibleLectures.map((lecture, index) => ( - - - - - - - + ))}
{lecture.id}{lecture.grade}{lecture.title}{lecture.credits} - - - -
diff --git a/src/SearchItem.tsx b/src/SearchItem.tsx new file mode 100644 index 00000000..6ec92761 --- /dev/null +++ b/src/SearchItem.tsx @@ -0,0 +1,25 @@ +import { Button, Td, Tr } from "@chakra-ui/react"; +import { memo } from "react"; +import { Lecture } from "./types.ts"; + +const SearchItem = memo(({ lecture, addSchedule }: { lecture: Lecture; addSchedule: (lecture: Lecture) => void }) => { + return ( + + {lecture.id} + {lecture.grade} + {lecture.title} + {lecture.credits} + + + + + + + ); +}); + +SearchItem.displayName = "SearchItem"; + +export default SearchItem; \ No newline at end of file From 31cc49d821c76efae6ac9aee8a39ecf701054993 Mon Sep 17 00:00:00 2001 From: q1Lim Date: Thu, 11 Sep 2025 20:37:23 +0900 Subject: [PATCH 05/16] =?UTF-8?q?feat:=20filteredLectures,=20lastPage,=20v?= =?UTF-8?q?isibleLectures,=20allMajors=EC=97=90=20useMemo=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20-=20changeSearchOption=20useAutoCallback=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SearchDialog.tsx | 81 +++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index b9d0302f..79236e67 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useMemo } from "react"; import { Box, Checkbox, @@ -124,42 +124,53 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { majors: [], }); - const getFilteredLectures = () => { - const { query = '', credits, grades, days, times, majors } = searchOptions; - return lectures - .filter(lecture => - lecture.title.toLowerCase().includes(query.toLowerCase()) || - lecture.id.toLowerCase().includes(query.toLowerCase()) - ) - .filter(lecture => grades.length === 0 || grades.includes(lecture.grade)) - .filter(lecture => majors.length === 0 || majors.includes(lecture.major)) - .filter(lecture => !credits || lecture.credits.startsWith(String(credits))) - .filter(lecture => { - if (days.length === 0) { - return true; - } - const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : []; - return schedules.some(s => days.includes(s.day)); - }) - .filter(lecture => { - if (times.length === 0) { - return true; - } - const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : []; - return schedules.some(s => s.range.some(time => times.includes(time))); - }); - } - - const filteredLectures = getFilteredLectures(); - const lastPage = Math.ceil(filteredLectures.length / PAGE_SIZE); - const visibleLectures = filteredLectures.slice(0, page * PAGE_SIZE); - const allMajors = [...new Set(lectures.map(lecture => lecture.major))]; + const filteredLectures = useMemo(() => { + const {query = '', credits, grades, days, times, majors} = searchOptions; + return lectures + .filter(lecture => + lecture.title.toLowerCase().includes(query.toLowerCase()) || + lecture.id.toLowerCase().includes(query.toLowerCase()) + ) + .filter(lecture => grades.length === 0 || grades.includes(lecture.grade)) + .filter(lecture => majors.length === 0 || majors.includes(lecture.major)) + .filter(lecture => !credits || lecture.credits.startsWith(String(credits))) + .filter(lecture => { + if (days.length === 0) { + return true; + } + const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : []; + return schedules.some(s => days.includes(s.day)); + }) + .filter(lecture => { + if (times.length === 0) { + return true; + } + const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : []; + return schedules.some(s => s.range.some(time => times.includes(time))); + }); + }, [lectures, searchOptions]); + + const lastPage = useMemo( + () => Math.max(1, Math.ceil(filteredLectures.length / PAGE_SIZE)), + [filteredLectures.length] + ); + const visibleLectures = useMemo( + () => filteredLectures.slice(0, page * PAGE_SIZE), + [filteredLectures, page] + ); + const allMajors = useMemo( + () => [...new Set(lectures.map((l) => l.major))], + [lectures] + ); - const changeSearchOption = (field: keyof SearchOption, value: SearchOption[typeof field]) => { + const changeSearchOption = useAutoCallback((field: keyof SearchOption, value: SearchOption[typeof field]) => { setPage(1); - setSearchOptions(({ ...searchOptions, [field]: value })); + setSearchOptions(prev => { + if (prev[field] === value) return prev; + return { ...prev, [field]: value }; + }); loaderWrapperRef.current?.scrollTo(0, 0); - }; + }); const addSchedule = useAutoCallback((lecture: Lecture) => { if (!searchInfo) return; @@ -292,7 +303,7 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { onChange={(values) => changeSearchOption('times', values.map(Number))} > - {searchOptions.times.sort((a, b) => a - b).map(time => ( + {[...searchOptions.times].sort((a, b) => a - b).map(time => ( {time}교시 Date: Thu, 11 Sep 2025 23:23:39 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=EA=B0=95=EC=9D=98=20Filter=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC=20-?= =?UTF-8?q?=20Credit=20-=20Day=20-=20Grade=20-=20Major=20-=20PeriodTime=20?= =?UTF-8?q?-=20Query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CreditFilter.tsx | 28 +++++++ src/DayFilter.tsx | 29 +++++++ src/GradeFilter.tsx | 29 +++++++ src/MajorFilter.tsx | 57 +++++++++++++ src/PeriodTimeFilter.tsx | 56 +++++++++++++ src/QueryFilter.tsx | 23 ++++++ src/SearchDialog.tsx | 172 ++++----------------------------------- src/SearchTableHead.tsx | 20 +++++ src/constants.ts | 27 ++++++ src/types.ts | 9 ++ 10 files changed, 292 insertions(+), 158 deletions(-) create mode 100644 src/CreditFilter.tsx create mode 100644 src/DayFilter.tsx create mode 100644 src/GradeFilter.tsx create mode 100644 src/MajorFilter.tsx create mode 100644 src/PeriodTimeFilter.tsx create mode 100644 src/QueryFilter.tsx create mode 100644 src/SearchTableHead.tsx diff --git a/src/CreditFilter.tsx b/src/CreditFilter.tsx new file mode 100644 index 00000000..e57b8e43 --- /dev/null +++ b/src/CreditFilter.tsx @@ -0,0 +1,28 @@ +import { FormControl, FormLabel, Select} from "@chakra-ui/react"; +import { SearchOption } from "./types.ts"; +import { memo } from "react"; + + +type CreditFilterProps = { + credits: SearchOption['credits']; + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; +} + +const CreditFilter = ({ credits, changeSearchOption }: CreditFilterProps) => { + return ( + + 학점 + + + ) +} + +export default memo(CreditFilter); \ No newline at end of file diff --git a/src/DayFilter.tsx b/src/DayFilter.tsx new file mode 100644 index 00000000..0f58b39d --- /dev/null +++ b/src/DayFilter.tsx @@ -0,0 +1,29 @@ +import { Checkbox, CheckboxGroup, FormControl, FormLabel, HStack } from "@chakra-ui/react"; +import { DAY_LABELS } from "./constants.ts"; +import { SearchOption } from "./types.ts"; +import { memo } from "react"; + +type DayFilterProps = { + days: SearchOption['days']; + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; +} + +const DayFilter = ({ days, changeSearchOption }: DayFilterProps) => { + return ( + + 요일 + changeSearchOption('days', value as string[])} + > + + {DAY_LABELS.map(day => ( + {day} + ))} + + + + ) +} + +export default memo(DayFilter); \ No newline at end of file diff --git a/src/GradeFilter.tsx b/src/GradeFilter.tsx new file mode 100644 index 00000000..84242b61 --- /dev/null +++ b/src/GradeFilter.tsx @@ -0,0 +1,29 @@ +import {Checkbox, CheckboxGroup, FormControl, FormLabel, HStack} from "@chakra-ui/react"; +import {SearchOption} from "./types.ts"; +import {memo} from "react"; + + +type GradeFilterProps = { + grades: SearchOption['grades']; + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; +} + +const GradeFilter = ({ grades, changeSearchOption }: GradeFilterProps) => { + return ( + + 학년 + changeSearchOption('grades', value.map(Number))} + > + + {[1, 2, 3, 4].map(grade => ( + {grade}학년 + ))} + + + + ) +} + +export default memo(GradeFilter); \ No newline at end of file diff --git a/src/MajorFilter.tsx b/src/MajorFilter.tsx new file mode 100644 index 00000000..98db22e7 --- /dev/null +++ b/src/MajorFilter.tsx @@ -0,0 +1,57 @@ +import { + Box, + Checkbox, + CheckboxGroup, + FormControl, + FormLabel, + Stack, + Tag, + TagCloseButton, + TagLabel, + Wrap +} from "@chakra-ui/react"; +import { memo } from "react"; +import { SearchOption } from "./types.ts"; + +type MajorFilterProps = { + majors: SearchOption['majors']; + allMajors: string[]; + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; +} + +const MajorFilter = ({ majors, allMajors, changeSearchOption }: MajorFilterProps) => { + return ( + + 전공 + changeSearchOption('majors', values as string[])} + > + + {majors.map(major => ( + + {major.split("

").pop()} + changeSearchOption('majors', majors.filter(v => v !== major))}/> + + ))} + + + {allMajors.map(major => ( + + + {major.replace(/

/gi, ' ')} + + + ))} + + + + ) +} + +MajorFilter.displayName = "MajorFilter"; + +export default memo(MajorFilter); diff --git a/src/PeriodTimeFilter.tsx b/src/PeriodTimeFilter.tsx new file mode 100644 index 00000000..8cc26eda --- /dev/null +++ b/src/PeriodTimeFilter.tsx @@ -0,0 +1,56 @@ +import { + Box, + Checkbox, + CheckboxGroup, + FormControl, + FormLabel, + Stack, + Tag, + TagCloseButton, + TagLabel, + Wrap +} from "@chakra-ui/react"; +import { SearchOption } from "./types.ts"; +import { memo } from "react"; +import { TIME_SLOTS } from "./constants.ts"; + + +type PeriodTimeFilterProps = { + times: SearchOption['times']; + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; +} + +const PeriodTimeFilter = ({ times, changeSearchOption }: PeriodTimeFilterProps) => { + return ( + + 시간 + changeSearchOption('times', values.map(Number))} + > + + {[...times].sort((a, b) => a - b).map(time => ( + + {time}교시 + changeSearchOption('times', times.filter(v => v !== time))}/> + + ))} + + + {TIME_SLOTS.map(({ id, label }) => ( + + + {id}교시({label}) + + + ))} + + + + ) +} + +export default memo(PeriodTimeFilter); \ No newline at end of file diff --git a/src/QueryFilter.tsx b/src/QueryFilter.tsx new file mode 100644 index 00000000..e081d810 --- /dev/null +++ b/src/QueryFilter.tsx @@ -0,0 +1,23 @@ +import { FormControl, FormLabel, Input } from "@chakra-ui/react"; +import { SearchOption } from "./types.ts"; +import { memo } from "react"; + +type QueryFilterProps = { + query: SearchOption['query']; + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; +} + +const QueryFilter = ({ query, changeSearchOption }: QueryFilterProps) => { + return ( + + 검색어 + changeSearchOption('query', e.target.value)} + /> + + ) +} + +export default memo(QueryFilter); \ No newline at end of file diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 79236e67..0c8edc50 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -1,39 +1,31 @@ import { useEffect, useRef, useState, useMemo } from "react"; import { Box, - Checkbox, - CheckboxGroup, - FormControl, - FormLabel, HStack, - Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, - Select, - Stack, Table, - Tag, - TagCloseButton, - TagLabel, Tbody, Text, - Th, - Thead, - Tr, VStack, - Wrap, } from "@chakra-ui/react"; import { useScheduleContext } from "./ScheduleContext.tsx"; import { Lecture } from "./types.ts"; import { parseSchedule } from "./utils.ts"; import axios, {AxiosResponse} from "axios"; -import { DAY_LABELS } from "./constants.ts"; import SearchItem from "./SearchItem.tsx"; import { useAutoCallback } from "./hooks/useAutoCallback.ts"; +import SearchTableHead from "./SearchTableHead.tsx"; +import MajorFilter from "./MajorFilter.tsx"; +import DayFilter from "./DayFilter.tsx"; +import GradeFilter from "./GradeFilter.tsx"; +import PeriodTimeFilter from "./PeriodTimeFilter.tsx"; +import CreditFilter from "./CreditFilter.tsx"; +import QueryFilter from "./QueryFilter.tsx"; interface Props { searchInfo: { @@ -53,33 +45,6 @@ interface SearchOption { credits?: number, } -const TIME_SLOTS = [ - { id: 1, label: "09:00~09:30" }, - { id: 2, label: "09:30~10:00" }, - { id: 3, label: "10:00~10:30" }, - { id: 4, label: "10:30~11:00" }, - { id: 5, label: "11:00~11:30" }, - { id: 6, label: "11:30~12:00" }, - { id: 7, label: "12:00~12:30" }, - { id: 8, label: "12:30~13:00" }, - { id: 9, label: "13:00~13:30" }, - { id: 10, label: "13:30~14:00" }, - { id: 11, label: "14:00~14:30" }, - { id: 12, label: "14:30~15:00" }, - { id: 13, label: "15:00~15:30" }, - { id: 14, label: "15:30~16:00" }, - { id: 15, label: "16:00~16:30" }, - { id: 16, label: "16:30~17:00" }, - { id: 17, label: "17:00~17:30" }, - { id: 18, label: "17:30~18:00" }, - { id: 19, label: "18:00~18:50" }, - { id: 20, label: "18:55~19:45" }, - { id: 21, label: "19:50~20:40" }, - { id: 22, label: "20:45~21:35" }, - { id: 23, label: "21:40~22:30" }, - { id: 24, label: "22:35~23:25" }, -]; - const PAGE_SIZE = 100; const fetchMajors = () => axios.get('/schedules-majors.json'); @@ -241,134 +206,25 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { - - 검색어 - changeSearchOption('query', e.target.value)} - /> - - - - 학점 - - + + - - 학년 - changeSearchOption('grades', value.map(Number))} - > - - {[1, 2, 3, 4].map(grade => ( - {grade}학년 - ))} - - - - - - 요일 - changeSearchOption('days', value as string[])} - > - - {DAY_LABELS.map(day => ( - {day} - ))} - - - + + - - 시간 - changeSearchOption('times', values.map(Number))} - > - - {[...searchOptions.times].sort((a, b) => a - b).map(time => ( - - {time}교시 - changeSearchOption('times', searchOptions.times.filter(v => v !== time))}/> - - ))} - - - {TIME_SLOTS.map(({ id, label }) => ( - - - {id}교시({label}) - - - ))} - - - - - - 전공 - changeSearchOption('majors', values as string[])} - > - - {searchOptions.majors.map(major => ( - - {major.split("

").pop()} - changeSearchOption('majors', searchOptions.majors.filter(v => v !== major))}/> - - ))} - - - {allMajors.map(major => ( - - - {major.replace(/

/gi, ' ')} - - - ))} - - - + + 검색결과: {filteredLectures.length}개 - - - - - - - - - - - +
과목코드학년과목명학점전공시간
diff --git a/src/SearchTableHead.tsx b/src/SearchTableHead.tsx new file mode 100644 index 00000000..9e616a45 --- /dev/null +++ b/src/SearchTableHead.tsx @@ -0,0 +1,20 @@ +import { Thead, Tr, Th } from "@chakra-ui/react"; +import { memo } from "react"; +import SearchItem from "./SearchItem.tsx"; + +const SearchTableHead = memo(() => ( + + + 과목코드 + 학년 + 과목명 + 학점 + 전공 + 시간 + + + +)); + +SearchItem.displayName = "SearchTableHead"; +export default SearchTableHead; \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index c7c2e46f..113d3c17 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,3 +7,30 @@ export const CellSize = { export const 초 = 1000; export const 분 = 60 * 초; + +export const TIME_SLOTS = [ + { id: 1, label: "09:00~09:30" }, + { id: 2, label: "09:30~10:00" }, + { id: 3, label: "10:00~10:30" }, + { id: 4, label: "10:30~11:00" }, + { id: 5, label: "11:00~11:30" }, + { id: 6, label: "11:30~12:00" }, + { id: 7, label: "12:00~12:30" }, + { id: 8, label: "12:30~13:00" }, + { id: 9, label: "13:00~13:30" }, + { id: 10, label: "13:30~14:00" }, + { id: 11, label: "14:00~14:30" }, + { id: 12, label: "14:30~15:00" }, + { id: 13, label: "15:00~15:30" }, + { id: 14, label: "15:30~16:00" }, + { id: 15, label: "16:00~16:30" }, + { id: 16, label: "16:30~17:00" }, + { id: 17, label: "17:00~17:30" }, + { id: 18, label: "17:30~18:00" }, + { id: 19, label: "18:00~18:50" }, + { id: 20, label: "18:55~19:45" }, + { id: 21, label: "19:50~20:40" }, + { id: 22, label: "20:45~21:35" }, + { id: 23, label: "21:40~22:30" }, + { id: 24, label: "22:35~23:25" }, +]; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 16118bf9..11343db7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,3 +13,12 @@ export interface Schedule { range: number[] room?: string; } + +export interface SearchOption { + query?: string; + grades: number[]; + days: string[]; + times: number[]; + majors: string[]; + credits?: number; +} \ No newline at end of file From 1cdb0a587b734e4c9e504c756c24e4eaa05f01f6 Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 00:09:35 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20useInfiniteScroll=20hook=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SearchDialog.tsx | 31 +++++++++-------------------- src/hooks/useInfiniteScroll.ts | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 src/hooks/useInfiniteScroll.ts diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 0c8edc50..6b0bd34a 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useMemo } from "react"; +import {useEffect, useRef, useState, useMemo, useCallback} from "react"; import { Box, HStack, @@ -26,6 +26,7 @@ import GradeFilter from "./GradeFilter.tsx"; import PeriodTimeFilter from "./PeriodTimeFilter.tsx"; import CreditFilter from "./CreditFilter.tsx"; import QueryFilter from "./QueryFilter.tsx"; +import { useInfiniteScroll } from "./hooks/useInfiniteScroll.ts"; interface Props { searchInfo: { @@ -166,27 +167,13 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { }) }, []); - useEffect(() => { - const $loader = loaderRef.current; - const $loaderWrapper = loaderWrapperRef.current; - - if (!$loader || !$loaderWrapper) { - return; - } - - const observer = new IntersectionObserver( - entries => { - if (entries[0].isIntersecting) { - setPage(prevPage => Math.min(lastPage, prevPage + 1)); - } - }, - { threshold: 0, root: $loaderWrapper } - ); - - observer.observe($loader); - - return () => observer.unobserve($loader); - }, [lastPage]); + useInfiniteScroll({ + onIntersect: useCallback(() => { + setPage((prev) => Math.min(lastPage, prev + 1)); + }, [lastPage]), + loaderRef: loaderRef, + loaderWrapperRef: loaderWrapperRef, + }); useEffect(() => { setSearchOptions(prev => ({ diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts new file mode 100644 index 00000000..dd3f2be1 --- /dev/null +++ b/src/hooks/useInfiniteScroll.ts @@ -0,0 +1,36 @@ +import React, { useEffect } from "react"; + +type useInfiniteScrollProps = { + onIntersect: () => void; + loaderRef: React.RefObject; + loaderWrapperRef: React.RefObject; + options?: IntersectionObserverInit; +}; + +export function useInfiniteScroll({ + onIntersect, + loaderRef, + loaderWrapperRef, + options = {}, + }: useInfiniteScrollProps) { + useEffect(() => { + const $loader = loaderRef.current; + const $loaderWrapper = loaderWrapperRef.current; + + if (!$loader || !$loaderWrapper) { + return; + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) { + onIntersect() + } + }, + { threshold: options?.threshold, root: $loaderWrapper } + ); + + observer.observe($loader); + return () => observer.unobserve($loader); + }, [onIntersect, loaderRef, loaderWrapperRef, options]); +} \ No newline at end of file From 88eb6754574bf75505734630285c5627d80046c9 Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 00:46:04 +0900 Subject: [PATCH 08/16] =?UTF-8?q?chore:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EC=86=8C=EC=86=8C=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CreditFilter.tsx | 2 ++ src/DayFilter.tsx | 2 ++ src/GradeFilter.tsx | 2 ++ src/PeriodTimeFilter.tsx | 2 ++ src/QueryFilter.tsx | 2 ++ src/SearchDialog.tsx | 12 +++--------- src/hooks/useInfiniteScroll.ts | 5 ++++- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/CreditFilter.tsx b/src/CreditFilter.tsx index e57b8e43..33a8b684 100644 --- a/src/CreditFilter.tsx +++ b/src/CreditFilter.tsx @@ -25,4 +25,6 @@ const CreditFilter = ({ credits, changeSearchOption }: CreditFilterProps) => { ) } +CreditFilter.displayName = "CreditFilter"; + export default memo(CreditFilter); \ No newline at end of file diff --git a/src/DayFilter.tsx b/src/DayFilter.tsx index 0f58b39d..067bb918 100644 --- a/src/DayFilter.tsx +++ b/src/DayFilter.tsx @@ -26,4 +26,6 @@ const DayFilter = ({ days, changeSearchOption }: DayFilterProps) => { ) } +DayFilter.displayName="DayFilter"; + export default memo(DayFilter); \ No newline at end of file diff --git a/src/GradeFilter.tsx b/src/GradeFilter.tsx index 84242b61..d0c3bac0 100644 --- a/src/GradeFilter.tsx +++ b/src/GradeFilter.tsx @@ -26,4 +26,6 @@ const GradeFilter = ({ grades, changeSearchOption }: GradeFilterProps) => { ) } +GradeFilter.displayName="GradeFilter"; + export default memo(GradeFilter); \ No newline at end of file diff --git a/src/PeriodTimeFilter.tsx b/src/PeriodTimeFilter.tsx index 8cc26eda..5f9d217a 100644 --- a/src/PeriodTimeFilter.tsx +++ b/src/PeriodTimeFilter.tsx @@ -53,4 +53,6 @@ const PeriodTimeFilter = ({ times, changeSearchOption }: PeriodTimeFilterProps) ) } +PeriodTimeFilter.displayName="PeriodTimeFilter"; + export default memo(PeriodTimeFilter); \ No newline at end of file diff --git a/src/QueryFilter.tsx b/src/QueryFilter.tsx index e081d810..c89810eb 100644 --- a/src/QueryFilter.tsx +++ b/src/QueryFilter.tsx @@ -20,4 +20,6 @@ const QueryFilter = ({ query, changeSearchOption }: QueryFilterProps) => { ) } +QueryFilter.displayName="QueryFilter"; + export default memo(QueryFilter); \ No newline at end of file diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 6b0bd34a..60e6c654 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -27,6 +27,7 @@ import PeriodTimeFilter from "./PeriodTimeFilter.tsx"; import CreditFilter from "./CreditFilter.tsx"; import QueryFilter from "./QueryFilter.tsx"; import { useInfiniteScroll } from "./hooks/useInfiniteScroll.ts"; +import { SearchOption } from "./types.ts"; interface Props { searchInfo: { @@ -37,15 +38,6 @@ interface Props { onClose: () => void; } -interface SearchOption { - query?: string, - grades: number[], - days: string[], - times: number[], - majors: string[], - credits?: number, -} - const PAGE_SIZE = 100; const fetchMajors = () => axios.get('/schedules-majors.json'); @@ -173,6 +165,8 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { }, [lastPage]), loaderRef: loaderRef, loaderWrapperRef: loaderWrapperRef, + options: { threshold: 0 }, + enabled: page < lastPage, }); useEffect(() => { diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts index dd3f2be1..98a521c5 100644 --- a/src/hooks/useInfiniteScroll.ts +++ b/src/hooks/useInfiniteScroll.ts @@ -5,6 +5,7 @@ type useInfiniteScrollProps = { loaderRef: React.RefObject; loaderWrapperRef: React.RefObject; options?: IntersectionObserverInit; + enabled?: boolean; }; export function useInfiniteScroll({ @@ -12,8 +13,10 @@ export function useInfiniteScroll({ loaderRef, loaderWrapperRef, options = {}, + enabled = true, }: useInfiniteScrollProps) { useEffect(() => { + if (!enabled) return; const $loader = loaderRef.current; const $loaderWrapper = loaderWrapperRef.current; @@ -32,5 +35,5 @@ export function useInfiniteScroll({ observer.observe($loader); return () => observer.unobserve($loader); - }, [onIntersect, loaderRef, loaderWrapperRef, options]); + }, [ enabled, onIntersect, loaderRef, loaderWrapperRef, options ]); } \ No newline at end of file From 3cf878eb8336040a51e94692a7015cdc42347c5d Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 01:15:43 +0900 Subject: [PATCH 09/16] =?UTF-8?q?chore:=20=EC=98=A4=ED=83=88=EC=9E=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SearchDialog.tsx | 5 +++-- src/SearchItem.tsx | 2 +- src/SearchTableHead.tsx | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 60e6c654..1b3d4886 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -154,8 +154,9 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { fetchAllLectures().then(results => { const end = performance.now(); console.log('모든 API 호출 완료 ', end) - console.log('API 호출에 걸린 시간(ms): ', end - start) - setLectures(results.flatMap(r => r.data)); + console.log('API 호출에 걸린 시간(ms): ', end - start); + const [majors, liberalArts] = results; + setLectures([...majors.data, ...liberalArts.data]); }) }, []); diff --git a/src/SearchItem.tsx b/src/SearchItem.tsx index 6ec92761..abad1368 100644 --- a/src/SearchItem.tsx +++ b/src/SearchItem.tsx @@ -4,7 +4,7 @@ import { Lecture } from "./types.ts"; const SearchItem = memo(({ lecture, addSchedule }: { lecture: Lecture; addSchedule: (lecture: Lecture) => void }) => { return ( - + {lecture.id} {lecture.grade} {lecture.title} diff --git a/src/SearchTableHead.tsx b/src/SearchTableHead.tsx index 9e616a45..704c2a92 100644 --- a/src/SearchTableHead.tsx +++ b/src/SearchTableHead.tsx @@ -1,6 +1,5 @@ import { Thead, Tr, Th } from "@chakra-ui/react"; import { memo } from "react"; -import SearchItem from "./SearchItem.tsx"; const SearchTableHead = memo(() => ( @@ -16,5 +15,5 @@ const SearchTableHead = memo(() => ( )); -SearchItem.displayName = "SearchTableHead"; +SearchTableHead.displayName = "SearchTableHead"; export default SearchTableHead; \ No newline at end of file From 26bcb6ad3c11588fb7c46f1868dee309cd2a6999 Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 01:48:49 +0900 Subject: [PATCH 10/16] =?UTF-8?q?fix:=20=EC=B5=9C=EC=B4=88=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EC=8B=9C=20=EB=AC=B4=ED=95=9C=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=EC=9D=B4=20=EB=8F=99=EC=9E=91=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20useInfiniteScroll=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SearchDialog.tsx | 38 +++++++++++++++++++++++---------- src/hooks/useInfiniteScroll.ts | 39 ---------------------------------- 2 files changed, 27 insertions(+), 50 deletions(-) delete mode 100644 src/hooks/useInfiniteScroll.ts diff --git a/src/SearchDialog.tsx b/src/SearchDialog.tsx index 1b3d4886..4d5c7a3b 100644 --- a/src/SearchDialog.tsx +++ b/src/SearchDialog.tsx @@ -26,7 +26,6 @@ import GradeFilter from "./GradeFilter.tsx"; import PeriodTimeFilter from "./PeriodTimeFilter.tsx"; import CreditFilter from "./CreditFilter.tsx"; import QueryFilter from "./QueryFilter.tsx"; -import { useInfiniteScroll } from "./hooks/useInfiniteScroll.ts"; import { SearchOption } from "./types.ts"; interface Props { @@ -160,15 +159,32 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { }) }, []); - useInfiniteScroll({ - onIntersect: useCallback(() => { - setPage((prev) => Math.min(lastPage, prev + 1)); - }, [lastPage]), - loaderRef: loaderRef, - loaderWrapperRef: loaderWrapperRef, - options: { threshold: 0 }, - enabled: page < lastPage, - }); + // TODO 우선 다른 분 코드를 참고해서 수정한 것이니 꼭 제발 꼭 다시 공부하기 + const observerRef = useRef(null); + + const setLoaderWrapperRef = useCallback( + (node: HTMLDivElement | null) => { + if (!node) return; // unmount 시 null 들어옴 + + const $loader = loaderRef.current; + if (!$loader) return; + + observerRef.current?.unobserve($loader); + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setPage((prev) => Math.min(lastPage, prev + 1)); + } + }, + { root: node } + ); + + observer.observe($loader); + observerRef.current = observer; + }, + [lastPage] + ); useEffect(() => { setSearchOptions(prev => ({ @@ -209,7 +225,7 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { - + {visibleLectures.map((lecture, index) => ( diff --git a/src/hooks/useInfiniteScroll.ts b/src/hooks/useInfiniteScroll.ts deleted file mode 100644 index 98a521c5..00000000 --- a/src/hooks/useInfiniteScroll.ts +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useEffect } from "react"; - -type useInfiniteScrollProps = { - onIntersect: () => void; - loaderRef: React.RefObject; - loaderWrapperRef: React.RefObject; - options?: IntersectionObserverInit; - enabled?: boolean; -}; - -export function useInfiniteScroll({ - onIntersect, - loaderRef, - loaderWrapperRef, - options = {}, - enabled = true, - }: useInfiniteScrollProps) { - useEffect(() => { - if (!enabled) return; - const $loader = loaderRef.current; - const $loaderWrapper = loaderWrapperRef.current; - - if (!$loader || !$loaderWrapper) { - return; - } - - const observer = new IntersectionObserver( - (entries) => { - if (entries[0]?.isIntersecting) { - onIntersect() - } - }, - { threshold: options?.threshold, root: $loaderWrapper } - ); - - observer.observe($loader); - return () => observer.unobserve($loader); - }, [ enabled, onIntersect, loaderRef, loaderWrapperRef, options ]); -} \ No newline at end of file From 6e06f9208b9e6e3bbfbf93e8ddec17124b26e837 Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 02:29:24 +0900 Subject: [PATCH 11/16] =?UTF-8?q?chore:=20=ED=8F=B4=EB=8D=94=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 6 ++--- .../schedule}/ScheduleTable.tsx | 6 ++--- .../schedule}/ScheduleTables.tsx | 4 ++-- src/{ => component/search}/SearchDialog.tsx | 22 +++++++++---------- src/{ => component/search}/SearchItem.tsx | 2 +- .../search}/SearchTableHead.tsx | 0 .../search/filter}/CreditFilter.tsx | 2 +- .../search/filter}/DayFilter.tsx | 4 ++-- .../search/filter}/GradeFilter.tsx | 2 +- .../search/filter}/MajorFilter.tsx | 2 +- .../search/filter}/PeriodTimeFilter.tsx | 4 ++-- .../search/filter}/QueryFilter.tsx | 2 +- src/{ => provider}/ScheduleContext.tsx | 4 ++-- src/{ => provider}/ScheduleDndProvider.tsx | 2 +- 14 files changed, 31 insertions(+), 31 deletions(-) rename src/{ => component/schedule}/ScheduleTable.tsx (97%) rename src/{ => component/schedule}/ScheduleTables.tsx (94%) rename src/{ => component/search}/SearchDialog.tsx (93%) rename src/{ => component/search}/SearchItem.tsx (95%) rename src/{ => component/search}/SearchTableHead.tsx (100%) rename src/{ => component/search/filter}/CreditFilter.tsx (94%) rename src/{ => component/search/filter}/DayFilter.tsx (89%) rename src/{ => component/search/filter}/GradeFilter.tsx (95%) rename src/{ => component/search/filter}/MajorFilter.tsx (97%) rename src/{ => component/search/filter}/PeriodTimeFilter.tsx (94%) rename src/{ => component/search/filter}/QueryFilter.tsx (93%) rename src/{ => provider}/ScheduleContext.tsx (90%) rename src/{ => provider}/ScheduleDndProvider.tsx (97%) diff --git a/src/App.tsx b/src/App.tsx index 664bf6df..21f0e46f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { ChakraProvider } from "@chakra-ui/react"; -import { ScheduleProvider } from "./ScheduleContext.tsx"; -import { ScheduleTables } from "./ScheduleTables.tsx"; -import ScheduleDndProvider from "./ScheduleDndProvider.tsx"; +import { ScheduleProvider } from "./provider/ScheduleContext.tsx"; +import { ScheduleTables } from "./component/schedule/ScheduleTables.tsx"; +import ScheduleDndProvider from "./provider/ScheduleDndProvider.tsx"; function App() { diff --git a/src/ScheduleTable.tsx b/src/component/schedule/ScheduleTable.tsx similarity index 97% rename from src/ScheduleTable.tsx rename to src/component/schedule/ScheduleTable.tsx index ea17b6a9..a31bb424 100644 --- a/src/ScheduleTable.tsx +++ b/src/component/schedule/ScheduleTable.tsx @@ -12,9 +12,9 @@ import { PopoverTrigger, Text, } from "@chakra-ui/react"; -import { CellSize, DAY_LABELS, 분 } from "./constants.ts"; -import { Schedule } from "./types.ts"; -import { fill2, parseHnM } from "./utils.ts"; +import { CellSize, DAY_LABELS, 분 } from "../../constants.ts"; +import { Schedule } from "../../types.ts"; +import { fill2, parseHnM } from "../../utils.ts"; import { useDndContext, useDraggable } from "@dnd-kit/core"; import { CSS } from "@dnd-kit/utilities"; import { ComponentProps, Fragment } from "react"; diff --git a/src/ScheduleTables.tsx b/src/component/schedule/ScheduleTables.tsx similarity index 94% rename from src/ScheduleTables.tsx rename to src/component/schedule/ScheduleTables.tsx index 44dbd7a7..99730afe 100644 --- a/src/ScheduleTables.tsx +++ b/src/component/schedule/ScheduleTables.tsx @@ -1,7 +1,7 @@ import { Button, ButtonGroup, Flex, Heading, Stack } from "@chakra-ui/react"; import ScheduleTable from "./ScheduleTable.tsx"; -import { useScheduleContext } from "./ScheduleContext.tsx"; -import SearchDialog from "./SearchDialog.tsx"; +import { useScheduleContext } from "../../provider/ScheduleContext.tsx"; +import SearchDialog from "../search/SearchDialog.tsx"; import { useState } from "react"; export const ScheduleTables = () => { diff --git a/src/SearchDialog.tsx b/src/component/search/SearchDialog.tsx similarity index 93% rename from src/SearchDialog.tsx rename to src/component/search/SearchDialog.tsx index 4d5c7a3b..e0f6e0ca 100644 --- a/src/SearchDialog.tsx +++ b/src/component/search/SearchDialog.tsx @@ -13,20 +13,20 @@ import { Text, VStack, } from "@chakra-ui/react"; -import { useScheduleContext } from "./ScheduleContext.tsx"; -import { Lecture } from "./types.ts"; -import { parseSchedule } from "./utils.ts"; +import { useScheduleContext } from "../../provider/ScheduleContext.tsx"; +import { Lecture } from "../../types.ts"; +import { parseSchedule } from "../../utils.ts"; import axios, {AxiosResponse} from "axios"; import SearchItem from "./SearchItem.tsx"; -import { useAutoCallback } from "./hooks/useAutoCallback.ts"; +import { useAutoCallback } from "../../hooks/useAutoCallback.ts"; import SearchTableHead from "./SearchTableHead.tsx"; -import MajorFilter from "./MajorFilter.tsx"; -import DayFilter from "./DayFilter.tsx"; -import GradeFilter from "./GradeFilter.tsx"; -import PeriodTimeFilter from "./PeriodTimeFilter.tsx"; -import CreditFilter from "./CreditFilter.tsx"; -import QueryFilter from "./QueryFilter.tsx"; -import { SearchOption } from "./types.ts"; +import MajorFilter from "./filter/MajorFilter.tsx"; +import DayFilter from "./filter/DayFilter.tsx"; +import GradeFilter from "./filter/GradeFilter.tsx"; +import PeriodTimeFilter from "./filter/PeriodTimeFilter.tsx"; +import CreditFilter from "./filter/CreditFilter.tsx"; +import QueryFilter from "./filter/QueryFilter.tsx"; +import { SearchOption } from "../../types.ts"; interface Props { searchInfo: { diff --git a/src/SearchItem.tsx b/src/component/search/SearchItem.tsx similarity index 95% rename from src/SearchItem.tsx rename to src/component/search/SearchItem.tsx index abad1368..97dac308 100644 --- a/src/SearchItem.tsx +++ b/src/component/search/SearchItem.tsx @@ -1,6 +1,6 @@ import { Button, Td, Tr } from "@chakra-ui/react"; import { memo } from "react"; -import { Lecture } from "./types.ts"; +import { Lecture } from "../../types.ts"; const SearchItem = memo(({ lecture, addSchedule }: { lecture: Lecture; addSchedule: (lecture: Lecture) => void }) => { return ( diff --git a/src/SearchTableHead.tsx b/src/component/search/SearchTableHead.tsx similarity index 100% rename from src/SearchTableHead.tsx rename to src/component/search/SearchTableHead.tsx diff --git a/src/CreditFilter.tsx b/src/component/search/filter/CreditFilter.tsx similarity index 94% rename from src/CreditFilter.tsx rename to src/component/search/filter/CreditFilter.tsx index 33a8b684..9855929f 100644 --- a/src/CreditFilter.tsx +++ b/src/component/search/filter/CreditFilter.tsx @@ -1,5 +1,5 @@ import { FormControl, FormLabel, Select} from "@chakra-ui/react"; -import { SearchOption } from "./types.ts"; +import { SearchOption } from "../../../types.ts"; import { memo } from "react"; diff --git a/src/DayFilter.tsx b/src/component/search/filter/DayFilter.tsx similarity index 89% rename from src/DayFilter.tsx rename to src/component/search/filter/DayFilter.tsx index 067bb918..a352dd47 100644 --- a/src/DayFilter.tsx +++ b/src/component/search/filter/DayFilter.tsx @@ -1,6 +1,6 @@ import { Checkbox, CheckboxGroup, FormControl, FormLabel, HStack } from "@chakra-ui/react"; -import { DAY_LABELS } from "./constants.ts"; -import { SearchOption } from "./types.ts"; +import { DAY_LABELS } from "../../../constants.ts"; +import { SearchOption } from "../../../types.ts"; import { memo } from "react"; type DayFilterProps = { diff --git a/src/GradeFilter.tsx b/src/component/search/filter/GradeFilter.tsx similarity index 95% rename from src/GradeFilter.tsx rename to src/component/search/filter/GradeFilter.tsx index d0c3bac0..83dde48b 100644 --- a/src/GradeFilter.tsx +++ b/src/component/search/filter/GradeFilter.tsx @@ -1,5 +1,5 @@ import {Checkbox, CheckboxGroup, FormControl, FormLabel, HStack} from "@chakra-ui/react"; -import {SearchOption} from "./types.ts"; +import {SearchOption} from "../../../types.ts"; import {memo} from "react"; diff --git a/src/MajorFilter.tsx b/src/component/search/filter/MajorFilter.tsx similarity index 97% rename from src/MajorFilter.tsx rename to src/component/search/filter/MajorFilter.tsx index 98db22e7..bf480e4a 100644 --- a/src/MajorFilter.tsx +++ b/src/component/search/filter/MajorFilter.tsx @@ -11,7 +11,7 @@ import { Wrap } from "@chakra-ui/react"; import { memo } from "react"; -import { SearchOption } from "./types.ts"; +import { SearchOption } from "../../../types.ts"; type MajorFilterProps = { majors: SearchOption['majors']; diff --git a/src/PeriodTimeFilter.tsx b/src/component/search/filter/PeriodTimeFilter.tsx similarity index 94% rename from src/PeriodTimeFilter.tsx rename to src/component/search/filter/PeriodTimeFilter.tsx index 5f9d217a..930ff199 100644 --- a/src/PeriodTimeFilter.tsx +++ b/src/component/search/filter/PeriodTimeFilter.tsx @@ -10,9 +10,9 @@ import { TagLabel, Wrap } from "@chakra-ui/react"; -import { SearchOption } from "./types.ts"; +import { SearchOption } from "../../../types.ts"; import { memo } from "react"; -import { TIME_SLOTS } from "./constants.ts"; +import { TIME_SLOTS } from "../../../constants.ts"; type PeriodTimeFilterProps = { diff --git a/src/QueryFilter.tsx b/src/component/search/filter/QueryFilter.tsx similarity index 93% rename from src/QueryFilter.tsx rename to src/component/search/filter/QueryFilter.tsx index c89810eb..cf86f981 100644 --- a/src/QueryFilter.tsx +++ b/src/component/search/filter/QueryFilter.tsx @@ -1,5 +1,5 @@ import { FormControl, FormLabel, Input } from "@chakra-ui/react"; -import { SearchOption } from "./types.ts"; +import { SearchOption } from "../../../types.ts"; import { memo } from "react"; type QueryFilterProps = { diff --git a/src/ScheduleContext.tsx b/src/provider/ScheduleContext.tsx similarity index 90% rename from src/ScheduleContext.tsx rename to src/provider/ScheduleContext.tsx index 529f0dd9..99ec01d7 100644 --- a/src/ScheduleContext.tsx +++ b/src/provider/ScheduleContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, PropsWithChildren, useContext, useState } from "react"; -import { Schedule } from "./types.ts"; -import dummyScheduleMap from "./dummyScheduleMap.ts"; +import { Schedule } from "../types.ts"; +import dummyScheduleMap from "../dummyScheduleMap.ts"; interface ScheduleContextType { schedulesMap: Record; diff --git a/src/ScheduleDndProvider.tsx b/src/provider/ScheduleDndProvider.tsx similarity index 97% rename from src/ScheduleDndProvider.tsx rename to src/provider/ScheduleDndProvider.tsx index ca15f527..5798b90e 100644 --- a/src/ScheduleDndProvider.tsx +++ b/src/provider/ScheduleDndProvider.tsx @@ -1,6 +1,6 @@ import { DndContext, Modifier, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { PropsWithChildren } from "react"; -import { CellSize, DAY_LABELS } from "./constants.ts"; +import { CellSize, DAY_LABELS } from "../constants.ts"; import { useScheduleContext } from "./ScheduleContext.tsx"; function createSnapModifier(): Modifier { From 1c0a4b41f9a07f62912638911b4d9e475e0c0914 Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 03:22:20 +0900 Subject: [PATCH 12/16] =?UTF-8?q?chore:=20gh-pages=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20-=20prettier=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 9 +- pnpm-lock.yaml | 188 ++++++++++++++++++++++++++ src/component/search/SearchDialog.tsx | 6 +- vite.config.ts | 12 +- 4 files changed, 208 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 9f30638a..790f3962 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "10주차-성능최적화-2-심화과제", "private": true, + "homepage": "https://q1Lim.github.io/front_6th_chapter4-2/", "version": "0.0.0", "type": "module", "scripts": { @@ -9,7 +10,11 @@ "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix", + "format": "prettier --write .", + "deploy": "gh-pages -d dist", + "predeploy": "pnpm run build" }, "dependencies": { "@chakra-ui/icons": "2.2.4", @@ -21,6 +26,7 @@ "axios": "latest", "framer-motion": "latest", "msw": "latest", + "prettier": "^3.6.2", "react": "latest", "react-dom": "latest" }, @@ -39,6 +45,7 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", + "gh-pages": "^6.3.0", "tsx": "^4.17.0", "typescript": "^5.4.5", "vite": "npm:rolldown-vite@latest", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 704a7bc1..7c2dd736 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: msw: specifier: latest version: 2.11.1(@types/node@22.15.29)(typescript@5.8.3) + prettier: + specifier: ^3.6.2 + version: 3.6.2 react: specifier: latest version: 19.1.1 @@ -84,6 +87,9 @@ importers: eslint-plugin-react-refresh: specifier: ^0.4.7 version: 0.4.20(eslint@8.57.1) + gh-pages: + specifier: ^6.3.0 + version: 6.3.0 tsx: specifier: ^4.17.0 version: 4.19.4 @@ -1243,6 +1249,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1316,6 +1325,13 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1399,6 +1415,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + email-addresses@5.0.0: + resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1441,6 +1460,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1529,13 +1552,29 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + filename-reserved-regex@2.0.0: + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} + + filenamify@4.3.0: + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} + engines: {node: '>=8'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1585,6 +1624,10 @@ packages: framesync@6.1.2: resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1618,6 +1661,11 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + gh-pages@6.3.0: + resolution: {integrity: sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==} + engines: {node: '>=10'} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1650,6 +1698,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -1784,6 +1835,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1858,6 +1912,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1897,6 +1955,10 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -1985,14 +2047,26 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2051,6 +2125,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + postcss@8.5.4: resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} engines: {node: ^10 || ^12 || >=14} @@ -2063,6 +2141,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2233,6 +2316,10 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -2309,6 +2396,10 @@ packages: strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + strip-outer@1.0.1: + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} + stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -2375,6 +2466,10 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + trim-repeated@1.0.0: + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -2416,6 +2511,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -3609,6 +3708,8 @@ snapshots: assertion-error@2.0.1: {} + async@3.2.6: {} + asynckit@0.4.0: {} axios@1.11.0: @@ -3689,6 +3790,10 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@13.1.0: {} + + commondir@1.0.1: {} + concat-map@0.0.1: {} convert-source-map@1.9.0: {} @@ -3755,6 +3860,8 @@ snapshots: eastasianwidth@0.2.0: {} + email-addresses@5.0.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3836,6 +3943,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@4.0.0: {} eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): @@ -3948,12 +4057,31 @@ snapshots: dependencies: flat-cache: 3.2.0 + filename-reserved-regex@2.0.0: {} + + filenamify@4.3.0: + dependencies: + filename-reserved-regex: 2.0.0 + strip-outer: 1.0.1 + trim-repeated: 1.0.0 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + find-root@1.1.0: {} + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4000,6 +4128,12 @@ snapshots: dependencies: tslib: 2.4.0 + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -4035,6 +4169,16 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + gh-pages@6.3.0: + dependencies: + async: 3.2.6 + commander: 13.1.0 + email-addresses: 5.0.0 + filenamify: 4.3.0 + find-cache-dir: 3.3.2 + fs-extra: 11.3.1 + globby: 11.1.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4078,6 +4222,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} graphql@16.11.0: {} @@ -4187,6 +4333,12 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -4243,6 +4395,10 @@ snapshots: lines-and-columns@1.2.4: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4279,6 +4435,10 @@ snapshots: '@babel/types': 7.27.6 source-map-js: 1.2.1 + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + make-dir@4.0.0: dependencies: semver: 7.7.2 @@ -4368,14 +4528,24 @@ snapshots: outvariant@1.4.3: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -4418,6 +4588,10 @@ snapshots: picomatch@4.0.3: {} + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + postcss@8.5.4: dependencies: nanoid: 3.3.11 @@ -4432,6 +4606,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.6.2: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -4606,6 +4782,8 @@ snapshots: scheduler@0.26.0: {} + semver@6.3.1: {} + semver@7.7.2: {} shebang-command@2.0.0: @@ -4668,6 +4846,10 @@ snapshots: dependencies: js-tokens: 9.0.1 + strip-outer@1.0.1: + dependencies: + escape-string-regexp: 1.0.5 + stylis@4.2.0: {} supports-color@7.2.0: @@ -4719,6 +4901,10 @@ snapshots: dependencies: tldts: 7.0.12 + trim-repeated@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + ts-api-utils@1.4.3(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -4748,6 +4934,8 @@ snapshots: undici-types@6.21.0: {} + universalify@2.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 diff --git a/src/component/search/SearchDialog.tsx b/src/component/search/SearchDialog.tsx index e0f6e0ca..38298195 100644 --- a/src/component/search/SearchDialog.tsx +++ b/src/component/search/SearchDialog.tsx @@ -38,9 +38,11 @@ interface Props { } const PAGE_SIZE = 100; +const base = + process.env.NODE_ENV === "production" ? "/front_6th_chapter4-2/" : "/"; -const fetchMajors = () => axios.get('/schedules-majors.json'); -const fetchLiberalArts = () => axios.get('/schedules-liberal-arts.json'); +const fetchMajors = () => axios.get(`${base}schedules-majors.json`); +const fetchLiberalArts = () => axios.get(`${base}schedules-liberal-arts.json`) let majorsCache: Promise> | null = null; let liberalArtsCache: Promise> | null = null; diff --git a/vite.config.ts b/vite.config.ts index 1cdac555..9e1a4aef 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,14 @@ -import { defineConfig as defineTestConfig, mergeConfig } from 'vitest/config'; -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react-swc'; +import { defineConfig as defineTestConfig, mergeConfig } from "vitest/config"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; + +const base = + process.env.NODE_ENV === "production" ? "/front_6th_chapter4-2/" : "/"; export default mergeConfig( defineConfig({ - plugins: [react()], + base, + plugins: [react()], }), defineTestConfig({ test: { From 094b4b321e4e1d3b2aadc08782799cadf08d93da Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 07:40:36 +0900 Subject: [PATCH 13/16] =?UTF-8?q?feat:=20Drag=20and=20Drop=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20-=20Provider=20=EB=B6=84=EB=A6=AC=20=ED=95=84?= =?UTF-8?q?=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 3 - src/component/schedule/ScheduleTable.tsx | 114 +++++------------- src/component/schedule/ScheduleTableGrid.tsx | 66 ++++++++++ .../schedule/ScheduleTableWrapper.tsx | 66 ++++++++++ src/component/schedule/ScheduleTables.tsx | 57 ++++----- src/provider/ScheduleContext.tsx | 4 +- src/provider/ScheduleDndProvider.tsx | 62 ++++++---- src/utils.ts | 13 ++ 8 files changed, 244 insertions(+), 141 deletions(-) create mode 100644 src/component/schedule/ScheduleTableGrid.tsx create mode 100644 src/component/schedule/ScheduleTableWrapper.tsx diff --git a/src/App.tsx b/src/App.tsx index 21f0e46f..137ea540 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,13 @@ import { ChakraProvider } from "@chakra-ui/react"; import { ScheduleProvider } from "./provider/ScheduleContext.tsx"; import { ScheduleTables } from "./component/schedule/ScheduleTables.tsx"; -import ScheduleDndProvider from "./provider/ScheduleDndProvider.tsx"; function App() { return ( - - ); diff --git a/src/component/schedule/ScheduleTable.tsx b/src/component/schedule/ScheduleTable.tsx index a31bb424..effc9ca8 100644 --- a/src/component/schedule/ScheduleTable.tsx +++ b/src/component/schedule/ScheduleTable.tsx @@ -1,9 +1,6 @@ import { Box, Button, - Flex, - Grid, - GridItem, Popover, PopoverArrow, PopoverBody, @@ -12,39 +9,27 @@ import { PopoverTrigger, Text, } from "@chakra-ui/react"; -import { CellSize, DAY_LABELS, 분 } from "../../constants.ts"; +import {CellSize, DAY_LABELS} from "../../constants.ts"; import { Schedule } from "../../types.ts"; -import { fill2, parseHnM } from "../../utils.ts"; import { useDndContext, useDraggable } from "@dnd-kit/core"; import { CSS } from "@dnd-kit/utilities"; -import { ComponentProps, Fragment } from "react"; +import {ComponentProps, memo} from "react"; +import ScheduleGrid from "./ScheduleTableGrid.tsx"; +import {useAutoCallback} from "../../hooks/useAutoCallback.ts"; interface Props { tableId: string; schedules: Schedule[]; onScheduleTimeClick?: (timeInfo: { day: string, time: number }) => void; - onDeleteButtonClick?: (timeInfo: { day: string, time: number }) => void; + onDeleteButtonClick?: (timeInfo: { day: string; time: number }) => void; } -const TIMES = [ - ...Array(18) - .fill(0) - .map((v, k) => v + k * 30 * 분) - .map((v) => `${parseHnM(v)}~${parseHnM(v + 30 * 분)}`), - - ...Array(6) - .fill(18 * 30 * 분) - .map((v, k) => v + k * 55 * 분) - .map((v) => `${parseHnM(v)}~${parseHnM(v + 50 * 분)}`), -] as const; - const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButtonClick }: Props) => { - - const getColor = (lectureId: string): string => { - const lectures = [...new Set(schedules.map(({ lecture }) => lecture.id))]; - const colors = ["#fdd", "#ffd", "#dff", "#ddf", "#fdf", "#dfd"]; - return colors[lectures.indexOf(lectureId) % colors.length]; - }; + const getColor = (lectureId: string): string => { + const lectures = [...new Set(schedules.map(({ lecture }) => lecture.id))]; + const colors = ["#fdd", "#ffd", "#dff", "#ddf", "#fdf", "#dfd"]; + return colors[lectures.indexOf(lectureId) % colors.length]; + }; const dndContext = useDndContext(); @@ -58,58 +43,28 @@ const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButton const activeTableId = getActiveTableId(); + const handleScheduleTimeClick = useAutoCallback( + (timeInfo: { day: string; time: number }) => { + onScheduleTimeClick?.(timeInfo); + }, + ); + + const handleDeleteButtonClick = useAutoCallback( + (day: string, time: number) => { + onDeleteButtonClick?.({ + day, + time, + }); + }, + ); + return ( - - - - 교시 - - - {DAY_LABELS.map((day) => ( - - - {day} - - - ))} - {TIMES.map((time, timeIndex) => ( - - 17 ? 'gray.200' : 'gray.100'} - > - - {fill2(timeIndex + 1)} ({time}) - - - {DAY_LABELS.map((day) => ( - 17 ? 'gray.100' : 'white'} - cursor="pointer" - _hover={{ bg: 'yellow.100' }} - onClick={() => onScheduleTimeClick?.({ day, time: timeIndex + 1 })} - /> - ))} - - ))} - + {schedules.map((schedule, index) => ( onDeleteButtonClick?.({ - day: schedule.day, - time: schedule.range[0], - })} + onDeleteButtonClick={handleDeleteButtonClick} /> ))} ); }; -const DraggableSchedule = ({ +const DraggableSchedule = memo(({ id, data, bg, onDeleteButtonClick }: { id: string; data: Schedule } & ComponentProps & { - onDeleteButtonClick: () => void -}) => { + onDeleteButtonClick: (day: string, time: number) => void; +})=> { const { day, range, room, lecture } = data; const { attributes, setNodeRef, listeners, transform } = useDraggable({ id }); const leftIndex = DAY_LABELS.indexOf(day as typeof DAY_LABELS[number]); @@ -142,7 +94,7 @@ const DraggableSchedule = ({ const size = range.length; return ( - + 강의를 삭제하시겠습니까? - ); -} +}); export default ScheduleTable; diff --git a/src/component/schedule/ScheduleTableGrid.tsx b/src/component/schedule/ScheduleTableGrid.tsx new file mode 100644 index 00000000..0de2a839 --- /dev/null +++ b/src/component/schedule/ScheduleTableGrid.tsx @@ -0,0 +1,66 @@ +import { memo, Fragment } from "react"; +import { Grid, GridItem, Flex, Text } from "@chakra-ui/react"; +import { CellSize, DAY_LABELS } from "../../constants.ts"; +import {fill2, TIMES} from "../../utils.ts"; + +type ScheduleGridProps = { + onScheduleTimeClick?: (timeInfo: { day: string; time: number }) => void; +}; + +const ScheduleTableGrid = ({ onScheduleTimeClick }: ScheduleGridProps)=> { + return ( + + + + 교시 + + + + {DAY_LABELS.map((day) => ( + + + {day} + + + ))} + + {TIMES.map((time, timeIndex) => ( + + 17 ? "gray.200" : "gray.100"} + > + + + {fill2(timeIndex + 1)} ({time}) + + + + + {DAY_LABELS.map((day) => ( + 17 ? "gray.100" : "white"} + cursor="pointer" + _hover={{ bg: "yellow.100" }} + onClick={() => onScheduleTimeClick?.({ day, time: timeIndex + 1 })} + /> + ))} + + ))} + + ); +} + +export default memo(ScheduleTableGrid); \ No newline at end of file diff --git a/src/component/schedule/ScheduleTableWrapper.tsx b/src/component/schedule/ScheduleTableWrapper.tsx new file mode 100644 index 00000000..eb1e4d3c --- /dev/null +++ b/src/component/schedule/ScheduleTableWrapper.tsx @@ -0,0 +1,66 @@ +import {useScheduleContext} from "../../provider/ScheduleContext.tsx"; +import {memo, useCallback} from "react"; +import {Button, ButtonGroup, Flex, Heading, Stack} from "@chakra-ui/react"; +import ScheduleDndProvider from "../../provider/ScheduleDndProvider.tsx"; +import ScheduleTable from "./ScheduleTable.tsx"; + +type ScheduleTableWrapperProps = { + tableId: string; + index: number; + schedules: ReturnType["schedulesMap"][string]; + disabledRemoveButton: boolean; + onOpenSearch: (tableId: string, extra?: { day?: string; time?: number }) => void; + onDuplicate: (tableId: string) => void; + onRemove: (tableId: string) => void; + onDeleteBlock: (tableId: string, day: string, time: number) => void; +}; + +const ScheduleTableWrapper = ({ + tableId, + index, + schedules, + disabledRemoveButton, + onOpenSearch, + onDuplicate, + onRemove, + onDeleteBlock, + }: ScheduleTableWrapperProps ) => { + const openSearch = useCallback( + (extra?: { day?: string; time?: number }) => onOpenSearch(tableId, extra), + [onOpenSearch, tableId] + ); + + const duplicate = useCallback(() => onDuplicate(tableId), [onDuplicate, tableId]); + const remove = useCallback(() => onRemove(tableId), [onRemove, tableId]); + + const handleDeleteBlock = useCallback( + ({ day, time }: { day: string; time: number }) => onDeleteBlock(tableId, day, time), + [onDeleteBlock, tableId] + ); + + return ( + + + 시간표 {index + 1} + + + + + + + + openSearch(timeInfo)} + onDeleteButtonClick={handleDeleteBlock} + /> + + + ) +}; + +export default memo(ScheduleTableWrapper); + diff --git a/src/component/schedule/ScheduleTables.tsx b/src/component/schedule/ScheduleTables.tsx index 99730afe..6c2e06e0 100644 --- a/src/component/schedule/ScheduleTables.tsx +++ b/src/component/schedule/ScheduleTables.tsx @@ -1,8 +1,8 @@ -import { Button, ButtonGroup, Flex, Heading, Stack } from "@chakra-ui/react"; -import ScheduleTable from "./ScheduleTable.tsx"; +import { Flex } from "@chakra-ui/react"; import { useScheduleContext } from "../../provider/ScheduleContext.tsx"; import SearchDialog from "../search/SearchDialog.tsx"; -import { useState } from "react"; +import { useCallback, useState } from "react"; +import ScheduleTableWrapper from "./ScheduleTableWrapper.tsx"; export const ScheduleTables = () => { const { schedulesMap, setSchedulesMap } = useScheduleContext(); @@ -14,45 +14,36 @@ export const ScheduleTables = () => { const disabledRemoveButton = Object.keys(schedulesMap).length === 1; - const duplicate = (targetId: string) => { + const duplicate = useCallback((targetId: string) => { setSchedulesMap(prev => ({ ...prev, [`schedule-${Date.now()}`]: [...prev[targetId]] - })) - }; + })); + }, [setSchedulesMap]); - const remove = (targetId: string) => { - setSchedulesMap(prev => { - delete prev[targetId]; - return { ...prev }; - }) - }; + const remove = useCallback((targetId: string) => { + setSchedulesMap(prev => { + const { [targetId]: _, ...rest } = prev; + return rest; + }); + }, [setSchedulesMap]); + + const onOpenSearch = useCallback((tableId: string, extra?: { day?: string; time?: number }) => { + setSearchInfo({ tableId, ...extra }); + }, []); + + const onDeleteBlock = useCallback((tableId: string, day: string, time: number) => { + setSchedulesMap(prev => ({ + ...prev, + [tableId]: prev[tableId].filter(schedule => schedule.day !== day || !schedule.range.includes(time)) + })); + }, [setSchedulesMap]); return ( <> {Object.entries(schedulesMap).map(([tableId, schedules], index) => ( - - - 시간표 {index + 1} - - - - - - - setSearchInfo({ tableId, ...timeInfo })} - onDeleteButtonClick={({ day, time }) => setSchedulesMap((prev) => ({ - ...prev, - [tableId]: prev[tableId].filter(schedule => schedule.day !== day || !schedule.range.includes(time)) - }))} - /> - + ))} setSearchInfo(null)}/> diff --git a/src/provider/ScheduleContext.tsx b/src/provider/ScheduleContext.tsx index 99ec01d7..feb442e4 100644 --- a/src/provider/ScheduleContext.tsx +++ b/src/provider/ScheduleContext.tsx @@ -1,7 +1,9 @@ -import React, { createContext, PropsWithChildren, useContext, useState } from "react"; +import React, {createContext, PropsWithChildren, useContext, useState} from "react"; import { Schedule } from "../types.ts"; import dummyScheduleMap from "../dummyScheduleMap.ts"; +// TODO: 컨텍스트 분리 필요 + interface ScheduleContextType { schedulesMap: Record; setSchedulesMap: React.Dispatch>>; diff --git a/src/provider/ScheduleDndProvider.tsx b/src/provider/ScheduleDndProvider.tsx index 5798b90e..53619249 100644 --- a/src/provider/ScheduleDndProvider.tsx +++ b/src/provider/ScheduleDndProvider.tsx @@ -1,7 +1,8 @@ import { DndContext, Modifier, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; -import { PropsWithChildren } from "react"; +import {PropsWithChildren} from "react"; import { CellSize, DAY_LABELS } from "../constants.ts"; import { useScheduleContext } from "./ScheduleContext.tsx"; +import {useAutoCallback} from "../hooks/useAutoCallback.ts"; function createSnapModifier(): Modifier { return ({ transform, containerNodeRect, draggingNodeRect }) => { @@ -17,12 +18,23 @@ function createSnapModifier(): Modifier { const maxX = containerRight - right; const maxY = containerBottom - bottom; - - return ({ - ...transform, - x: Math.min(Math.max(Math.round(transform.x / CellSize.WIDTH) * CellSize.WIDTH, minX), maxX), - y: Math.min(Math.max(Math.round(transform.y / CellSize.HEIGHT) * CellSize.HEIGHT, minY), maxY), - }) + return { + ...transform, + x: Math.min( + Math.max( + Math.round(transform.x / CellSize.WIDTH) * CellSize.WIDTH, + minX, + ), + maxX, + ), + y: Math.min( + Math.max( + Math.round(transform.y / CellSize.HEIGHT) * CellSize.HEIGHT, + minY, + ), + maxY, + ), + }; }; } @@ -39,29 +51,33 @@ export default function ScheduleDndProvider({ children }: PropsWithChildren) { ); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleDragEnd = (event: any) => { + const handleDragEnd = useAutoCallback((event: any) => { const { active, delta } = event; const { x, y } = delta; const [tableId, index] = active.id.split(':'); - const schedule = schedulesMap[tableId][index]; - const nowDayIndex = DAY_LABELS.indexOf(schedule.day as typeof DAY_LABELS[number]) - const moveDayIndex = Math.floor(x / 80); - const moveTimeIndex = Math.floor(y / 30); - setSchedulesMap({ - ...schedulesMap, - [tableId]: schedulesMap[tableId].map((targetSchedule, targetIndex) => { - if (targetIndex !== Number(index)) { - return { ...targetSchedule } - } + setSchedulesMap((prev) => { + const schedule = schedulesMap[tableId][index]; + const nowDayIndex = DAY_LABELS.indexOf(schedule.day as typeof DAY_LABELS[number]) + const moveDayIndex = Math.floor(x / 80); + const moveTimeIndex = Math.floor(y / 30); + + const newSchedule = prev[tableId].map((targetSchedule, targetIndex) => { + if (targetIndex !== Number(index)) { + return { ...targetSchedule } + } + return { + ...targetSchedule, + day: DAY_LABELS[nowDayIndex + moveDayIndex], + range: targetSchedule.range.map(time => time + moveTimeIndex), + }; + }); return { - ...targetSchedule, - day: DAY_LABELS[nowDayIndex + moveDayIndex], - range: targetSchedule.range.map(time => time + moveTimeIndex), + ...prev, + [tableId]: newSchedule } - }) }) - }; + }); return ( diff --git a/src/utils.ts b/src/utils.ts index 8b6eb66f..5b75ff39 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import {분} from "./constants.ts"; + export const fill2 = (n: number) => `0${n}`.substr(-2); export const parseHnM = (current: number) => { @@ -28,3 +30,14 @@ export const parseSchedule = (schedule: string) => { return { day, range, room }; }); }; + +export const TIMES = [ + ...Array(18) + .fill(0) + .map((v, k) => v + k * 30 * 분) + .map((v) => `${parseHnM(v)}~${parseHnM(v + 30 * 분)}`), + ...Array(6) + .fill(18 * 30 * 분) + .map((v, k) => v + k * 55 * 분) + .map((v) => `${parseHnM(v)}~${parseHnM(v + 50 * 분)}`), +] as const; \ No newline at end of file From 7f678511c1c1c54c872cc14b814ab9da8dc17788 Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 14:23:55 +0900 Subject: [PATCH 14/16] =?UTF-8?q?feat:=20Context=20Actions,=20State=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/ScheduleTableWrapper.tsx | 4 +- src/component/schedule/ScheduleTables.tsx | 11 +-- src/component/search/SearchDialog.tsx | 5 +- src/provider/ScheduleContext.tsx | 74 +++++++++++++------ src/provider/ScheduleDndProvider.tsx | 6 +- 5 files changed, 65 insertions(+), 35 deletions(-) diff --git a/src/component/schedule/ScheduleTableWrapper.tsx b/src/component/schedule/ScheduleTableWrapper.tsx index eb1e4d3c..4f947ec3 100644 --- a/src/component/schedule/ScheduleTableWrapper.tsx +++ b/src/component/schedule/ScheduleTableWrapper.tsx @@ -1,13 +1,13 @@ -import {useScheduleContext} from "../../provider/ScheduleContext.tsx"; import {memo, useCallback} from "react"; import {Button, ButtonGroup, Flex, Heading, Stack} from "@chakra-ui/react"; import ScheduleDndProvider from "../../provider/ScheduleDndProvider.tsx"; import ScheduleTable from "./ScheduleTable.tsx"; +import {Schedule} from "../../types.ts"; type ScheduleTableWrapperProps = { tableId: string; index: number; - schedules: ReturnType["schedulesMap"][string]; + schedules: Schedule[]; disabledRemoveButton: boolean; onOpenSearch: (tableId: string, extra?: { day?: string; time?: number }) => void; onDuplicate: (tableId: string) => void; diff --git a/src/component/schedule/ScheduleTables.tsx b/src/component/schedule/ScheduleTables.tsx index 6c2e06e0..e9a00f1d 100644 --- a/src/component/schedule/ScheduleTables.tsx +++ b/src/component/schedule/ScheduleTables.tsx @@ -1,11 +1,12 @@ import { Flex } from "@chakra-ui/react"; -import { useScheduleContext } from "../../provider/ScheduleContext.tsx"; import SearchDialog from "../search/SearchDialog.tsx"; import { useCallback, useState } from "react"; import ScheduleTableWrapper from "./ScheduleTableWrapper.tsx"; +import { useScheduleState, useScheduleActions } from "../../provider/ScheduleContext.tsx"; export const ScheduleTables = () => { - const { schedulesMap, setSchedulesMap } = useScheduleContext(); + const schedulesMap = useScheduleState(); + const { setSchedulesMap, onDeleteScheduleButtonClick } = useScheduleActions(); const [searchInfo, setSearchInfo] = useState<{ tableId: string; day?: string; @@ -33,11 +34,7 @@ export const ScheduleTables = () => { }, []); const onDeleteBlock = useCallback((tableId: string, day: string, time: number) => { - setSchedulesMap(prev => ({ - ...prev, - [tableId]: prev[tableId].filter(schedule => schedule.day !== day || !schedule.range.includes(time)) - })); - }, [setSchedulesMap]); + onDeleteScheduleButtonClick(tableId, day, time)}, [onDeleteScheduleButtonClick]); return ( <> diff --git a/src/component/search/SearchDialog.tsx b/src/component/search/SearchDialog.tsx index 38298195..4c420743 100644 --- a/src/component/search/SearchDialog.tsx +++ b/src/component/search/SearchDialog.tsx @@ -13,7 +13,7 @@ import { Text, VStack, } from "@chakra-ui/react"; -import { useScheduleContext } from "../../provider/ScheduleContext.tsx"; +import { useScheduleActions } from "../../provider/ScheduleContext.tsx"; import { Lecture } from "../../types.ts"; import { parseSchedule } from "../../utils.ts"; import axios, {AxiosResponse} from "axios"; @@ -67,9 +67,8 @@ const fetchAllLectures = async () => { ]); }; -// TODO: 이 컴포넌트에서 불필요한 연산이 발생하지 않도록 다양한 방식으로 시도해주세요. const SearchDialog = ({ searchInfo, onClose }: Props) => { - const { setSchedulesMap } = useScheduleContext(); + const { setSchedulesMap } = useScheduleActions(); const loaderWrapperRef = useRef(null); const loaderRef = useRef(null); diff --git a/src/provider/ScheduleContext.tsx b/src/provider/ScheduleContext.tsx index feb442e4..fa9a56a1 100644 --- a/src/provider/ScheduleContext.tsx +++ b/src/provider/ScheduleContext.tsx @@ -1,30 +1,62 @@ -import React, {createContext, PropsWithChildren, useContext, useState} from "react"; +import React, {createContext, PropsWithChildren, useContext, useMemo, useState} from "react"; import { Schedule } from "../types.ts"; import dummyScheduleMap from "../dummyScheduleMap.ts"; +import {useAutoCallback} from "../hooks/useAutoCallback.ts"; -// TODO: 컨텍스트 분리 필요 +const ScheduleStateContext = createContext|null>(null); -interface ScheduleContextType { - schedulesMap: Record; - setSchedulesMap: React.Dispatch>>; -} - -const ScheduleContext = createContext(undefined); +const ScheduleActionsContext = createContext<{ + setSchedulesMap: React.Dispatch< + React.SetStateAction> + >; + onDeleteScheduleButtonClick: ( + tableId: string, + day: string, + time: number + ) => void; +} | null>(null); -export const useScheduleContext = () => { - const context = useContext(ScheduleContext); - if (context === undefined) { - throw new Error('useSchedule must be used within a ScheduleProvider'); - } - return context; +export const useScheduleState = () => { + const context = useContext(ScheduleStateContext); + if (!context) { + throw new Error('useScheduleState must be used within a ScheduleProvider'); + } + return context; }; +export const useScheduleActions = () => { + const context = useContext(ScheduleActionsContext); + if (!context) { + throw new Error('useScheduleActions must be used within a ScheduleProvider'); + } + return context; +} + export const ScheduleProvider = ({ children }: PropsWithChildren) => { - const [schedulesMap, setSchedulesMap] = useState>(dummyScheduleMap); + const [schedulesMap, setSchedulesMap] = useState>(dummyScheduleMap); - return ( - - {children} - - ); -}; + const onDeleteScheduleButtonClick = useAutoCallback( + (tableId: string, day: string, time: number) => { + setSchedulesMap((prev) => ({ + ...prev, + [tableId]: prev[tableId].filter( + (s) => s.day !== day || !s.range.includes(time) + ), + })); + } + ); + + const actions = useMemo(() => ({ + setSchedulesMap, + onDeleteScheduleButtonClick, + + }), [setSchedulesMap, onDeleteScheduleButtonClick]); + + return ( + + + {children} + + + ); +} \ No newline at end of file diff --git a/src/provider/ScheduleDndProvider.tsx b/src/provider/ScheduleDndProvider.tsx index 53619249..e9fd1772 100644 --- a/src/provider/ScheduleDndProvider.tsx +++ b/src/provider/ScheduleDndProvider.tsx @@ -1,8 +1,8 @@ import { DndContext, Modifier, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import {PropsWithChildren} from "react"; import { CellSize, DAY_LABELS } from "../constants.ts"; -import { useScheduleContext } from "./ScheduleContext.tsx"; import {useAutoCallback} from "../hooks/useAutoCallback.ts"; +import {useScheduleActions, useScheduleState} from "./ScheduleContext.tsx"; function createSnapModifier(): Modifier { return ({ transform, containerNodeRect, draggingNodeRect }) => { @@ -41,7 +41,9 @@ function createSnapModifier(): Modifier { const modifiers = [createSnapModifier()] export default function ScheduleDndProvider({ children }: PropsWithChildren) { - const { schedulesMap, setSchedulesMap } = useScheduleContext(); + const schedulesMap = useScheduleState(); + const { setSchedulesMap } = useScheduleActions(); + const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { From 95478e5579ad6c7234f188595b479db12617e971 Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 15:27:59 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix:=20setSchedulesMap=EC=97=90=EC=84=9C?= =?UTF-8?q?=20prev=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/provider/ScheduleDndProvider.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/provider/ScheduleDndProvider.tsx b/src/provider/ScheduleDndProvider.tsx index e9fd1772..e71e7bda 100644 --- a/src/provider/ScheduleDndProvider.tsx +++ b/src/provider/ScheduleDndProvider.tsx @@ -2,7 +2,7 @@ import { DndContext, Modifier, PointerSensor, useSensor, useSensors } from "@dnd import {PropsWithChildren} from "react"; import { CellSize, DAY_LABELS } from "../constants.ts"; import {useAutoCallback} from "../hooks/useAutoCallback.ts"; -import {useScheduleActions, useScheduleState} from "./ScheduleContext.tsx"; +import {useScheduleActions} from "./ScheduleContext.tsx"; function createSnapModifier(): Modifier { return ({ transform, containerNodeRect, draggingNodeRect }) => { @@ -41,7 +41,6 @@ function createSnapModifier(): Modifier { const modifiers = [createSnapModifier()] export default function ScheduleDndProvider({ children }: PropsWithChildren) { - const schedulesMap = useScheduleState(); const { setSchedulesMap } = useScheduleActions(); const sensors = useSensors( @@ -59,7 +58,7 @@ export default function ScheduleDndProvider({ children }: PropsWithChildren) { const [tableId, index] = active.id.split(':'); setSchedulesMap((prev) => { - const schedule = schedulesMap[tableId][index]; + const schedule = prev[tableId][index]; const nowDayIndex = DAY_LABELS.indexOf(schedule.day as typeof DAY_LABELS[number]) const moveDayIndex = Math.floor(x / 80); const moveTimeIndex = Math.floor(y / 30); From b39a8afd8b790bb6b760db4883875e815aab308b Mon Sep 17 00:00:00 2001 From: q1Lim Date: Fri, 12 Sep 2025 15:58:14 +0900 Subject: [PATCH 16/16] =?UTF-8?q?chore:=20prettier=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- pnpm-lock.yaml | 72 +- src/App.tsx | 13 +- src/component/schedule/ScheduleTable.tsx | 178 ++- src/component/schedule/ScheduleTableGrid.tsx | 112 +- .../schedule/ScheduleTableWrapper.tsx | 122 +- src/component/schedule/ScheduleTables.tsx | 82 +- src/component/search/SearchDialog.tsx | 303 +++-- src/component/search/SearchItem.tsx | 42 +- src/component/search/SearchTableHead.tsx | 32 +- src/component/search/filter/CreditFilter.tsx | 40 +- src/component/search/filter/DayFilter.tsx | 45 +- src/component/search/filter/GradeFilter.tsx | 44 +- src/component/search/filter/MajorFilter.tsx | 101 +- .../search/filter/PeriodTimeFilter.tsx | 104 +- src/component/search/filter/QueryFilter.tsx | 34 +- src/constants.ts | 60 +- src/dummyScheduleMap.ts | 1203 +++++++---------- src/hooks/useAutoCallback.ts | 18 +- src/main.tsx | 10 +- src/provider/ScheduleContext.tsx | 94 +- src/provider/ScheduleDndProvider.tsx | 100 +- src/types.ts | 30 +- src/utils.ts | 53 +- 24 files changed, 1355 insertions(+), 1541 deletions(-) diff --git a/package.json b/package.json index 790f3962..803ba885 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "axios": "latest", "framer-motion": "latest", "msw": "latest", - "prettier": "^3.6.2", "react": "latest", "react-dom": "latest" }, @@ -43,9 +42,12 @@ "@vitest/coverage-v8": "^2.0.3", "@vitest/ui": "^1.6.0", "eslint": "^8.57.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", "gh-pages": "^6.3.0", + "prettier": "^3.6.2", "tsx": "^4.17.0", "typescript": "^5.4.5", "vite": "npm:rolldown-vite@latest", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c2dd736..830526f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: msw: specifier: latest version: 2.11.1(@types/node@22.15.29)(typescript@5.8.3) - prettier: - specifier: ^3.6.2 - version: 3.6.2 react: specifier: latest version: 19.1.1 @@ -81,6 +78,12 @@ importers: eslint: specifier: ^8.57.0 version: 8.57.1 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@8.57.1) + eslint-plugin-prettier: + specifier: ^5.5.4 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2) eslint-plugin-react-hooks: specifier: ^4.6.2 version: 4.6.2(eslint@8.57.1) @@ -90,6 +93,9 @@ importers: gh-pages: specifier: ^6.3.0 version: 6.3.0 + prettier: + specifier: ^3.6.2 + version: 3.6.2 tsx: specifier: ^4.17.0 version: 4.19.4 @@ -701,6 +707,10 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1468,6 +1478,26 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + eslint-plugin-react-hooks@4.6.2: resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} engines: {node: '>=10'} @@ -1523,6 +1553,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2141,6 +2174,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + prettier@3.6.2: resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} @@ -2411,6 +2448,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -3192,6 +3233,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@pkgr/core@0.2.9': {} + '@polka/url@1.0.0-next.29': {} '@popperjs/core@2.11.8': {} @@ -3947,6 +3990,19 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-config-prettier@10.1.8(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2): + dependencies: + eslint: 8.57.1 + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@8.57.1) + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -4031,6 +4087,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4606,6 +4664,10 @@ snapshots: prelude-ls@1.2.1: {} + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + prettier@3.6.2: {} pretty-format@27.5.1: @@ -4858,6 +4920,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 diff --git a/src/App.tsx b/src/App.tsx index 137ea540..fc55e59a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,15 @@ -import { ChakraProvider } from "@chakra-ui/react"; -import { ScheduleProvider } from "./provider/ScheduleContext.tsx"; -import { ScheduleTables } from "./component/schedule/ScheduleTables.tsx"; +import { ChakraProvider } from "@chakra-ui/react" +import { ScheduleProvider } from "./provider/ScheduleContext.tsx" +import { ScheduleTables } from "./component/schedule/ScheduleTables.tsx" function App() { - return ( - + - ); + ) } -export default App; +export default App diff --git a/src/component/schedule/ScheduleTable.tsx b/src/component/schedule/ScheduleTable.tsx index effc9ca8..88587377 100644 --- a/src/component/schedule/ScheduleTable.tsx +++ b/src/component/schedule/ScheduleTable.tsx @@ -8,62 +8,54 @@ import { PopoverContent, PopoverTrigger, Text, -} from "@chakra-ui/react"; -import {CellSize, DAY_LABELS} from "../../constants.ts"; -import { Schedule } from "../../types.ts"; -import { useDndContext, useDraggable } from "@dnd-kit/core"; -import { CSS } from "@dnd-kit/utilities"; -import {ComponentProps, memo} from "react"; -import ScheduleGrid from "./ScheduleTableGrid.tsx"; -import {useAutoCallback} from "../../hooks/useAutoCallback.ts"; +} from "@chakra-ui/react" +import { CellSize, DAY_LABELS } from "../../constants.ts" +import { Schedule } from "../../types.ts" +import { useDndContext, useDraggable } from "@dnd-kit/core" +import { CSS } from "@dnd-kit/utilities" +import { ComponentProps, memo } from "react" +import ScheduleGrid from "./ScheduleTableGrid.tsx" +import { useAutoCallback } from "../../hooks/useAutoCallback.ts" interface Props { - tableId: string; - schedules: Schedule[]; - onScheduleTimeClick?: (timeInfo: { day: string, time: number }) => void; - onDeleteButtonClick?: (timeInfo: { day: string; time: number }) => void; + tableId: string + schedules: Schedule[] + onScheduleTimeClick?: (timeInfo: { day: string; time: number }) => void + onDeleteButtonClick?: (timeInfo: { day: string; time: number }) => void } const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButtonClick }: Props) => { - const getColor = (lectureId: string): string => { - const lectures = [...new Set(schedules.map(({ lecture }) => lecture.id))]; - const colors = ["#fdd", "#ffd", "#dff", "#ddf", "#fdf", "#dfd"]; - return colors[lectures.indexOf(lectureId) % colors.length]; - }; + const getColor = (lectureId: string): string => { + const lectures = [...new Set(schedules.map(({ lecture }) => lecture.id))] + const colors = ["#fdd", "#ffd", "#dff", "#ddf", "#fdf", "#dfd"] + return colors[lectures.indexOf(lectureId) % colors.length] + } - const dndContext = useDndContext(); + const dndContext = useDndContext() const getActiveTableId = () => { - const activeId = dndContext.active?.id; + const activeId = dndContext.active?.id if (activeId) { - return String(activeId).split(":")[0]; + return String(activeId).split(":")[0] } - return null; + return null } - const activeTableId = getActiveTableId(); + const activeTableId = getActiveTableId() - const handleScheduleTimeClick = useAutoCallback( - (timeInfo: { day: string; time: number }) => { - onScheduleTimeClick?.(timeInfo); - }, - ); + const handleScheduleTimeClick = useAutoCallback((timeInfo: { day: string; time: number }) => { + onScheduleTimeClick?.(timeInfo) + }) - const handleDeleteButtonClick = useAutoCallback( - (day: string, time: number) => { - onDeleteButtonClick?.({ - day, - time, - }); - }, - ); + const handleDeleteButtonClick = useAutoCallback((day: string, time: number) => { + onDeleteButtonClick?.({ + day, + time, + }) + }) return ( - + {schedules.map((schedule, index) => ( @@ -76,57 +68,61 @@ const ScheduleTable = ({ tableId, schedules, onScheduleTimeClick, onDeleteButton /> ))} - ); -}; + ) +} -const DraggableSchedule = memo(({ - id, - data, - bg, - onDeleteButtonClick -}: { id: string; data: Schedule } & ComponentProps & { - onDeleteButtonClick: (day: string, time: number) => void; -})=> { - const { day, range, room, lecture } = data; - const { attributes, setNodeRef, listeners, transform } = useDraggable({ id }); - const leftIndex = DAY_LABELS.indexOf(day as typeof DAY_LABELS[number]); - const topIndex = range[0] - 1; - const size = range.length; +const DraggableSchedule = memo( + ({ + id, + data, + bg, + onDeleteButtonClick, + }: { id: string; data: Schedule } & ComponentProps & { + onDeleteButtonClick: (day: string, time: number) => void + }) => { + const { day, range, room, lecture } = data + const { attributes, setNodeRef, listeners, transform } = useDraggable({ id }) + const leftIndex = DAY_LABELS.indexOf(day as (typeof DAY_LABELS)[number]) + const topIndex = range[0] - 1 + const size = range.length - return ( - - - - {lecture.title} - {room} - - - event.stopPropagation()}> - - - - 강의를 삭제하시겠습니까? - - - - - ); -}); + return ( + + + + + {lecture.title} + + {room} + + + event.stopPropagation()}> + + + + 강의를 삭제하시겠습니까? + + + + + ) + }, +) -export default ScheduleTable; +export default ScheduleTable diff --git a/src/component/schedule/ScheduleTableGrid.tsx b/src/component/schedule/ScheduleTableGrid.tsx index 0de2a839..301fc4e1 100644 --- a/src/component/schedule/ScheduleTableGrid.tsx +++ b/src/component/schedule/ScheduleTableGrid.tsx @@ -1,66 +1,62 @@ -import { memo, Fragment } from "react"; -import { Grid, GridItem, Flex, Text } from "@chakra-ui/react"; -import { CellSize, DAY_LABELS } from "../../constants.ts"; -import {fill2, TIMES} from "../../utils.ts"; +import { memo, Fragment } from "react" +import { Grid, GridItem, Flex, Text } from "@chakra-ui/react" +import { CellSize, DAY_LABELS } from "../../constants.ts" +import { fill2, TIMES } from "../../utils.ts" type ScheduleGridProps = { - onScheduleTimeClick?: (timeInfo: { day: string; time: number }) => void; -}; + onScheduleTimeClick?: (timeInfo: { day: string; time: number }) => void +} -const ScheduleTableGrid = ({ onScheduleTimeClick }: ScheduleGridProps)=> { - return ( - - - - 교시 - - +const ScheduleTableGrid = ({ onScheduleTimeClick }: ScheduleGridProps) => { + return ( + + + + 교시 + + - {DAY_LABELS.map((day) => ( - - - {day} - - - ))} + {DAY_LABELS.map((day) => ( + + + {day} + + + ))} - {TIMES.map((time, timeIndex) => ( - - 17 ? "gray.200" : "gray.100"} - > - - - {fill2(timeIndex + 1)} ({time}) - - - + {TIMES.map((time, timeIndex) => ( + + 17 ? "gray.200" : "gray.100"}> + + + {fill2(timeIndex + 1)} ({time}) + + + - {DAY_LABELS.map((day) => ( - 17 ? "gray.100" : "white"} - cursor="pointer" - _hover={{ bg: "yellow.100" }} - onClick={() => onScheduleTimeClick?.({ day, time: timeIndex + 1 })} - /> - ))} - - ))} - - ); + {DAY_LABELS.map((day) => ( + 17 ? "gray.100" : "white"} + cursor="pointer" + _hover={{ bg: "yellow.100" }} + onClick={() => onScheduleTimeClick?.({ day, time: timeIndex + 1 })} + /> + ))} + + ))} + + ) } -export default memo(ScheduleTableGrid); \ No newline at end of file +export default memo(ScheduleTableGrid) diff --git a/src/component/schedule/ScheduleTableWrapper.tsx b/src/component/schedule/ScheduleTableWrapper.tsx index 4f947ec3..312df216 100644 --- a/src/component/schedule/ScheduleTableWrapper.tsx +++ b/src/component/schedule/ScheduleTableWrapper.tsx @@ -1,66 +1,72 @@ -import {memo, useCallback} from "react"; -import {Button, ButtonGroup, Flex, Heading, Stack} from "@chakra-ui/react"; -import ScheduleDndProvider from "../../provider/ScheduleDndProvider.tsx"; -import ScheduleTable from "./ScheduleTable.tsx"; -import {Schedule} from "../../types.ts"; +import { memo, useCallback } from "react" +import { Button, ButtonGroup, Flex, Heading, Stack } from "@chakra-ui/react" +import ScheduleDndProvider from "../../provider/ScheduleDndProvider.tsx" +import ScheduleTable from "./ScheduleTable.tsx" +import { Schedule } from "../../types.ts" type ScheduleTableWrapperProps = { - tableId: string; - index: number; - schedules: Schedule[]; - disabledRemoveButton: boolean; - onOpenSearch: (tableId: string, extra?: { day?: string; time?: number }) => void; - onDuplicate: (tableId: string) => void; - onRemove: (tableId: string) => void; - onDeleteBlock: (tableId: string, day: string, time: number) => void; -}; + tableId: string + index: number + schedules: Schedule[] + disabledRemoveButton: boolean + onOpenSearch: (tableId: string, extra?: { day?: string; time?: number }) => void + onDuplicate: (tableId: string) => void + onRemove: (tableId: string) => void + onDeleteBlock: (tableId: string, day: string, time: number) => void +} const ScheduleTableWrapper = ({ - tableId, - index, - schedules, - disabledRemoveButton, - onOpenSearch, - onDuplicate, - onRemove, - onDeleteBlock, - }: ScheduleTableWrapperProps ) => { - const openSearch = useCallback( - (extra?: { day?: string; time?: number }) => onOpenSearch(tableId, extra), - [onOpenSearch, tableId] - ); + tableId, + index, + schedules, + disabledRemoveButton, + onOpenSearch, + onDuplicate, + onRemove, + onDeleteBlock, +}: ScheduleTableWrapperProps) => { + const openSearch = useCallback( + (extra?: { day?: string; time?: number }) => onOpenSearch(tableId, extra), + [onOpenSearch, tableId], + ) - const duplicate = useCallback(() => onDuplicate(tableId), [onDuplicate, tableId]); - const remove = useCallback(() => onRemove(tableId), [onRemove, tableId]); + const duplicate = useCallback(() => onDuplicate(tableId), [onDuplicate, tableId]) + const remove = useCallback(() => onRemove(tableId), [onRemove, tableId]) - const handleDeleteBlock = useCallback( - ({ day, time }: { day: string; time: number }) => onDeleteBlock(tableId, day, time), - [onDeleteBlock, tableId] - ); + const handleDeleteBlock = useCallback( + ({ day, time }: { day: string; time: number }) => onDeleteBlock(tableId, day, time), + [onDeleteBlock, tableId], + ) - return ( - - - 시간표 {index + 1} - - - - - - - - openSearch(timeInfo)} - onDeleteButtonClick={handleDeleteBlock} - /> - - - ) -}; - -export default memo(ScheduleTableWrapper); + return ( + + + + 시간표 {index + 1} + + + + + + + + + openSearch(timeInfo)} + onDeleteButtonClick={handleDeleteBlock} + /> + + + ) +} +export default memo(ScheduleTableWrapper) diff --git a/src/component/schedule/ScheduleTables.tsx b/src/component/schedule/ScheduleTables.tsx index e9a00f1d..91120696 100644 --- a/src/component/schedule/ScheduleTables.tsx +++ b/src/component/schedule/ScheduleTables.tsx @@ -1,49 +1,69 @@ -import { Flex } from "@chakra-ui/react"; -import SearchDialog from "../search/SearchDialog.tsx"; -import { useCallback, useState } from "react"; -import ScheduleTableWrapper from "./ScheduleTableWrapper.tsx"; -import { useScheduleState, useScheduleActions } from "../../provider/ScheduleContext.tsx"; +import { Flex } from "@chakra-ui/react" +import SearchDialog from "../search/SearchDialog.tsx" +import { useCallback, useState } from "react" +import ScheduleTableWrapper from "./ScheduleTableWrapper.tsx" +import { useScheduleState, useScheduleActions } from "../../provider/ScheduleContext.tsx" export const ScheduleTables = () => { - const schedulesMap = useScheduleState(); - const { setSchedulesMap, onDeleteScheduleButtonClick } = useScheduleActions(); + const schedulesMap = useScheduleState() + const { setSchedulesMap, onDeleteScheduleButtonClick } = useScheduleActions() const [searchInfo, setSearchInfo] = useState<{ - tableId: string; - day?: string; - time?: number; - } | null>(null); + tableId: string + day?: string + time?: number + } | null>(null) - const disabledRemoveButton = Object.keys(schedulesMap).length === 1; + const disabledRemoveButton = Object.keys(schedulesMap).length === 1 - const duplicate = useCallback((targetId: string) => { - setSchedulesMap(prev => ({ - ...prev, - [`schedule-${Date.now()}`]: [...prev[targetId]] - })); - }, [setSchedulesMap]); + const duplicate = useCallback( + (targetId: string) => { + setSchedulesMap((prev) => ({ + ...prev, + [`schedule-${Date.now()}`]: [...prev[targetId]], + })) + }, + [setSchedulesMap], + ) - const remove = useCallback((targetId: string) => { - setSchedulesMap(prev => { - const { [targetId]: _, ...rest } = prev; - return rest; - }); - }, [setSchedulesMap]); + const remove = useCallback( + (targetId: string) => { + setSchedulesMap((prev) => { + const { [targetId]: _, ...rest } = prev + return rest + }) + }, + [setSchedulesMap], + ) const onOpenSearch = useCallback((tableId: string, extra?: { day?: string; time?: number }) => { - setSearchInfo({ tableId, ...extra }); - }, []); + setSearchInfo({ tableId, ...extra }) + }, []) - const onDeleteBlock = useCallback((tableId: string, day: string, time: number) => { - onDeleteScheduleButtonClick(tableId, day, time)}, [onDeleteScheduleButtonClick]); + const onDeleteBlock = useCallback( + (tableId: string, day: string, time: number) => { + onDeleteScheduleButtonClick(tableId, day, time) + }, + [onDeleteScheduleButtonClick], + ) return ( <> {Object.entries(schedulesMap).map(([tableId, schedules], index) => ( - + ))} - setSearchInfo(null)}/> + setSearchInfo(null)} /> - ); + ) } diff --git a/src/component/search/SearchDialog.tsx b/src/component/search/SearchDialog.tsx index 4c420743..c3b0cc4c 100644 --- a/src/component/search/SearchDialog.tsx +++ b/src/component/search/SearchDialog.tsx @@ -1,4 +1,4 @@ -import {useEffect, useRef, useState, useMemo, useCallback} from "react"; +import { useEffect, useRef, useState, useMemo, useCallback } from "react" import { Box, HStack, @@ -12,196 +12,193 @@ import { Tbody, Text, VStack, -} from "@chakra-ui/react"; -import { useScheduleActions } from "../../provider/ScheduleContext.tsx"; -import { Lecture } from "../../types.ts"; -import { parseSchedule } from "../../utils.ts"; -import axios, {AxiosResponse} from "axios"; -import SearchItem from "./SearchItem.tsx"; -import { useAutoCallback } from "../../hooks/useAutoCallback.ts"; -import SearchTableHead from "./SearchTableHead.tsx"; -import MajorFilter from "./filter/MajorFilter.tsx"; -import DayFilter from "./filter/DayFilter.tsx"; -import GradeFilter from "./filter/GradeFilter.tsx"; -import PeriodTimeFilter from "./filter/PeriodTimeFilter.tsx"; -import CreditFilter from "./filter/CreditFilter.tsx"; -import QueryFilter from "./filter/QueryFilter.tsx"; -import { SearchOption } from "../../types.ts"; +} from "@chakra-ui/react" +import { useScheduleActions } from "../../provider/ScheduleContext.tsx" +import { Lecture } from "../../types.ts" +import { parseSchedule } from "../../utils.ts" +import axios, { AxiosResponse } from "axios" +import SearchItem from "./SearchItem.tsx" +import { useAutoCallback } from "../../hooks/useAutoCallback.ts" +import SearchTableHead from "./SearchTableHead.tsx" +import MajorFilter from "./filter/MajorFilter.tsx" +import DayFilter from "./filter/DayFilter.tsx" +import GradeFilter from "./filter/GradeFilter.tsx" +import PeriodTimeFilter from "./filter/PeriodTimeFilter.tsx" +import CreditFilter from "./filter/CreditFilter.tsx" +import QueryFilter from "./filter/QueryFilter.tsx" +import { SearchOption } from "../../types.ts" interface Props { searchInfo: { - tableId: string; - day?: string; - time?: number; - } | null; - onClose: () => void; + tableId: string + day?: string + time?: number + } | null + onClose: () => void } -const PAGE_SIZE = 100; -const base = - process.env.NODE_ENV === "production" ? "/front_6th_chapter4-2/" : "/"; +const PAGE_SIZE = 100 +const base = process.env.NODE_ENV === "production" ? "/front_6th_chapter4-2/" : "/" -const fetchMajors = () => axios.get(`${base}schedules-majors.json`); +const fetchMajors = () => axios.get(`${base}schedules-majors.json`) const fetchLiberalArts = () => axios.get(`${base}schedules-liberal-arts.json`) -let majorsCache: Promise> | null = null; -let liberalArtsCache: Promise> | null = null; +let majorsCache: Promise> | null = null +let liberalArtsCache: Promise> | null = null const fetchAllLectures = async () => { - if (!majorsCache) { - majorsCache = fetchMajors().catch(error => { majorsCache = null; throw error; }); - } - if (!liberalArtsCache) { - liberalArtsCache = fetchLiberalArts().catch(error => { liberalArtsCache = null; throw error; }); - } - - const [majors, liberalArts] = await Promise.all([majorsCache, liberalArtsCache]); - - return Promise.all([ - (console.log("API Call 1", performance.now()), majors), - (console.log("API Call 2", performance.now()), liberalArts), - (console.log("API Call 3", performance.now()), majors), - (console.log("API Call 4", performance.now()), liberalArts), - (console.log("API Call 5", performance.now()), majors), - (console.log("API Call 6", performance.now()), liberalArts), - ]); -}; + if (!majorsCache) { + majorsCache = fetchMajors().catch((error) => { + majorsCache = null + throw error + }) + } + if (!liberalArtsCache) { + liberalArtsCache = fetchLiberalArts().catch((error) => { + liberalArtsCache = null + throw error + }) + } + + const [majors, liberalArts] = await Promise.all([majorsCache, liberalArtsCache]) + + return Promise.all([ + (console.log("API Call 1", performance.now()), majors), + (console.log("API Call 2", performance.now()), liberalArts), + (console.log("API Call 3", performance.now()), majors), + (console.log("API Call 4", performance.now()), liberalArts), + (console.log("API Call 5", performance.now()), majors), + (console.log("API Call 6", performance.now()), liberalArts), + ]) +} const SearchDialog = ({ searchInfo, onClose }: Props) => { - const { setSchedulesMap } = useScheduleActions(); + const { setSchedulesMap } = useScheduleActions() - const loaderWrapperRef = useRef(null); - const loaderRef = useRef(null); - const [lectures, setLectures] = useState([]); - const [page, setPage] = useState(1); + const loaderWrapperRef = useRef(null) + const loaderRef = useRef(null) + const [lectures, setLectures] = useState([]) + const [page, setPage] = useState(1) const [searchOptions, setSearchOptions] = useState({ - query: '', + query: "", grades: [], days: [], times: [], majors: [], - }); + }) const filteredLectures = useMemo(() => { - const {query = '', credits, grades, days, times, majors} = searchOptions; - return lectures - .filter(lecture => - lecture.title.toLowerCase().includes(query.toLowerCase()) || - lecture.id.toLowerCase().includes(query.toLowerCase()) - ) - .filter(lecture => grades.length === 0 || grades.includes(lecture.grade)) - .filter(lecture => majors.length === 0 || majors.includes(lecture.major)) - .filter(lecture => !credits || lecture.credits.startsWith(String(credits))) - .filter(lecture => { - if (days.length === 0) { - return true; - } - const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : []; - return schedules.some(s => days.includes(s.day)); - }) - .filter(lecture => { - if (times.length === 0) { - return true; - } - const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : []; - return schedules.some(s => s.range.some(time => times.includes(time))); - }); - }, [lectures, searchOptions]); - - const lastPage = useMemo( - () => Math.max(1, Math.ceil(filteredLectures.length / PAGE_SIZE)), - [filteredLectures.length] - ); - const visibleLectures = useMemo( - () => filteredLectures.slice(0, page * PAGE_SIZE), - [filteredLectures, page] - ); - const allMajors = useMemo( - () => [...new Set(lectures.map((l) => l.major))], - [lectures] - ); + const { query = "", credits, grades, days, times, majors } = searchOptions + return lectures + .filter( + (lecture) => + lecture.title.toLowerCase().includes(query.toLowerCase()) || + lecture.id.toLowerCase().includes(query.toLowerCase()), + ) + .filter((lecture) => grades.length === 0 || grades.includes(lecture.grade)) + .filter((lecture) => majors.length === 0 || majors.includes(lecture.major)) + .filter((lecture) => !credits || lecture.credits.startsWith(String(credits))) + .filter((lecture) => { + if (days.length === 0) { + return true + } + const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [] + return schedules.some((s) => days.includes(s.day)) + }) + .filter((lecture) => { + if (times.length === 0) { + return true + } + const schedules = lecture.schedule ? parseSchedule(lecture.schedule) : [] + return schedules.some((s) => s.range.some((time) => times.includes(time))) + }) + }, [lectures, searchOptions]) + + const lastPage = useMemo(() => Math.max(1, Math.ceil(filteredLectures.length / PAGE_SIZE)), [filteredLectures.length]) + const visibleLectures = useMemo(() => filteredLectures.slice(0, page * PAGE_SIZE), [filteredLectures, page]) + const allMajors = useMemo(() => [...new Set(lectures.map((l) => l.major))], [lectures]) const changeSearchOption = useAutoCallback((field: keyof SearchOption, value: SearchOption[typeof field]) => { - setPage(1); - setSearchOptions(prev => { - if (prev[field] === value) return prev; - return { ...prev, [field]: value }; - }); - loaderWrapperRef.current?.scrollTo(0, 0); - }); + setPage(1) + setSearchOptions((prev) => { + if (prev[field] === value) return prev + return { ...prev, [field]: value } + }) + loaderWrapperRef.current?.scrollTo(0, 0) + }) const addSchedule = useAutoCallback((lecture: Lecture) => { - if (!searchInfo) return; + if (!searchInfo) return - const { tableId } = searchInfo; + const { tableId } = searchInfo - const schedules = parseSchedule(lecture.schedule).map(schedule => ({ + const schedules = parseSchedule(lecture.schedule).map((schedule) => ({ ...schedule, - lecture - })); + lecture, + })) - setSchedulesMap(prev => ({ + setSchedulesMap((prev) => ({ ...prev, - [tableId]: [...prev[tableId], ...schedules] - })); + [tableId]: [...prev[tableId], ...schedules], + })) - onClose(); - }); + onClose() + }) useEffect(() => { - const start = performance.now(); - console.log('API 호출 시작: ', start) - fetchAllLectures().then(results => { - const end = performance.now(); - console.log('모든 API 호출 완료 ', end) - console.log('API 호출에 걸린 시간(ms): ', end - start); - const [majors, liberalArts] = results; - setLectures([...majors.data, ...liberalArts.data]); + const start = performance.now() + console.log("API 호출 시작: ", start) + fetchAllLectures().then((results) => { + const end = performance.now() + console.log("모든 API 호출 완료 ", end) + console.log("API 호출에 걸린 시간(ms): ", end - start) + const [majors, liberalArts] = results + setLectures([...majors.data, ...liberalArts.data]) }) - }, []); - - // TODO 우선 다른 분 코드를 참고해서 수정한 것이니 꼭 제발 꼭 다시 공부하기 - const observerRef = useRef(null); + }, []) - const setLoaderWrapperRef = useCallback( - (node: HTMLDivElement | null) => { - if (!node) return; // unmount 시 null 들어옴 + // TODO 우선 다른 분 코드를 참고해서 수정한 것이니 꼭 제발 꼭 다시 공부하기 + const observerRef = useRef(null) - const $loader = loaderRef.current; - if (!$loader) return; + const setLoaderWrapperRef = useCallback( + (node: HTMLDivElement | null) => { + if (!node) return // unmount 시 null 들어옴 - observerRef.current?.unobserve($loader); + const $loader = loaderRef.current + if (!$loader) return - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - setPage((prev) => Math.min(lastPage, prev + 1)); - } - }, - { root: node } - ); + observerRef.current?.unobserve($loader) - observer.observe($loader); - observerRef.current = observer; + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setPage((prev) => Math.min(lastPage, prev + 1)) + } }, - [lastPage] - ); + { root: node }, + ) + + observer.observe($loader) + observerRef.current = observer + }, + [lastPage], + ) useEffect(() => { - setSearchOptions(prev => ({ + setSearchOptions((prev) => ({ ...prev, days: searchInfo?.day ? [searchInfo.day] : [], times: searchInfo?.time ? [searchInfo.time] : [], })) - setPage(1); - }, [searchInfo]); + setPage(1) + }, [searchInfo]) return ( - + 수업 검색 - + @@ -210,17 +207,19 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { - - + + - - + + - - 검색결과: {filteredLectures.length}개 - + 검색결과: {filteredLectures.length}개
@@ -234,14 +233,14 @@ const SearchDialog = ({ searchInfo, onClose }: Props) => { ))}
- + - ); -}; + ) +} -export default SearchDialog; \ No newline at end of file +export default SearchDialog diff --git a/src/component/search/SearchItem.tsx b/src/component/search/SearchItem.tsx index 97dac308..f7b444ae 100644 --- a/src/component/search/SearchItem.tsx +++ b/src/component/search/SearchItem.tsx @@ -1,25 +1,25 @@ -import { Button, Td, Tr } from "@chakra-ui/react"; -import { memo } from "react"; -import { Lecture } from "../../types.ts"; +import { Button, Td, Tr } from "@chakra-ui/react" +import { memo } from "react" +import { Lecture } from "../../types.ts" const SearchItem = memo(({ lecture, addSchedule }: { lecture: Lecture; addSchedule: (lecture: Lecture) => void }) => { - return ( - - {lecture.id} - {lecture.grade} - {lecture.title} - {lecture.credits} - - - - - - - ); -}); + return ( + + {lecture.id} + {lecture.grade} + {lecture.title} + {lecture.credits} + + + + + + + ) +}) -SearchItem.displayName = "SearchItem"; +SearchItem.displayName = "SearchItem" -export default SearchItem; \ No newline at end of file +export default SearchItem diff --git a/src/component/search/SearchTableHead.tsx b/src/component/search/SearchTableHead.tsx index 704c2a92..c8935dee 100644 --- a/src/component/search/SearchTableHead.tsx +++ b/src/component/search/SearchTableHead.tsx @@ -1,19 +1,19 @@ -import { Thead, Tr, Th } from "@chakra-ui/react"; -import { memo } from "react"; +import { Thead, Tr, Th } from "@chakra-ui/react" +import { memo } from "react" const SearchTableHead = memo(() => ( - - - 과목코드 - 학년 - 과목명 - 학점 - 전공 - 시간 - - - -)); + + + 과목코드 + 학년 + 과목명 + 학점 + 전공 + 시간 + + + +)) -SearchTableHead.displayName = "SearchTableHead"; -export default SearchTableHead; \ No newline at end of file +SearchTableHead.displayName = "SearchTableHead" +export default SearchTableHead diff --git a/src/component/search/filter/CreditFilter.tsx b/src/component/search/filter/CreditFilter.tsx index 9855929f..b90b75ff 100644 --- a/src/component/search/filter/CreditFilter.tsx +++ b/src/component/search/filter/CreditFilter.tsx @@ -1,30 +1,26 @@ -import { FormControl, FormLabel, Select} from "@chakra-ui/react"; -import { SearchOption } from "../../../types.ts"; -import { memo } from "react"; - +import { FormControl, FormLabel, Select } from "@chakra-ui/react" +import { SearchOption } from "../../../types.ts" +import { memo } from "react" type CreditFilterProps = { - credits: SearchOption['credits']; - changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; + credits: SearchOption["credits"] + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void } const CreditFilter = ({ credits, changeSearchOption }: CreditFilterProps) => { - return ( - - 학점 - - - ) + return ( + + 학점 + + + ) } -CreditFilter.displayName = "CreditFilter"; +CreditFilter.displayName = "CreditFilter" -export default memo(CreditFilter); \ No newline at end of file +export default memo(CreditFilter) diff --git a/src/component/search/filter/DayFilter.tsx b/src/component/search/filter/DayFilter.tsx index a352dd47..56af8a14 100644 --- a/src/component/search/filter/DayFilter.tsx +++ b/src/component/search/filter/DayFilter.tsx @@ -1,31 +1,30 @@ -import { Checkbox, CheckboxGroup, FormControl, FormLabel, HStack } from "@chakra-ui/react"; -import { DAY_LABELS } from "../../../constants.ts"; -import { SearchOption } from "../../../types.ts"; -import { memo } from "react"; +import { Checkbox, CheckboxGroup, FormControl, FormLabel, HStack } from "@chakra-ui/react" +import { DAY_LABELS } from "../../../constants.ts" +import { SearchOption } from "../../../types.ts" +import { memo } from "react" type DayFilterProps = { - days: SearchOption['days']; - changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; + days: SearchOption["days"] + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void } const DayFilter = ({ days, changeSearchOption }: DayFilterProps) => { - return ( - - 요일 - changeSearchOption('days', value as string[])} - > - - {DAY_LABELS.map(day => ( - {day} - ))} - - - - ) + return ( + + 요일 + changeSearchOption("days", value as string[])}> + + {DAY_LABELS.map((day) => ( + + {day} + + ))} + + + + ) } -DayFilter.displayName="DayFilter"; +DayFilter.displayName = "DayFilter" -export default memo(DayFilter); \ No newline at end of file +export default memo(DayFilter) diff --git a/src/component/search/filter/GradeFilter.tsx b/src/component/search/filter/GradeFilter.tsx index 83dde48b..6f868143 100644 --- a/src/component/search/filter/GradeFilter.tsx +++ b/src/component/search/filter/GradeFilter.tsx @@ -1,31 +1,29 @@ -import {Checkbox, CheckboxGroup, FormControl, FormLabel, HStack} from "@chakra-ui/react"; -import {SearchOption} from "../../../types.ts"; -import {memo} from "react"; - +import { Checkbox, CheckboxGroup, FormControl, FormLabel, HStack } from "@chakra-ui/react" +import { SearchOption } from "../../../types.ts" +import { memo } from "react" type GradeFilterProps = { - grades: SearchOption['grades']; - changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; + grades: SearchOption["grades"] + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void } const GradeFilter = ({ grades, changeSearchOption }: GradeFilterProps) => { - return ( - - 학년 - changeSearchOption('grades', value.map(Number))} - > - - {[1, 2, 3, 4].map(grade => ( - {grade}학년 - ))} - - - - ) + return ( + + 학년 + changeSearchOption("grades", value.map(Number))}> + + {[1, 2, 3, 4].map((grade) => ( + + {grade}학년 + + ))} + + + + ) } -GradeFilter.displayName="GradeFilter"; +GradeFilter.displayName = "GradeFilter" -export default memo(GradeFilter); \ No newline at end of file +export default memo(GradeFilter) diff --git a/src/component/search/filter/MajorFilter.tsx b/src/component/search/filter/MajorFilter.tsx index bf480e4a..c42ae8f3 100644 --- a/src/component/search/filter/MajorFilter.tsx +++ b/src/component/search/filter/MajorFilter.tsx @@ -1,57 +1,62 @@ import { - Box, - Checkbox, - CheckboxGroup, - FormControl, - FormLabel, - Stack, - Tag, - TagCloseButton, - TagLabel, - Wrap -} from "@chakra-ui/react"; -import { memo } from "react"; -import { SearchOption } from "../../../types.ts"; + Box, + Checkbox, + CheckboxGroup, + FormControl, + FormLabel, + Stack, + Tag, + TagCloseButton, + TagLabel, + Wrap, +} from "@chakra-ui/react" +import { memo } from "react" +import { SearchOption } from "../../../types.ts" type MajorFilterProps = { - majors: SearchOption['majors']; - allMajors: string[]; - changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; + majors: SearchOption["majors"] + allMajors: string[] + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void } const MajorFilter = ({ majors, allMajors, changeSearchOption }: MajorFilterProps) => { - return ( - - 전공 - changeSearchOption('majors', values as string[])} - > - - {majors.map(major => ( - - {major.split("

").pop()} - changeSearchOption('majors', majors.filter(v => v !== major))}/> - - ))} - - - {allMajors.map(major => ( - - - {major.replace(/

/gi, ' ')} - - - ))} - - - - ) + return ( + + 전공 + changeSearchOption("majors", values as string[])} + > + + {majors.map((major) => ( + + {major.split("

").pop()} + + changeSearchOption( + "majors", + majors.filter((v) => v !== major), + ) + } + /> + + ))} + + + {allMajors.map((major) => ( + + + {major.replace(/

/gi, " ")} + + + ))} + + + + ) } -MajorFilter.displayName = "MajorFilter"; +MajorFilter.displayName = "MajorFilter" -export default memo(MajorFilter); +export default memo(MajorFilter) diff --git a/src/component/search/filter/PeriodTimeFilter.tsx b/src/component/search/filter/PeriodTimeFilter.tsx index 930ff199..73fbd926 100644 --- a/src/component/search/filter/PeriodTimeFilter.tsx +++ b/src/component/search/filter/PeriodTimeFilter.tsx @@ -1,58 +1,64 @@ import { - Box, - Checkbox, - CheckboxGroup, - FormControl, - FormLabel, - Stack, - Tag, - TagCloseButton, - TagLabel, - Wrap -} from "@chakra-ui/react"; -import { SearchOption } from "../../../types.ts"; -import { memo } from "react"; -import { TIME_SLOTS } from "../../../constants.ts"; - + Box, + Checkbox, + CheckboxGroup, + FormControl, + FormLabel, + Stack, + Tag, + TagCloseButton, + TagLabel, + Wrap, +} from "@chakra-ui/react" +import { SearchOption } from "../../../types.ts" +import { memo } from "react" +import { TIME_SLOTS } from "../../../constants.ts" type PeriodTimeFilterProps = { - times: SearchOption['times']; - changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; + times: SearchOption["times"] + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void } const PeriodTimeFilter = ({ times, changeSearchOption }: PeriodTimeFilterProps) => { - return ( - - 시간 - changeSearchOption('times', values.map(Number))} - > - - {[...times].sort((a, b) => a - b).map(time => ( - - {time}교시 - changeSearchOption('times', times.filter(v => v !== time))}/> - - ))} - - - {TIME_SLOTS.map(({ id, label }) => ( - - - {id}교시({label}) - - - ))} - - - - ) + return ( + + 시간 + changeSearchOption("times", values.map(Number))} + > + + {[...times] + .sort((a, b) => a - b) + .map((time) => ( + + {time}교시 + + changeSearchOption( + "times", + times.filter((v) => v !== time), + ) + } + /> + + ))} + + + {TIME_SLOTS.map(({ id, label }) => ( + + + {id}교시({label}) + + + ))} + + + + ) } -PeriodTimeFilter.displayName="PeriodTimeFilter"; +PeriodTimeFilter.displayName = "PeriodTimeFilter" -export default memo(PeriodTimeFilter); \ No newline at end of file +export default memo(PeriodTimeFilter) diff --git a/src/component/search/filter/QueryFilter.tsx b/src/component/search/filter/QueryFilter.tsx index cf86f981..9fdd9d4d 100644 --- a/src/component/search/filter/QueryFilter.tsx +++ b/src/component/search/filter/QueryFilter.tsx @@ -1,25 +1,25 @@ -import { FormControl, FormLabel, Input } from "@chakra-ui/react"; -import { SearchOption } from "../../../types.ts"; -import { memo } from "react"; +import { FormControl, FormLabel, Input } from "@chakra-ui/react" +import { SearchOption } from "../../../types.ts" +import { memo } from "react" type QueryFilterProps = { - query: SearchOption['query']; - changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void; + query: SearchOption["query"] + changeSearchOption: (field: keyof SearchOption, value: SearchOption[typeof field]) => void } const QueryFilter = ({ query, changeSearchOption }: QueryFilterProps) => { - return ( - - 검색어 - changeSearchOption('query', e.target.value)} - /> - - ) + return ( + + 검색어 + changeSearchOption("query", e.target.value)} + /> + + ) } -QueryFilter.displayName="QueryFilter"; +QueryFilter.displayName = "QueryFilter" -export default memo(QueryFilter); \ No newline at end of file +export default memo(QueryFilter) diff --git a/src/constants.ts b/src/constants.ts index 113d3c17..be794b78 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,36 +1,36 @@ -export const DAY_LABELS = ["월", "화", "수", "목", "금", "토"] as const; +export const DAY_LABELS = ["월", "화", "수", "목", "금", "토"] as const export const CellSize = { WIDTH: 80, HEIGHT: 30, -}; +} -export const 초 = 1000; -export const 분 = 60 * 초; +export const 초 = 1000 +export const 분 = 60 * 초 -export const TIME_SLOTS = [ - { id: 1, label: "09:00~09:30" }, - { id: 2, label: "09:30~10:00" }, - { id: 3, label: "10:00~10:30" }, - { id: 4, label: "10:30~11:00" }, - { id: 5, label: "11:00~11:30" }, - { id: 6, label: "11:30~12:00" }, - { id: 7, label: "12:00~12:30" }, - { id: 8, label: "12:30~13:00" }, - { id: 9, label: "13:00~13:30" }, - { id: 10, label: "13:30~14:00" }, - { id: 11, label: "14:00~14:30" }, - { id: 12, label: "14:30~15:00" }, - { id: 13, label: "15:00~15:30" }, - { id: 14, label: "15:30~16:00" }, - { id: 15, label: "16:00~16:30" }, - { id: 16, label: "16:30~17:00" }, - { id: 17, label: "17:00~17:30" }, - { id: 18, label: "17:30~18:00" }, - { id: 19, label: "18:00~18:50" }, - { id: 20, label: "18:55~19:45" }, - { id: 21, label: "19:50~20:40" }, - { id: 22, label: "20:45~21:35" }, - { id: 23, label: "21:40~22:30" }, - { id: 24, label: "22:35~23:25" }, -]; \ No newline at end of file +export const TIME_SLOTS = [ + { id: 1, label: "09:00~09:30" }, + { id: 2, label: "09:30~10:00" }, + { id: 3, label: "10:00~10:30" }, + { id: 4, label: "10:30~11:00" }, + { id: 5, label: "11:00~11:30" }, + { id: 6, label: "11:30~12:00" }, + { id: 7, label: "12:00~12:30" }, + { id: 8, label: "12:30~13:00" }, + { id: 9, label: "13:00~13:30" }, + { id: 10, label: "13:30~14:00" }, + { id: 11, label: "14:00~14:30" }, + { id: 12, label: "14:30~15:00" }, + { id: 13, label: "15:00~15:30" }, + { id: 14, label: "15:30~16:00" }, + { id: 15, label: "16:00~16:30" }, + { id: 16, label: "16:30~17:00" }, + { id: 17, label: "17:00~17:30" }, + { id: 18, label: "17:30~18:00" }, + { id: 19, label: "18:00~18:50" }, + { id: 20, label: "18:55~19:45" }, + { id: 21, label: "19:50~20:40" }, + { id: 22, label: "20:45~21:35" }, + { id: 23, label: "21:40~22:30" }, + { id: 24, label: "22:35~23:25" }, +] diff --git a/src/dummyScheduleMap.ts b/src/dummyScheduleMap.ts index debdcb78..c013522e 100644 --- a/src/dummyScheduleMap.ts +++ b/src/dummyScheduleMap.ts @@ -1,746 +1,495 @@ export default { "schedule-1": [ { - "day": "월", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "2공521", - "lecture": { - "id": "529540", - "title": "SW융합코딩1", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "월1~6(2공521)", - "grade": 1 - } - }, - { - "day": "화", - "range": [ - 1, - 2, - 3 - ], - "room": "미디어509", - "lecture": { - "id": "527790", - "title": "객체지향프로그래밍(SW)", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "화1~3(미디어509)

목1~3(미디어509)", - "grade": 2 - } - }, - { - "day": "목", - "range": [ - 1, - 2, - 3 - ], - "room": "미디어509", - "lecture": { - "id": "527790", - "title": "객체지향프로그래밍(SW)", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "화1~3(미디어509)

목1~3(미디어509)", - "grade": 2 - } - }, - { - "day": "수", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "소프트304", - "lecture": { - "id": "540970", - "title": "파이썬프로그래밍(SW융합)", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "수1~6(소프트304)", - "grade": 2 - } - }, - { - "day": "금", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "2공524", - "lecture": { - "id": "359210", - "title": "선형대수", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "금1~6(2공524)", - "grade": 2 - } - }, - { - "day": "목", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18 - ], - "room": "소프트414", - "lecture": { - "id": "548310", - "title": "실무중심종합설계프로젝트(티맥스)", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "목1~18(소프트414)

토1~18(소프트414)", - "grade": 3 - } - }, - { - "day": "토", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18 - ], - "room": "소프트414", - "lecture": { - "id": "548310", - "title": "실무중심종합설계프로젝트(티맥스)", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "목1~18(소프트414)

토1~18(소프트414)", - "grade": 3 - } - } + day: "월", + range: [1, 2, 3, 4, 5, 6], + room: "2공521", + lecture: { + id: "529540", + title: "SW융합코딩1", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "월1~6(2공521)", + grade: 1, + }, + }, + { + day: "화", + range: [1, 2, 3], + room: "미디어509", + lecture: { + id: "527790", + title: "객체지향프로그래밍(SW)", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "화1~3(미디어509)

목1~3(미디어509)", + grade: 2, + }, + }, + { + day: "목", + range: [1, 2, 3], + room: "미디어509", + lecture: { + id: "527790", + title: "객체지향프로그래밍(SW)", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "화1~3(미디어509)

목1~3(미디어509)", + grade: 2, + }, + }, + { + day: "수", + range: [1, 2, 3, 4, 5, 6], + room: "소프트304", + lecture: { + id: "540970", + title: "파이썬프로그래밍(SW융합)", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "수1~6(소프트304)", + grade: 2, + }, + }, + { + day: "금", + range: [1, 2, 3, 4, 5, 6], + room: "2공524", + lecture: { + id: "359210", + title: "선형대수", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "금1~6(2공524)", + grade: 2, + }, + }, + { + day: "목", + range: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], + room: "소프트414", + lecture: { + id: "548310", + title: "실무중심종합설계프로젝트(티맥스)", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "목1~18(소프트414)

토1~18(소프트414)", + grade: 3, + }, + }, + { + day: "토", + range: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], + room: "소프트414", + lecture: { + id: "548310", + title: "실무중심종합설계프로젝트(티맥스)", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "목1~18(소프트414)

토1~18(소프트414)", + grade: 3, + }, + }, ], "schedule-2": [ { - "day": "월", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "국제205_PC", - "lecture": { - "id": "525770", - "title": "자료구조기초및실습", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "월1~6(국제205_PC)", - "grade": 2 - } - }, - { - "day": "화", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "소프트227", - "lecture": { - "id": "372460", - "title": "알고리즘", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "화1~6(소프트227)", - "grade": 3 - } - }, - { - "day": "수", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "room": "2공524", - "lecture": { - "id": "388600", - "title": "인공지능", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "수1~9(2공524)", - "grade": 3 - } - }, - { - "day": "목", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "소프트516", - "lecture": { - "id": "524820", - "title": "오픈소스SW활용", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "목1~6(소프트516)", - "grade": 3 - } - }, - { - "day": "금", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11 - ], - "room": "소프트414", - "lecture": { - "id": "548300", - "title": "인공지능입문및실습(티맥스)", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "금1~11(소프트414)", - "grade": 3 - } - }, - { - "day": "토", - "range": [ - 1, - 2 - ], - "room": "", - "lecture": { - "id": "451150", - "title": "노래-목소리3", - "credits": "1(0)", - "major": "음악·예술대학

공연영화학부 뮤지컬전공", - "schedule": "토1~2", - "grade": 3 - } - } + day: "월", + range: [1, 2, 3, 4, 5, 6], + room: "국제205_PC", + lecture: { + id: "525770", + title: "자료구조기초및실습", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "월1~6(국제205_PC)", + grade: 2, + }, + }, + { + day: "화", + range: [1, 2, 3, 4, 5, 6], + room: "소프트227", + lecture: { + id: "372460", + title: "알고리즘", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "화1~6(소프트227)", + grade: 3, + }, + }, + { + day: "수", + range: [1, 2, 3, 4, 5, 6, 7, 8, 9], + room: "2공524", + lecture: { + id: "388600", + title: "인공지능", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "수1~9(2공524)", + grade: 3, + }, + }, + { + day: "목", + range: [1, 2, 3, 4, 5, 6], + room: "소프트516", + lecture: { + id: "524820", + title: "오픈소스SW활용", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "목1~6(소프트516)", + grade: 3, + }, + }, + { + day: "금", + range: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + room: "소프트414", + lecture: { + id: "548300", + title: "인공지능입문및실습(티맥스)", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "금1~11(소프트414)", + grade: 3, + }, + }, + { + day: "토", + range: [1, 2], + room: "", + lecture: { + id: "451150", + title: "노래-목소리3", + credits: "1(0)", + major: "음악·예술대학

공연영화학부 뮤지컬전공", + schedule: "토1~2", + grade: 3, + }, + }, ], "schedule-3": [ { - "day": "월", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11 - ], - "room": "소프트414", - "lecture": { - "id": "548290", - "title": "운영체제및실습(티맥스)", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "월1~11(소프트414)", - "grade": 4 - } - }, - { - "day": "화", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11 - ], - "room": "소프트414", - "lecture": { - "id": "548280", - "title": "데이터베이스와SQL실습(티맥스)", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합경제경영전공", - "schedule": "화1~11(소프트414)", - "grade": 4 - } - }, - { - "day": "수", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "소프트406", - "lecture": { - "id": "366770", - "title": "시스템분석및설계", - "credits": "3(0)", - "major": "SW융합대학

SW융합학부

SW융합바이오전공", - "schedule": "수1~6(소프트406)", - "grade": 4 - } - }, - { - "day": "목", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "미디어403", - "lecture": { - "id": "539800", - "title": "캡스톤디자인(정보통계)", - "credits": "3(0)", - "major": "SW융합대학

정보통계학과", - "schedule": "목1~6(미디어403)", - "grade": 4 - } - }, - { - "day": "금", - "range": [ - 1, - 2 - ], - "room": "치114", - "lecture": { - "id": "394090", - "title": "임상보철학2", - "credits": "1(0)", - "major": "치과대학

치의학과", - "schedule": "금1~2(치114)", - "grade": 4 - } - }, - { - "day": "토", - "range": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "room": "", - "lecture": { - "id": "550040", - "title": "반도체기초공학및산업의이해", - "credits": "3(0)", - "major": "공과대학

반도체WAVE융합전공", - "schedule": "토1~6", - "grade": 4 - } - } + day: "월", + range: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + room: "소프트414", + lecture: { + id: "548290", + title: "운영체제및실습(티맥스)", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "월1~11(소프트414)", + grade: 4, + }, + }, + { + day: "화", + range: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + room: "소프트414", + lecture: { + id: "548280", + title: "데이터베이스와SQL실습(티맥스)", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합경제경영전공", + schedule: "화1~11(소프트414)", + grade: 4, + }, + }, + { + day: "수", + range: [1, 2, 3, 4, 5, 6], + room: "소프트406", + lecture: { + id: "366770", + title: "시스템분석및설계", + credits: "3(0)", + major: "SW융합대학

SW융합학부

SW융합바이오전공", + schedule: "수1~6(소프트406)", + grade: 4, + }, + }, + { + day: "목", + range: [1, 2, 3, 4, 5, 6], + room: "미디어403", + lecture: { + id: "539800", + title: "캡스톤디자인(정보통계)", + credits: "3(0)", + major: "SW융합대학

정보통계학과", + schedule: "목1~6(미디어403)", + grade: 4, + }, + }, + { + day: "금", + range: [1, 2], + room: "치114", + lecture: { + id: "394090", + title: "임상보철학2", + credits: "1(0)", + major: "치과대학

치의학과", + schedule: "금1~2(치114)", + grade: 4, + }, + }, + { + day: "토", + range: [1, 2, 3, 4, 5, 6], + room: "", + lecture: { + id: "550040", + title: "반도체기초공학및산업의이해", + credits: "3(0)", + major: "공과대학

반도체WAVE융합전공", + schedule: "토1~6", + grade: 4, + }, + }, ], "schedule-4": [ { - "day": "월", - "range": [ - 11, - 12, - 13, - 14, - 15, - 16 - ], - "room": "예323", - "lecture": { - "id": "343070", - "title": "문학사세미나", - "credits": "3(0)", - "major": "예술대학

문예창작과", - "schedule": "월11~16(예323)", - "grade": 1 - } - }, - { - "day": "화", - "range": [ - 3, - 4, - 5, - 6, - 7, - 8 - ], - "room": "예018", - "lecture": { - "id": "361960", - "title": "소설창작세미나1", - "credits": "3(0)", - "major": "예술대학

문예창작과", - "schedule": "화3~8(예018)", - "grade": 3 - } - }, - { - "day": "수", - "range": [ - 3, - 4, - 5, - 6, - 7, - 8 - ], - "room": "예술관D동207", - "lecture": { - "id": "533510", - "title": "영상문학의이론과창작", - "credits": "3(0)", - "major": "예술대학

문예창작과", - "schedule": "수3~8(예술관D동207)", - "grade": 2 - } - }, - { - "day": "목", - "range": [ - 3, - 4, - 5, - 6, - 7, - 8 - ], - "room": "예술관D동308", - "lecture": { - "id": "533520", - "title": "비평창작연습", - "credits": "3(0)", - "major": "예술대학

문예창작과", - "schedule": "목3~8(예술관D동308)", - "grade": 3 - } - }, - { - "day": "금", - "range": [ - 3, - 4, - 5, - 6, - 7, - 8 - ], - "room": "예술관D동308", - "lecture": { - "id": "481130", - "title": "소설창작연습", - "credits": "3(0)", - "major": "예술대학

문예창작과", - "schedule": "금3~8(예술관D동308)", - "grade": 2 - } - } + day: "월", + range: [11, 12, 13, 14, 15, 16], + room: "예323", + lecture: { + id: "343070", + title: "문학사세미나", + credits: "3(0)", + major: "예술대학

문예창작과", + schedule: "월11~16(예323)", + grade: 1, + }, + }, + { + day: "화", + range: [3, 4, 5, 6, 7, 8], + room: "예018", + lecture: { + id: "361960", + title: "소설창작세미나1", + credits: "3(0)", + major: "예술대학

문예창작과", + schedule: "화3~8(예018)", + grade: 3, + }, + }, + { + day: "수", + range: [3, 4, 5, 6, 7, 8], + room: "예술관D동207", + lecture: { + id: "533510", + title: "영상문학의이론과창작", + credits: "3(0)", + major: "예술대학

문예창작과", + schedule: "수3~8(예술관D동207)", + grade: 2, + }, + }, + { + day: "목", + range: [3, 4, 5, 6, 7, 8], + room: "예술관D동308", + lecture: { + id: "533520", + title: "비평창작연습", + credits: "3(0)", + major: "예술대학

문예창작과", + schedule: "목3~8(예술관D동308)", + grade: 3, + }, + }, + { + day: "금", + range: [3, 4, 5, 6, 7, 8], + room: "예술관D동308", + lecture: { + id: "481130", + title: "소설창작연습", + credits: "3(0)", + major: "예술대학

문예창작과", + schedule: "금3~8(예술관D동308)", + grade: 2, + }, + }, ], "schedule-5": [ { - "day": "월", - "range": [ - 3, - 4, - 5, - 6 - ], - "room": "의228", - "lecture": { - "id": "432030", - "title": "해부학", - "credits": "2(0)", - "major": "간호대학

간호학과", - "schedule": "월3~6(의228)", - "grade": 1 - } - }, - { - "day": "화", - "range": [ - 1, - 2, - 3 - ], - "room": "의228", - "lecture": { - "id": "323070", - "title": "기본간호학1", - "credits": "3(0)", - "major": "간호대학

간호학과", - "schedule": "화1~3(의228)

목10~12(의228)", - "grade": 2 - } - }, - { - "day": "목", - "range": [ - 7, - 8, - 9 - ], - "room": "의228", - "lecture": { - "id": "323070", - "title": "기본간호학1", - "credits": "3(0)", - "major": "간호대학

간호학과", - "schedule": "화1~3(의228)

목10~12(의228)", - "grade": 2 - } - }, - { - "day": "수", - "range": [ - 1, - 2, - 3, - 4 - ], - "room": "의230", - "lecture": { - "id": "411690", - "title": "지역사회간호학3", - "credits": "2(0)", - "major": "간호대학

간호학과", - "schedule": "수1~4(의230)", - "grade": 4 - } - }, - { - "day": "금", - "range": [ - 1, - 2, - 3 - ], - "room": "인521", - "lecture": { - "id": "409440", - "title": "중급일본어강독1", - "credits": "3(0)", - "major": "외국어대학

아시아중동학부 일본학전공", - "schedule": "화15~17(인521)

목8~10(인424)", - "grade": 2 - } - }, - { - "day": "목", - "range": [ - 1, - 2, - 3 - ], - "room": "인424", - "lecture": { - "id": "409440", - "title": "중급일본어강독1", - "credits": "3(0)", - "major": "외국어대학

아시아중동학부 일본학전공", - "schedule": "화15~17(인521)

목8~10(인424)", - "grade": 2 - } - } + day: "월", + range: [3, 4, 5, 6], + room: "의228", + lecture: { + id: "432030", + title: "해부학", + credits: "2(0)", + major: "간호대학

간호학과", + schedule: "월3~6(의228)", + grade: 1, + }, + }, + { + day: "화", + range: [1, 2, 3], + room: "의228", + lecture: { + id: "323070", + title: "기본간호학1", + credits: "3(0)", + major: "간호대학

간호학과", + schedule: "화1~3(의228)

목10~12(의228)", + grade: 2, + }, + }, + { + day: "목", + range: [7, 8, 9], + room: "의228", + lecture: { + id: "323070", + title: "기본간호학1", + credits: "3(0)", + major: "간호대학

간호학과", + schedule: "화1~3(의228)

목10~12(의228)", + grade: 2, + }, + }, + { + day: "수", + range: [1, 2, 3, 4], + room: "의230", + lecture: { + id: "411690", + title: "지역사회간호학3", + credits: "2(0)", + major: "간호대학

간호학과", + schedule: "수1~4(의230)", + grade: 4, + }, + }, + { + day: "금", + range: [1, 2, 3], + room: "인521", + lecture: { + id: "409440", + title: "중급일본어강독1", + credits: "3(0)", + major: "외국어대학

아시아중동학부 일본학전공", + schedule: "화15~17(인521)

목8~10(인424)", + grade: 2, + }, + }, + { + day: "목", + range: [1, 2, 3], + room: "인424", + lecture: { + id: "409440", + title: "중급일본어강독1", + credits: "3(0)", + major: "외국어대학

아시아중동학부 일본학전공", + schedule: "화15~17(인521)

목8~10(인424)", + grade: 2, + }, + }, ], "schedule-6": [ { - "day": "화", - "range": [ - 9, - 10 - ], - "room": "음악133", - "lecture": { - "id": "471870", - "title": "연주A", - "credits": "1(0)", - "major": "음악·예술대학

음악학부 기악전공(피아노)", - "schedule": "화9~10(음악133)", - "grade": 1 - } - }, - { - "day": "토", - "range": [ - 3, - 4 - ], - "room": "", - "lecture": { - "id": "502420", - "title": "피아노실기A", - "credits": "1(0)", - "major": "음악·예술대학

음악학부 기악전공(피아노)", - "schedule": "토3~4", - "grade": 1 - } - }, - { - "day": "토", - "range": [ - 7, - 8 - ], - "room": "", - "lecture": { - "id": "502420", - "title": "피아노실기A", - "credits": "1(0)", - "major": "음악·예술대학

음악학부 기악전공(피아노)", - "schedule": "토7~8", - "grade": 1 - } - }, - { - "day": "월", - "range": [ - 13, - 14, - 15, - 16 - ], - "room": "음악104", - "lecture": { - "id": "318720", - "title": "국악사1", - "credits": "2(0)", - "major": "음악·예술대학

음악학부 기악전공", - "schedule": "월13~16(음악104)", - "grade": 2 - } - }, - { - "day": "목", - "range": [ - 9, - 10, - 11, - 12 - ], - "room": "음악106", - "lecture": { - "id": "358200", - "title": "서양음악사1", - "credits": "2(0)", - "major": "음악·예술대학

음악학부 기악전공", - "schedule": "목9~12(음악106)", - "grade": 2 - } - }, - { - "day": "화", - "range": [ - 1, - 2, - 3, - 4 - ], - "room": "음악105", - "lecture": { - "id": "367110", - "title": "시창청음", - "credits": "2(0)", - "major": "음악·예술대학

음악학부 기악전공", - "schedule": "화5~8(음악105)", - "grade": 1 - } - }, - { - "day": "금", - "range": [ - 5, - 6, - 7, - 8 - ], - "room": "음악105", - "lecture": { - "id": "358200", - "title": "서양음악사1", - "credits": "2(0)", - "major": "음악·예술대학

음악학부 기악전공", - "schedule": "금5~8(음악105)", - "grade": 2 - } - } - ] + day: "화", + range: [9, 10], + room: "음악133", + lecture: { + id: "471870", + title: "연주A", + credits: "1(0)", + major: "음악·예술대학

음악학부 기악전공(피아노)", + schedule: "화9~10(음악133)", + grade: 1, + }, + }, + { + day: "토", + range: [3, 4], + room: "", + lecture: { + id: "502420", + title: "피아노실기A", + credits: "1(0)", + major: "음악·예술대학

음악학부 기악전공(피아노)", + schedule: "토3~4", + grade: 1, + }, + }, + { + day: "토", + range: [7, 8], + room: "", + lecture: { + id: "502420", + title: "피아노실기A", + credits: "1(0)", + major: "음악·예술대학

음악학부 기악전공(피아노)", + schedule: "토7~8", + grade: 1, + }, + }, + { + day: "월", + range: [13, 14, 15, 16], + room: "음악104", + lecture: { + id: "318720", + title: "국악사1", + credits: "2(0)", + major: "음악·예술대학

음악학부 기악전공", + schedule: "월13~16(음악104)", + grade: 2, + }, + }, + { + day: "목", + range: [9, 10, 11, 12], + room: "음악106", + lecture: { + id: "358200", + title: "서양음악사1", + credits: "2(0)", + major: "음악·예술대학

음악학부 기악전공", + schedule: "목9~12(음악106)", + grade: 2, + }, + }, + { + day: "화", + range: [1, 2, 3, 4], + room: "음악105", + lecture: { + id: "367110", + title: "시창청음", + credits: "2(0)", + major: "음악·예술대학

음악학부 기악전공", + schedule: "화5~8(음악105)", + grade: 1, + }, + }, + { + day: "금", + range: [5, 6, 7, 8], + room: "음악105", + lecture: { + id: "358200", + title: "서양음악사1", + credits: "2(0)", + major: "음악·예술대학

음악학부 기악전공", + schedule: "금5~8(음악105)", + grade: 2, + }, + }, + ], } diff --git a/src/hooks/useAutoCallback.ts b/src/hooks/useAutoCallback.ts index 64ee2646..7a12c60a 100644 --- a/src/hooks/useAutoCallback.ts +++ b/src/hooks/useAutoCallback.ts @@ -1,14 +1,14 @@ -import { useCallback, useRef } from "react"; +import { useCallback, useRef } from "react" // eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnyFunction = (...args: any[]) => any; +type AnyFunction = (...args: any[]) => any export function useAutoCallback(fn: T): T { - const ref = useRef(fn); - ref.current = fn; + const ref = useRef(fn) + ref.current = fn - const autoCallback = useCallback((...args: Parameters) => { - return ref.current(...args); - }, []); - return autoCallback as T; -} \ No newline at end of file + const autoCallback = useCallback((...args: Parameters) => { + return ref.current(...args) + }, []) + return autoCallback as T +} diff --git a/src/main.tsx b/src/main.tsx index 96533e77..0b43f87c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,9 @@ -import App from "./App.tsx"; -import { ChakraProvider } from "@chakra-ui/react"; -import { createRoot } from "react-dom/client"; +import App from "./App.tsx" +import { ChakraProvider } from "@chakra-ui/react" +import { createRoot } from "react-dom/client" createRoot(document.getElementById("root")!).render( - - + + , ) diff --git a/src/provider/ScheduleContext.tsx b/src/provider/ScheduleContext.tsx index fa9a56a1..0cb38a86 100644 --- a/src/provider/ScheduleContext.tsx +++ b/src/provider/ScheduleContext.tsx @@ -1,62 +1,52 @@ -import React, {createContext, PropsWithChildren, useContext, useMemo, useState} from "react"; -import { Schedule } from "../types.ts"; -import dummyScheduleMap from "../dummyScheduleMap.ts"; -import {useAutoCallback} from "../hooks/useAutoCallback.ts"; +import React, { createContext, PropsWithChildren, useContext, useMemo, useState } from "react" +import { Schedule } from "../types.ts" +import dummyScheduleMap from "../dummyScheduleMap.ts" +import { useAutoCallback } from "../hooks/useAutoCallback.ts" -const ScheduleStateContext = createContext|null>(null); +const ScheduleStateContext = createContext | null>(null) const ScheduleActionsContext = createContext<{ - setSchedulesMap: React.Dispatch< - React.SetStateAction> - >; - onDeleteScheduleButtonClick: ( - tableId: string, - day: string, - time: number - ) => void; -} | null>(null); + setSchedulesMap: React.Dispatch>> + onDeleteScheduleButtonClick: (tableId: string, day: string, time: number) => void +} | null>(null) export const useScheduleState = () => { - const context = useContext(ScheduleStateContext); - if (!context) { - throw new Error('useScheduleState must be used within a ScheduleProvider'); - } - return context; -}; + const context = useContext(ScheduleStateContext) + if (!context) { + throw new Error("useScheduleState must be used within a ScheduleProvider") + } + return context +} export const useScheduleActions = () => { - const context = useContext(ScheduleActionsContext); - if (!context) { - throw new Error('useScheduleActions must be used within a ScheduleProvider'); - } - return context; + const context = useContext(ScheduleActionsContext) + if (!context) { + throw new Error("useScheduleActions must be used within a ScheduleProvider") + } + return context } export const ScheduleProvider = ({ children }: PropsWithChildren) => { - const [schedulesMap, setSchedulesMap] = useState>(dummyScheduleMap); - - const onDeleteScheduleButtonClick = useAutoCallback( - (tableId: string, day: string, time: number) => { - setSchedulesMap((prev) => ({ - ...prev, - [tableId]: prev[tableId].filter( - (s) => s.day !== day || !s.range.includes(time) - ), - })); - } - ); - - const actions = useMemo(() => ({ - setSchedulesMap, - onDeleteScheduleButtonClick, - - }), [setSchedulesMap, onDeleteScheduleButtonClick]); - - return ( - - - {children} - - - ); -} \ No newline at end of file + const [schedulesMap, setSchedulesMap] = useState>(dummyScheduleMap) + + const onDeleteScheduleButtonClick = useAutoCallback((tableId: string, day: string, time: number) => { + setSchedulesMap((prev) => ({ + ...prev, + [tableId]: prev[tableId].filter((s) => s.day !== day || !s.range.includes(time)), + })) + }) + + const actions = useMemo( + () => ({ + setSchedulesMap, + onDeleteScheduleButtonClick, + }), + [setSchedulesMap, onDeleteScheduleButtonClick], + ) + + return ( + + {children} + + ) +} diff --git a/src/provider/ScheduleDndProvider.tsx b/src/provider/ScheduleDndProvider.tsx index e71e7bda..9395062e 100644 --- a/src/provider/ScheduleDndProvider.tsx +++ b/src/provider/ScheduleDndProvider.tsx @@ -1,88 +1,76 @@ -import { DndContext, Modifier, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; -import {PropsWithChildren} from "react"; -import { CellSize, DAY_LABELS } from "../constants.ts"; -import {useAutoCallback} from "../hooks/useAutoCallback.ts"; -import {useScheduleActions} from "./ScheduleContext.tsx"; +import { DndContext, Modifier, PointerSensor, useSensor, useSensors } from "@dnd-kit/core" +import { PropsWithChildren } from "react" +import { CellSize, DAY_LABELS } from "../constants.ts" +import { useAutoCallback } from "../hooks/useAutoCallback.ts" +import { useScheduleActions } from "./ScheduleContext.tsx" function createSnapModifier(): Modifier { return ({ transform, containerNodeRect, draggingNodeRect }) => { - const containerTop = containerNodeRect?.top ?? 0; - const containerLeft = containerNodeRect?.left ?? 0; - const containerBottom = containerNodeRect?.bottom ?? 0; - const containerRight = containerNodeRect?.right ?? 0; + const containerTop = containerNodeRect?.top ?? 0 + const containerLeft = containerNodeRect?.left ?? 0 + const containerBottom = containerNodeRect?.bottom ?? 0 + const containerRight = containerNodeRect?.right ?? 0 - const { top = 0, left = 0, bottom = 0, right = 0 } = draggingNodeRect ?? {}; + const { top = 0, left = 0, bottom = 0, right = 0 } = draggingNodeRect ?? {} - const minX = containerLeft - left + 120 + 1; - const minY = containerTop - top + 40 + 1; - const maxX = containerRight - right; - const maxY = containerBottom - bottom; + const minX = containerLeft - left + 120 + 1 + const minY = containerTop - top + 40 + 1 + const maxX = containerRight - right + const maxY = containerBottom - bottom - return { - ...transform, - x: Math.min( - Math.max( - Math.round(transform.x / CellSize.WIDTH) * CellSize.WIDTH, - minX, - ), - maxX, - ), - y: Math.min( - Math.max( - Math.round(transform.y / CellSize.HEIGHT) * CellSize.HEIGHT, - minY, - ), - maxY, - ), - }; - }; + return { + ...transform, + x: Math.min(Math.max(Math.round(transform.x / CellSize.WIDTH) * CellSize.WIDTH, minX), maxX), + y: Math.min(Math.max(Math.round(transform.y / CellSize.HEIGHT) * CellSize.HEIGHT, minY), maxY), + } + } } const modifiers = [createSnapModifier()] export default function ScheduleDndProvider({ children }: PropsWithChildren) { - const { setSchedulesMap } = useScheduleActions(); + const { setSchedulesMap } = useScheduleActions() const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, - }) - ); + }), + ) // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleDragEnd = useAutoCallback((event: any) => { - const { active, delta } = event; - const { x, y } = delta; - const [tableId, index] = active.id.split(':'); + const { active, delta } = event + const { x, y } = delta + const [tableId, index] = active.id.split(":") setSchedulesMap((prev) => { - const schedule = prev[tableId][index]; - const nowDayIndex = DAY_LABELS.indexOf(schedule.day as typeof DAY_LABELS[number]) - const moveDayIndex = Math.floor(x / 80); - const moveTimeIndex = Math.floor(y / 30); + const schedule = prev[tableId][index] + const nowDayIndex = DAY_LABELS.indexOf(schedule.day as (typeof DAY_LABELS)[number]) + const moveDayIndex = Math.floor(x / 80) + const moveTimeIndex = Math.floor(y / 30) - const newSchedule = prev[tableId].map((targetSchedule, targetIndex) => { - if (targetIndex !== Number(index)) { - return { ...targetSchedule } - } - return { - ...targetSchedule, - day: DAY_LABELS[nowDayIndex + moveDayIndex], - range: targetSchedule.range.map(time => time + moveTimeIndex), - }; - }); + const newSchedule = prev[tableId].map((targetSchedule, targetIndex) => { + if (targetIndex !== Number(index)) { + return { ...targetSchedule } + } return { - ...prev, - [tableId]: newSchedule + ...targetSchedule, + day: DAY_LABELS[nowDayIndex + moveDayIndex], + range: targetSchedule.range.map((time) => time + moveTimeIndex), } + }) + return { + ...prev, + [tableId]: newSchedule, + } }) - }); + }) return ( {children} - ); + ) } diff --git a/src/types.ts b/src/types.ts index 11343db7..f64ff71b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,24 +1,24 @@ export interface Lecture { - id: string; - title: string; - credits: string; - major: string; - schedule: string; - grade: number; + id: string + title: string + credits: string + major: string + schedule: string + grade: number } export interface Schedule { lecture: Lecture - day: string; + day: string range: number[] - room?: string; + room?: string } export interface SearchOption { - query?: string; - grades: number[]; - days: string[]; - times: number[]; - majors: string[]; - credits?: number; -} \ No newline at end of file + query?: string + grades: number[] + days: string[] + times: number[] + majors: string[] + credits?: number +} diff --git a/src/utils.ts b/src/utils.ts index 5b75ff39..678fe885 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,43 +1,42 @@ -import {분} from "./constants.ts"; +import { 분 } from "./constants.ts" -export const fill2 = (n: number) => `0${n}`.substr(-2); +export const fill2 = (n: number) => `0${n}`.substr(-2) export const parseHnM = (current: number) => { - const date = new Date(current); - return `${fill2(date.getHours())}:${fill2(date.getMinutes())}`; -}; + const date = new Date(current) + return `${fill2(date.getHours())}:${fill2(date.getMinutes())}` +} const getTimeRange = (value: string): number[] => { - const [start, end] = value.split("~").map(Number); - if (end === undefined) return [start]; + const [start, end] = value.split("~").map(Number) + if (end === undefined) return [start] return Array(end - start + 1) .fill(start) - .map((v, k) => v + k); + .map((v, k) => v + k) } export const parseSchedule = (schedule: string) => { - const schedules = schedule.split('

'); - return schedules.map(schedule => { - - const reg = /^([가-힣])(\d+(~\d+)?)(.*)/; + const schedules = schedule.split("

") + return schedules.map((schedule) => { + const reg = /^([가-힣])(\d+(~\d+)?)(.*)/ - const [day] = schedule.split(/(\d+)/); + const [day] = schedule.split(/(\d+)/) - const range = getTimeRange(schedule.replace(reg, "$2")); + const range = getTimeRange(schedule.replace(reg, "$2")) - const room = schedule.replace(reg, "$4")?.replace(/\(|\)/g, ""); + const room = schedule.replace(reg, "$4")?.replace(/\(|\)/g, "") - return { day, range, room }; - }); -}; + return { day, range, room } + }) +} export const TIMES = [ - ...Array(18) - .fill(0) - .map((v, k) => v + k * 30 * 분) - .map((v) => `${parseHnM(v)}~${parseHnM(v + 30 * 분)}`), - ...Array(6) - .fill(18 * 30 * 분) - .map((v, k) => v + k * 55 * 분) - .map((v) => `${parseHnM(v)}~${parseHnM(v + 50 * 분)}`), -] as const; \ No newline at end of file + ...Array(18) + .fill(0) + .map((v, k) => v + k * 30 * 분) + .map((v) => `${parseHnM(v)}~${parseHnM(v + 30 * 분)}`), + ...Array(6) + .fill(18 * 30 * 분) + .map((v, k) => v + k * 55 * 분) + .map((v) => `${parseHnM(v)}~${parseHnM(v + 50 * 분)}`), +] as const