diff --git a/components/DynamicQuill.tsx b/components/DynamicQuill.tsx new file mode 100644 index 000000000..5854e08a4 --- /dev/null +++ b/components/DynamicQuill.tsx @@ -0,0 +1,10 @@ +// dynamic import로 Quill 불러오는 컴포넌트 +import dynamic from 'next/dynamic'; + +// TODO : 로딩 컴포넌트 구체화 필요함. +const DynamicQuill = dynamic(() => import('react-quill'), { + ssr: false, + loading: () =>

Loading ...

, +}); + +export default DynamicQuill; diff --git a/components/Layout/Layout.tsx b/components/Layout/Layout.tsx index 59cbb3596..177777b3d 100644 --- a/components/Layout/Layout.tsx +++ b/components/Layout/Layout.tsx @@ -1,6 +1,5 @@ import { useRouter } from 'next/router'; import { useRecoilValue } from 'recoil'; -import { colorModeState } from 'utils/recoil/colorMode'; import { openCurrentMatchState } from 'utils/recoil/match'; import AdminReject from 'components/admin/AdminReject'; import AdminLayout from 'components/admin/Layout'; @@ -10,7 +9,7 @@ import Header from 'components/Layout/Header'; import HeaderStateContext from 'components/Layout/HeaderContext'; import MainPageProfile from 'components/Layout/MainPageProfile'; import Megaphone from 'components/Layout/MegaPhone'; -import StyledButton from 'components/UI/StyledButton'; +import RecruitLayout from 'components/recruit/RecruitLayout'; import Statistics from 'pages/statistics'; import useAnnouncementCheck from 'hooks/Layout/useAnnouncementCheck'; import useGetUserSeason from 'hooks/Layout/useGetUserSeason'; @@ -19,6 +18,8 @@ import useSetAfterGameModal from 'hooks/Layout/useSetAfterGameModal'; import { useUser } from 'hooks/Layout/useUser'; import useAxiosResponse from 'hooks/useAxiosResponse'; import styles from 'styles/Layout/Layout.module.scss'; +import PlayButton from './PlayButton'; +import UserLayout from './UserLayout'; type AppLayoutProps = { children: React.ReactNode; @@ -26,9 +27,7 @@ type AppLayoutProps = { export default function AppLayout({ children }: AppLayoutProps) { const user = useUser(); - const colorMode = useRecoilValue(colorModeState); const presentPath = useRouter().asPath; - const router = useRouter(); const openCurrentMatch = useRecoilValue(openCurrentMatchState); useAxiosResponse(); @@ -37,58 +36,38 @@ export default function AppLayout({ children }: AppLayoutProps) { useLiveCheck(presentPath); useAnnouncementCheck(presentPath); - const onClickMatch = () => { - router.replace('/'); - router.push(`/match`); - }; + if (!user || !user.intraId) return null; - if (!user) return null; + if (presentPath.includes('/admin')) { + if (!user.isAdmin) return ; + return {children}; + } - return presentPath.includes('/admin') ? ( - user.isAdmin ? ( - {children} - ) : ( - - ) - ) : ( -
-
-
- {presentPath === '/statistics' && user.isAdmin ? ( - - ) : ( - user.intraId && ( - <> - -
- - {presentPath !== '/match' && - presentPath !== '/manual' && - presentPath !== '/store' && ( -
-
- - Play - -
-
- )} -
- - {openCurrentMatch && } - {presentPath === '/' && } -
- {children} -
- - ) - )} -
+ if (presentPath.includes('/recruit')) { + return {children}; + } + + // NOTE : 외부 툴을 사용해보고 외부 툴로 대체가 가능하다면 삭제 예정 + if (presentPath === '/statistics' && user.isAdmin) + return ( + + + + ); + + return ( + + +
+ + +
+ + {openCurrentMatch && } + {presentPath === '/' && }
-
+ {children} +
+ ); } diff --git a/components/Layout/MenuBar/MenuBarElement.tsx b/components/Layout/MenuBar/MenuBarElement.tsx index 8f6cba703..3225f5ca8 100644 --- a/components/Layout/MenuBar/MenuBarElement.tsx +++ b/components/Layout/MenuBar/MenuBarElement.tsx @@ -14,10 +14,12 @@ import CurrentMatchEmoji from 'public/image/menu_currentMatch.svg'; import HallOfFameEmoji from 'public/image/menu_halloffame.svg'; import ManualEmoji from 'public/image/menu_manual.svg'; import RankingEmoji from 'public/image/menu_ranking.svg'; +import RecruitEmoji from 'public/image/menu_recruit.svg'; import ReportEmoji from 'public/image/menu_report.svg'; import SignOutEmoji from 'public/image/menu_signOut.svg'; import StatisticsEmoji from 'public/image/menu_statistics.svg'; import { useUser } from 'hooks/Layout/useUser'; +import useCheckRecruit from 'hooks/recruit/useCheckRecruit'; import useAxiosGet from 'hooks/useAxiosGet'; import styles from 'styles/Layout/MenuBar.module.scss'; @@ -69,10 +71,20 @@ const MenuItem = ({ itemName, onClick }: menuItemProps) => { name: '관리자', svg: , }, + Recruit: { + name: '지원하기', + svg: , + }, }; return (
-
{menuList[itemName].svg}
+
+ {menuList[itemName].svg} +
{menuList[itemName].name}
); @@ -89,6 +101,7 @@ const MenuLink = ({ link, onClick, itemName }: MenuLinkProps) => { export const MainMenu = () => { const HeaderState = useContext(HeaderContext); const setModal = useSetRecoilState(modalState); + const { isRecruiting } = useCheckRecruit(); const getAnnouncementHandler = useAxiosGet({ url: '/pingpong/announcement', @@ -106,6 +119,13 @@ export const MainMenu = () => { return (
); } diff --git a/components/admin/announcement/AnnounceEdit.tsx b/components/admin/announcement/AnnounceEdit.tsx index 6cb143410..2af9c27c2 100644 --- a/components/admin/announcement/AnnounceEdit.tsx +++ b/components/admin/announcement/AnnounceEdit.tsx @@ -1,19 +1,14 @@ -import dynamic from 'next/dynamic'; import { useEffect, useState } from 'react'; import { useSetRecoilState } from 'recoil'; import { QUILL_EDIT_MODULES, QUILL_FORMATS } from 'types/quillTypes'; import { instanceInManage, instance } from 'utils/axios'; import { toastState } from 'utils/recoil/toast'; +import DynamicQuill from 'components/DynamicQuill'; import { useUser } from 'hooks/Layout/useUser'; import styles from 'styles/admin/announcement/AnnounceEdit.module.scss'; import 'react-quill/dist/quill.snow.css'; import 'react-quill/dist/quill.bubble.css'; -const Quill = dynamic(() => import('react-quill'), { - ssr: false, - loading: () =>

Loading ...

, -}); - export default function AnnounceEdit() { const user = useUser(); const setSnackbar = useSetRecoilState(toastState); @@ -92,7 +87,7 @@ export default function AnnounceEdit() { {content ? (
Notice!
-
- import('react-quill'), { - ssr: false, - loading: () =>

Loading ...

, -}); - const tableTitle: { [key: string]: string } = { content: '내용', createdAt: '생성 시간', @@ -97,7 +92,7 @@ export default function AnnounceList() { className={styles.tableBodyItemQuill} key={index} > - ({ + recruitment: [], + totalPage: 0, + currentPage: 0, + }); + const [view, setView] = useState('recruitment'); + const [selectedRecruit, setSelectedRecruit] = useState(1); + const [currentPage, setCurrentPage] = useState(1); + const setSnackBar = useSetRecoilState(toastState); + + const getRecruitHandler = useCallback(async () => { + try { + // const res = await instanceInManage.get( + // `/recruitments?page=${currentPage}&size=20` + // ); + const res = await mockInstance.get(`admin/recruitments`); + setRecruitData({ + recruitment: res.data.recruitment, + totalPage: res.data.totalPages, + currentPage: res.data.number + 1, + }); + } catch (e: any) { + setSnackBar({ + toastName: 'get recruitment', + severity: 'error', + message: `API 요청에 문제가 발생했습니다.`, + clicked: true, + }); + } + }, [currentPage]); + + // const detailRecruit = (recruit: Irecruit) => { + + // }; + + const recruitmentApplicant = (recruitId: number) => { + setSelectedRecruit(recruitId); + setView('detail'); + }; + + useEffect(() => { + getRecruitHandler(); + }, [currentPage]); + + if (view === 'detail') { + return ; + } + + const renderTableCell = (recruit: Irecruit, columnName: string) => { + if (columnName === 'detailRecruitment') { + return ; + } + + if (columnName === 'detaillUser') { + return ( + + ); + } + + if (columnName === 'usedAt') { + return ( +
+ {`${dateToStringShort( + new Date(recruit.startDate) + )} ~ ${dateToStringShort(new Date(recruit.endDate))}`} +
+ ); + } + + return ( + + ); + }; + + return ( + <> + + + + + {recruitData.recruitment.length > 0 ? ( + recruitData.recruitment.map((recruit: Irecruit) => ( + + {tableFormat['recruitment'].columns.map( + (columnName: string, index: number) => ( + + {renderTableCell(recruit, columnName)} + + ) + )} + + )) + ) : ( + + )} + +
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+ + ); +} + +export default RecruitmentsHistoryList; diff --git a/components/admin/recruitments/RecruitmentsMain.tsx b/components/admin/recruitments/RecruitmentsMain.tsx new file mode 100644 index 000000000..0683e7d6a --- /dev/null +++ b/components/admin/recruitments/RecruitmentsMain.tsx @@ -0,0 +1,32 @@ +import { Dispatch, SetStateAction } from 'react'; +import styles from 'styles/admin/store/StoreMain.module.scss'; +import RecruitmentEdit from './recruitmentsEdit/RecruitmentEdit'; +import RecruitmentsHistoryList from './RecruitmentsHistoryList'; + +interface RecruitmentsMainProps { + setPageType: Dispatch>; +} + +function RecruitmentsMain({ setPageType }: RecruitmentsMainProps) { + //return menutab + return ( +
+
+ +
+
+
변경 이력
+ +
+
+ ); +} + +export default RecruitmentsMain; diff --git a/components/admin/recruitments/recruitmentsEdit/RecruitmentEdit.tsx b/components/admin/recruitments/recruitmentsEdit/RecruitmentEdit.tsx new file mode 100644 index 000000000..5b2ea595c --- /dev/null +++ b/components/admin/recruitments/recruitmentsEdit/RecruitmentEdit.tsx @@ -0,0 +1,53 @@ +import { Dispatch, SetStateAction } from 'react'; +import { IrecruitEditInfo } from 'types/admin/adminRecruitmentsTypes'; +import useRecruitmentEditInfo from 'hooks/recruitments/useRecruitmentEditInfo'; +import styles from 'styles/admin/recruitments/recruitmentEdit/RecruitmentEdit.module.scss'; +import ActionSelectorButtons from './components/ActionSelectorButtons'; +import QuestionFormBuilder from './components/QuestionFormBuilder'; +import QuillDescriptionEditor from './components/QuillDescriptionEditor'; +import TitleTimeRangeSelector from './components/TitleTimeRangeSelector'; + +const initRecruitmentEditInfo: IrecruitEditInfo = { + title: '', + startDate: '', + endDate: '', + generation: '', + content: '', + form: [], +}; + +interface RecruitmentEditProps { + setPageType: Dispatch>; +} +export default function RecruitmentEdit({ setPageType }: RecruitmentEditProps) { + const { recruitmentEditInfo, setRecruitmentEditInfoField, formManager } = + useRecruitmentEditInfo(initRecruitmentEditInfo); + + return ( +
+ + + + + +
+ ); +} diff --git a/components/admin/recruitments/recruitmentsEdit/components/ActionSelectorButtons.tsx b/components/admin/recruitments/recruitmentsEdit/components/ActionSelectorButtons.tsx new file mode 100644 index 000000000..5e16ed246 --- /dev/null +++ b/components/admin/recruitments/recruitmentsEdit/components/ActionSelectorButtons.tsx @@ -0,0 +1,55 @@ +import { + Button, + FormControl, + InputLabel, + MenuItem, + Select, +} from '@mui/material'; +import { IrecruitEditInfo } from 'types/admin/adminRecruitmentsTypes'; +import styles from 'styles/admin/recruitments/recruitmentEdit/components/ActionSelectorButtons.module.scss'; + +interface ActionSelectorButtonsProps { + recruitmentEditInfo: IrecruitEditInfo; + actionType: 'CREATE' | 'MODIFY'; +} + +export default function recruitmentsEdit({ + recruitmentEditInfo, + actionType, +}: ActionSelectorButtonsProps) { + return ( +
+ {actionType === 'CREATE' ? ( +
+ + 기존 공고 + + + +
+ ) : ( + <> + )} + {actionType === 'CREATE' && ( + + )} + {actionType === 'MODIFY' && ( + + )} +
+ ); +} diff --git a/components/admin/recruitments/recruitmentsEdit/components/QuestionFormBuilder.tsx b/components/admin/recruitments/recruitmentsEdit/components/QuestionFormBuilder.tsx new file mode 100644 index 000000000..f2609ee42 --- /dev/null +++ b/components/admin/recruitments/recruitmentsEdit/components/QuestionFormBuilder.tsx @@ -0,0 +1,289 @@ +import React from 'react'; +import { DropResult } from 'react-beautiful-dnd'; +import { CheckBox } from '@mui/icons-material'; +import AddIcon from '@mui/icons-material/Add'; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import ClearIcon from '@mui/icons-material/Clear'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + Paper, + Radio, + RadioGroup, + Select, + SelectChangeEvent, + TextField, + Tooltip, +} from '@mui/material'; +import { IcheckItem, Iquestion } from 'types/admin/adminRecruitmentsTypes'; +import DraggableList from 'components/UI/DraggableList'; +import { IFormManager } from 'hooks/recruitments/useRecruitmentEditInfo'; +import styles from 'styles/admin/recruitments/recruitmentEdit/components/QuestionFormBuilder.module.scss'; + +interface CheckInputProps { + checkList: IcheckItem[] | undefined; + questionIdx: number; + formManager: IFormManager; +} + +function MultiCheckInput({ + checkList, + questionIdx, + formManager, +}: CheckInputProps) { + const inputChangeHandler = ( + { target }: React.ChangeEvent, + idx: number + ) => { + formManager.setCheckItemContent(questionIdx, idx, target.value); + }; + return ( + <> + {checkList && + checkList.map((checkItem, idx) => { + return ( + + + + + + inputChangeHandler(e, idx)} + /> + + + + formManager.removeCheckItemFromQuestion(idx, questionIdx) + } + > + + + + + ); + })} + formManager.addCheckItemToQuestion(questionIdx)} + style={{ width: '100%' }} + > + + + + ); +} + +function SingleCheckInput({ + checkList, + questionIdx, + formManager, +}: CheckInputProps) { + const inputChangeHandler = ( + { target }: React.ChangeEvent, + idx: number + ) => { + formManager.setCheckItemContent(questionIdx, idx, target.value); + }; + + return ( + + {checkList && + checkList.map((checkItem, idx) => { + return ( + + + + + + inputChangeHandler(e, idx)} + /> + + + + formManager.removeCheckItemFromQuestion(idx, questionIdx) + } + > + + + + + ); + })} + formManager.addCheckItemToQuestion(questionIdx)} + > + + + + ); +} + +interface QuestionProps { + idx: number; + question: Iquestion; + formManager: IFormManager; + isFocused: boolean; + setFocusedQuestion: React.Dispatch>; +} + +function Question({ + idx, + question, + formManager, + isFocused, + setFocusedQuestion, +}: QuestionProps) { + const selectChangehandler = ({ target }: SelectChangeEvent) => { + formManager.changeQuestionInputType(idx, target.value); + }; + + const inputChangeHandler = ({ + target, + }: React.ChangeEvent) => { + formManager.setQuestionContent(idx, target.value); + }; + + return ( + setFocusedQuestion(idx)} + > + + + + + + + 질문 유형 + + + + + {question.inputType === 'SINGLE_CHECK' && ( + + )} + {question.inputType === 'MULTI_CHECK' && ( + + )} + + formManager.removeQuestion(idx)} + > + + + + + ); +} + +interface QuestionFormBuilderProps { + form: Iquestion[]; + formManager: IFormManager; +} + +export default function QuestionFormBuilder({ + form, + formManager, +}: QuestionFormBuilderProps) { + const [focusedQuestionIdx, setFocusedQuestion] = React.useState< + number | null + >(null); + + const addQuestionHandler = () => { + if (focusedQuestionIdx === null) { + formManager.addEmptyQuestion(form.length - 1, 'TEXT'); + setFocusedQuestion(form.length); + } else { + formManager.addEmptyQuestion(focusedQuestionIdx, 'TEXT'); + setFocusedQuestion(focusedQuestionIdx + 1); + } + }; + + const onDragEndHandler = ({ destination, source }: DropResult) => { + if (!destination) return; + formManager.switchQuestionIndex(source.index, destination.index); + setFocusedQuestion(destination.index); + }; + + console.log(focusedQuestionIdx); + + return ( +
+
+ + {form.map((question, idx) => { + return ( + + ); + })} + +
+ + addQuestionHandler()} + > + + + +
+
+
+ ); +} diff --git a/components/admin/recruitments/recruitmentsEdit/components/QuillDescriptionEditor.tsx b/components/admin/recruitments/recruitmentsEdit/components/QuillDescriptionEditor.tsx new file mode 100644 index 000000000..90671851c --- /dev/null +++ b/components/admin/recruitments/recruitmentsEdit/components/QuillDescriptionEditor.tsx @@ -0,0 +1,37 @@ +import dynamic from 'next/dynamic'; +import { Paper } from '@mui/material'; +import { QUILL_EDIT_MODULES, QUILL_FORMATS } from 'types/quillTypes'; +import 'react-quill/dist/quill.snow.css'; +import styles from 'styles/admin/recruitments/recruitmentEdit/components/QuillDescriptionEditor.module.scss'; + +const Quill = dynamic(() => import('react-quill'), { + ssr: false, + loading: () =>

Loading ...

, +}); + +interface QuillDescriptionEditorProps { + content: string; + setRecruitmentEditInfoField: (fieldName: string, value: any) => void; +} + +export default function QuillDescriptionEditor({ + content, + setRecruitmentEditInfoField, +}: QuillDescriptionEditorProps) { + const quillChangeHandler = (value: string) => { + setRecruitmentEditInfoField('content', value); + }; + + return ( + + + + ); +} diff --git a/components/admin/recruitments/recruitmentsEdit/components/TitleTimeRangeSelector.tsx b/components/admin/recruitments/recruitmentsEdit/components/TitleTimeRangeSelector.tsx new file mode 100644 index 000000000..d70397b2f --- /dev/null +++ b/components/admin/recruitments/recruitmentsEdit/components/TitleTimeRangeSelector.tsx @@ -0,0 +1,82 @@ +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { IrecruitEditInfo } from 'types/admin/adminRecruitmentsTypes'; +import { AdminTableHead } from 'components/admin/common/AdminTable'; +import styles from 'styles/admin/recruitments/recruitmentEdit/components/TitleTimeRangeSelector.module.scss'; + +const tableTitle: { [key: string]: string } = { + title: '모집 제목', + startDate: '지원서 모집 시작', + endDate: '지원서 모집 마감', + generation: '기수', +}; + +interface TitleTimeRangeSelectorProps { + recruitmentEditInfo: IrecruitEditInfo; + setRecruitmentEditInfoField: (fieldName: string, value: any) => void; +} + +export default function TitleTimeRangeSelector({ + recruitmentEditInfo, + setRecruitmentEditInfoField, +}: TitleTimeRangeSelectorProps) { + const inputChangeHandler = ({ + target, + }: React.ChangeEvent) => { + setRecruitmentEditInfoField(target.name, target.value); + }; + + return ( +
+ + + + + + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/components/admin/recruitments/recruitmentsuser/DetailRecruitUserList.tsx b/components/admin/recruitments/recruitmentsuser/DetailRecruitUserList.tsx new file mode 100644 index 000000000..0d310e2b1 --- /dev/null +++ b/components/admin/recruitments/recruitmentsuser/DetailRecruitUserList.tsx @@ -0,0 +1,148 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + 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; +} + +const tableTitle: { [key: string]: string } = { + id: 'ID', + usedAt: '적용 시간', + title: '제목', + status: '상태', + detailRecruitment: '공고 상세보기', + detaillUser: '지원자 보기', +}; + +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.content).join(', ') || + '' + } + maxLen={16} + /> + + ))} + + ); + }; + + if (!recruitUserData.applications.length) { + return ( + + + + + + +
+
+ ); + } + + return ( + <> + + + + + {recruitUserData.applications.map(renderTableCell)} + +
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+ + ); +} + +export default DetailRecruitUserList; diff --git a/components/admin/recruitments/recruitmentsuser/MenuTab.tsx b/components/admin/recruitments/recruitmentsuser/MenuTab.tsx new file mode 100644 index 000000000..bfbf53821 --- /dev/null +++ b/components/admin/recruitments/recruitmentsuser/MenuTab.tsx @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react'; +import DetailRecruitUserList from 'components/admin/recruitments/recruitmentsuser/DetailRecruitUserList'; +import NotificationResults from 'components/admin/recruitments/recruitmentsuser/NotificationResults'; +import styles from 'styles/admin/recruitments/MenuTab.module.scss'; +import RecruitmentsHistoryList from '../RecruitmentsHistoryList'; + +function MenuTab({ recruitId }: { recruitId: number }) { + const [view, setView] = useState('menu'); + const [tabIdx, setTabIdx] = useState(0); + const [child, setChild] = useState( + + ); + const tabContents = [ + { + contentId: 0, + contentName: '상세정보', + }, + { + contentId: 1, + contentName: '결과 입력', + }, + ]; + + useEffect(() => { + switch (tabIdx) { + case 0: + setChild(); + break; + case 1: + setChild(); + break; + } + }, [tabIdx, recruitId]); + + return ( + <> + {view === 'history' ? ( + + ) : ( + <> +
+ +
    + {tabContents.map((content) => ( +
  • setTabIdx(content.contentId)} + > + {content.contentName} +
  • + ))} +
