diff --git a/components/admin/common/AdminTable.tsx b/components/admin/common/AdminTable.tsx index afff054ba..44d328221 100644 --- a/components/admin/common/AdminTable.tsx +++ b/components/admin/common/AdminTable.tsx @@ -71,3 +71,24 @@ export function AdminContent({
{content?.toString()}
); } + +export function DetailContentHover({ + content, + maxLen, +}: { + content?: string; + maxLen: number; +}) { + if (!content) return
N/A
; + + return content?.length > maxLen ? ( +
+
+ {(content?.toString() || '').slice(0, maxLen)}... +
+ {/*
{content}
*/} +
+ ) : ( +
{content?.toString()}
+ ); +} diff --git a/components/admin/recruitments/recruitmentsuser/DetailRecruitUserList.tsx b/components/admin/recruitments/recruitmentsuser/DetailRecruitUserList.tsx index 812a39b62..17b76fc95 100644 --- a/components/admin/recruitments/recruitmentsuser/DetailRecruitUserList.tsx +++ b/components/admin/recruitments/recruitmentsuser/DetailRecruitUserList.tsx @@ -1,115 +1,38 @@ -import { useCallback, useEffect, useState } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useState } from 'react'; import { Paper, Table, TableBody, + TableHead, TableCell, TableContainer, TableRow, } from '@mui/material'; -import { - IrecruitUserTable, - Iquestion, -} from 'types/admin/adminRecruitmentsTypes'; -// import { instanceInManage } from 'utils/axios'; -import { mockInstance } from 'utils/mockAxios'; -import { toastState } from 'utils/recoil/toast'; import { AdminEmptyItem, AdminTableHead, - AdminContent, } from 'components/admin/common/AdminTable'; import PageNation from 'components/Pagination'; -import styles from 'styles/admin/recruitments/Recruitments.module.scss'; - -//무한 스크롤로 변경 -//필터 추가 -/* -추가할 기능 -가로세로 길이 조절 -가로세로 위치 변경 -*/ -export interface IrecruitTable { - applications: IrecruitUserTable['applications']; - totalPage: number; - currentPage: number; -} +import useRecruitmentUserFilter from 'hooks/recruitments/useRecruitmentUserFilter'; +import styles from 'styles/admin/recruitments/RecruitmentsUser.module.scss'; +import FilterQptionsUI from './FilterOptions'; +import renderTableCells from './renderTableCells'; const tableTitle: { [key: string]: string } = { - id: 'ID', - usedAt: '적용 시간', - title: '제목', + id: '', + intraId: 'intraId', status: '상태', - detailRecruitment: '공고 상세보기', - detaillUser: '지원자 보기', + question: '질문', }; function DetailRecruitUserList({ recruitId }: { recruitId: number }) { - const [recruitUserData, setRecruitUserData] = useState({ - applications: [], - totalPage: 0, - currentPage: 0, - }); const [currentPage, setCurrentPage] = useState(1); - const setSnackBar = useSetRecoilState(toastState); - - const getRecruitUserHandler = useCallback(async () => { - try { - // const res = await instanceInManage.get( - // `/recruitments/${recruitId}/applications` - // ); - const id = recruitId; - const res = await mockInstance.get(`/admin/recruitments/${id}`); - setRecruitUserData({ - applications: res.data.applications, - totalPage: res.data.totalPages, - currentPage: res.data.number + 1, - }); - } catch (e: any) { - setSnackBar({ - toastName: 'get recruitment', - severity: 'error', - message: `API 요청에 문제가 발생했습니다.`, - clicked: true, - }); - } - }, [currentPage]); - - useEffect(() => { - getRecruitUserHandler(); - }, [currentPage]); - - const renderTableCell = ( - recruit: IrecruitUserTable['applications'][number] - ) => { - return ( - - - - - {recruit.form?.map((formItem: Iquestion, index: number) => ( - - item.contents).join(', ') || - '' - } - maxLen={16} - /> - - ))} - - ); - }; + const { recruitUserData, questions } = useRecruitmentUserFilter( + recruitId, + currentPage + ); - if (!recruitUserData.applications.length) { + if (!recruitUserData.applications) { return ( @@ -124,11 +47,25 @@ function DetailRecruitUserList({ recruitId }: { recruitId: number }) { return ( <> + {FilterQptionsUI(recruitUserData.applications)}
- + + + + intraId + status + {questions.map((question, index) => ( + + {question} + + ))} + + - {recruitUserData.applications.map(renderTableCell)} + {recruitUserData.applications.map((recruit) => + renderTableCells(recruit, questions) + )}
diff --git a/components/admin/recruitments/recruitmentsuser/FilterOptions.tsx b/components/admin/recruitments/recruitmentsuser/FilterOptions.tsx new file mode 100644 index 000000000..ee44cf0bb --- /dev/null +++ b/components/admin/recruitments/recruitmentsuser/FilterOptions.tsx @@ -0,0 +1,79 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + FormControl, + Select, + OutlinedInput, + MenuItem, + ListItemText, + SelectChangeEvent, +} from '@mui/material'; +import { + IcheckItem, + IrecruitUserTable, +} from 'types/admin/adminRecruitmentsTypes'; +import useRecruitmentUserFilter from 'hooks/recruitments/useRecruitmentUserFilter'; +import styles from 'styles/admin/recruitments/RecruitmentsUser.module.scss'; +import RecruitSearchBar from './RecruitSearchBar'; + +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250, + }, + }, +}; + +function FilterQptionsUI(recruitUserData: IrecruitUserTable[]) { + const [answers, setAnswers] = useState>([]); + const { checklistIds, handleChecklistChange } = useRecruitmentUserFilter(); + + useEffect(() => { + setAnswers( + recruitUserData.reduce((acc, recruit) => { + recruit.form.forEach((formItem) => { + if (formItem.inputType !== 'TEXT') { + formItem.checkedList?.forEach((item) => { + if (!acc.some((answer) => answer.checkId === item.checkId)) { + acc.push(item); + } + }); + } + }); + return acc; + }, [] as Array) + ); + }, [recruitUserData]); + + return ( +
+
+ +
+
+ + + +
+
+ ); +} + +export default FilterQptionsUI; diff --git a/components/admin/recruitments/recruitmentsuser/NotificationResults.tsx b/components/admin/recruitments/recruitmentsuser/NotificationResults.tsx index a94586f91..05b2b4142 100644 --- a/components/admin/recruitments/recruitmentsuser/NotificationResults.tsx +++ b/components/admin/recruitments/recruitmentsuser/NotificationResults.tsx @@ -26,7 +26,7 @@ import { AdminTableHead, } from 'components/admin/common/AdminTable'; import PageNation from 'components/Pagination'; -import styles from 'styles/admin/recruitments/Recruitments.module.scss'; +import styles from 'styles/admin/recruitments/RecruitmentsUser.module.scss'; import 'react-datepicker/dist/react-datepicker.css'; const tableTitle: { [key: string]: string } = { diff --git a/components/admin/recruitments/recruitmentsuser/RecruitSearchBar.tsx b/components/admin/recruitments/recruitmentsuser/RecruitSearchBar.tsx new file mode 100644 index 000000000..25dc602d0 --- /dev/null +++ b/components/admin/recruitments/recruitmentsuser/RecruitSearchBar.tsx @@ -0,0 +1,56 @@ +import { GoSearch } from 'react-icons/go'; +import { IoIosCloseCircle } from 'react-icons/io'; +import useRecruitmentUserFilter from 'hooks/recruitments/useRecruitmentUserFilter'; +import useSearchBar from 'hooks/useSearchBar'; +import styles from 'styles/admin/common/AdminSearchBar.module.scss'; + +const MAX_SEARCH_LENGTH = 15; + +export default function RecruitSearchBar() { + const { keyword, setKeyword, searchBarRef } = useSearchBar(); + + const { initSearch } = useRecruitmentUserFilter(); + + const adminhandleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.currentTarget.blur(); + initSearch(keyword); + } + }; + + return ( +
+ { + setKeyword(e.target.value); + }} + onKeyDown={adminhandleKeyDown} + placeholder='검색하기...' + maxLength={MAX_SEARCH_LENGTH} + value={keyword} + /> +
+ {keyword ? ( + { + initSearch(); + setKeyword(''); + }} + > + + + ) : ( + { + initSearch(); + }} + > + + + )} +
+
+ ); +} diff --git a/components/admin/recruitments/recruitmentsuser/renderTableCells.tsx b/components/admin/recruitments/recruitmentsuser/renderTableCells.tsx new file mode 100644 index 000000000..81ee30ed0 --- /dev/null +++ b/components/admin/recruitments/recruitmentsuser/renderTableCells.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { TableCell, TableRow, IconButton, TableRowProps } from '@mui/material'; +import { IrecruitUserTable } from 'types/admin/adminRecruitmentsTypes'; +import { DetailContentHover } from 'components/admin/common/AdminTable'; +import styles from 'styles/admin/recruitments/RecruitmentsUser.module.scss'; + +interface ExpandableTableRowProps extends TableRowProps { + children: React.ReactNode; + expandComponent: React.ReactNode; +} + +const ExpandableTableRow: React.FC = ({ + children, + expandComponent, + ...otherProps +}) => { + const [isExpanded, setIsExpanded] = useState(false); + const childrenCount = React.Children.count(children); + + const expandedClickHandler = () => { + setIsExpanded(!isExpanded); + }; + + return ( + <> + + + + {isExpanded ? : } + + + {children} + + {isExpanded && ( + + + {expandComponent} + + + )} + + ); +}; + +function renderTableCells(recruit: IrecruitUserTable, questions: string[]) { + const answers = questions.map((question) => { + const formItem = recruit.form.find((form) => form.question === question); + if (!formItem) return 'N/A'; + + switch (formItem.inputType) { + case 'TEXT': + return formItem.answer; + case 'SINGLE_CHECK': + return formItem.checkedList?.map((item) => item.content).join(', '); + case 'MULTI_CHECK': + return formItem.checkedList?.map((item) => item.content).join(', '); + default: + return 'N/A'; + } + }); + + return ( + +
+ intraId: {recruit.intraId} +
+
+ status: {recruit.status} +
+ {recruit.form.map((form, index) => ( +
+ {form.question}:{' '} + {form.answer + ? form.answer + : form.checkedList?.map((item) => item.content).join(', ')} +
+ ))} + + } + > + {recruit.intraId} + {recruit.status} + {answers.map( + (answer: string | undefined, index: React.Key | null | undefined) => ( + +
+ +
+
+ ) + )} +
+ ); +} + +export default renderTableCells; diff --git a/constants/admin/table.ts b/constants/admin/table.ts index 2fc12ee69..8962b4b57 100644 --- a/constants/admin/table.ts +++ b/constants/admin/table.ts @@ -174,14 +174,7 @@ export const tableFormat: TableFormat = { }, recruitUserList: { name: '지원자 목록', - columns: [ - 'id', - 'intraId', - 'status', - 'createdAt', - 'detailUser', - 'detailRecruit', - ], + columns: ['', 'intraId', 'status', 'question'], }, recruitEditTitle: { name: '공고 수정', diff --git a/hooks/recruitments/useRecruitmentUserFilter.ts b/hooks/recruitments/useRecruitmentUserFilter.ts new file mode 100644 index 000000000..8ee2bca7b --- /dev/null +++ b/hooks/recruitments/useRecruitmentUserFilter.ts @@ -0,0 +1,93 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { SelectChangeEvent } from '@mui/material'; +import { + IcheckItem, + IrecruitArrayTable, +} from 'types/admin/adminRecruitmentsTypes'; +import { mockInstance } from 'utils/mockAxios'; +import { toastState } from 'utils/recoil/toast'; + +const useRucruitmentUserFilter = (currentPage?: number, recruitId?: number) => { + const [recruitUserData, setRecruitUserData] = useState({ + applications: [], + totalPage: 0, + currentPage: 0, + }); + const setSnackBar = useSetRecoilState(toastState); + const [checklistIds, setChecklistIds] = useState>([]); + const [searchString, setSearchString] = useState(''); + + const getRecruitUserHandler = useCallback(async () => { + try { + // const res = await instanceInManage.get( + // `/recruitments/${recruitId}/applications`, { + // params: { + // page: currentPage, + // size: 20, + // question: questionId, + // checks: checklistIds.map((check) => check).join(','), + // search: search, + // } + // } + // ); + const id = recruitId; + const res = await mockInstance.get(`/admin/recruitments/${id}`); + setRecruitUserData({ + applications: res.data.applications, + totalPage: res.data.totalPages, + currentPage: res.data.number + 3, + }); + } catch (e: any) { + setSnackBar({ + toastName: 'get recruitment', + severity: 'error', + message: `API 요청에 문제가 발생했습니다.`, + clicked: true, + }); + } + }, [currentPage, searchString, checklistIds]); + + useEffect(() => { + getRecruitUserHandler(); + }, [currentPage, searchString, checklistIds]); + + const questions = recruitUserData.applications.reduce( + (acc: string[], application: { form: { question: string }[] }) => { + application.form.forEach(({ question }) => { + if (acc.indexOf(question) === -1) { + acc.push(question); + } + }); + return acc; + }, + [] + ); + + const handleChecklistChange = ( + event: SelectChangeEvent + ) => { + const { + target: { value }, + } = event; + typeof value !== 'string' ? setChecklistIds(value) : value; + }; + + const initSearch = useCallback((searchString?: string) => { + setSearchString(searchString || ''); + }, []); + + return { + checklistIds, + searchString, + setChecklistIds, + setSearchString, + getRecruitUserHandler, + recruitUserData, + questions, + initSearch, + handleChecklistChange, + }; +}; + +export default useRucruitmentUserFilter; diff --git a/pages/api/pingpong/admin/recruitments/[id].ts b/pages/api/pingpong/admin/recruitments/[id].ts index 529b35f96..4e331bc33 100644 --- a/pages/api/pingpong/admin/recruitments/[id].ts +++ b/pages/api/pingpong/admin/recruitments/[id].ts @@ -9,24 +9,45 @@ const fullRecruitData1 = { form: [ { questionId: 1, - question: '이름', + question: '질문 1', inputType: 'TEXT', - answer: '홍길동', + answer: '답변 1', }, { questionId: 2, - question: '나이', + question: '질문 2', inputType: 'SINGLE_CHECK', - answer: '20', + checkedList: [{ checkId: 1, content: '선택지 1' }], }, { questionId: 3, - question: '성별', + question: '질문 3', inputType: 'MULTI_CHECK', - checkList: [ + checkedList: [ { - checkId: 1, - content: '남', + checkId: 2, + content: '선택지 2', + }, + { + checkId: 3, + content: + '선택지 3ㅁㄴㅇㄹㄴㅁㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㄹㅁㄴㄹㅁㄴㅁㄴㄹㅁ', + }, + ], + }, + { + questionId: 4, + question: '질문 4', + inputType: 'MULTI_CHECK', + checkedList: [ + { + checkId: 2, + content: '선택지 2', + }, + { + checkId: 4, + content: + '선택지 2sadfsadfasdfasfsadfasfasdㅁㄴㅇㄹㅁㄴㄹㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹㄴㅁㅇㄹㅁㄴㄹㅁㄴㄹㄴㅁㄹㅁㄴㄹㅁㄴㄹㅁㄴㄹㅁㄹ', }, ], }, @@ -52,11 +73,11 @@ const fullRecruitData1 = { { questionId: 3, question: '성별', - inputType: 'TEXT', - checkList: [ + inputType: 'SINGLE_CHECK', + checkedList: [ { - checkId: 1, - content: '여', + checkId: 6, + content: '남', }, ], }, @@ -82,10 +103,10 @@ const fullRecruitData1 = { { questionId: 3, question: '성별', - inputType: 'TEXT', - checkList: [ + inputType: 'SINGLE_CHECK', + checkedList: [ { - checkId: 1, + checkId: 5, content: '여', }, ], diff --git a/styles/admin/common/AdminTable.module.scss b/styles/admin/common/AdminTable.module.scss index 8480a9890..ffcf1cecf 100644 --- a/styles/admin/common/AdminTable.module.scss +++ b/styles/admin/common/AdminTable.module.scss @@ -28,3 +28,29 @@ text-align: center; } } + +.tableBodyItemHover { + position: relative; +} + +.hoverInfo { + position: absolute; + bottom: 0.5rem; + box-sizing: border-box; + display: block; + width: 200%; + padding: 16px; + cursor: pointer; + visibility: hidden; + border-radius: 1rem; + opacity: 0; + transition: visibility 0s, opacity 0.5s linear; + transform: translate3d(0, 105%, 0); +} + +.tableBodyItemHover:hover .hoverInfo { + z-index: 1; + visibility: visible; + background-color: #cbced1; + opacity: 1; +} diff --git a/styles/admin/recruitments/RecruitmentsUser.module.scss b/styles/admin/recruitments/RecruitmentsUser.module.scss new file mode 100644 index 000000000..f004f3c8a --- /dev/null +++ b/styles/admin/recruitments/RecruitmentsUser.module.scss @@ -0,0 +1,86 @@ +@import 'styles/admin/common/Pagination.module.scss'; +@import 'styles/common.scss'; +@import 'styles/admin/common.scss'; + +.tableContainer { + width: 100px; + margin: auto; + overflow-x: scroll; + + .table { + width: max-content; + min-width: 100%; + .tableHeader { + background-color: lightgray; + .tableHeaderItem { + padding: 0.5rem; + font-size: $small-font; + font-weight: 600; + line-height: 150%; + text-align: center; + } + } + .tableBody { + .tableRow { + &:nth-child(odd) { + background-color: #f9fafb; + } + &:nth-child(even) { + background-color: #fff; + } + .tableBodyItem { + padding: 10px; + font-size: $small-font; + line-height: 150%; + text-align: center; + } + } + } + } +} +.searchWrap { + display: flex; + justify-content: flex-end; + align-items: center; + padding-bottom: 0.3rem; + :nth-child(2) { + margin: 0 0 0 20px; + } +} + +.deleteBtn { + width: 5rem; + height: 1.5rem; + color: #ffffff; + cursor: pointer; + background: #2678f3; + border: none; + border-radius: 8px; + justify-content: center; + align-items: center; + &:disabled { + cursor: not-allowed; + background-color: #929fb3; + } +} + +.interview { + display: flex; + justify-content: center; + align-items: center; +} + +.button { + @include admin-button('PALE-BLUE'); + padding: 0.5rem 1.3rem; + margin-top: 1rem; +} + +.filterWrap { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0.3rem; +} + +@include pagination; diff --git a/types/admin/adminRecruitmentsTypes.ts b/types/admin/adminRecruitmentsTypes.ts index 2b4257546..7f899c79f 100644 --- a/types/admin/adminRecruitmentsTypes.ts +++ b/types/admin/adminRecruitmentsTypes.ts @@ -16,13 +16,13 @@ export interface Iquestion { question: string; inputType: 'TEXT' | 'SINGLE_CHECK' | 'MULTI_CHECK'; answer?: string; - checkList?: Array; + checkedList?: Array; } export interface IcheckItem { checkId?: number; sortNum?: number; - contents: string; + content: string; } export interface Inotication { @@ -44,12 +44,16 @@ export interface InoticationTable { } export interface IrecruitUserTable { - applications: { - applicationId: number; - intraId: string; - status?: '합격' | '불합격' | '심사중'; - form: Array; - }[]; + applicationId: number; + intraId: string; + status?: '합격' | '불합격' | '심사중'; + form: Iquestion[]; +} + +export interface IrecruitArrayTable { + applications: IrecruitUserTable[]; + totalPage: number; + currentPage: number; } export interface RecruitmentsPages {