diff --git a/src/App.tsx b/src/App.tsx index 984fef05..8294a28c 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 CotatoThemeProvider from '@theme/context/CotatoThemeProvider'; import GlobalBackgroundSvgComponent from '@components/GlobalBackgroundSvgComponent'; import { FAQ } from '@pages/FAQ'; @@ -23,6 +24,7 @@ import AgreementConfirmDialog from '@components/AgreementConfirmDialog'; import CSRoutes from '@pages/CS/CSRoutes'; import { About } from '@pages/About'; import 'react-toastify/dist/ReactToastify.css'; +import { ToastContainer } from 'react-toastify'; function App() { const location = useLocation(); @@ -49,6 +51,7 @@ function App() {
+
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/assets/drop_box_background_yellow_lg.svg b/src/assets/drop_box_background_yellow_lg.svg new file mode 100644 index 00000000..4f4cb8f1 --- /dev/null +++ b/src/assets/drop_box_background_yellow_lg.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/GenerationDropBox.tsx b/src/components/CotatoDropBox.tsx similarity index 52% rename from src/components/GenerationDropBox.tsx rename to src/components/CotatoDropBox.tsx index 01ee22ac..73de9935 100644 --- a/src/components/GenerationDropBox.tsx +++ b/src/components/CotatoDropBox.tsx @@ -1,24 +1,23 @@ import React, { useEffect, useRef, useState } from 'react'; import { styled, useTheme } from 'styled-components'; -import generationSort from '@utils/newGenerationSort'; -import { CotatoGenerationInfoResponse } from 'cotato-openapi-clients'; -import { DropBoxColorEnum } from '@/enums/DropBoxColor'; +import { CotatoGenerationInfoResponse, CotatoSessionListResponse } from 'cotato-openapi-clients'; import drop_box_background_blue from '@assets/drop_box_background_blue.svg'; -import { useNavigate, useSearchParams } from 'react-router-dom'; -import { useGeneration } from '@/hooks/useGeneration'; +import drop_box_background_yellow from '@assets/drop_box_background_yellow.svg'; +import drop_box_background_yellow_lg from '@assets/drop_box_background_yellow_lg.svg'; import CotatoIcon from './CotatoIcon'; // // // -interface GenerationDropBoxProps { - /** - * generation change event - * @param generation selected generation - */ - handleGenerationChange: (generation: CotatoGenerationInfoResponse) => void; - color?: DropBoxColorEnum; +type CotatoDropBoxType = CotatoGenerationInfoResponse | CotatoSessionListResponse; + +interface CotatoDropBoxProps { + list: T[]; + onChange: (item: T) => void; + reversed?: boolean; + defaultItemId?: number; + color?: string; width?: string; height?: string; disableQueryParams?: boolean; @@ -40,87 +39,109 @@ 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 GenerationDropBox = ({ - handleGenerationChange, - color = DropBoxColorEnum.BLUE, +const CotatoDropBox = ({ + list, + onChange, + reversed = true, + defaultItemId, + color = 'blue', width = '8rem', height = '3.2rem', - disableQueryParams = false, -}: GenerationDropBoxProps) => { +}: CotatoDropBoxProps) => { const theme = useTheme(); - const navigate = useNavigate(); - - const [searchParams, setSearchParams] = useSearchParams(); - 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(null); - const generationDropBoxRef = useRef(null); + const dropBoxRef = useRef(null); - const isInProduction = process.env.NODE_ENV === 'production'; + // const isInProduction = process.env.NODE_ENV === 'production'; /** - * 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) { - return { - background: `url(${drop_box_background_blue})`, - arrowColor: theme.colors.sub2[80], - }; - } + const isTypeGeneration = ( + generation: CotatoGenerationInfoResponse, + ): generation is CotatoGenerationInfoResponse => { + return (generation as CotatoGenerationInfoResponse).generationNumber !== undefined; + }; - throw new TypeError('invalid color type'); + /** + * + */ + const isTypeSession = ( + session: CotatoSessionListResponse, + ): session is CotatoSessionListResponse => { + return (session as CotatoSessionListResponse).sessionNumber !== undefined; }; /** * */ - const setGenerationSearchParam = (generation: CotatoGenerationInfoResponse) => { - if (disableQueryParams) { - navigate(`/cs/${generation.generationId}`); - return; + const StringFormatter = (item: T | null) => { + if (!item) { + return ''; + } + + if (isTypeGeneration(item)) { + return `${item.generationNumber}기`; } - if (generation?.generationId) { - setSearchParams({ generationId: generation.generationId.toString() }); + if (isTypeSession(item)) { + return `${item.title}`; } + + return ''; }; /** - * + * get drop box style of color + * @returns drop box style { background: url of drop box background, arrowColor: color code of arrow button} */ - const handleDropDownChange = () => { - setIsDropBoxOpen(!isDropBoxOpen); + const getDropBoxStyle = () => { + if (color === 'blue') { + return { + background: `url(${drop_box_background_blue})`, + arrowColor: theme.colors.sub2[80], + }; + } + + if (color === 'yellow') { + return { + background: `url(${width === '12rem' ? drop_box_background_yellow_lg : drop_box_background_yellow})`, + arrowColor: theme.colors.primary40, + }; + } + + return { + background: `url(${drop_box_background_blue})`, + arrowColor: theme.colors.sub2[80], + }; }; /** * */ - const handleGenerationSelect = (generation: CotatoGenerationInfoResponse) => { - setSelectedGeneration(generation); - handleGenerationChange(generation); - setGenerationSearchParam(generation); + const handleDropDownChange = () => { + setIsDropBoxOpen(!isDropBoxOpen); }; /** * */ - const handleGenerationClick = (generation: CotatoGenerationInfoResponse) => { + const handleItemClick = (generation: T) => { handleDropDownChange(); - handleGenerationSelect(generation); + setSelecedItem(generation); + onChange(generation); }; /** @@ -131,10 +152,7 @@ const GenerationDropBox = ({ return ( - - {selectedGeneration?.generationNumber} - {selectedGeneration && '기'} - + {StringFormatter(selectedItem)} {isDropBoxOpen ? ( ) : ( @@ -151,24 +169,18 @@ const GenerationDropBox = ({ return (
    - {generations - .filter( - (generation) => - !isInProduction || - (generation?.generationNumber && generation.generationNumber >= 8), - ) - .map((generation) => ( -
  • handleGenerationClick(generation)} - > - {generation === selectedGeneration && ( - theme.colors.sub3[40]} /> - )} - {generation.generationNumber}기 -
  • - ))} + {dropBoxList.map((item, index) => ( +
  • handleItemClick(item)} + > + {item === selectedItem && ( + + )} + {StringFormatter(item)} +
  • + ))}
); @@ -178,42 +190,47 @@ const GenerationDropBox = ({ * */ useEffect(() => { - window.addEventListener('mousedown', (e) => { - if ( - generationDropBoxRef.current && - !generationDropBoxRef.current.contains(e.target as Node) && - isDropBoxOpen - ) { - handleDropDownChange(); - } - }); - return () => window.removeEventListener('mousedown', () => {}); - }, [generationDropBoxRef, isDropBoxOpen]); + let newList = [...list]; - /** - * - */ - useEffect(() => { - if (!rawGenerations || isGenerationLoading) { - return; + if (reversed) { + newList = [...newList].reverse(); } - const sortedGenerations = generationSort(rawGenerations); - // .filter( - // (generation) => generation.generationNumber && generation.generationNumber >= 8, - // ); - setGenerations(sortedGenerations); + setDropBoxList(newList); - const generationId = searchParams.get('generationId'); - const searchedGeneration = sortedGenerations.find( - (generation) => generation.generationId === Number(generationId), - ); + if (defaultItemId) { + const defaultItem = newList.find((item) => { + if (isTypeGeneration(item)) { + return item.generationId === defaultItemId; + } + + if (isTypeSession(item)) { + return item.sessionId === defaultItemId; + } + + return false; + }); - handleGenerationSelect(searchedGeneration || sortedGenerations[0]); - }, [rawGenerations, isGenerationLoading]); + setSelecedItem(defaultItem ?? newList[0]); + } else { + setSelecedItem(newList[0]); + } + }, [list, defaultItemId]); + + /** + * + */ + useEffect(() => { + window.addEventListener('mousedown', (e) => { + if (dropBoxRef.current && !dropBoxRef.current.contains(e.target as Node) && isDropBoxOpen) { + handleDropDownChange(); + } + }); + return () => window.removeEventListener('mousedown', () => {}); + }, [dropBoxRef, isDropBoxOpen]); return ( - + {renderDropBox()} {renderDropDownList()} @@ -248,6 +265,9 @@ const SelectText = styled.span` font-family: Ycomputer; color: ${({ theme }) => theme.colors.gray100}; font-size: ${({ theme }) => theme.size.lg}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `; const StyledCotatoIcon = styled(CotatoIcon)` @@ -306,6 +326,9 @@ const DropDownList = styled.div` font-family: Ycomputer; font-size: ${({ theme }) => theme.size.lg}; line-height: 3rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; &.selected { background-color: ${({ theme }) => theme.colors.gray10}; @@ -320,4 +343,4 @@ const StyledCheckIcon = styled(CotatoIcon)` top: 0.75rem; `; -export default GenerationDropBox; +export default CotatoDropBox; 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; 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/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, }; diff --git a/src/pages/Attendance/Attendance.routes.tsx b/src/pages/Attendance/Attendance.routes.tsx index 78a9f657..f1a3bdfa 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')); // // @@ -29,10 +30,14 @@ const AttendanceRoutes = () => { element={} /> } /> - } /> + } + /> + } /> } /> diff --git a/src/pages/Attendance/List/AttendanceList.tsx b/src/pages/Attendance/List/AttendanceList.tsx index 1496cbb0..1984202b 100644 --- a/src/pages/Attendance/List/AttendanceList.tsx +++ b/src/pages/Attendance/List/AttendanceList.tsx @@ -67,8 +67,9 @@ const AttendanceList = () => { * */ const handleClickReport = () => { - const currentMonth = new Date().getMonth() + 1; - navigate(`/attendance/report/${generationId}/month/${currentMonth}`); + navigate( + `/attendance/report/generation/${generationId}/session/${attendanceList.at(-1)?.sessionId}`, + ); }; /** diff --git a/src/pages/Attendance/Report/AttendanceReport.tsx b/src/pages/Attendance/Report/AttendanceReport.tsx index bfcc3748..db9241ce 100644 --- a/src/pages/Attendance/Report/AttendanceReport.tsx +++ b/src/pages/Attendance/Report/AttendanceReport.tsx @@ -1,124 +1,15 @@ 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 AttendanceReportSubFilters from './AttendanceReportSubFilters'; +import AttendanceReportHeader from './AttendanceReportHeader'; +import AttendanceReportTable from './AttendanceReportTable'; // // // 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,60 +20,11 @@ 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} - - ); - })} - -
-
+ + {/* */} + + + ); diff --git a/src/pages/Attendance/Report/AttendanceReportAll.tsx b/src/pages/Attendance/Report/AttendanceReportAll.tsx new file mode 100644 index 00000000..f3a5c354 --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportAll.tsx @@ -0,0 +1,25 @@ +import { Container, Stack } from '@mui/material'; +import React from 'react'; +import AttendanceReportSubFilters from './AttendanceReportSubFilters'; +import AttendanceReportAllTable from './AttendanceReportAllTable'; + +// +// +// + +const 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..286a2439 --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportAllTable.tsx @@ -0,0 +1,120 @@ +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 AttedanceTableLayout from './components/AttedanceTableLayout'; + +// +// +// + +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; diff --git a/src/pages/Attendance/Report/AttendanceReportHeader.tsx b/src/pages/Attendance/Report/AttendanceReportHeader.tsx new file mode 100644 index 00000000..1c0c025e --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportHeader.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { Box, Button, Stack, Typography } from '@mui/material'; +import styled, { useTheme } from 'styled-components'; +import { useGeneration } from '@/hooks/useGeneration'; +import CotatoDropBox from '@components/CotatoDropBox'; +import { CotatoGenerationInfoResponse, CotatoSessionListResponse } from 'cotato-openapi-clients'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useSession } from '@/hooks/useSession'; +import CotatoIcon from '@components/CotatoIcon'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; + +// +// +// + +const AttendanceReportHeader = () => { + const theme = useTheme(); + const navigate = useNavigate(); + const { isLandScapeOrSmaller } = useBreakpoints(); + + const { generationId } = useParams(); + const { sessionId } = useParams(); + + const { generations, targetGeneration } = useGeneration({ + generationId: generationId, + }); + const { sessions, targetSession } = useSession({ + generationId: Number(generationId), + sessionId: Number(sessionId), + }); + + /** + * + */ + const handlePreviousClick = () => { + navigate('/attendance'); + }; + + /** + * + */ + const handleGenerationChange = (generations: CotatoGenerationInfoResponse) => { + navigate(`/attendance/report/generation/${generations.generationId}/session/${sessionId}`); + }; + + /** + * + */ + const handleSessionChange = (session: CotatoSessionListResponse) => { + navigate(`/attendance/report/generation/${generationId}/session/${session.sessionId}`); + }; + + /** + * + */ + const handleExportExcelClick = () => { + alert('출시 예정입니다 :)'); + }; + + return ( + + + + + 출석부 확인하기 + + + + + {generations && ( + + )} + {sessions && ( + + )} + + + + + + + ); +}; + +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; +`; diff --git a/src/pages/Attendance/Report/AttendanceReportSearch.tsx b/src/pages/Attendance/Report/AttendanceReportSearch.tsx new file mode 100644 index 00000000..ae93243f --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportSearch.tsx @@ -0,0 +1,62 @@ +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(() => { + 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..d7b5edf7 --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportSubFilterActions.tsx @@ -0,0 +1,92 @@ +import React from 'react'; + +import { Stack, Typography } from '@mui/material'; + +import styled from 'styled-components'; +import { useAttendanceReportFilter } from '../hooks/useAttendanceReportFilter'; +import { useMatch } from 'react-router-dom'; +import { STATUS_ASSETS } from '../constants'; + +// +// +// + +const AttendanceReportSubFilterActions = () => { + // + const { currentStatus, toggleStatus } = useAttendanceReportFilter(); + + /** + * + */ + const checkIsStatusSelected = (status: string) => { + return currentStatus.includes(status); + }; + + // + const match = useMatch('/attendance/report/generation/:generationId/all'); + + // + const disabled = match ? true : false; + + // + // + // + return ( + + {STATUS_ASSETS.map(({ status, icon, text }) => ( + { + toggleStatus(status); + }} + > + {icon} + {text} + + ))} + + ); +}; + +export default AttendanceReportSubFilterActions; + +// +// +// + +const StyledButton = styled.button<{ $selected?: boolean; $disabled?: 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}; + + // 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)<{ $disabled?: boolean }>` + color: ${({ theme }) => theme.colors.common.black_const}; + color: ${({ theme, $disabled }) => + $disabled ? theme.colors.gray10 : 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/Report/AttendanceReportTable.tsx b/src/pages/Attendance/Report/AttendanceReportTable.tsx new file mode 100644 index 00000000..55c4d7e0 --- /dev/null +++ b/src/pages/Attendance/Report/AttendanceReportTable.tsx @@ -0,0 +1,211 @@ +import useGetAttendances from '@/hooks/useGetAttendances'; +import { Table, TableBody, TableCell, TableRow } from '@mui/material'; +import React, { useEffect } from 'react'; +import { useAttendancesAttendanceIdRecordsGet } from '../hooks/useAttendancesAttendanceIdRecordsGet'; +import { useBreakpoints } from '@/hooks/useBreakpoints'; +import AttedanceTableLayout from './components/AttedanceTableLayout'; +import { getCurrentStatistic } from '../utils/util'; +import AttendanceStatusDropdown from './components/AttendanceStatusDropdown'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { v4 as uuid } from 'uuid'; + +// +// +// + +const TABLE_HEAD = ['이름', '출석 상태']; + +// +// +// + +const AttendanceReportTable = () => { + const { isMobileOrSmaller } = useBreakpoints(); + + const [searchParams] = useSearchParams(); + const status = searchParams.getAll('status') ?? []; + const search = searchParams.get('search') || ''; + + // + const { generationId } = useParams(); + const { sessionId } = useParams(); + + // + const { currentAttendance } = useGetAttendances({ + sessionId: Number(sessionId), + generationId: Number(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/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, + }, +); 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 new file mode 100644 index 00000000..791a5a0d --- /dev/null +++ b/src/pages/Attendance/constants/index.tsx @@ -0,0 +1,77 @@ +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 = [ + { + status: 'offline', + icon: theme.colors.sub3[40]} />, + text: '대면', + }, + + { + status: 'online', + icon: , + text: '비대면', + }, + + { + status: 'late', + icon: theme.colors.secondary80} />, + text: '지각', + }, + + { + status: 'absent', + icon: , + 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 = ['대면', '비대면', '지각', '결석']; 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, + }; +}; 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; +}; 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; +}; 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; +}; diff --git a/src/pages/CS/CSHome.tsx b/src/pages/CS/CSHome.tsx index 65ef7ae7..35bfe2ba 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 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'; -import GenerationDropBox from '@components/GenerationDropBox'; import CotatoIcon from '@components/CotatoIcon'; import { IconButton } from '@mui/material'; 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,13 +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') && ( diff --git a/src/pages/MyPage/CSRecord.tsx b/src/pages/MyPage/CSRecord.tsx index 24db9049..b44f5529 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'; // @@ -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 0a1b0cf5..65624b6d 100644 --- a/src/pages/Session/SessionHome.tsx +++ b/src/pages/Session/SessionHome.tsx @@ -11,9 +11,8 @@ import { CotatoLocalTime, CotatoSessionListResponse, } from 'cotato-openapi-clients'; -import GenerationDropBox from '@components/GenerationDropBox'; +import CotatoDropBox from '@components/CotatoDropBox'; 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'; @@ -24,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( @@ -47,6 +48,7 @@ const SessionHome = () => { const [selectedSession, setSelectedSession] = useState(null); const isTabletOrSmaller = useMediaQuery(`(max-width:${device.tablet})`); + const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); /** @@ -84,6 +86,7 @@ const SessionHome = () => { */ const handleGenerationChange = (generation: CotatoGenerationInfoResponse) => { setSelectedGeneration(generation); + setSearchParams({ generationId: generation.generationId!.toString() }); }; /** @@ -204,7 +207,6 @@ const SessionHome = () => { * */ const handleSessionUpdate = (session: SessionUploadInfo) => { - console.log(session); if (!session.sessionId) { return; } @@ -304,12 +306,16 @@ const SessionHome = () => { const renderSettingTab = () => { return ( - + {generations && ( + + )} {userData?.role === 'ADMIN' && !isTabletOrSmaller && ( setIsAddModalOpen(true)} /> )} @@ -378,6 +384,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]); + /** * */