From 5d8c75d0a4a0a6ce1073596bbcfd2d6bed4f2483 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Tue, 22 Oct 2024 00:04:34 +0900 Subject: [PATCH 01/52] feat: implement sub filter actions --- .../Report/AttendanceReportSearch.tsx | 66 +++++++++++ .../AttendanceReportSubFilterActions.tsx | 103 +++++++++++++++++ .../Report/AttendanceReportSubFilters.tsx | 22 ++++ .../hooks/useAttendanceReportFilter.ts | 105 ++++++++++++++++++ 4 files changed, 296 insertions(+) create mode 100644 src/pages/Attendance/Report/AttendanceReportSearch.tsx create mode 100644 src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx create mode 100644 src/pages/Attendance/Report/AttendanceReportSubFilters.tsx create mode 100644 src/pages/Attendance/hooks/useAttendanceReportFilter.ts diff --git a/src/pages/Attendance/Report/AttendanceReportSearch.tsx b/src/pages/Attendance/Report/AttendanceReportSearch.tsx new file mode 100644 index 00000000..1243de09 --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportSearch.tsx @@ -0,0 +1,66 @@ +import React, { useEffect } from 'react'; +import CotatoTextField from '@components/CotatoTextField/CotatoTextField'; +import { Box } from '@mui/material'; +import { useDebounce } from 'react-use'; +import { useAttendanceReportFilter } from '../hooks/useAttendanceReportFilter'; + +// +// +// + +const DEBOUNCE_TIME = 500; + +// +// +// + +const AttendanceReportSearch = () => { + // + const { updateSearchParams } = useAttendanceReportFilter(); + + // + const [searchValue, setSearchValue] = React.useState(''); + const [debouncedSearchValue, setDebouncedSearchValue] = React.useState(''); + + // + // + // + useDebounce( + () => { + setDebouncedSearchValue(searchValue); + }, + DEBOUNCE_TIME, + [searchValue], + ); + + // + // + // + useEffect(() => { + if (!debouncedSearchValue) { + updateSearchParams(''); + } + + updateSearchParams(debouncedSearchValue); + }, [debouncedSearchValue]); + + // + // + // + + return ( + + { + setSearchValue(e.target.value); + }} + /> + + ); +}; + +export default AttendanceReportSearch; diff --git a/src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx b/src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx new file mode 100644 index 00000000..22f5be5f --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import CotatoIcon from '@components/CotatoIcon'; +import { Stack, Typography } from '@mui/material'; +import { ReactComponent as OnlineIcon } from '@assets/attendance_online_icon.svg'; +import { ReactComponent as AbsentIcon } from '@assets/attendance_absent_icon.svg'; +import styled from 'styled-components'; +import { useAttendanceReportFilter } from '../hooks/useAttendanceReportFilter'; + +// +// +// + +const STATUS_BUTTONS = [ + { + status: 'offline', + icon: theme.colors.sub3[40]} />, + text: '대면', + }, + + { + status: 'online', + icon: , + text: '비대면', + }, + + { + status: 'late', + icon: theme.colors.secondary80} />, + text: '지각', + }, + + { + status: 'absent', + icon: , + text: '결석', + }, +]; + +// +// +// + +const AttendanceReportSubFilterActions = () => { + // + const { currentStatus, toggleStatus } = useAttendanceReportFilter(); + + /** + * + */ + const checkIsStatusSelected = (status: string) => { + return currentStatus.includes(status); + }; + + // + // + // + return ( + + {STATUS_BUTTONS.map(({ status, icon, text }) => ( + { + toggleStatus(status); + }} + > + {icon} + {text} + + ))} + + ); +}; + +export default AttendanceReportSubFilterActions; + +// +// +// + +const StyledButton = styled.button<{ $selected?: boolean }>` + display: flex; + align-items: center; + justify-content: center; + width: 8rem; + height: 2.75rem; + gap: 0.75rem; + outline: none; + border: none; + border-radius: 0.5rem; + background-color: ${({ theme, $selected }) => + $selected ? theme.colors.primary80 : theme.colors.gray10}; + cursor: pointer; + + &:hover { + transition: background-color 0.3s; + ${({ theme, $selected }) => !$selected && `background-color: ${theme.colors.gray20};`} + } +`; + +const StyledTypography = styled(Typography)` + color: ${({ theme }) => theme.colors.common.black_const}; +`; diff --git a/src/pages/Attendance/Report/AttendanceReportSubFilters.tsx b/src/pages/Attendance/Report/AttendanceReportSubFilters.tsx new file mode 100644 index 00000000..c7ec3533 --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportSubFilters.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Stack } from '@mui/material'; +import AttendanceReportSubFilterActions from './AttendanceReportSubFilterActions'; +import AttendanceReportSearch from './AttendanceReportSearch'; + +// +// +// + +const AttendanceReportSubFilters = () => { + // + // + // + return ( + + + + + ); +}; + +export default AttendanceReportSubFilters; diff --git a/src/pages/Attendance/hooks/useAttendanceReportFilter.ts b/src/pages/Attendance/hooks/useAttendanceReportFilter.ts new file mode 100644 index 00000000..ac1f4c04 --- /dev/null +++ b/src/pages/Attendance/hooks/useAttendanceReportFilter.ts @@ -0,0 +1,105 @@ +import { AttendResponseAttendanceTypeEnum } from '@/enums/attend'; +import { CotatoAttendResponseStatusEnum } from 'cotato-openapi-clients'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +// +// +// + +type QueryParams = { + [key: string]: string | string[] | null | undefined; +}; + +// +// +// + +export const useAttendanceReportFilter = () => { + const navigate = useNavigate(); + + // + const [searchParams] = useSearchParams(); + + /** + * update search params + */ + const updateSearchParams = (value: string) => { + const newParams = new URLSearchParams(searchParams.toString()); + + if (!value) { + newParams.delete('search'); + } else { + newParams.set('search', value); + } + + applyQueryParams(newParams); + }; + + /*** + * add query string to url + */ + const applyQueryParams = (params: URLSearchParams) => { + navigate(`?${params.toString()}`); + }; + + /** + * add query param to url + * if already key exists, append value to key + */ + const addQueryParams = (params: QueryParams) => { + const queryParams = new URLSearchParams(location.search); + + Object.keys(params).forEach((key: string) => { + const value = params[key]; + if (Array.isArray(value)) { + value.forEach((v) => queryParams.append(key, v)); + } else if (value) { + queryParams.append(key, value); + } + }); + + applyQueryParams(queryParams); + }; + + /** + * delete query string from url + * if key has multiple values, delete only one value + */ + const deleteQueryParams = (key: string, deleteValue: string) => { + const currentSearchParams = searchParams.getAll(key); + + const newParams = new URLSearchParams(searchParams.toString()); + const newValues = currentSearchParams.filter((value) => value !== deleteValue); + + newParams.delete(key); + + // if newValues is not empty, append new values + if (newValues.length) { + newValues.forEach((value) => newParams.append(key, value)); + } + + applyQueryParams(newParams); + }; + + /** + * + */ + const toggleStatus = ( + status: Omit | AttendResponseAttendanceTypeEnum, + ) => { + const currentStatus = searchParams.getAll('status'); + + if (currentStatus?.includes(status as string)) { + deleteQueryParams('status', status as string); + return; + } + + addQueryParams({ status: status as string }); + }; + + return { + currentStatus: searchParams.getAll('status'), + toggleStatus, + updateSearchParams, + }; +}; From 2f558a185dcf991aa1283794367a5ccb4a90ce59 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Tue, 22 Oct 2024 00:04:44 +0900 Subject: [PATCH 02/52] feat: implement cotato textfield --- .../CotatoTextField/CotatoTextField.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/components/CotatoTextField/CotatoTextField.tsx diff --git a/src/components/CotatoTextField/CotatoTextField.tsx b/src/components/CotatoTextField/CotatoTextField.tsx new file mode 100644 index 00000000..14b9c982 --- /dev/null +++ b/src/components/CotatoTextField/CotatoTextField.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import CotatoIcon from '@components/CotatoIcon'; +import { Box, IconButton, TextField, TextFieldProps } from '@mui/material'; +import { useTheme } from 'styled-components'; + +// +// +// + +const CotatoTextField = ({ ...props }: TextFieldProps) => { + const theme = useTheme(); + + /** + * + */ + const handleClear = () => { + const event = { + target: { + value: '', + }, + } as React.ChangeEvent; + props.onChange?.(event); + }; + + // + // + // + + return ( + + theme.colors.common.black} + size="1.25rem" + /> + + ), + endAdornment: props.value ? ( + + theme.colors.common.black} size="1rem" /> + + ) : null, + }, + }} + sx={{ + '& .MuiInputBase-input': { + color: theme.colors.common.black, + }, + gap: '0.5rem', + '& .MuiInputBase-root': { + gap: '0.5rem', + }, + '& .MuiInput-underline:before': { + borderBottomColor: theme.colors.common.black, + }, + '& .MuiInput-underline:hover:not(.Mui-disabled):before': { + borderBottomColor: theme.colors.primary60, + }, + '& .MuiInput-underline:after': { + borderBottomColor: theme.colors.primary100, + }, + }} + {...props} + /> + ); +}; + +export default CotatoTextField; From 71188c9db0bc5a075dc585837cd113b1d402adca Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Tue, 22 Oct 2024 00:06:15 +0900 Subject: [PATCH 03/52] chore: edit path for develop --- src/pages/Attendance/Attendance.routes.tsx | 2 +- src/pages/Attendance/List/AttendanceList.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/Attendance/Attendance.routes.tsx b/src/pages/Attendance/Attendance.routes.tsx index 78a9f657..f1143df6 100644 --- a/src/pages/Attendance/Attendance.routes.tsx +++ b/src/pages/Attendance/Attendance.routes.tsx @@ -32,7 +32,7 @@ const AttendanceRoutes = () => { path="attend/generation/:generationId/session/:sessionId/:attendanceType/:status" element={} /> - } /> + } /> } /> diff --git a/src/pages/Attendance/List/AttendanceList.tsx b/src/pages/Attendance/List/AttendanceList.tsx index dff1e9c0..491aaaee 100644 --- a/src/pages/Attendance/List/AttendanceList.tsx +++ b/src/pages/Attendance/List/AttendanceList.tsx @@ -67,8 +67,7 @@ const AttendanceList = () => { * */ const handleClickReport = () => { - const currentMonth = new Date().getMonth() + 1; - navigate(`/attendance/report/${generationId}/month/${currentMonth}`); + navigate(`/attendance/report/${generationId}`); }; /** From 05e5077b5d78e43bfd2b2e90dfcfe34cb2f675db Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Tue, 22 Oct 2024 00:06:31 +0900 Subject: [PATCH 04/52] feat: remove unused code --- .../Attendance/Report/AttendanceReport.tsx | 174 +----------------- 1 file changed, 6 insertions(+), 168 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReport.tsx b/src/pages/Attendance/Report/AttendanceReport.tsx index bfcc3748..d548de0e 100644 --- a/src/pages/Attendance/Report/AttendanceReport.tsx +++ b/src/pages/Attendance/Report/AttendanceReport.tsx @@ -1,124 +1,13 @@ import React from 'react'; -import { - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, -} from '@mui/material'; -import { Container, Stack } from '@mui/system'; -import fetcherWithParams from '@utils/fetcherWithParams'; -import { CotatoAttendanceRecordResponse, CotatoAttendanceStatistic } from 'cotato-openapi-clients'; - -import { useParams } from 'react-router-dom'; -import useSWR from 'swr'; -import fetcher from '@utils/fetcher'; -import useGetAttendances from '@/hooks/useGetAttendances'; -import { useSession } from '@/hooks/useSession'; - -// -// -// - -const STATISTICS_MAP = { - offline: '대면', - online: '비대면', - late: '지각', - absent: '결석', -} as unknown as CotatoAttendanceStatistic; +import { Container } from '@mui/system'; +import AttendanceReportSubFilters from './AttendanceReportSubFilters'; // // // const AttendanceReport = () => { - const { generationId, month } = useParams(); - - const { sessions } = useSession({ generationId: Number(generationId) }); - const latestSession = sessions?.at(-1); - - const { currentAttendance } = useGetAttendances({ - generationId: Number(generationId), - sessionId: latestSession?.sessionId, - }); - - // data for month - const { data: monthRecords } = useSWR( - '/v2/api/attendances/records', - (url: string) => - fetcherWithParams(url, { - generationId, - month, - }), - { - revalidateOnFocus: false, - }, - ); - - // data for latest session - const { data: currentRecord } = useSWR( - `/v2/api/attendances/${currentAttendance?.attendanceId}/records`, - fetcher, - { - revalidateOnFocus: false, - }, - ); - - /** - * Get current statistic - */ - const getCurrentStatistic = (report: CotatoAttendanceRecordResponse) => { - const currentStatistics = currentRecord?.find( - (record) => record.memberInfo?.memberId === report.memberInfo?.memberId, - )?.statistic; - - // online, offline, late, absent - const keys = Object.keys(currentStatistics || {}); - - // 0이 아닌 값이 있는 인덱스 == online, offline, late, absent 중 하나 - const currentInfoIndex = Object.values(currentStatistics || {}).findIndex( - (value) => value !== 0, - ); - - const currentStatistic = keys[currentInfoIndex] as keyof CotatoAttendanceStatistic; - - return currentStatistic; - }; - - /** - * - */ - const renderTableHead = () => { - return ( - - - - 멤버 - - - 최근 세션 - - - 대면 - - - 비대면 - - - 지각 - - - 결석 - - - - ); - }; - // // // @@ -129,61 +18,10 @@ const AttendanceReport = () => { height: '100%', }} > - - - - 임시 출석 현황 - - - - - {renderTableHead()} - - {monthRecords?.map((report) => { - const currentStatistic = getCurrentStatistic(report); - - return ( - - {/* 멤버 */} - - {report.memberInfo?.name} - - - {/* 최근 세션 */} - - - {typeof currentStatistic === 'string' - ? STATISTICS_MAP[currentStatistic] - : '--'} - - - - {/* 대면 */} - - {report.statistic?.offline} - - - {/* 비대면 */} - - {report.statistic?.online} - - - {/* 지각 */} - {report.statistic?.late} - - {/* 결석 */} - {report.statistic?.online} - - ); - })} - -
-
-
+ {/* */} + {/* */} + + {/* */} ); }; From 01a509e9fe0e3e1dcbfec6056a49397c5f8e809b Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Thu, 24 Oct 2024 00:34:56 +0900 Subject: [PATCH 05/52] chore: remove useless code --- src/pages/Attendance/Report/AttendanceReportSearch.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportSearch.tsx b/src/pages/Attendance/Report/AttendanceReportSearch.tsx index 1243de09..15ea84fa 100644 --- a/src/pages/Attendance/Report/AttendanceReportSearch.tsx +++ b/src/pages/Attendance/Report/AttendanceReportSearch.tsx @@ -37,9 +37,9 @@ const AttendanceReportSearch = () => { // // useEffect(() => { - if (!debouncedSearchValue) { - updateSearchParams(''); - } + // if (!debouncedSearchValue) { + // updateSearchParams(''); + // } updateSearchParams(debouncedSearchValue); }, [debouncedSearchValue]); From 902fbf7d3066be65b7138a0c0daa8d15aa3c0787 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Thu, 24 Oct 2024 00:35:50 +0900 Subject: [PATCH 06/52] chore: remove comment --- src/pages/Attendance/Report/AttendanceReportSearch.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportSearch.tsx b/src/pages/Attendance/Report/AttendanceReportSearch.tsx index 15ea84fa..ae93243f 100644 --- a/src/pages/Attendance/Report/AttendanceReportSearch.tsx +++ b/src/pages/Attendance/Report/AttendanceReportSearch.tsx @@ -37,10 +37,6 @@ const AttendanceReportSearch = () => { // // useEffect(() => { - // if (!debouncedSearchValue) { - // updateSearchParams(''); - // } - updateSearchParams(debouncedSearchValue); }, [debouncedSearchValue]); From e3af666d9eedf5bb9471e3cfa951bcb86602090f Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Tue, 29 Oct 2024 23:26:39 +0900 Subject: [PATCH 07/52] chore: edit routes --- src/pages/Attendance/Attendance.routes.tsx | 5 +++-- src/pages/Attendance/List/AttendanceList.tsx | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/Attendance/Attendance.routes.tsx b/src/pages/Attendance/Attendance.routes.tsx index f1143df6..9a0d24c3 100644 --- a/src/pages/Attendance/Attendance.routes.tsx +++ b/src/pages/Attendance/Attendance.routes.tsx @@ -29,10 +29,11 @@ const AttendanceRoutes = () => { element={} /> } /> - } /> + } /> + } /> } /> diff --git a/src/pages/Attendance/List/AttendanceList.tsx b/src/pages/Attendance/List/AttendanceList.tsx index 491aaaee..a9277e4b 100644 --- a/src/pages/Attendance/List/AttendanceList.tsx +++ b/src/pages/Attendance/List/AttendanceList.tsx @@ -67,7 +67,7 @@ const AttendanceList = () => { * */ const handleClickReport = () => { - navigate(`/attendance/report/${generationId}`); + navigate(`/attendance/report/generation/${generationId}`); }; /** From afce52816cad67439d1e25aec124714420ee3248 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Tue, 29 Oct 2024 23:26:49 +0900 Subject: [PATCH 08/52] chore: add skeleton --- src/pages/Attendance/Report/AttendanceReportAll.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/pages/Attendance/Report/AttendanceReportAll.tsx diff --git a/src/pages/Attendance/Report/AttendanceReportAll.tsx b/src/pages/Attendance/Report/AttendanceReportAll.tsx new file mode 100644 index 00000000..c7eff79a --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportAll.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +// +// +// + +const AttendanceReportAll = () => { + return
AttendanceReportAll
; +}; + +export default AttendanceReportAll; From 34d50b54012eca5beef819f78cf517a861aac840 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Tue, 29 Oct 2024 23:28:09 +0900 Subject: [PATCH 09/52] feat: add attendance report all --- src/pages/Attendance/Attendance.routes.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/Attendance/Attendance.routes.tsx b/src/pages/Attendance/Attendance.routes.tsx index 9a0d24c3..647c49f9 100644 --- a/src/pages/Attendance/Attendance.routes.tsx +++ b/src/pages/Attendance/Attendance.routes.tsx @@ -10,6 +10,7 @@ const AsyncAttendanceAttend = React.lazy(() => import('./Attend/AttendanceAttend const AsyncAttendanceList = React.lazy(() => import('./List/AttendanceList')); const AsyncAttendanceReport = React.lazy(() => import('./Report/AttendanceReport')); const AsyncAttendanceResult = React.lazy(() => import('./Attend/AttendanceAttendResult')); +const AsyncAttendanceReportAll = React.lazy(() => import('./Report/AttendanceReportAll')); // // @@ -33,7 +34,7 @@ const AttendanceRoutes = () => { element={} /> } /> - } /> + } /> } /> From 599d93f6d624fddc4d08d168c7f999d3721ad313 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Wed, 30 Oct 2024 00:36:07 +0900 Subject: [PATCH 10/52] feat: add disabled design --- .../AttendanceReportSubFilterActions.tsx | 65 ++++++++----------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx b/src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx index 22f5be5f..d7b5edf7 100644 --- a/src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx +++ b/src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx @@ -1,40 +1,11 @@ import React from 'react'; -import CotatoIcon from '@components/CotatoIcon'; + import { Stack, Typography } from '@mui/material'; -import { ReactComponent as OnlineIcon } from '@assets/attendance_online_icon.svg'; -import { ReactComponent as AbsentIcon } from '@assets/attendance_absent_icon.svg'; + import styled from 'styled-components'; import { useAttendanceReportFilter } from '../hooks/useAttendanceReportFilter'; - -// -// -// - -const STATUS_BUTTONS = [ - { - status: 'offline', - icon: theme.colors.sub3[40]} />, - text: '대면', - }, - - { - status: 'online', - icon: , - text: '비대면', - }, - - { - status: 'late', - icon: theme.colors.secondary80} />, - text: '지각', - }, - - { - status: 'absent', - icon: , - text: '결석', - }, -]; +import { useMatch } from 'react-router-dom'; +import { STATUS_ASSETS } from '../constants'; // // @@ -51,21 +22,29 @@ const AttendanceReportSubFilterActions = () => { return currentStatus.includes(status); }; + // + const match = useMatch('/attendance/report/generation/:generationId/all'); + + // + const disabled = match ? true : false; + // // // return ( - {STATUS_BUTTONS.map(({ status, icon, text }) => ( + {STATUS_ASSETS.map(({ status, icon, text }) => ( { toggleStatus(status); }} > {icon} - {text} + {text} ))} @@ -78,7 +57,7 @@ export default AttendanceReportSubFilterActions; // // -const StyledButton = styled.button<{ $selected?: boolean }>` +const StyledButton = styled.button<{ $selected?: boolean; $disabled?: boolean }>` display: flex; align-items: center; justify-content: center; @@ -90,14 +69,24 @@ const StyledButton = styled.button<{ $selected?: boolean }>` border-radius: 0.5rem; background-color: ${({ theme, $selected }) => $selected ? theme.colors.primary80 : theme.colors.gray10}; - cursor: pointer; + + // disabled + background-color: ${({ theme, $disabled }) => $disabled && theme.colors.gray30}; &:hover { transition: background-color 0.3s; ${({ theme, $selected }) => !$selected && `background-color: ${theme.colors.gray20};`} + + // disabled + ${({ theme, $disabled }) => $disabled && `background-color: ${theme.colors.gray30};`} } + + opacity: ${({ $disabled }) => ($disabled ? 0.8 : 1)}; + cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')}; `; -const StyledTypography = styled(Typography)` +const StyledTypography = styled(Typography)<{ $disabled?: boolean }>` color: ${({ theme }) => theme.colors.common.black_const}; + color: ${({ theme, $disabled }) => + $disabled ? theme.colors.gray10 : theme.colors.common.black_const}; `; From 1002154eefc9a3ba5fe4e249126c296aec3bd132 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Wed, 30 Oct 2024 00:36:17 +0900 Subject: [PATCH 11/52] feat: add assets --- src/pages/Attendance/constants/index.tsx | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/pages/Attendance/constants/index.tsx diff --git a/src/pages/Attendance/constants/index.tsx b/src/pages/Attendance/constants/index.tsx new file mode 100644 index 00000000..8dab71bc --- /dev/null +++ b/src/pages/Attendance/constants/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import CotatoIcon from '@components/CotatoIcon'; +import { ReactComponent as OnlineIcon } from '@assets/attendance_online_icon.svg'; +import { ReactComponent as AbsentIcon } from '@assets/attendance_absent_icon.svg'; + +export const STATUS_ASSETS = [ + { + status: 'offline', + icon: theme.colors.sub3[40]} />, + text: '대면', + }, + + { + status: 'online', + icon: , + text: '비대면', + }, + + { + status: 'late', + icon: theme.colors.secondary80} />, + text: '지각', + }, + + { + status: 'absent', + icon: , + text: '결석', + }, +]; From 3b09b30cb9825a70e21727df14d6499c077119cd Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Wed, 30 Oct 2024 00:36:45 +0900 Subject: [PATCH 12/52] feat: implement attendance hooks --- .../Attendance/hooks/useAttendancesRecords.ts | 63 +++++++++++++++++++ .../hooks/useAttendancesRecordsMembers.tsx | 60 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/pages/Attendance/hooks/useAttendancesRecords.ts create mode 100644 src/pages/Attendance/hooks/useAttendancesRecordsMembers.tsx diff --git a/src/pages/Attendance/hooks/useAttendancesRecords.ts b/src/pages/Attendance/hooks/useAttendancesRecords.ts new file mode 100644 index 00000000..f8d9b4f5 --- /dev/null +++ b/src/pages/Attendance/hooks/useAttendancesRecords.ts @@ -0,0 +1,63 @@ +import { useRef } from 'react'; +import { CotatoAttendanceRecordResponse } from 'cotato-openapi-clients'; +import useSWR from 'swr'; +import fetcher from '@utils/fetcher'; + +// +// +// + +interface UseAttendancesRecordsParams { + generationId?: string; + name?: string; +} + +interface UseAttendancesRecordsReturn { + attendancesRecords: CotatoAttendanceRecordResponse[] | []; + filteredAttendancesRecords: CotatoAttendanceRecordResponse[] | []; + isAttendancesRecordsLoading: boolean; + isAttendancesRecordsError: boolean; + mutateAttendancesRecords: () => void; +} + +// +// +// + +export const useAttendancesRecords = ({ + generationId, + name, +}: UseAttendancesRecordsParams): UseAttendancesRecordsReturn => { + const _return = useRef({} as UseAttendancesRecordsReturn); + + // + const { + data: attendancesRecords, + isLoading: isAttendancesRecordsLoading, + error: isAttendancesRecordsError, + mutate: mutateAttendancesRecords, + } = useSWR( + `/v2/api/attendances/records?generationId=${generationId}`, + fetcher, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }, + ); + + const filteredAttendancesRecords = attendancesRecords?.filter( + (record) => record.memberInfo?.name?.toLowerCase().includes(name?.toLowerCase() ?? '') ?? false, + ); + + // + // + // + + _return.current.attendancesRecords = attendancesRecords ?? []; + _return.current.filteredAttendancesRecords = filteredAttendancesRecords ?? []; + _return.current.isAttendancesRecordsLoading = isAttendancesRecordsLoading; + _return.current.isAttendancesRecordsError = isAttendancesRecordsError; + _return.current.mutateAttendancesRecords = mutateAttendancesRecords; + + return _return.current; +}; diff --git a/src/pages/Attendance/hooks/useAttendancesRecordsMembers.tsx b/src/pages/Attendance/hooks/useAttendancesRecordsMembers.tsx new file mode 100644 index 00000000..49365bba --- /dev/null +++ b/src/pages/Attendance/hooks/useAttendancesRecordsMembers.tsx @@ -0,0 +1,60 @@ +import { useRef } from 'react'; +import fetcherWithParams from '@utils/fetcherWithParams'; +import { + CotatoAttendanceRecordResponse, + CotatoMemberAttendanceRecordsResponse, +} from 'cotato-openapi-clients'; +import useSWR from 'swr'; + +// +// +// + +interface UseAttendancesRecordsMembersParams { + generationId?: string; +} + +interface UseAttendancesRecordsMembersReturn { + attendancesRecordsMembers: CotatoMemberAttendanceRecordsResponse['memberAttendResponses'] | []; + isAttendancesRecordsMembersLoading: boolean; + isAttendancesRecordsMembersError: boolean; + mutateAttendancesRecordsMembers: () => void; +} + +// +// +// + +export const useAttendancesRecordsMembers = ({ + generationId, +}: UseAttendancesRecordsMembersParams): UseAttendancesRecordsMembersReturn => { + const _return = useRef( + {} as UseAttendancesRecordsMembersReturn, + ); + + // + const { + data, + isLoading: isAttendancesRecordsMembersLoading, + error: isAttendancesRecordsMembersError, + mutate: mutateAttendancesRecordsMembers, + } = useSWR( + '/v2/api/attendances/records/members', + (url: string) => fetcherWithParams(url, { generationId }), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }, + ); + + // + // + // + + _return.current.attendancesRecordsMembers = data?.memberAttendResponses ?? []; + _return.current.isAttendancesRecordsMembersLoading = isAttendancesRecordsMembersLoading; + _return.current.isAttendancesRecordsMembersError = isAttendancesRecordsMembersError; + _return.current.mutateAttendancesRecordsMembers = mutateAttendancesRecordsMembers; + + return _return.current; +}; From af3ed4f824abefbca76acc970814966f67536b47 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Wed, 30 Oct 2024 00:37:16 +0900 Subject: [PATCH 13/52] feat: add report all table, filters --- .../Attendance/Report/AttendanceReportAll.tsx | 16 +- .../Report/AttendanceReportAllTable.tsx | 146 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/pages/Attendance/Report/AttendanceReportAllTable.tsx diff --git a/src/pages/Attendance/Report/AttendanceReportAll.tsx b/src/pages/Attendance/Report/AttendanceReportAll.tsx index c7eff79a..f3a5c354 100644 --- a/src/pages/Attendance/Report/AttendanceReportAll.tsx +++ b/src/pages/Attendance/Report/AttendanceReportAll.tsx @@ -1,11 +1,25 @@ +import { Container, Stack } from '@mui/material'; import React from 'react'; +import AttendanceReportSubFilters from './AttendanceReportSubFilters'; +import AttendanceReportAllTable from './AttendanceReportAllTable'; // // // const AttendanceReportAll = () => { - return
AttendanceReportAll
; + return ( + + + + + + + ); }; export default AttendanceReportAll; diff --git a/src/pages/Attendance/Report/AttendanceReportAllTable.tsx b/src/pages/Attendance/Report/AttendanceReportAllTable.tsx new file mode 100644 index 00000000..4b0f234b --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportAllTable.tsx @@ -0,0 +1,146 @@ +import { + Stack, + Table, + TableBody, + TableCell, + TableCellProps, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; +import React, { useEffect } from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; + +import { STATUS_ASSETS } from '../constants'; +import { useAttendancesRecords } from '../hooks/useAttendancesRecords'; +import styled from 'styled-components'; + +// +// +// + +const NAME_FIELD = [ + { icon: null, text: '이름', status: 'name' }, +] as unknown as typeof STATUS_ASSETS; + +const TABLE_HEAD = NAME_FIELD.concat(STATUS_ASSETS); + +// +// +// + +const AttendanceReportAllTable = () => { + // + const { generationId } = useParams<{ generationId: string }>(); + + // + const [searchParams] = useSearchParams(); + const search = searchParams.get('search') || ''; + + // + const { attendancesRecords, filteredAttendancesRecords } = useAttendancesRecords({ + generationId, + name: search, + }); + + // + const [currentRecords, setCurrentRecords] = React.useState(attendancesRecords); + + /** + * + */ + const renderTableHead = () => { + return ( + + + {TABLE_HEAD.map((head) => ( + + + {head.icon} + {head.text} + + + ))} + + + ); + }; + + /** + * + */ + const renderTableBody = () => { + return ( + + {currentRecords?.map((record) => ( + + {record.memberInfo?.name} + {record.statistic?.online} + {record.statistic?.offline} + {record.statistic?.late} + {record.statistic?.absent} + + ))} + + ); + }; + + // + // if search exists, set filtered records + // + useEffect(() => { + if (search) { + setCurrentRecords(filteredAttendancesRecords); + return; + } + + setCurrentRecords(attendancesRecords); + }, [search, attendancesRecords, filteredAttendancesRecords]); + + // + // + // + + return ( + + + {renderTableHead()} + {renderTableBody()} +
+
+ ); +}; + +export default AttendanceReportAllTable; + +// +// +// + +const StyledTableContainer = styled(TableContainer)` + border: 1px solid ${({ theme }) => theme.colors.primary60}; +`; + +const StyledTableHead = styled(TableHead)` + background-color: ${({ theme }) => theme.colors.pastelTone.yellow[100]}; +`; + +const StyledTableRow = styled(TableRow)` + background-color: ${({ theme }) => theme.colors.common.white_const}; +`; + +/** + * + */ +const CenterAlignedTableCell: React.FC = ({ children, ...props }) => { + return ( + + {children} + + ); +}; + +const StyledTableCell = styled(CenterAlignedTableCell)` + width: 20%; + border-bottom: 1px solid ${({ theme }) => theme.colors.primary60} !important; +`; From 6aab9857a4c1056ed3bffb37ffc1f6b88b246125 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Thu, 31 Oct 2024 00:05:25 +0900 Subject: [PATCH 14/52] feat: implement report all table --- .../Attendance/Report/AttendanceReport.tsx | 9 +- .../Report/AttendanceReportTable.tsx | 213 ++++++++++++++++++ .../components/AttendanceStatusDropdown.tsx | 128 +++++++++++ src/pages/Attendance/constants/index.tsx | 47 ++++ 4 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 src/pages/Attendance/Report/AttendanceReportTable.tsx create mode 100644 src/pages/Attendance/Report/components/AttendanceStatusDropdown.tsx diff --git a/src/pages/Attendance/Report/AttendanceReport.tsx b/src/pages/Attendance/Report/AttendanceReport.tsx index d548de0e..dca139f1 100644 --- a/src/pages/Attendance/Report/AttendanceReport.tsx +++ b/src/pages/Attendance/Report/AttendanceReport.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { Container } from '@mui/system'; +import { Container, Stack } from '@mui/system'; import AttendanceReportSubFilters from './AttendanceReportSubFilters'; +import AttendanceReportTable from './AttendanceReportTable'; // // @@ -20,8 +21,10 @@ const AttendanceReport = () => { > {/* */} {/* */} - - {/* */} + + + + ); }; diff --git a/src/pages/Attendance/Report/AttendanceReportTable.tsx b/src/pages/Attendance/Report/AttendanceReportTable.tsx new file mode 100644 index 00000000..eaaa71fc --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportTable.tsx @@ -0,0 +1,213 @@ +import { useGeneration } from '@/hooks/useGeneration'; +import useGetAttendances from '@/hooks/useGetAttendances'; +import { Table, TableBody, TableCell, TableRow } from '@mui/material'; +import React, { useEffect } from 'react'; +import { useAttendancesAttendanceIdRecordsGet } from '../hooks/useAttendancesAttendanceIdRecordsGet'; +import { useSession } from '@/hooks/useSession'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import AttedanceTableLayout from './components/AttedanceTableLayout'; +import { getCurrentStatistic } from '../utils/util'; +import AttendanceStatusDropdown from './components/AttendanceStatusDropdown'; +import { useSearchParams } from 'react-router-dom'; + +// +// +// + +const TABLE_HEAD = ['이름', '출석 상태']; + +// +// +// + +const AttendanceReportTable = () => { + const { isMobileOrSmaller } = useBreakpoints(); + + const [searchParams] = useSearchParams(); + const status = searchParams.getAll('status') ?? []; + const search = searchParams.get('search') || ''; + + // + const { currentGeneration } = useGeneration(); + + // TODO: need to get the latest session by params + const { sessions } = useSession({ generationId: currentGeneration?.generationId }); + + // + const { currentAttendance } = useGetAttendances({ + sessionId: sessions?.at(-1)?.sessionId, + generationId: currentGeneration?.generationId, + }); + + // + const attendanceId = currentAttendance?.attendanceId; + + // + const { attendancesAttendanceIdRecords } = useAttendancesAttendanceIdRecordsGet({ + attendanceId: attendanceId ?? 0, + }); + + // + const [currentRecords, setCurrentRecords] = React.useState(attendancesAttendanceIdRecords); + + /** + * + */ + const renderTableHead = () => { + if (isMobileOrSmaller) { + return ( + + {TABLE_HEAD.map((head) => ( + + {head} + + ))} + + ); + } + + return ( + + {TABLE_HEAD.map((head) => ( + <> + + {head} + + + ))} + {TABLE_HEAD.map((head) => ( + <> + + {head} + + + ))} + + ); + }; + + /** + * + */ const renderEmptyTableCell = () => { + return ( + <> + + + + ); + }; + + /** + * + */ + const renderTableBody = () => { + if (isMobileOrSmaller) { + return currentRecords?.map((record) => ( + <> + + + {record.memberInfo?.name} + + + + + + + )); + } + + return Array.from({ length: Math.ceil(currentRecords?.length / 2) }, (_, i) => { + const firstRecord = currentRecords[i * 2]; + const secondRecord = currentRecords?.[i * 2 + 1]; + + return ( + + + {firstRecord?.memberInfo?.name} + + + + + {secondRecord ? ( + <> + + {secondRecord?.memberInfo?.name} + + + + + + ) : ( + renderEmptyTableCell() + )} + + ); + }); + }; + + // + // + // + useEffect(() => { + if (!attendancesAttendanceIdRecords) { + return; + } + + let filteredRecords = attendancesAttendanceIdRecords; + + // filter records by search + if (search) { + filteredRecords = filteredRecords?.filter((record) => + record.memberInfo?.name?.toLowerCase().includes(search.toLowerCase()), + ); + + console.log(filteredRecords); + + setCurrentRecords(filteredRecords); + } + + // filter records by status + if (status.length) { + filteredRecords = filteredRecords?.filter((record) => + status.some((status) => + getCurrentStatistic(record).toLowerCase().includes(status.toLowerCase()), + ), + ); + + setCurrentRecords(filteredRecords); + + return; + } + + if (!search && !status.length) { + setCurrentRecords(attendancesAttendanceIdRecords); + } + }, [status.length, attendancesAttendanceIdRecords, search]); + + // + // + // + + return ( + + + {renderTableHead()} + {renderTableBody()} +
+
+ ); +}; + +export default AttendanceReportTable; diff --git a/src/pages/Attendance/Report/components/AttendanceStatusDropdown.tsx b/src/pages/Attendance/Report/components/AttendanceStatusDropdown.tsx new file mode 100644 index 00000000..68beb2b9 --- /dev/null +++ b/src/pages/Attendance/Report/components/AttendanceStatusDropdown.tsx @@ -0,0 +1,128 @@ +import api from '@/api/api'; +import { MenuItem, Stack, TextField } from '@mui/material'; +import { ATTENDANCE_ASSETS_MAP } from '@pages/Attendance/constants'; +import React from 'react'; +import styled from 'styled-components'; +import { toast } from 'react-toastify'; +import { useAttendancesAttendanceIdRecordsGet } from '@pages/Attendance/hooks/useAttendancesAttendanceIdRecordsGet'; + +// +// +// + +interface AttendanceStatusDropdownProps { + status: string; + attendanceId?: number; + memberId?: number; +} + +// +// +// + +const AttendanceStatusDropdown: React.FC = ({ + status, + attendanceId, + memberId, +}) => { + // + const { mutateAttendancesAttendanceIdRecords } = useAttendancesAttendanceIdRecordsGet({ + attendanceId: attendanceId ?? 0, + }); + + /** + * + */ + const handleStatusChange = async ( + e: React.ChangeEvent<{ value: unknown }>, + memberId?: number, + ) => { + const status = e.target.value as string; + + try { + await api.patch(`/v2/api/attendances/${attendanceId}/records`, { + memberId, + attendanceResult: status.toUpperCase(), + }); + + mutateAttendancesAttendanceIdRecords(); + toast.success('출석 상태가 성공적으로 변경되었습니다.'); + } catch (error) { + toast.error('출석 상태 변경에 실패했습니다.'); + } + }; + + // + // + // + + return ( + { + const icon = ATTENDANCE_ASSETS_MAP[value as keyof typeof ATTENDANCE_ASSETS_MAP]?.icon; + const text = ATTENDANCE_ASSETS_MAP[value as keyof typeof ATTENDANCE_ASSETS_MAP]?.text; + + return ( + + {icon} + {text} + + ); + }, + sx: { + '.MuiSelect-select': { + padding: '0.25rem', + height: '2.5rem', + }, + }, + }, + }} + onChange={(e) => handleStatusChange(e, memberId)} + > + {Object.keys(ATTENDANCE_ASSETS_MAP).map((asset) => { + const icon = ATTENDANCE_ASSETS_MAP[asset as keyof typeof ATTENDANCE_ASSETS_MAP]?.icon; + const text = ATTENDANCE_ASSETS_MAP[asset as keyof typeof ATTENDANCE_ASSETS_MAP]?.text; + + return ( + + + {icon} + {text} + + + ); + })} + + ); +}; + +export default AttendanceStatusDropdown; + +// +// +// + +const StyledTextField = styled(TextField)` + width: 8.75rem; + background-color: ${({ theme }) => theme.colors.gray10}; + border-radius: 0.5rem; +`; diff --git a/src/pages/Attendance/constants/index.tsx b/src/pages/Attendance/constants/index.tsx index 8dab71bc..791a5a0d 100644 --- a/src/pages/Attendance/constants/index.tsx +++ b/src/pages/Attendance/constants/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import CotatoIcon from '@components/CotatoIcon'; import { ReactComponent as OnlineIcon } from '@assets/attendance_online_icon.svg'; import { ReactComponent as AbsentIcon } from '@assets/attendance_absent_icon.svg'; +import { CotatoAttendanceStatistic } from 'cotato-openapi-clients'; export const STATUS_ASSETS = [ { @@ -28,3 +29,49 @@ export const STATUS_ASSETS = [ text: '결석', }, ]; + +export const ATTENDANCE_ASSETS_ICON_MAP: Record = { + offline: STATUS_ASSETS[0].icon, + online: STATUS_ASSETS[1].icon, + late: STATUS_ASSETS[2].icon, + absent: STATUS_ASSETS[3].icon, +}; + +export const ATTENDANCE_ASSETS_TEXT_MAP: Record = { + offline: STATUS_ASSETS[0].text, + online: STATUS_ASSETS[1].text, + late: STATUS_ASSETS[2].text, + absent: STATUS_ASSETS[3].text, +}; + +export const ATTENDANCE_ASSETS_MAP: Record< + keyof CotatoAttendanceStatistic, + { text: string; icon: JSX.Element } +> = { + offline: { + text: '대면', + icon: theme.colors.sub3[40]} />, + }, + online: { + text: '비대면', + icon: , + }, + late: { + text: '지각', + icon: theme.colors.secondary80} />, + }, + absent: { + text: '결석', + icon: , + }, +}; + +export const STATISTICS_MAP = { + offline: '대면', + online: '비대면', + late: '지각', + absent: '결석', + undefined: '-', +}; + +export const ATTENDANCE_STATUS_TEXT = ['대면', '비대면', '지각', '결석']; From 51214f21f109160edb93e7f286f97047e19db865 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Thu, 31 Oct 2024 00:05:52 +0900 Subject: [PATCH 15/52] feat: implement attendances attendance id records get hook --- .../useAttendancesAttendanceIdRecordsGet.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/pages/Attendance/hooks/useAttendancesAttendanceIdRecordsGet.ts diff --git a/src/pages/Attendance/hooks/useAttendancesAttendanceIdRecordsGet.ts b/src/pages/Attendance/hooks/useAttendancesAttendanceIdRecordsGet.ts new file mode 100644 index 00000000..4bf3599b --- /dev/null +++ b/src/pages/Attendance/hooks/useAttendancesAttendanceIdRecordsGet.ts @@ -0,0 +1,57 @@ +import fetcher from '@utils/fetcher'; +import { CotatoAttendanceRecordResponse } from 'cotato-openapi-clients'; +import { useRef } from 'react'; +import useSWR from 'swr'; + +// +// +// + +interface UseAttendancesAttendanceIdRecordsGetParams { + attendanceId?: number; +} + +interface UseAttendancesAttendanceIdRecordsGetReturn { + attendancesAttendanceIdRecords: CotatoAttendanceRecordResponse[] | []; + isAttendancesAttendanceIdRecordsLoading: boolean; + isAttendancesAttendanceIdRecordsError: boolean; + mutateAttendancesAttendanceIdRecords: () => void; +} + +// +// +// + +export const useAttendancesAttendanceIdRecordsGet = ({ + attendanceId, +}: UseAttendancesAttendanceIdRecordsGetParams): UseAttendancesAttendanceIdRecordsGetReturn => { + const _return = useRef( + {} as UseAttendancesAttendanceIdRecordsGetReturn, + ); + + // + const { + data: attendancesAttendanceIdRecords, + isLoading: isAttendancesAttendanceIdRecordsLoading, + error: isAttendancesAttendanceIdRecordsError, + mutate: mutateAttendancesAttendanceIdRecords, + } = useSWR( + `/v2/api/attendances/${attendanceId}/records`, + fetcher, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }, + ); + + // + // + // + + _return.current.attendancesAttendanceIdRecords = attendancesAttendanceIdRecords ?? []; + _return.current.isAttendancesAttendanceIdRecordsLoading = isAttendancesAttendanceIdRecordsLoading; + _return.current.isAttendancesAttendanceIdRecordsError = isAttendancesAttendanceIdRecordsError; + _return.current.mutateAttendancesAttendanceIdRecords = mutateAttendancesAttendanceIdRecords; + + return _return.current; +}; From 3ebd42fcfb366e62577e55676443b8aaac924c3e Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Thu, 31 Oct 2024 00:06:11 +0900 Subject: [PATCH 16/52] fix: add global toast container, css --- src/App.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index a2fc9c4c..6e99c06c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import FindID from '@pages/Login/FindID'; import FindPWProcess from '@pages/Login/FindPWProcess'; import ReadyState from '@components/ReadyState'; import NotFound from '@components/NotFound'; +import 'react-toastify/dist/ReactToastify.css'; import CotatoThemeProvider from '@theme/context/CotatoThemeProvider'; import GlobalBackgroundSvgComponent from '@components/GlobalBackgroundSvgComponent'; @@ -22,6 +23,7 @@ import AttendanceRoutes from '@pages/Attendance/Attendance.routes'; import { AttendanceFab } from '@components/attendance/attendance-fab'; import AgreementConfirmDialog from '@components/AgreementConfirmDialog'; import CSRoutes from '@pages/CS/CSRoutes'; +import { ToastContainer } from 'react-toastify'; function App() { const location = useLocation(); @@ -48,6 +50,7 @@ function App() {
+
From b1c3bc6914a8faa31303c3313fa1f1e4488ba3f5 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Thu, 31 Oct 2024 00:06:44 +0900 Subject: [PATCH 17/52] feat: replace shared style into shared component --- .../Report/AttendanceReportAllTable.tsx | 86 +++++++------------ .../components/AttedanceTableLayout.tsx | 46 ++++++++++ 2 files changed, 76 insertions(+), 56 deletions(-) create mode 100644 src/pages/Attendance/Report/components/AttedanceTableLayout.tsx diff --git a/src/pages/Attendance/Report/AttendanceReportAllTable.tsx b/src/pages/Attendance/Report/AttendanceReportAllTable.tsx index 4b0f234b..286a2439 100644 --- a/src/pages/Attendance/Report/AttendanceReportAllTable.tsx +++ b/src/pages/Attendance/Report/AttendanceReportAllTable.tsx @@ -1,19 +1,11 @@ -import { - Stack, - Table, - TableBody, - TableCell, - TableCellProps, - TableContainer, - TableHead, - TableRow, -} from '@mui/material'; +import { Stack, Table, TableBody, TableRow } from '@mui/material'; import React, { useEffect } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { STATUS_ASSETS } from '../constants'; import { useAttendancesRecords } from '../hooks/useAttendancesRecords'; -import styled from 'styled-components'; + +import AttedanceTableLayout from './components/AttedanceTableLayout'; // // @@ -51,18 +43,18 @@ const AttendanceReportAllTable = () => { */ const renderTableHead = () => { return ( - + {TABLE_HEAD.map((head) => ( - + {head.icon} {head.text} - + ))} - + ); }; @@ -73,13 +65,27 @@ const AttendanceReportAllTable = () => { return ( {currentRecords?.map((record) => ( - - {record.memberInfo?.name} - {record.statistic?.online} - {record.statistic?.offline} - {record.statistic?.late} - {record.statistic?.absent} - + + + {record.memberInfo?.name} + + + + {record.statistic?.online} + + + + {record.statistic?.offline} + + + + {record.statistic?.late} + + + + {record.statistic?.absent} + + ))} ); @@ -102,45 +108,13 @@ const AttendanceReportAllTable = () => { // return ( - + {renderTableHead()} {renderTableBody()}
-
+ ); }; export default AttendanceReportAllTable; - -// -// -// - -const StyledTableContainer = styled(TableContainer)` - border: 1px solid ${({ theme }) => theme.colors.primary60}; -`; - -const StyledTableHead = styled(TableHead)` - background-color: ${({ theme }) => theme.colors.pastelTone.yellow[100]}; -`; - -const StyledTableRow = styled(TableRow)` - background-color: ${({ theme }) => theme.colors.common.white_const}; -`; - -/** - * - */ -const CenterAlignedTableCell: React.FC = ({ children, ...props }) => { - return ( - - {children} - - ); -}; - -const StyledTableCell = styled(CenterAlignedTableCell)` - width: 20%; - border-bottom: 1px solid ${({ theme }) => theme.colors.primary60} !important; -`; diff --git a/src/pages/Attendance/Report/components/AttedanceTableLayout.tsx b/src/pages/Attendance/Report/components/AttedanceTableLayout.tsx new file mode 100644 index 00000000..78719aaa --- /dev/null +++ b/src/pages/Attendance/Report/components/AttedanceTableLayout.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { TableCell, TableCellProps, TableContainer, TableHead, TableRow } from '@mui/material'; +import styled from 'styled-components'; + +// +// +// + +const StyledTableContainer = styled(TableContainer)` + border: 1px solid ${({ theme }) => theme.colors.primary60}; +`; + +const StyledTableHead = styled(TableHead)` + background-color: ${({ theme }) => theme.colors.pastelTone.yellow[100]}; +`; + +const StyledTableRow = styled(TableRow)` + background-color: ${({ theme }) => theme.colors.common.white_const}; +`; + +/** + * + */ +const CenterAlignedTableCell: React.FC = ({ children, ...props }) => { + return ( + + {children} + + ); +}; + +const StyledTableCell = styled(CenterAlignedTableCell)` + width: 20%; + border-bottom: 1px solid ${({ theme }) => theme.colors.primary60} !important; +`; + +export default Object.assign( + {}, + { + TableContainer: StyledTableContainer, + TableHead: StyledTableHead, + TableRow: StyledTableRow, + TableCell: StyledTableCell, + TableHeadTableCell: StyledTableCell, + }, +); From 973c040bb6fa8e93019e84ec65b64cc2cf7590f5 Mon Sep 17 00:00:00 2001 From: Donghyun Hwang Date: Thu, 31 Oct 2024 00:06:59 +0900 Subject: [PATCH 18/52] feat: add get attendance statistics util --- src/pages/Attendance/utils/util.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/pages/Attendance/utils/util.ts diff --git a/src/pages/Attendance/utils/util.ts b/src/pages/Attendance/utils/util.ts new file mode 100644 index 00000000..6202a741 --- /dev/null +++ b/src/pages/Attendance/utils/util.ts @@ -0,0 +1,17 @@ +import { CotatoAttendanceRecordResponse, CotatoAttendanceStatistic } from 'cotato-openapi-clients'; + +/** + * Get current statistic + */ +export const getCurrentStatistic = (record: CotatoAttendanceRecordResponse | undefined) => { + // online, offline, late, absent + + const keys = Object.keys(record?.statistic || {}); + + // 0이 아닌 값이 있는 인덱스 == online, offline, late, absent 중 하나 + const currentInfoIndex = Object.values(record?.statistic || {}).findIndex((value) => value !== 0); + + const currentStatistic = keys[currentInfoIndex] as keyof CotatoAttendanceStatistic; + + return currentInfoIndex === -1 ? 'undefined' : currentStatistic; +}; From 8d71d38c9e618797e5f1f3f6b8ae60c9b7886abf Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Thu, 31 Oct 2024 02:32:13 +0900 Subject: [PATCH 19/52] feat: implement report header layout --- .../Attendance/Report/AttendanceReport.tsx | 3 +- .../Report/AttendanceReportHeader.tsx | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/pages/Attendance/Report/AttendanceReportHeader.tsx diff --git a/src/pages/Attendance/Report/AttendanceReport.tsx b/src/pages/Attendance/Report/AttendanceReport.tsx index d548de0e..1a649b8f 100644 --- a/src/pages/Attendance/Report/AttendanceReport.tsx +++ b/src/pages/Attendance/Report/AttendanceReport.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Container } from '@mui/system'; import AttendanceReportSubFilters from './AttendanceReportSubFilters'; +import AttendanceReportHeader from './AttendanceReportHeader'; // // @@ -18,7 +19,7 @@ const AttendanceReport = () => { height: '100%', }} > - {/* */} + {/* */} {/* */} diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx new file mode 100644 index 00000000..7ecd546f --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Box, Stack, Typography } from '@mui/material'; +import { useTheme } from 'styled-components'; +import GenerationDropBox from '@components/GenerationDropBox'; + +const AttendanceReportHeader = () => { + const theme = useTheme(); + + return ( + + + + 출석부 확인하기 + + + + +
기수 선택
+
세션 선택
+
+ 엑셀 +
+
+ ); +}; + +export default AttendanceReportHeader; From 29e85fb7ffd89bd17a5f070b982004053d8e7bea Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Thu, 31 Oct 2024 12:33:13 +0900 Subject: [PATCH 20/52] chore: add comment --- src/pages/Attendance/Report/AttendanceReportHeader.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index 7ecd546f..036bacdc 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -1,7 +1,10 @@ import React from 'react'; import { Box, Stack, Typography } from '@mui/material'; import { useTheme } from 'styled-components'; -import GenerationDropBox from '@components/GenerationDropBox'; + +// +// +// const AttendanceReportHeader = () => { const theme = useTheme(); From 35894fd956fff6b2a64688ecf94c4975b3a70d76 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Thu, 31 Oct 2024 12:43:03 +0900 Subject: [PATCH 21/52] refactor: delete drop box color enum --- src/components/GenerationDropBox.tsx | 13 +++++++------ src/enums/DropBoxColor.ts | 8 -------- src/pages/Session/SessionHome.tsx | 3 +-- 3 files changed, 8 insertions(+), 16 deletions(-) delete mode 100644 src/enums/DropBoxColor.ts diff --git a/src/components/GenerationDropBox.tsx b/src/components/GenerationDropBox.tsx index 45e054d6..9759e712 100644 --- a/src/components/GenerationDropBox.tsx +++ b/src/components/GenerationDropBox.tsx @@ -3,7 +3,6 @@ import { styled, useTheme } from 'styled-components'; import { ReactComponent as CheckIcon } from '@assets/check_icon_dotted.svg'; import generationSort from '@utils/newGenerationSort'; import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; -import { DropBoxColorEnum } from '@/enums/DropBoxColor'; import drop_box_background_blue from '@assets/drop_box_background_blue.svg'; import { useSearchParams } from 'react-router-dom'; import { useGeneration } from '@/hooks/useGeneration'; @@ -19,7 +18,7 @@ interface GenerationDropBoxProps { * @param generation selected generation */ handleGenerationChange: (generation: CotatoGenerationInfoResponse) => void; - color?: DropBoxColorEnum; + color?: string; width?: string; height?: string; } @@ -48,7 +47,7 @@ const FADE_DURATION = 300; */ const GenerationDropBox = ({ handleGenerationChange, - color = DropBoxColorEnum.BLUE, + color = 'blue', width = '8rem', height = '3.2rem', }: GenerationDropBoxProps) => { @@ -70,17 +69,19 @@ const GenerationDropBox = ({ /** * get drop box style of color * @returns drop box style { background: url of drop box background, arrowColor: color code of arrow button} - * @throws invalid color type */ const getDropBoxStyle = () => { - if (color === DropBoxColorEnum.BLUE) { + if (color === 'blue') { return { background: `url(${drop_box_background_blue})`, arrowColor: theme.colors.sub2[80], }; } - throw new TypeError('invalid color type'); + return { + background: `url(${drop_box_background_blue})`, + arrowColor: theme.colors.sub2[80], + }; }; /** diff --git a/src/enums/DropBoxColor.ts b/src/enums/DropBoxColor.ts deleted file mode 100644 index 1bf97ffb..00000000 --- a/src/enums/DropBoxColor.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * enum for DropBoxColor - * - * - **BLUE** - */ -export enum DropBoxColorEnum { - BLUE = 'blue', -} diff --git a/src/pages/Session/SessionHome.tsx b/src/pages/Session/SessionHome.tsx index 0a1b0cf5..6d6984a0 100644 --- a/src/pages/Session/SessionHome.tsx +++ b/src/pages/Session/SessionHome.tsx @@ -13,7 +13,6 @@ import { } from 'cotato-openapi-clients'; import GenerationDropBox from '@components/GenerationDropBox'; import { useMediaQuery } from '@mui/material'; -import { DropBoxColorEnum } from '@/enums/DropBoxColor'; import { device } from '@theme/media'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Pagination, Scrollbar } from 'swiper/modules'; @@ -305,7 +304,7 @@ const SessionHome = () => { return ( Date: Thu, 31 Oct 2024 15:57:41 +0900 Subject: [PATCH 22/52] feat: set generation id query string in handle function --- src/components/GenerationDropBox.tsx | 22 +--------------------- src/pages/Session/SessionHome.tsx | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/components/GenerationDropBox.tsx b/src/components/GenerationDropBox.tsx index 9759e712..5990a963 100644 --- a/src/components/GenerationDropBox.tsx +++ b/src/components/GenerationDropBox.tsx @@ -4,7 +4,6 @@ import { ReactComponent as CheckIcon } from '@assets/check_icon_dotted.svg'; import generationSort from '@utils/newGenerationSort'; import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; import drop_box_background_blue from '@assets/drop_box_background_blue.svg'; -import { useSearchParams } from 'react-router-dom'; import { useGeneration } from '@/hooks/useGeneration'; import CotatoIcon from './CotatoIcon'; @@ -53,7 +52,6 @@ const GenerationDropBox = ({ }: GenerationDropBoxProps) => { const theme = useTheme(); - const [searchParams, setSearchParams] = useSearchParams(); const { generations: rawGenerations, isGenerationLoading } = useGeneration(); const [isDropBoxOpen, setIsDropBoxOpen] = useState(false); @@ -84,15 +82,6 @@ const GenerationDropBox = ({ }; }; - /** - * - */ - const setGenerationSearchParam = (generation: CotatoGenerationInfoResponse) => { - if (generation?.generationId) { - setSearchParams({ generationId: generation.generationId.toString() }); - } - }; - /** * */ @@ -106,7 +95,6 @@ const GenerationDropBox = ({ const handleGenerationSelect = (generation: CotatoGenerationInfoResponse) => { setSelectedGeneration(generation); handleGenerationChange(generation); - setGenerationSearchParam(generation); }; /** @@ -191,17 +179,9 @@ const GenerationDropBox = ({ } const sortedGenerations = generationSort(rawGenerations); - // .filter( - // (generation) => generation.generationNumber && generation.generationNumber >= 8, - // ); setGenerations(sortedGenerations); - const generationId = searchParams.get('generationId'); - const searchedGeneration = sortedGenerations.find( - (generation) => generation.generationId === Number(generationId), - ); - - handleGenerationSelect(searchedGeneration || sortedGenerations[0]); + handleGenerationSelect(sortedGenerations[0]); }, [rawGenerations, isGenerationLoading]); return ( diff --git a/src/pages/Session/SessionHome.tsx b/src/pages/Session/SessionHome.tsx index 6d6984a0..053fbde9 100644 --- a/src/pages/Session/SessionHome.tsx +++ b/src/pages/Session/SessionHome.tsx @@ -23,13 +23,15 @@ import 'swiper/css'; import 'swiper/css/pagination'; import 'swiper/css/scrollbar'; import { toast } from 'react-toastify'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useGeneration } from '@/hooks/useGeneration'; // // // const SessionHome = () => { + const { currentGeneration, generations } = useGeneration(); const [selectedGeneration, setSelectedGeneration] = useState(); const { data: sessionList, mutate: mutateSessionList } = useSWR( @@ -46,6 +48,7 @@ const SessionHome = () => { const [selectedSession, setSelectedSession] = useState(null); const isTabletOrSmaller = useMediaQuery(`(max-width:${device.tablet})`); + const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); /** @@ -83,6 +86,7 @@ const SessionHome = () => { */ const handleGenerationChange = (generation: CotatoGenerationInfoResponse) => { setSelectedGeneration(generation); + setSearchParams({ generationId: generation.generationId!.toString() }); }; /** @@ -203,7 +207,6 @@ const SessionHome = () => { * */ const handleSessionUpdate = (session: SessionUploadInfo) => { - console.log(session); if (!session.sessionId) { return; } @@ -377,6 +380,26 @@ const SessionHome = () => { ); }; + /** + * set generationId from url + */ + useEffect(() => { + if (!currentGeneration || !generations) { + return; + } + + const generationId = searchParams.get('generationId'); + + if (generationId) { + setSelectedGeneration( + generations?.find((generation) => generation.generationId === Number(generationId)), + ); + } else { + setSearchParams({ generationId: currentGeneration!.generationId!.toString() }); + setSelectedGeneration(currentGeneration); + } + }, [currentGeneration, generations]); + /** * */ From f5306ea99e6dec3c0aa171ad5e6d59bc450e8b9b Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 00:46:35 +0900 Subject: [PATCH 23/52] refactor: change component name for dropbox --- .../{GenerationDropBox.tsx => CotatoDropBox.tsx} | 8 ++++---- src/pages/MyPage/CSRecord.tsx | 4 ++-- src/pages/Session/SessionHome.tsx | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename src/components/{GenerationDropBox.tsx => CotatoDropBox.tsx} (98%) diff --git a/src/components/GenerationDropBox.tsx b/src/components/CotatoDropBox.tsx similarity index 98% rename from src/components/GenerationDropBox.tsx rename to src/components/CotatoDropBox.tsx index 5990a963..e632bc3a 100644 --- a/src/components/GenerationDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -11,7 +11,7 @@ import CotatoIcon from './CotatoIcon'; // // -interface GenerationDropBoxProps { +interface CotatoDropBoxProps { /** * generation change event * @param generation selected generation @@ -44,12 +44,12 @@ const FADE_DURATION = 300; * @param width drop box width (default: 8rem) * @param height drop box height (default: 3.2rem) */ -const GenerationDropBox = ({ +const CotatoDropBox = ({ handleGenerationChange, color = 'blue', width = '8rem', height = '3.2rem', -}: GenerationDropBoxProps) => { +}: CotatoDropBoxProps) => { const theme = useTheme(); const { generations: rawGenerations, isGenerationLoading } = useGeneration(); @@ -292,4 +292,4 @@ const StyledCheckIcon = styled(CheckIcon)` top: 0.75rem; `; -export default GenerationDropBox; +export default CotatoDropBox; diff --git a/src/pages/MyPage/CSRecord.tsx b/src/pages/MyPage/CSRecord.tsx index 24db9049..64159555 100644 --- a/src/pages/MyPage/CSRecord.tsx +++ b/src/pages/MyPage/CSRecord.tsx @@ -5,7 +5,7 @@ import useSWR from 'swr'; import localeKr from '@/assets/locale/locale_kr.json'; import useUser from '@/hooks/useUser'; import { useGeneration } from '@/hooks/useGeneration'; -import GenerationDropBox from '@components/GenerationDropBox'; +import CotatoDropBox from '@components/CotatoDropBox'; import { useSearchParams } from 'react-router-dom'; // @@ -48,7 +48,7 @@ const CSRecord = () => { 내가 풀어본 CS 문제풀이 - { setSelectedGenerationId(generation?.generationId?.toString()); }} diff --git a/src/pages/Session/SessionHome.tsx b/src/pages/Session/SessionHome.tsx index 053fbde9..f65c5bdb 100644 --- a/src/pages/Session/SessionHome.tsx +++ b/src/pages/Session/SessionHome.tsx @@ -11,7 +11,7 @@ import { CotatoLocalTime, CotatoSessionListResponse, } from 'cotato-openapi-clients'; -import GenerationDropBox from '@components/GenerationDropBox'; +import CotatoDropBox from '@components/CotatoDropBox'; import { useMediaQuery } from '@mui/material'; import { device } from '@theme/media'; import { Swiper, SwiperSlide } from 'swiper/react'; @@ -306,7 +306,7 @@ const SessionHome = () => { const renderSettingTab = () => { return ( - Date: Fri, 1 Nov 2024 01:23:00 +0900 Subject: [PATCH 24/52] refactor: change list value from props --- src/components/CotatoDropBox.tsx | 107 ++++++++++++++++-------------- src/pages/MyPage/CSRecord.tsx | 14 ++-- src/pages/Session/SessionHome.tsx | 15 +++-- 3 files changed, 76 insertions(+), 60 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index e632bc3a..711c3892 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useRef, useState } from 'react'; import { styled, useTheme } from 'styled-components'; import { ReactComponent as CheckIcon } from '@assets/check_icon_dotted.svg'; -import generationSort from '@utils/newGenerationSort'; import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; import drop_box_background_blue from '@assets/drop_box_background_blue.svg'; -import { useGeneration } from '@/hooks/useGeneration'; import CotatoIcon from './CotatoIcon'; // @@ -12,11 +10,9 @@ import CotatoIcon from './CotatoIcon'; // interface CotatoDropBoxProps { - /** - * generation change event - * @param generation selected generation - */ - handleGenerationChange: (generation: CotatoGenerationInfoResponse) => void; + list: CotatoGenerationInfoResponse[]; + onChange: (generation: CotatoGenerationInfoResponse) => void; + reversed?: boolean; color?: string; width?: string; height?: string; @@ -38,29 +34,29 @@ const FADE_DURATION = 300; // /** - * generation drop box component - * @param handleGenerationChange generation change event + * cotato drop box component + * @param list drop box list + * @param onChange list value change event + * @param reversed drop box list reversed (default: true) * @param color drop box color (default: blue) * @param width drop box width (default: 8rem) * @param height drop box height (default: 3.2rem) */ const CotatoDropBox = ({ - handleGenerationChange, + list, + onChange, + reversed = true, color = 'blue', width = '8rem', height = '3.2rem', }: CotatoDropBoxProps) => { const theme = useTheme(); - const { generations: rawGenerations, isGenerationLoading } = useGeneration(); - const [isDropBoxOpen, setIsDropBoxOpen] = useState(false); - const [generations, setGenerations] = useState([]); - const [selectedGeneration, setSelectedGeneration] = useState( - null, - ); + const [dropBoxList, setDropBoxList] = useState([]); + const [selectedItem, setSelecedItem] = useState(); - const generationDropBoxRef = useRef(null); + const dropBoxRef = useRef(null); const isInProduction = process.env.NODE_ENV === 'production'; @@ -92,17 +88,10 @@ const CotatoDropBox = ({ /** * */ - const handleGenerationSelect = (generation: CotatoGenerationInfoResponse) => { - setSelectedGeneration(generation); - handleGenerationChange(generation); - }; - - /** - * - */ - const handleGenerationClick = (generation: CotatoGenerationInfoResponse) => { + const handleItemClick = (generation: CotatoGenerationInfoResponse) => { handleDropDownChange(); - handleGenerationSelect(generation); + setSelecedItem(generation); + onChange(generation); }; /** @@ -114,8 +103,8 @@ const CotatoDropBox = ({ return ( - {selectedGeneration?.generationNumber} - {selectedGeneration && '기'} + {selectedItem?.generationNumber} + {selectedItem && '기'} {isDropBoxOpen ? ( @@ -133,7 +122,17 @@ const CotatoDropBox = ({ return (
    - {generations + {dropBoxList.map((item) => ( +
  • handleItemClick(item)} + > + {item === selectedItem && } + {item.generationNumber}기 +
  • + ))} + {/* {generations .filter( (generation) => !isInProduction || @@ -148,44 +147,54 @@ const CotatoDropBox = ({ {generation === selectedGeneration && } {generation.generationNumber}기 - ))} + ))} */}
); }; + /** + * + */ + useEffect(() => { + if (reversed) { + const reversedList = [...list].reverse(); + setDropBoxList(reversedList); + setSelecedItem(reversedList[0]); + } else { + setDropBoxList(list); + setSelecedItem(list[0]); + } + }, [list]); + /** * */ useEffect(() => { window.addEventListener('mousedown', (e) => { - if ( - generationDropBoxRef.current && - !generationDropBoxRef.current.contains(e.target as Node) && - isDropBoxOpen - ) { + if (dropBoxRef.current && !dropBoxRef.current.contains(e.target as Node) && isDropBoxOpen) { handleDropDownChange(); } }); return () => window.removeEventListener('mousedown', () => {}); - }, [generationDropBoxRef, isDropBoxOpen]); + }, [dropBoxRef, isDropBoxOpen]); - /** - * - */ - useEffect(() => { - if (!rawGenerations || isGenerationLoading) { - return; - } + // /** + // * + // */ + // useEffect(() => { + // if (!rawGenerations || isGenerationLoading) { + // return; + // } - const sortedGenerations = generationSort(rawGenerations); - setGenerations(sortedGenerations); + // const sortedGenerations = generationSort(rawGenerations); + // setGenerations(sortedGenerations); - handleGenerationSelect(sortedGenerations[0]); - }, [rawGenerations, isGenerationLoading]); + // handleGenerationSelect(sortedGenerations[0]); + // }, [rawGenerations, isGenerationLoading]); return ( - + {renderDropBox()} {renderDropDownList()} diff --git a/src/pages/MyPage/CSRecord.tsx b/src/pages/MyPage/CSRecord.tsx index 64159555..b44f5529 100644 --- a/src/pages/MyPage/CSRecord.tsx +++ b/src/pages/MyPage/CSRecord.tsx @@ -27,6 +27,7 @@ const medalImgSrcs = [ const CSRecord = () => { const { user } = useUser(); const [params] = useSearchParams(); + const { generations } = useGeneration(); const generationId = params.get('generationId'); const [selectedGenerationId, setSelectedGenerationId] = React.useState( @@ -48,11 +49,14 @@ const CSRecord = () => { 내가 풀어본 CS 문제풀이 - { - setSelectedGenerationId(generation?.generationId?.toString()); - }} - /> + {generations && ( + { + setSelectedGenerationId(generation?.generationId?.toString()); + }} + /> + )} {/* */} diff --git a/src/pages/Session/SessionHome.tsx b/src/pages/Session/SessionHome.tsx index f65c5bdb..af954b7a 100644 --- a/src/pages/Session/SessionHome.tsx +++ b/src/pages/Session/SessionHome.tsx @@ -306,12 +306,15 @@ const SessionHome = () => { const renderSettingTab = () => { return ( - + {generations && ( + + )} {userData?.role === 'ADMIN' && !isTabletOrSmaller && ( setIsAddModalOpen(true)} /> )} From afd90b0c7e1f366cb10f07d07c0dfa1f5499be2f Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 01:24:46 +0900 Subject: [PATCH 25/52] refactor: change item type to generic --- src/components/CotatoDropBox.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index 711c3892..e0103eab 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -9,9 +9,11 @@ import CotatoIcon from './CotatoIcon'; // // +type T = CotatoGenerationInfoResponse; + interface CotatoDropBoxProps { - list: CotatoGenerationInfoResponse[]; - onChange: (generation: CotatoGenerationInfoResponse) => void; + list: T[]; + onChange: (generation: T) => void; reversed?: boolean; color?: string; width?: string; @@ -53,8 +55,8 @@ const CotatoDropBox = ({ const theme = useTheme(); const [isDropBoxOpen, setIsDropBoxOpen] = useState(false); - const [dropBoxList, setDropBoxList] = useState([]); - const [selectedItem, setSelecedItem] = useState(); + const [dropBoxList, setDropBoxList] = useState([]); + const [selectedItem, setSelecedItem] = useState(); const dropBoxRef = useRef(null); @@ -88,7 +90,7 @@ const CotatoDropBox = ({ /** * */ - const handleItemClick = (generation: CotatoGenerationInfoResponse) => { + const handleItemClick = (generation: T) => { handleDropDownChange(); setSelecedItem(generation); onChange(generation); From 41ae56e0a91b852fd5c35323366e1fb742a7b6e3 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 01:55:02 +0900 Subject: [PATCH 26/52] refactor: change type to generic --- src/components/CotatoDropBox.tsx | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index e0103eab..ac4e7900 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -9,9 +9,9 @@ import CotatoIcon from './CotatoIcon'; // // -type T = CotatoGenerationInfoResponse; +type CotatoDropBoxType = CotatoGenerationInfoResponse; -interface CotatoDropBoxProps { +interface CotatoDropBoxProps { list: T[]; onChange: (generation: T) => void; reversed?: boolean; @@ -44,14 +44,14 @@ const FADE_DURATION = 300; * @param width drop box width (default: 8rem) * @param height drop box height (default: 3.2rem) */ -const CotatoDropBox = ({ +const CotatoDropBox = ({ list, onChange, reversed = true, color = 'blue', width = '8rem', height = '3.2rem', -}: CotatoDropBoxProps) => { +}: CotatoDropBoxProps) => { const theme = useTheme(); const [isDropBoxOpen, setIsDropBoxOpen] = useState(false); @@ -62,6 +62,15 @@ const CotatoDropBox = ({ const isInProduction = process.env.NODE_ENV === 'production'; + /** + * + */ + const isTypeGeneration = ( + generation: CotatoGenerationInfoResponse, + ): generation is CotatoGenerationInfoResponse => { + return (generation as CotatoGenerationInfoResponse).generationId !== undefined; + }; + /** * get drop box style of color * @returns drop box style { background: url of drop box background, arrowColor: color code of arrow button} @@ -159,13 +168,18 @@ const CotatoDropBox = ({ * */ useEffect(() => { + let newList = [...list]; + if (isInProduction && isTypeGeneration(list[0])) { + newList = newList.filter((generation) => generation.generationNumber! >= 8); + } + if (reversed) { - const reversedList = [...list].reverse(); + const reversedList = [...newList].reverse(); setDropBoxList(reversedList); setSelecedItem(reversedList[0]); } else { - setDropBoxList(list); - setSelecedItem(list[0]); + setDropBoxList(newList); + setSelecedItem(newList[0]); } }, [list]); From 8a18a8accb6720a0c2174e5ee54e584acb69df3e Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 02:02:41 +0900 Subject: [PATCH 27/52] refactor: use string formatter function in stirng --- src/components/CotatoDropBox.tsx | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index ac4e7900..586de167 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -13,7 +13,7 @@ type CotatoDropBoxType = CotatoGenerationInfoResponse; interface CotatoDropBoxProps { list: T[]; - onChange: (generation: T) => void; + onChange: (item: T) => void; reversed?: boolean; color?: string; width?: string; @@ -56,7 +56,7 @@ const CotatoDropBox = ({ const [isDropBoxOpen, setIsDropBoxOpen] = useState(false); const [dropBoxList, setDropBoxList] = useState([]); - const [selectedItem, setSelecedItem] = useState(); + const [selectedItem, setSelecedItem] = useState(null); const dropBoxRef = useRef(null); @@ -71,6 +71,21 @@ const CotatoDropBox = ({ return (generation as CotatoGenerationInfoResponse).generationId !== undefined; }; + /** + * + */ + const StringFormatter = (item: T | null) => { + if (!item) { + return ''; + } + + if (isTypeGeneration(item)) { + return `${item.generationNumber}기`; + } + + return ''; + }; + /** * get drop box style of color * @returns drop box style { background: url of drop box background, arrowColor: color code of arrow button} @@ -113,10 +128,7 @@ const CotatoDropBox = ({ return ( - - {selectedItem?.generationNumber} - {selectedItem && '기'} - + {StringFormatter(selectedItem)} {isDropBoxOpen ? ( ) : ( @@ -133,14 +145,14 @@ const CotatoDropBox = ({ return (
    - {dropBoxList.map((item) => ( + {dropBoxList.map((item, index) => (
  • handleItemClick(item)} > {item === selectedItem && } - {item.generationNumber}기 + {StringFormatter(item)}
  • ))} {/* {generations From 0dc3fe3596af4382e105d50014a072b8de69b024 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 02:17:24 +0900 Subject: [PATCH 28/52] feat: add drop box yellow color --- src/assets/drop_box_background_yellow.svg | 26 +++++++++++++++++++++++ src/components/CotatoDropBox.tsx | 8 +++++++ 2 files changed, 34 insertions(+) create mode 100644 src/assets/drop_box_background_yellow.svg diff --git a/src/assets/drop_box_background_yellow.svg b/src/assets/drop_box_background_yellow.svg new file mode 100644 index 00000000..2b7f4217 --- /dev/null +++ b/src/assets/drop_box_background_yellow.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index 586de167..96b4265b 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -3,6 +3,7 @@ import { styled, useTheme } from 'styled-components'; import { ReactComponent as CheckIcon } from '@assets/check_icon_dotted.svg'; import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; import drop_box_background_blue from '@assets/drop_box_background_blue.svg'; +import drop_box_background_yellow from '@assets/drop_box_background_yellow.svg'; import CotatoIcon from './CotatoIcon'; // @@ -98,6 +99,13 @@ const CotatoDropBox = ({ }; } + if (color === 'yellow') { + return { + background: `url(${drop_box_background_yellow})`, + arrowColor: theme.colors.primary40, + }; + } + return { background: `url(${drop_box_background_blue})`, arrowColor: theme.colors.sub2[80], From 1894b6cb229b4eb64b0271636b2b99a290071bc4 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 02:17:44 +0900 Subject: [PATCH 29/52] feat: add generation drop box in report --- .../Attendance/Report/AttendanceReportHeader.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index 036bacdc..a57bfd3c 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { Box, Stack, Typography } from '@mui/material'; import { useTheme } from 'styled-components'; +import { useGeneration } from '@/hooks/useGeneration'; +import CotatoDropBox from '@components/CotatoDropBox'; +import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; +import { useNavigate } from 'react-router-dom'; // // @@ -8,6 +12,12 @@ import { useTheme } from 'styled-components'; const AttendanceReportHeader = () => { const theme = useTheme(); + const { generations } = useGeneration(); + const navigate = useNavigate(); + + const handleGenerationChange = (generations: CotatoGenerationInfoResponse) => { + navigate(`/attendance/report/generation/${generations.generationId}`); + }; return ( { -
    기수 선택
    + {generations && ( + + )}
    세션 선택
    엑셀 From 9dc43a8f236b5255bee7b6c238b37eba250091aa Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 02:33:42 +0900 Subject: [PATCH 30/52] feat: add session type to drop box --- src/components/CotatoDropBox.tsx | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index 96b4265b..b25f5166 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { styled, useTheme } from 'styled-components'; import { ReactComponent as CheckIcon } from '@assets/check_icon_dotted.svg'; -import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; +import { CotatoGenerationInfoResponse, CotatoSessionListResponse } from 'cotato-openapi-clients'; import drop_box_background_blue from '@assets/drop_box_background_blue.svg'; import drop_box_background_yellow from '@assets/drop_box_background_yellow.svg'; import CotatoIcon from './CotatoIcon'; @@ -10,7 +10,7 @@ import CotatoIcon from './CotatoIcon'; // // -type CotatoDropBoxType = CotatoGenerationInfoResponse; +type CotatoDropBoxType = CotatoGenerationInfoResponse | CotatoSessionListResponse; interface CotatoDropBoxProps { list: T[]; @@ -69,7 +69,16 @@ const CotatoDropBox = ({ const isTypeGeneration = ( generation: CotatoGenerationInfoResponse, ): generation is CotatoGenerationInfoResponse => { - return (generation as CotatoGenerationInfoResponse).generationId !== undefined; + return (generation as CotatoGenerationInfoResponse).generationNumber !== undefined; + }; + + /** + * + */ + const isTypeSession = ( + session: CotatoSessionListResponse, + ): session is CotatoSessionListResponse => { + return (session as CotatoSessionListResponse).sessionNumber !== undefined; }; /** @@ -84,6 +93,10 @@ const CotatoDropBox = ({ return `${item.generationNumber}기`; } + if (isTypeSession(item)) { + return `${item.title}`; + } + return ''; }; @@ -190,7 +203,9 @@ const CotatoDropBox = ({ useEffect(() => { let newList = [...list]; if (isInProduction && isTypeGeneration(list[0])) { - newList = newList.filter((generation) => generation.generationNumber! >= 8); + newList = newList.filter( + (generation: CotatoGenerationInfoResponse) => generation.generationNumber! >= 8, + ); } if (reversed) { From e4df1560c92eb34a0266c73adced60342edf5bd2 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 02:46:09 +0900 Subject: [PATCH 31/52] feat: add session drop button in report --- .../Attendance/Report/AttendanceReportHeader.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index a57bfd3c..ee147592 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -3,8 +3,9 @@ import { Box, Stack, Typography } from '@mui/material'; import { useTheme } from 'styled-components'; import { useGeneration } from '@/hooks/useGeneration'; import CotatoDropBox from '@components/CotatoDropBox'; -import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; +import { CotatoGenerationInfoResponse, CotatoSessionListResponse } from 'cotato-openapi-clients'; import { useNavigate } from 'react-router-dom'; +import { useSession } from '@/hooks/useSession'; // // @@ -12,13 +13,19 @@ import { useNavigate } from 'react-router-dom'; const AttendanceReportHeader = () => { const theme = useTheme(); - const { generations } = useGeneration(); const navigate = useNavigate(); + const { generations, currentGeneration } = useGeneration(); + const { sessions } = useSession({ generationId: currentGeneration?.generationId }); + const handleGenerationChange = (generations: CotatoGenerationInfoResponse) => { navigate(`/attendance/report/generation/${generations.generationId}`); }; + const handleSessionChange = (session: CotatoSessionListResponse) => { + alert(session.title); + }; + return ( { {generations && ( )} -
    세션 선택
    + {sessions && ( + + )}
    엑셀
    From 62c54cdca1279d803db8e937e108fc098c720e6d Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 03:13:30 +0900 Subject: [PATCH 32/52] feat: add excel export button --- .../Report/AttendanceReportHeader.tsx | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index ee147592..d3c828a1 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { Box, Stack, Typography } from '@mui/material'; +import { Box, Button, Stack, Typography } from '@mui/material'; import { useTheme } from 'styled-components'; import { useGeneration } from '@/hooks/useGeneration'; import CotatoDropBox from '@components/CotatoDropBox'; import { CotatoGenerationInfoResponse, CotatoSessionListResponse } from 'cotato-openapi-clients'; import { useNavigate } from 'react-router-dom'; import { useSession } from '@/hooks/useSession'; +import CotatoIcon from '@components/CotatoIcon'; // // @@ -26,6 +27,10 @@ const AttendanceReportHeader = () => { alert(session.title); }; + const handleExportExcelClick = () => { + alert('출시 예정입니다 :)'); + }; + return ( { )} - 엑셀 + + +
    ); From 23d454d518d387d537abaeafd68c21f404dc9d30 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 03:19:09 +0900 Subject: [PATCH 33/52] feat: add sessionId param in report page --- src/pages/Attendance/Attendance.routes.tsx | 5 ++++- src/pages/Attendance/List/AttendanceList.tsx | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/Attendance/Attendance.routes.tsx b/src/pages/Attendance/Attendance.routes.tsx index 647c49f9..f1a3bdfa 100644 --- a/src/pages/Attendance/Attendance.routes.tsx +++ b/src/pages/Attendance/Attendance.routes.tsx @@ -33,7 +33,10 @@ const AttendanceRoutes = () => { path="/attend/generation/:generationId/session/:sessionId/:attendanceType/:status" element={} /> - } /> + } + /> } /> } /> diff --git a/src/pages/Attendance/List/AttendanceList.tsx b/src/pages/Attendance/List/AttendanceList.tsx index a9277e4b..7b9fdb08 100644 --- a/src/pages/Attendance/List/AttendanceList.tsx +++ b/src/pages/Attendance/List/AttendanceList.tsx @@ -67,7 +67,9 @@ const AttendanceList = () => { * */ const handleClickReport = () => { - navigate(`/attendance/report/generation/${generationId}`); + navigate( + `/attendance/report/generation/${generationId}/session/${attendanceList.at(-1)?.sessionId}`, + ); }; /** From 154c0ad378584b5691983ff07108ccc6b3f59071 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 03:26:49 +0900 Subject: [PATCH 34/52] feat: get sessionId and generationId from url --- .../Attendance/Report/AttendanceReportTable.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportTable.tsx b/src/pages/Attendance/Report/AttendanceReportTable.tsx index eaaa71fc..3c0ef8af 100644 --- a/src/pages/Attendance/Report/AttendanceReportTable.tsx +++ b/src/pages/Attendance/Report/AttendanceReportTable.tsx @@ -8,7 +8,7 @@ import { useBreakpoints } from '@/hooks/useBreakpoints'; import AttedanceTableLayout from './components/AttedanceTableLayout'; import { getCurrentStatistic } from '../utils/util'; import AttendanceStatusDropdown from './components/AttendanceStatusDropdown'; -import { useSearchParams } from 'react-router-dom'; +import { useParams, useSearchParams } from 'react-router-dom'; // // @@ -30,13 +30,22 @@ const AttendanceReportTable = () => { // const { currentGeneration } = useGeneration(); + const { generationId } = useParams(); + const { sessionId } = useParams(); + // TODO: need to get the latest session by params const { sessions } = useSession({ generationId: currentGeneration?.generationId }); + // + // const { currentAttendance } = useGetAttendances({ + // sessionId: sessions?.at(-1)?.sessionId, + // generationId: currentGeneration?.generationId, + // }); + // const { currentAttendance } = useGetAttendances({ - sessionId: sessions?.at(-1)?.sessionId, - generationId: currentGeneration?.generationId, + sessionId: Number(sessionId), + generationId: Number(generationId), }); // From f5aa131c856e5679586df47cd9c4d65c1a2d127b Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 03:19:09 +0900 Subject: [PATCH 35/52] feat: add sessionId param in report page --- src/pages/Attendance/Attendance.routes.tsx | 5 ++++- src/pages/Attendance/List/AttendanceList.tsx | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/Attendance/Attendance.routes.tsx b/src/pages/Attendance/Attendance.routes.tsx index 647c49f9..f1a3bdfa 100644 --- a/src/pages/Attendance/Attendance.routes.tsx +++ b/src/pages/Attendance/Attendance.routes.tsx @@ -33,7 +33,10 @@ const AttendanceRoutes = () => { path="/attend/generation/:generationId/session/:sessionId/:attendanceType/:status" element={} /> - } /> + } + /> } /> } /> diff --git a/src/pages/Attendance/List/AttendanceList.tsx b/src/pages/Attendance/List/AttendanceList.tsx index a9277e4b..7b9fdb08 100644 --- a/src/pages/Attendance/List/AttendanceList.tsx +++ b/src/pages/Attendance/List/AttendanceList.tsx @@ -67,7 +67,9 @@ const AttendanceList = () => { * */ const handleClickReport = () => { - navigate(`/attendance/report/generation/${generationId}`); + navigate( + `/attendance/report/generation/${generationId}/session/${attendanceList.at(-1)?.sessionId}`, + ); }; /** From 9c1b03713056a450411ddb7a837d077587c1bf3f Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 03:48:33 +0900 Subject: [PATCH 36/52] feat: add target session return value --- src/hooks/useSession.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSession.ts b/src/hooks/useSession.ts index ead5e6a0..324a462b 100644 --- a/src/hooks/useSession.ts +++ b/src/hooks/useSession.ts @@ -10,10 +10,12 @@ import useSWR from 'swr'; interface UseSessionProps { generationId?: number; + sessionId?: number; } interface UseSessionReturn { sessions: CotatoSessionListResponse[] | undefined; + targetSession: CotatoSessionListResponse | undefined; isSessionLoading: boolean; isSessionError: any; } @@ -22,7 +24,7 @@ interface UseSessionReturn { // // -export function useSession({ generationId }: UseSessionProps) { +export function useSession({ generationId, sessionId }: UseSessionProps) { const _return = useRef({} as UseSessionReturn); const { data, isLoading, error } = useSWR( @@ -35,8 +37,11 @@ export function useSession({ generationId }: UseSessionProps) { }, ); + _return.current.targetSession = data?.find((session) => session.sessionId === sessionId); + _return.current = { sessions: data || [], + targetSession: _return.current.targetSession, isSessionLoading: isLoading, isSessionError: error, }; From e7c4bb302b9b28a035805648120120112dcc708d Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 03:52:50 +0900 Subject: [PATCH 37/52] feat: add default selected value in drop box --- src/components/CotatoDropBox.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index b25f5166..27e61cb0 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -16,6 +16,7 @@ interface CotatoDropBoxProps { list: T[]; onChange: (item: T) => void; reversed?: boolean; + defaultItem?: T; color?: string; width?: string; height?: string; @@ -49,6 +50,7 @@ const CotatoDropBox = ({ list, onChange, reversed = true, + defaultItem, color = 'blue', width = '8rem', height = '3.2rem', @@ -211,10 +213,10 @@ const CotatoDropBox = ({ if (reversed) { const reversedList = [...newList].reverse(); setDropBoxList(reversedList); - setSelecedItem(reversedList[0]); + setSelecedItem(defaultItem ?? reversedList[0]); } else { setDropBoxList(newList); - setSelecedItem(newList[0]); + setSelecedItem(defaultItem ?? newList[0]); } }, [list]); From b34563dace63801260df390eada5a2c8cbccef7a Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 04:13:39 +0900 Subject: [PATCH 38/52] fix: change default value to id --- src/components/CotatoDropBox.tsx | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index 27e61cb0..b90bddf9 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -16,7 +16,7 @@ interface CotatoDropBoxProps { list: T[]; onChange: (item: T) => void; reversed?: boolean; - defaultItem?: T; + defaultItemId?: number; color?: string; width?: string; height?: string; @@ -50,7 +50,7 @@ const CotatoDropBox = ({ list, onChange, reversed = true, - defaultItem, + defaultItemId, color = 'blue', width = '8rem', height = '3.2rem', @@ -211,14 +211,29 @@ const CotatoDropBox = ({ } if (reversed) { - const reversedList = [...newList].reverse(); - setDropBoxList(reversedList); - setSelecedItem(defaultItem ?? reversedList[0]); - } else { - setDropBoxList(newList); + newList = [...newList].reverse(); + } + + setDropBoxList(newList); + + if (defaultItemId) { + const defaultItem = newList.find((item) => { + if (isTypeGeneration(item)) { + return item.generationId === defaultItemId; + } + + if (isTypeSession(item)) { + return item.sessionId === defaultItemId; + } + + return false; + }); + setSelecedItem(defaultItem ?? newList[0]); + } else { + setSelecedItem(newList[0]); } - }, [list]); + }, [list, defaultItemId]); /** * From f6277c41c4a34147a530bc47690ea41ea51f6e43 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 04:16:33 +0900 Subject: [PATCH 39/52] feat: get generation id, session id from url --- .../Report/AttendanceReportHeader.tsx | 30 +++++++++++++++---- src/pages/Session/SessionHome.tsx | 1 + 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index d3c828a1..62e243da 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -4,7 +4,7 @@ import { useTheme } from 'styled-components'; import { useGeneration } from '@/hooks/useGeneration'; import CotatoDropBox from '@components/CotatoDropBox'; import { CotatoGenerationInfoResponse, CotatoSessionListResponse } from 'cotato-openapi-clients'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useSession } from '@/hooks/useSession'; import CotatoIcon from '@components/CotatoIcon'; @@ -16,11 +16,19 @@ const AttendanceReportHeader = () => { const theme = useTheme(); const navigate = useNavigate(); - const { generations, currentGeneration } = useGeneration(); - const { sessions } = useSession({ generationId: currentGeneration?.generationId }); + const { generationId } = useParams(); + const { sessionId } = useParams(); + + const { generations, currentGeneration, targetGeneration } = useGeneration({ + generationId: generationId, + }); + const { sessions, targetSession } = useSession({ + generationId: currentGeneration?.generationId, + sessionId: Number(sessionId), + }); const handleGenerationChange = (generations: CotatoGenerationInfoResponse) => { - navigate(`/attendance/report/generation/${generations.generationId}`); + navigate(`/attendance/report/generation/${generations.generationId}/session/${sessionId}`); }; const handleSessionChange = (session: CotatoSessionListResponse) => { @@ -56,10 +64,20 @@ const AttendanceReportHeader = () => { {generations && ( - + )} {sessions && ( - + )} diff --git a/src/pages/Session/SessionHome.tsx b/src/pages/Session/SessionHome.tsx index af954b7a..65624b6d 100644 --- a/src/pages/Session/SessionHome.tsx +++ b/src/pages/Session/SessionHome.tsx @@ -310,6 +310,7 @@ const SessionHome = () => { Date: Fri, 1 Nov 2024 04:18:21 +0900 Subject: [PATCH 40/52] feat: set url session id from handle session function --- src/pages/Attendance/Report/AttendanceReportHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index 62e243da..0fc00e50 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -32,7 +32,7 @@ const AttendanceReportHeader = () => { }; const handleSessionChange = (session: CotatoSessionListResponse) => { - alert(session.title); + navigate(`/attendance/report/generation/${generationId}/session/${session.sessionId}`); }; const handleExportExcelClick = () => { From 2992a40197e3c5f996d9d3d2fbac4455186b7b4f Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 04:31:09 +0900 Subject: [PATCH 41/52] fix: fetch sessions with generation id from url --- src/pages/Attendance/Report/AttendanceReportHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index 0fc00e50..459961db 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -19,11 +19,11 @@ const AttendanceReportHeader = () => { const { generationId } = useParams(); const { sessionId } = useParams(); - const { generations, currentGeneration, targetGeneration } = useGeneration({ + const { generations, targetGeneration } = useGeneration({ generationId: generationId, }); const { sessions, targetSession } = useSession({ - generationId: currentGeneration?.generationId, + generationId: Number(generationId), sessionId: Number(sessionId), }); From c5395c82c9035f03f9e8e4877a7c5a3872f8777e Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 04:53:34 +0900 Subject: [PATCH 42/52] feat: implement previous button --- .../Report/AttendanceReportHeader.tsx | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index 459961db..3dc0a2a3 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Box, Button, Stack, Typography } from '@mui/material'; -import { useTheme } from 'styled-components'; +import styled, { useTheme } from 'styled-components'; import { useGeneration } from '@/hooks/useGeneration'; import CotatoDropBox from '@components/CotatoDropBox'; import { CotatoGenerationInfoResponse, CotatoSessionListResponse } from 'cotato-openapi-clients'; @@ -27,6 +27,10 @@ const AttendanceReportHeader = () => { sessionId: Number(sessionId), }); + const handlePreviousClick = () => { + navigate('/attendance'); + }; + const handleGenerationChange = (generations: CotatoGenerationInfoResponse) => { navigate(`/attendance/report/generation/${generations.generationId}/session/${sessionId}`); }; @@ -49,7 +53,12 @@ const AttendanceReportHeader = () => { border: '4px solid blue', }} > - + + { }; export default AttendanceReportHeader; + +// +// +// + +const StyledIcon = styled(CotatoIcon)` + position: absolute; + top: 50%; + left: 0; + transform: translateX(-50%); + rotate: 90deg; + width: 2rem !important; + height: 2rem !important; + cursor: pointer; +`; From ac2f480e0d65a27b5295a36f1e1a42964a7eba87 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 05:07:49 +0900 Subject: [PATCH 43/52] feat: remove excel test in mobile view --- src/pages/Attendance/Report/AttendanceReportHeader.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index 3dc0a2a3..b9e0094b 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -7,6 +7,7 @@ import { CotatoGenerationInfoResponse, CotatoSessionListResponse } from 'cotato- import { useNavigate, useParams } from 'react-router-dom'; import { useSession } from '@/hooks/useSession'; import CotatoIcon from '@components/CotatoIcon'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; // // @@ -15,6 +16,7 @@ import CotatoIcon from '@components/CotatoIcon'; const AttendanceReportHeader = () => { const theme = useTheme(); const navigate = useNavigate(); + const { isLandScapeOrSmaller } = useBreakpoints(); const { generationId } = useParams(); const { sessionId } = useParams(); @@ -70,7 +72,7 @@ const AttendanceReportHeader = () => { 출석부 확인하기 - + {generations && ( { fontSize: '1rem', }} > - 엑셀로 내보내기 + {!isLandScapeOrSmaller && '엑셀로 내보내기'} From 07eb209d94f078a70160a38a1111cbeecf4639fa Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 05:08:57 +0900 Subject: [PATCH 44/52] feat: remove layout border --- src/pages/Attendance/Report/AttendanceReportHeader.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index b9e0094b..5125b35f 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -49,10 +49,9 @@ const AttendanceReportHeader = () => { From f907c792ed45a1f9722e77d54eb81c2ca72ff1bc Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 05:34:33 +0900 Subject: [PATCH 45/52] chore: remove unused code --- src/components/CotatoDropBox.tsx | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index b90bddf9..597b5a59 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -178,22 +178,6 @@ const CotatoDropBox = ({ {StringFormatter(item)} ))} - {/* {generations - .filter( - (generation) => - !isInProduction || - (generation?.generationNumber && generation.generationNumber >= 8), - ) - .map((generation) => ( -
  • handleGenerationClick(generation)} - > - {generation === selectedGeneration && } - {generation.generationNumber}기 -
  • - ))} */}
); @@ -247,20 +231,6 @@ const CotatoDropBox = ({ return () => window.removeEventListener('mousedown', () => {}); }, [dropBoxRef, isDropBoxOpen]); - // /** - // * - // */ - // useEffect(() => { - // if (!rawGenerations || isGenerationLoading) { - // return; - // } - - // const sortedGenerations = generationSort(rawGenerations); - // setGenerations(sortedGenerations); - - // handleGenerationSelect(sortedGenerations[0]); - // }, [rawGenerations, isGenerationLoading]); - return ( {renderDropBox()} From 8260f7c4d18ba7a87a2d4c07ceddb6fdcbf6a715 Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 05:50:08 +0900 Subject: [PATCH 46/52] feat: apply cotato drop box in cs home --- src/components/GenerationSelect.tsx | 178 ---------------------------- src/pages/CS/CSHome.tsx | 28 +++-- 2 files changed, 19 insertions(+), 187 deletions(-) delete mode 100644 src/components/GenerationSelect.tsx diff --git a/src/components/GenerationSelect.tsx b/src/components/GenerationSelect.tsx deleted file mode 100644 index ced3ef7e..00000000 --- a/src/components/GenerationSelect.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { css, styled } from 'styled-components'; -import arrow_down_thin from '@assets/arrow_dwon_thin.svg'; -import arrow_up_thin from '@assets/arrow_up_thin.svg'; -import generationSort from '@utils/generationSort'; -import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; -import { useParams } from 'react-router-dom'; -import { useGeneration } from '@/hooks/useGeneration'; - -interface Props { - /** - * 기수 변경시 발생해야 하는 로직을 담는 함수 - * @param generation - * @returns - */ - onChangeGeneration: (generation?: CotatoGenerationInfoResponse) => void; - /** - * 현재 선택된 기수 - */ - selectedGeneration?: CotatoGenerationInfoResponse; -} - -// -// -// - -const GenerationSelect = ({ onChangeGeneration, selectedGeneration }: Props) => { - const { generationId } = useParams(); - const { generations, targetGeneration } = useGeneration({ generationId }); - - const [isOpen, setIsOpen] = useState(false); - const [sortedGenerations, setSortedGenerations] = useState< - CotatoGenerationInfoResponse[] | undefined - >([]); - - const generationDropRef = useRef(null); - - /** - * - */ - const onClickGeneration = useCallback((generation: CotatoGenerationInfoResponse) => { - onChangeGeneration(generation); - setIsOpen(false); - }, []); - - // - // - // - useEffect(() => { - const handleClick = (e: MouseEvent) => { - if (generationDropRef.current && !generationDropRef.current.contains(e.target as Node)) { - setIsOpen(false); - } - }; - window.addEventListener('mousedown', handleClick); - return () => window.removeEventListener('mousedown', handleClick); - }, [generationDropRef]); - - // - // - // - useEffect(() => { - if (generations) { - const newGenerations = generationSort(generations); - setSortedGenerations(newGenerations); - } - }, [generations]); - - // - // - // - useEffect(() => { - if (generationId && targetGeneration) { - onChangeGeneration(targetGeneration); - } - }, [targetGeneration]); - - return ( - - setIsOpen(!isOpen)}> -

- {selectedGeneration?.generationNumber ? `${selectedGeneration?.generationNumber}기` : ''} -

- {isOpen ? ( - arrow-up - ) : ( - arrow-down - )} - {isOpen && ( - -
    - {sortedGenerations?.map((generation) => ( -
  • onClickGeneration(generation)}> - {`${generation.generationNumber}기`} -
  • - ))} -
-
- )} -
-
- ); -}; - -export default GenerationSelect; - -const GenerationSelectWrapper = styled.div` - position: relative; - width: 127px; -`; - -const SelectMenu = styled.div<{ isopen: string }>` - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 40px; - flex-shrink: 0; - border-radius: 5px; - border: 2px solid #bebebe; - background: #f3f7ff; - - > p { - margin-left: 16px; - font-family: NanumSquareRound; - font-size: 16px; - font-style: normal; - line-height: normal; - ${(props) => - props.isopen === 'open' - ? css` - color: #000; - font-weight: 500; - ` - : css` - color: #969595; - font-weight: 400; - `} - } - - > img { - cursor: pointer; - margin-right: 4px; - } -`; - -const GenerationList = styled.div` - z-index: 1; - position: absolute; - top: 40px; - left: 0; - width: 100%; - min-height: 40px; - flex-shrink: 0; - border-radius: 5px; - background: #f3f7ff; - - ul { - display: flex; - flex-direction: column; - padding: 0px; - margin: 0px; - } - - li { - display: flex; - justify-content: flex-start; - list-style: none; - cursor: pointer; - color: #000; - font-family: NanumSquareRound; - font-size: 16px; - font-style: normal; - font-weight: 500; - line-height: normal; - padding: 12px 16px; - } -`; diff --git a/src/pages/CS/CSHome.tsx b/src/pages/CS/CSHome.tsx index c1e66ca8..a603096f 100644 --- a/src/pages/CS/CSHome.tsx +++ b/src/pages/CS/CSHome.tsx @@ -1,19 +1,20 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { styled } from 'styled-components'; import CSContent from '@pages/CS/CSContent'; import { ReactComponent as SettingIcon } from '@assets/setting_icon.svg'; import { ReactComponent as AddIcon } from '@assets/add_icon.svg'; -import GenerationSelect from '@components/GenerationSelect'; +import CotatoDropBox from '@components/CotatoDropBox'; import CSModal from '@pages/CS/CSModal'; import { IEducation } from '@/typing/db'; import api from '@/api/api'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; import { useGeneration } from '@/hooks/useGeneration'; import useUser from '@/hooks/useUser'; const CSHome = () => { - const { currentGeneration } = useGeneration(); + const { generationId } = useParams(); + const { generations } = useGeneration(); const { user } = useUser(); const [educations, setEducations] = useState(); @@ -21,7 +22,7 @@ const CSHome = () => { const [modifyEducation, setModifyEducation] = useState(); const [selectedGeneration, setSelectedGeneration] = useState< undefined | CotatoGenerationInfoResponse - >(currentGeneration); + >(); const navigate = useNavigate(); @@ -66,16 +67,25 @@ const CSHome = () => { setIsCSModalOpen(false); }, []); + useEffect(() => { + if (!generationId || !generations) { + return; + } + + const generation = generations?.find( + (generation) => generation.generationId === Number(generationId), + ); + setSelectedGeneration(generation); + fetchEducations(Number(generationId)); + }, [generations, generationId]); + return ( <> CS 문제풀이 - + {generations && } {(user?.role === 'ADMIN' || user?.role === 'EDUCATION') && ( From 194125592c9eb3da9c0560a27231c6ff3582f65e Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 07:14:17 +0900 Subject: [PATCH 47/52] fix: remove production condition --- src/components/CotatoDropBox.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/CotatoDropBox.tsx b/src/components/CotatoDropBox.tsx index 597b5a59..2beb89b1 100644 --- a/src/components/CotatoDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -63,7 +63,7 @@ const CotatoDropBox = ({ const dropBoxRef = useRef(null); - const isInProduction = process.env.NODE_ENV === 'production'; + // const isInProduction = process.env.NODE_ENV === 'production'; /** * @@ -188,11 +188,11 @@ const CotatoDropBox = ({ */ useEffect(() => { let newList = [...list]; - if (isInProduction && isTypeGeneration(list[0])) { - newList = newList.filter( - (generation: CotatoGenerationInfoResponse) => generation.generationNumber! >= 8, - ); - } + // if (isInProduction && isTypeGeneration(list[0])) { + // newList = newList.filter( + // (generation: CotatoGenerationInfoResponse) => generation.generationNumber! >= 8, + // ); + // } if (reversed) { newList = [...newList].reverse(); From 34a432bd207f1f2429f5c2a4598c0308cff1d78e Mon Sep 17 00:00:00 2001 From: WONYOUNG-HC Date: Fri, 1 Nov 2024 07:45:08 +0900 Subject: [PATCH 48/52] feat: disable excel button --- src/pages/Attendance/Report/AttendanceReportHeader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx index 5125b35f..2665a4e3 100644 --- a/src/pages/Attendance/Report/AttendanceReportHeader.tsx +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -93,6 +93,7 @@ const AttendanceReportHeader = () => {