From 554925eced9576d609d5ba674f568c453bc6a632 Mon Sep 17 00:00:00 2001 From: ars-ki-00 Date: Thu, 27 Feb 2025 00:06:48 +0900 Subject: [PATCH] =?UTF-8?q?[webclient]=20=EC=8B=9C=EA=B0=84=EB=8C=80=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=A0=88=EA=B1=B0=EC=8B=9C=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/snutt-webclient/src/entities/search.ts | 4 + .../implSearchSnuttApiRepository.ts | 3 +- .../src/pages/main/main-searchbar/index.tsx | 26 +++-- .../main-searchbar-filter-dialog/index.tsx | 9 +- .../index.tsx | 11 +- .../src/usecases/timeMaskService.test.ts | 102 +++++++++--------- .../src/usecases/timeMaskService.ts | 52 ++++++--- .../src/apis/snutt-timetable/schemas.ts | 1 - 8 files changed, 122 insertions(+), 86 deletions(-) diff --git a/apps/snutt-webclient/src/entities/search.ts b/apps/snutt-webclient/src/entities/search.ts index dad91a30..d572153a 100644 --- a/apps/snutt-webclient/src/entities/search.ts +++ b/apps/snutt-webclient/src/entities/search.ts @@ -1,3 +1,5 @@ +import { type SearchTimeDto } from '@sf/snutt-api/src/apis/snutt-timetable/schemas'; + import type { BaseLecture } from './lecture'; import type { Semester } from './semester'; @@ -11,6 +13,8 @@ export type SearchFilter = { academicYear: string[]; category: string[]; categoryPre2025: string[]; + times: SearchTimeDto[]; + timesToExclude: SearchTimeDto[]; credit: number[]; department: string[]; classification: string[]; diff --git a/apps/snutt-webclient/src/infrastructures/implSearchSnuttApiRepository.ts b/apps/snutt-webclient/src/infrastructures/implSearchSnuttApiRepository.ts index 3bad32de..1af3774a 100644 --- a/apps/snutt-webclient/src/infrastructures/implSearchSnuttApiRepository.ts +++ b/apps/snutt-webclient/src/infrastructures/implSearchSnuttApiRepository.ts @@ -39,8 +39,9 @@ export const implSearchSnuttApiRepository = ({ etc: filter.etc, limit: filter.limit, semester: filter.semester, - time_mask: filter.timeMask, title: filter.title, + times: filter.times, + timesToExclude: filter.timesToExclude, year: filter.year, page: filter.page, offset: filter.offset, diff --git a/apps/snutt-webclient/src/pages/main/main-searchbar/index.tsx b/apps/snutt-webclient/src/pages/main/main-searchbar/index.tsx index fed972ab..eb3cf02e 100644 --- a/apps/snutt-webclient/src/pages/main/main-searchbar/index.tsx +++ b/apps/snutt-webclient/src/pages/main/main-searchbar/index.tsx @@ -1,3 +1,4 @@ +import { type SearchTimeDto } from '@sf/snutt-api/src/apis/snutt-timetable/schemas'; import { type FormEvent, useState } from 'react'; import styled from 'styled-components'; @@ -32,11 +33,12 @@ export type SearchForm = { etc: SearchFilter['etc']; classification: SearchFilter['classification']; department: SearchFilter['department']; - manualBitmask: number[]; - timeType: 'auto' | 'manual' | null; + times: SearchFilter['times']; + timeType: 'manual' | 'auto' | null; }; const initialForm = { + title: '', academicYear: [], credit: [], etc: [], @@ -44,9 +46,8 @@ const initialForm = { categoryPre2025: [], classification: [], department: [], + times: [], timeType: null, - title: '', - manualBitmask: [], }; const isInitialForm = (form: SearchForm) => JSON.stringify(initialForm) === JSON.stringify(form); @@ -83,20 +84,17 @@ export const MainSearchbar = ({ onSearch, currentFullTimetable, resetSearchResul year, semester, title: searchForm.title, - timeMask: - searchForm.timeType === 'manual' - ? searchForm.manualBitmask - : searchForm.timeType === 'auto' - ? currentFullTimetable - ? timeMaskService.getTimetableEmptyTimeBitMask(currentFullTimetable) - : undefined - : undefined, + times: undefinedIfEmpty(searchForm.times), + timesToExclude: + searchForm.timeType === 'auto' && currentFullTimetable + ? timeMaskService.getTimesByTimeTable(currentFullTimetable) + : undefined, limit: 200, }, }); }; - const onChangeBitMask = (manualBitmask: number[]) => setSearchForm((sf) => ({ ...sf, manualBitmask })); + const onChangeTimes = (times: SearchTimeDto[]) => setSearchForm((sf) => ({ ...sf, times })); const onChangeCheckbox = < F extends 'academicYear' | 'category' | 'categoryPre2025' | 'classification' | 'credit' | 'department' | 'etc', @@ -138,7 +136,7 @@ export const MainSearchbar = ({ onSearch, currentFullTimetable, resetSearchResul onChangeCheckbox={onChangeCheckbox} onChangeDepartment={(department) => setSearchForm((sf) => ({ ...sf, department }))} onChangeTimeRadio={(timeType) => setSearchForm((sf) => ({ ...sf, timeType }))} - onChangeBitMask={onChangeBitMask} + onChangeTimes={onChangeTimes} /> diff --git a/apps/snutt-webclient/src/pages/main/main-searchbar/main-searchbar-filter-dialog/index.tsx b/apps/snutt-webclient/src/pages/main/main-searchbar/main-searchbar-filter-dialog/index.tsx index 6c82f6e0..c949c080 100644 --- a/apps/snutt-webclient/src/pages/main/main-searchbar/main-searchbar-filter-dialog/index.tsx +++ b/apps/snutt-webclient/src/pages/main/main-searchbar/main-searchbar-filter-dialog/index.tsx @@ -1,3 +1,4 @@ +import { type SearchTimeDto } from '@sf/snutt-api/src/apis/snutt-timetable/schemas'; import { useQuery } from '@tanstack/react-query'; import { useState } from 'react'; import styled from 'styled-components'; @@ -24,9 +25,9 @@ type Props = { field: F, e: SearchForm[F][number], ) => void; - onChangeTimeRadio: (value: 'auto' | 'manual' | null) => void; + onChangeTimeRadio: (value: 'manual' | 'auto' | null) => void; onChangeDepartment: (value: string[]) => void; - onChangeBitMask: (bm: number[]) => void; + onChangeTimes: (times: SearchTimeDto[]) => void; }; export const MainSearchbarFilterDialog = ({ @@ -37,7 +38,7 @@ export const MainSearchbarFilterDialog = ({ searchForm, onChangeCheckbox, onChangeTimeRadio, - onChangeBitMask, + onChangeTimes, onChangeDepartment, }: Props) => { const [isTimeModalOpen, setTimeModalOpen] = useState(false); @@ -286,7 +287,7 @@ export const MainSearchbarFilterDialog = ({ setTimeModalOpen(false)} - onChangeBitMask={onChangeBitMask} + onChangeTimes={onChangeTimes} /> ); diff --git a/apps/snutt-webclient/src/pages/main/main-searchbar/main-searchbar-filter-dialog/main-searchbar-filter-time-select-dialog/index.tsx b/apps/snutt-webclient/src/pages/main/main-searchbar/main-searchbar-filter-dialog/main-searchbar-filter-time-select-dialog/index.tsx index 845b5fdd..1e988c96 100644 --- a/apps/snutt-webclient/src/pages/main/main-searchbar/main-searchbar-filter-dialog/main-searchbar-filter-time-select-dialog/index.tsx +++ b/apps/snutt-webclient/src/pages/main/main-searchbar/main-searchbar-filter-dialog/main-searchbar-filter-time-select-dialog/index.tsx @@ -1,3 +1,4 @@ +import { type SearchTimeDto } from '@sf/snutt-api/src/apis/snutt-timetable/schemas'; import { useState } from 'react'; import styled from 'styled-components'; @@ -10,12 +11,16 @@ import { useGuardContext } from '@/hooks/useGuardContext'; import { MainSearchbarFilterTimeSelectCell } from './main-searchbar-filter-time-select-cell'; -type Props = { open: boolean; onClose: () => void; onChangeBitMask: (bm: number[]) => void }; +type Props = { + open: boolean; + onClose: () => void; + onChangeTimes: (times: SearchTimeDto[]) => void; +}; /** * @note 테스트코드가 붙어있지 않습니다. 수정할 때 주의해 주세요! */ -export const MainSearchbarFilterTimeSelectDialog = ({ open, onClose, onChangeBitMask }: Props) => { +export const MainSearchbarFilterTimeSelectDialog = ({ open, onClose, onChangeTimes }: Props) => { const { timeMaskService, errorService } = useGuardContext(ServiceContext); const [dragStart, setDragStart] = useState(null); const [currentDrag, setCurrentDrag] = useState(null); @@ -24,7 +29,7 @@ export const MainSearchbarFilterTimeSelectDialog = ({ open, onClose, onChangeBit ); const onConfirm = () => { - onChangeBitMask(timeMaskService.getBitMask(cellStatus)); + onChangeTimes(timeMaskService.getTimes(cellStatus)); onClose(); }; diff --git a/apps/snutt-webclient/src/usecases/timeMaskService.test.ts b/apps/snutt-webclient/src/usecases/timeMaskService.test.ts index 0701c4f7..b4a8e9be 100644 --- a/apps/snutt-webclient/src/usecases/timeMaskService.test.ts +++ b/apps/snutt-webclient/src/usecases/timeMaskService.test.ts @@ -1,6 +1,4 @@ -import { describe, expect, it, test } from 'vitest'; - -import type { FullTimetable } from '@/entities/timetable'; +import { expect, test } from 'vitest'; import { getTimeMaskService } from './timeMaskService'; @@ -47,55 +45,57 @@ test('getUpdatedCellStatus', () => { ]); }); -test('getBitMask', () => { - expect( - timeMaskService.getBitMask([ - [false, false, true], - [false, false, false], - ]), - ).toStrictEqual([0, 0, 2]); +// TODO: 테스트 수정 - expect( - timeMaskService.getBitMask([ - [false, false, true], - [false, false, false], - [false, true, false], - [true, false, false], - [false, true, false], - [false, true, false], - ]), - ).toStrictEqual([4, 11, 32]); -}); +// test('getBitMask', () => { +// expect( +// timeMaskService.getBitMask([ +// [false, false, true], +// [false, false, false], +// ]), +// ).toStrictEqual([0, 0, 2]); -describe('getTimetableEmptyTimeBitMask', () => { - it('empty case', () => { - expect( - timeMaskService.getTimetableEmptyTimeBitMask({ lecture_list: [] } as unknown as FullTimetable), - ).toStrictEqual([1073741823, 1073741823, 1073741823, 1073741823, 1073741823, 1073741823, 1073741823]); - }); +// expect( +// timeMaskService.getBitMask([ +// [false, false, true], +// [false, false, false], +// [false, true, false], +// [true, false, false], +// [false, true, false], +// [false, true, false], +// ]), +// ).toStrictEqual([4, 11, 32]); +// }); - it('simple case', () => { - expect( - timeMaskService.getTimetableEmptyTimeBitMask({ - lecture_list: [{ class_time_mask: [0, 536870912, 0, 0, 0, 0, 0] }], - } as unknown as FullTimetable), - ).toStrictEqual([1073741823, 536870911, 1073741823, 1073741823, 1073741823, 1073741823, 1073741823]); - }); +// describe('getTimetableEmptyTimeBitMask', () => { +// it('empty case', () => { +// expect( +// timeMaskService.getTimetableEmptyTimeBitMask({ lecture_list: [] } as unknown as FullTimetable), +// ).toStrictEqual([1073741823, 1073741823, 1073741823, 1073741823, 1073741823, 1073741823, 1073741823]); +// }); - it('complex case', () => { - expect( - timeMaskService.getTimetableEmptyTimeBitMask({ - lecture_list: [ - { class_time_mask: [4064, 0, 3584, 0, 0, 0, 0] }, - { class_time_mask: [28672, 0, 28672, 0, 0, 0, 0] }, - { class_time_mask: [0, 0, 786432, 0, 0, 0, 0] }, - { class_time_mask: [0, 229376, 0, 229376, 0, 0, 0] }, - { class_time_mask: [0, 3584, 0, 3584, 0, 0, 0] }, - { class_time_mask: [14680064, 0, 14680064, 0, 0, 0, 0] }, - { class_time_mask: [0, 28672, 0, 28672, 0, 0, 0] }, - { class_time_mask: [0, 192, 0, 192, 0, 0, 12582912] }, - ], - } as FullTimetable), - ).toStrictEqual([1059029023, 1073479999, 1058243071, 1073479999, 1073741823, 1073741823, 1061158911]); - }); -}); +// it('simple case', () => { +// expect( +// timeMaskService.getTimetableEmptyTimeBitMask({ +// lecture_list: [{ class_time_mask: [0, 536870912, 0, 0, 0, 0, 0] }], +// } as unknown as FullTimetable), +// ).toStrictEqual([1073741823, 536870911, 1073741823, 1073741823, 1073741823, 1073741823, 1073741823]); +// }); + +// it('complex case', () => { +// expect( +// timeMaskService.getTimetableEmptyTimeBitMask({ +// lecture_list: [ +// { class_time_mask: [4064, 0, 3584, 0, 0, 0, 0] }, +// { class_time_mask: [28672, 0, 28672, 0, 0, 0, 0] }, +// { class_time_mask: [0, 0, 786432, 0, 0, 0, 0] }, +// { class_time_mask: [0, 229376, 0, 229376, 0, 0, 0] }, +// { class_time_mask: [0, 3584, 0, 3584, 0, 0, 0] }, +// { class_time_mask: [14680064, 0, 14680064, 0, 0, 0, 0] }, +// { class_time_mask: [0, 28672, 0, 28672, 0, 0, 0] }, +// { class_time_mask: [0, 192, 0, 192, 0, 0, 12582912] }, +// ], +// } as FullTimetable), +// ).toStrictEqual([1059029023, 1073479999, 1058243071, 1073479999, 1073741823, 1073741823, 1061158911]); +// }); +// }); diff --git a/apps/snutt-webclient/src/usecases/timeMaskService.ts b/apps/snutt-webclient/src/usecases/timeMaskService.ts index 588ba903..20f7667a 100644 --- a/apps/snutt-webclient/src/usecases/timeMaskService.ts +++ b/apps/snutt-webclient/src/usecases/timeMaskService.ts @@ -1,4 +1,6 @@ -import type { CellStatus, DragMode, Position, TimeMask } from '@/entities/timeMask'; +import { type SearchTimeDto } from '@sf/snutt-api/src/apis/snutt-timetable/schemas'; + +import type { CellStatus, DragMode, Position } from '@/entities/timeMask'; import type { FullTimetable } from '@/entities/timetable'; export interface TimeMaskService { @@ -6,8 +8,8 @@ export interface TimeMaskService { getUpdatedCellStatus(prev: CellStatus, dragStart: Position, dragEnd: Position): CellStatus; checkIsInArea(target: Position, from: Position, to: Position): boolean; getDragMode(cellStatus: CellStatus, dragStart: Position): DragMode; - getBitMask(cellStatus: CellStatus): TimeMask; - getTimetableEmptyTimeBitMask(timetable: FullTimetable): TimeMask; + getTimes(cellStatus: CellStatus): SearchTimeDto[]; + getTimesByTimeTable(timetable: FullTimetable): SearchTimeDto[]; } export const getTimeMaskService = (): TimeMaskService => { @@ -24,17 +26,43 @@ export const getTimeMaskService = (): TimeMaskService => { ), checkIsInArea, getDragMode, - getBitMask: (cellStatus) => { + getTimes: (cellStatus) => { const transposed = cellStatus[0].map((_, j) => cellStatus.map((row) => row[j])); // 행-열 반전 - return transposed.map((row) => row.map(Number).reduce((acc, cur) => acc * 2 + cur, 0)) as TimeMask; + + const result = transposed.flatMap((cells, day) => + cells.reduce((acc: SearchTimeDto[], cur, index) => { + if (!cur) return acc; + + const startMinute = (index + 16) * 30; + + const prev = acc.find((target) => target.endMinute === startMinute - 1); + + return [ + ...acc.filter((target) => target.endMinute !== startMinute - 1), + { + day, + startMinute: prev?.startMinute || startMinute, + endMinute: startMinute + 30 - 1, + } as SearchTimeDto, + ]; + }, []), + ); + + return result; + }, + getTimesByTimeTable: (timetable) => { + return timetable.lecture_list.flatMap((lecture) => { + return lecture.class_time_json.map((classTime) => { + const [startHour, startMinute] = classTime.start_time.split(':'); + const [endHour, endMinute] = classTime.end_time.split(':'); + return { + day: classTime.day, + startMinute: Number(startHour) * 60 + Number(startMinute), + endMinute: Number(endHour) * 60 + Number(endMinute), + }; + }); + }); }, - getTimetableEmptyTimeBitMask: (timetable) => - (timetable?.lecture_list ?? []) - .reduce( - (acc, cur) => acc.map((mask, day) => mask | cur.class_time_mask[day]) as TimeMask, - [0, 0, 0, 0, 0, 0, 0], - ) - .map((mask) => mask ^ 0x3fffffff) as TimeMask, }; }; diff --git a/packages/snutt-api/src/apis/snutt-timetable/schemas.ts b/packages/snutt-api/src/apis/snutt-timetable/schemas.ts index 87045aeb..baade259 100644 --- a/packages/snutt-api/src/apis/snutt-timetable/schemas.ts +++ b/packages/snutt-api/src/apis/snutt-timetable/schemas.ts @@ -164,7 +164,6 @@ export type SearchQueryLegacy = { department?: string[]; category?: string[]; categoryPre2025?: string[]; - time_mask?: Int32[]; times?: SearchTimeDto[]; timesToExclude?: SearchTimeDto[]; etc?: string[];