+
{child}
+
+ + )} + + ); +} + +export default MenuTab; diff --git a/components/admin/recruitments/recruitmentsuser/NotificationResults.tsx b/components/admin/recruitments/recruitmentsuser/NotificationResults.tsx new file mode 100644 index 000000000..a94586f91 --- /dev/null +++ b/components/admin/recruitments/recruitmentsuser/NotificationResults.tsx @@ -0,0 +1,190 @@ +import { useCallback, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import Button from '@mui/material/Button'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import { + Inotication, + InoticationTable, +} from 'types/admin/adminRecruitmentsTypes'; +// import { instanceInManage } from 'utils/axios'; +import { mockInstance } from 'utils/mockAxios'; +import { modalState } from 'utils/recoil/modal'; +import { toastState } from 'utils/recoil/toast'; +import { tableFormat } from 'constants/admin/table'; +import { + AdminEmptyItem, + AdminTableHead, +} from 'components/admin/common/AdminTable'; +import PageNation from 'components/Pagination'; +import styles from 'styles/admin/recruitments/Recruitments.module.scss'; +import 'react-datepicker/dist/react-datepicker.css'; + +const tableTitle: { [key: string]: string } = { + id: 'ID', + intraId: 'Intra ID', + interview: '면접', + result: '결과', +}; + +export interface notiMessageType { + content: string; +} + +function NotificationResults({ recruitId }: { recruitId: number }) { + const [notificationData, setNotificationData] = useState({ + noticationList: [], + totalPage: 0, + currentPage: 0, + }); + const [currentPage, setCurrentPage] = useState(1); + const [alignment, setAlignment] = useState>({}); + const setSnackBar = useSetRecoilState(toastState); + const [startDate, setStartDate] = useState(new Date()); + const setModal = useSetRecoilState(modalState); + + const onEditTemplate = () => { + setModal({ + modalName: 'ADMIN-RECRUIT_MESSAGE_TEMPLATE', + }); + }; + + const handleAlignment = ( + event: React.MouseEvent, + newAlignment: string | null, + id: number + ) => { + setAlignment((prev) => ({ ...prev, [id]: newAlignment })); + }; + + const getRecruitNotiHandler = useCallback(async () => { + try { + // const res = await instanceInManage.get( + // `/admin/recruitments?page=${currentPage}&size=20` + // ); + const id = recruitId; + const res = await mockInstance.get(`/admin/recruitments/${id}`); + setNotificationData({ + noticationList: res.data.applications, + totalPage: res.data.totalPages, + currentPage: res.data.number + 1, + }); + } catch (e: any) { + setSnackBar({ + toastName: 'get notification', + severity: 'error', + message: `API 요청에 문제가 발생했습니다.`, + clicked: true, + }); + } + }, [currentPage]); + + useEffect(() => { + getRecruitNotiHandler(); + }, [currentPage]); + + if (!notificationData.noticationList.length) { + return ( + + + + + + +
+
+ ); + } + + const renderTableCell = (recruit: Inotication, columnName: string) => { + if (columnName === 'interview') { + return ( +
+ setStartDate(date)} + /> +   + +
+ ); + } + + if (columnName === 'result') { + return ( +
+ + handleAlignment(event, newAlignment, recruit.applicationId) + } + > + + 합 격 + + + 불합격 + + +   + +
+ ); + } + + return recruit[columnName as keyof Inotication]?.toString(); + }; + return ( + <> + + + + + {notificationData.noticationList.map((recruit: Inotication) => ( + + {tableFormat['notificationList'].columns.map( + (columnName: string, index: number) => ( + + {renderTableCell(recruit, columnName)} + + ) + )} + + ))} + +
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+ + + ); +} + +export default NotificationResults; diff --git a/components/admin/recruitments/recruitmentsuser/tmplateEditor.tsx b/components/admin/recruitments/recruitmentsuser/tmplateEditor.tsx new file mode 100644 index 000000000..d4569ab62 --- /dev/null +++ b/components/admin/recruitments/recruitmentsuser/tmplateEditor.tsx @@ -0,0 +1,87 @@ +import { useState } from 'react'; +import { useResetRecoilState, useSetRecoilState } from 'recoil'; +import { IRecruitMessageTemplate } from 'types/recruit/recruitments'; +import { mockInstance } from 'utils/mockAxios'; +// import { instanceInManage } from 'utils/axios'; +import { modalState } from 'utils/recoil/modal'; +import { toastState } from 'utils/recoil/toast'; +import styles from 'styles/admin/modal/AdminRecruitMessageTemplateModal.module.scss'; + +function TemplateEditor({ messageType, message }: IRecruitMessageTemplate) { + const resetModal = useResetRecoilState(modalState); + const [tempalte, setTemplate] = useState({ + messageType: messageType, + message: message, + }); + const [alert, setAlert] = useState(''); + const setSnackbar = useSetRecoilState(toastState); + const titles = { + INTERVIEW: '면접', + PASS: '합격', + FAIL: '불합격', + }; + const DATE = '${선택날짜}'; // 치환 문구 + + const inputHandler = (e: React.ChangeEvent) => { + const message = e.target.value; + if (!message) { + setAlert(`내용을 입력해주세요.`); + } else if (messageType === 'INTERVIEW' && !message.includes(DATE)) { + setAlert(`${DATE}를 포함해주세요.`); + } else { + setAlert(''); + setTemplate({ ...tempalte, message: message }); + } + }; + + const onClick = async () => { + if (alert) { + setSnackbar({ + toastName: `alert`, + severity: 'error', + message: `치환 문구가 필요합니다.`, + clicked: true, + }); + return; + } + try { + // await instanceInManage.post('/recruitments/result/message', tempalte); + await mockInstance.post('/admin/recruitments/result/message', tempalte); + setSnackbar({ + toastName: `post request`, + severity: 'success', + message: `템플릿이 성공적으로 등록되었습니다.`, + clicked: true, + }); + resetModal(); + } catch (e: any) { + setSnackbar({ + toastName: `bad request`, + severity: 'error', + message: `템플릿 등록에 문제가 발생했습니다.`, + clicked: true, + }); + } + }; + + return ( +
+
{`${titles[messageType]} 안내`}
+
+