diff --git a/frontend/src/api/domain/apply/apply.ts b/frontend/src/api/domain/apply/apply.ts index 03127761c..361b55e37 100644 --- a/frontend/src/api/domain/apply/apply.ts +++ b/frontend/src/api/domain/apply/apply.ts @@ -23,6 +23,12 @@ const applyApis = { path: `/${postId}/submit`, body, }), + + modify: async ({ postId, body }: { postId: string; body: RecruitmentPost }) => + apiClient.patch({ + path: `/${postId}`, + body, + }), }; export default applyApis; diff --git a/frontend/src/components/postManagement/PostManageBoard/index.tsx b/frontend/src/components/postManagement/PostManageBoard/index.tsx new file mode 100644 index 000000000..a465aa7cf --- /dev/null +++ b/frontend/src/components/postManagement/PostManageBoard/index.tsx @@ -0,0 +1,161 @@ +import { useEffect, useRef, useState } from 'react'; +import ReactQuill from 'react-quill-new'; +import { RecruitmentInfoState } from '@customTypes/dashboard'; + +import DateInput from '@components/common/DateInput'; +import InputField from '@components/common/InputField'; + +import usePostManagement from '@hooks/usePostManagement'; +import useForm from '@hooks/utils/useForm'; +import formatDate from '@utils/formatDate'; + +import { validateTitle } from '@domain/validations/recruitment'; +import TextEditor from '@components/common/TextEditor'; +import Button from '@components/common/Button'; +import S from './style'; + +interface PostManageBoardProps { + postId: string; +} + +export default function PostManageBoard({ postId }: PostManageBoardProps) { + const wrapperRef = useRef(null); + const quillRef = useRef(null); + + const { isLoading, postState, setPostState, modifyPostMutator } = usePostManagement({ postId }); + const startDateText = postState ? formatDate(postState.startDate) : ''; + const endDateText = postState ? formatDate(postState.endDate) : ''; + const [contentText, setContentText] = useState(''); + + const { register, errors } = useForm({ initialValues: postState }); + + useEffect(() => { + setContentText(quillRef.current?.unprivilegedEditor?.getText()); + }, [quillRef]); + + useEffect(() => { + if (wrapperRef.current && !isLoading) { + wrapperRef.current.scrollTop = 0; + } + }, [isLoading]); + + if (isLoading || !postState) { + return
로딩 중입니다...
; + } + + const isModifyButtonValid = !!( + postState && + postState.startDate && + postState.endDate && + postState.title.trim() && + contentText?.trim() && + !Object.values(errors).some((error) => error) + ); + + const handleStartDate = (e: React.ChangeEvent) => { + setPostState((prev) => ({ + ...prev, + startDate: new Date(e.target.value).toISOString(), + })); + }; + + const handleEndDate = (e: React.ChangeEvent) => { + setPostState((prev) => ({ + ...prev, + endDate: new Date(e.target.value).toISOString(), + })); + }; + + const handleTitle = (e: React.ChangeEvent) => { + setPostState((prev) => ({ + ...prev, + title: e.target.value, + })); + }; + + const handlePostingContentChange = (string: string) => { + setPostState((prev) => ({ + ...prev, + postingContent: string, + })); + setContentText(quillRef.current?.unprivilegedEditor?.getText()); + }; + + const handleSubmit = (event: React.MouseEvent) => { + event.preventDefault(); + modifyPostMutator.mutate(); + }; + + return ( + + + +

모집일자

+ 정해진 날짜가 지나면 자동으로 공고가 마감되며 비활성화 처리됩니다. +
+ + + + + + + + +
+ + + +

공고 제목

+
+ +
+ + + +

상세 정보

+
+ +
+ + + + + + +
+ ); +} diff --git a/frontend/src/components/postManagement/PostManageBoard/style.ts b/frontend/src/components/postManagement/PostManageBoard/style.ts new file mode 100644 index 000000000..fae945608 --- /dev/null +++ b/frontend/src/components/postManagement/PostManageBoard/style.ts @@ -0,0 +1,83 @@ +import styled from '@emotion/styled'; + +const Wrapper = styled.div` + width: 100%; + height: 100%; + padding: 2rem 6rem; + + display: flex; + flex-direction: column; + gap: 4rem; + + overflow-y: auto; +`; + +const Section = styled.div` + display: flex; + flex-direction: column; + gap: 1.6rem; +`; + +const SectionTitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; + ${({ theme }) => theme.typography.common.default}; + + h2 { + ${({ theme }) => theme.typography.heading[500]}; + } + + span { + color: ${({ theme }) => theme.baseColors.grayscale[800]}; + } +`; + +const DatePickerContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + + max-width: 50rem; +`; + +const DatePickerBox = styled.div` + width: 22rem; +`; + +const RecruitTitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; +`; + +const RecruitDetailContainer = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; + height: 44rem; + margin-bottom: 5.2rem; +`; + +const ModifyButtonContainer = styled.div` + width: 100%; + max-width: 30rem; + height: 5.2rem; + margin: 2.4rem auto; +`; + +const S = { + Wrapper, + Section, + SectionTitleContainer, + + DatePickerContainer, + DatePickerBox, + + RecruitTitleContainer, + RecruitDetailContainer, + + ModifyButtonContainer, +}; + +export default S; diff --git a/frontend/src/constants/constants.ts b/frontend/src/constants/constants.ts index 42b129287..2018fec29 100644 --- a/frontend/src/constants/constants.ts +++ b/frontend/src/constants/constants.ts @@ -8,6 +8,7 @@ export const DASHBOARD_TAB_MENUS: Record = { applicant: '지원자 관리', rejected: '불합격자 관리', process: '모집 과정 관리', + post: '공고 관리', apply: '지원서 관리', } as const; diff --git a/frontend/src/hooks/useApplyManagement/index.tsx b/frontend/src/hooks/useApplyManagement/index.tsx index 46da5bb84..78b2fd833 100644 --- a/frontend/src/hooks/useApplyManagement/index.tsx +++ b/frontend/src/hooks/useApplyManagement/index.tsx @@ -81,7 +81,7 @@ export default function useApplyManagement({ postId }: UseApplyManagementProps): toast.success('지원서의 사전 질문 항목 수정에 성공했습니다.'); }, onError: () => { - toast.success('지원서의 사전 질문 항목 수정에 실패했습니다.'); + toast.error('지원서의 사전 질문 항목 수정에 실패했습니다.'); }, }); diff --git a/frontend/src/hooks/usePostManagement/index.ts b/frontend/src/hooks/usePostManagement/index.ts new file mode 100644 index 000000000..f538b1467 --- /dev/null +++ b/frontend/src/hooks/usePostManagement/index.ts @@ -0,0 +1,51 @@ +import { useEffect, useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { RecruitmentInfoState } from '@customTypes/dashboard'; +import { applyQueries } from '@hooks/apply'; +import applyApis from '@api/domain/apply/apply'; +import QUERY_KEYS from '@hooks/queryKeys'; +import { RecruitmentPost } from '@customTypes/apply'; +import { useToast } from '@contexts/ToastContext'; + +interface usePostManagementProps { + postId: string; +} + +const INITIAL_POST_INFO: RecruitmentInfoState = { + title: '', + startDate: '', + endDate: '', + postingContent: '', +}; + +export default function usePostManagement({ postId }: usePostManagementProps) { + const { data: postInfo, isLoading } = applyQueries.useGetRecruitmentPost({ postId }); + const [postState, setPostState] = useState(INITIAL_POST_INFO); + const toast = useToast(); + const queryClient = useQueryClient(); + + useEffect(() => { + if (!isLoading && postInfo && Object.keys(postInfo).length > 0) { + setPostState(postInfo); + } + }, [isLoading, postInfo]); + + const modifyPostMutator = useMutation({ + mutationFn: () => applyApis.modify({ postId, body: postState as RecruitmentPost }), + onSuccess: async () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.RECRUITMENT_INFO, postId] }); + toast.success('공고의 내용 수정에 성공했습니다.'); + }, + onError: () => { + toast.error('공고의 내용 수정에 실패했습니다.'); + }, + }); + + return { + isLoading, + postState, + setPostState, + modifyPostMutator, + }; +} diff --git a/frontend/src/mocks/handlers/applyHandlers.ts b/frontend/src/mocks/handlers/applyHandlers.ts index ea76ecc8e..c36d71db5 100644 --- a/frontend/src/mocks/handlers/applyHandlers.ts +++ b/frontend/src/mocks/handlers/applyHandlers.ts @@ -25,6 +25,17 @@ const applyHandlers = [ return Success(); }), + + http.patch(`${APPLY}/:postId`, async ({ request }) => { + const url = new URL(request.url); + const postId = url.pathname.split('/').pop(); + + if (!postId) { + return NotFoundError(); + } + + return Success(); + }), ]; export default applyHandlers; diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index 8a155b2ce..22e6361fb 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -5,6 +5,7 @@ import Tab from '@components/common/Tab'; import ProcessBoard from '@components/dashboard/ProcessBoard'; import ApplyManagement from '@components/applyManagement'; import ProcessManageBoard from '@components/processManagement/ProcessManageBoard'; +import PostManageBoard from '@components/postManagement/PostManageBoard'; import OpenInNewTab from '@components/common/OpenInNewTab'; import CopyToClipboard from '@components/common/CopyToClipboard'; @@ -17,7 +18,7 @@ import { SpecificProcessIdProvider } from '@contexts/SpecificProcessIdContext'; import S from './style'; -export type DashboardTabItems = '지원자 관리' | '모집 과정 관리' | '불합격자 관리' | '지원서 관리'; +export type DashboardTabItems = '지원자 관리' | '모집 과정 관리' | '불합격자 관리' | '공고 관리' | '지원서 관리'; export default function Dashboard() { const { dashboardId, postId } = useParams() as { dashboardId: string; postId: string }; @@ -56,8 +57,8 @@ export default function Dashboard() { ))} - {/* TODO: [08.21-lurgi] 현재 모달이 여러개를 컨트롤 할 수 없는 관계로 새로 렌더링 합니다. - 추후에 Modal에 id값을 부여하여 여러개의 모달을 컨트롤 할 수 있게 변경해야합니다. + {/* TODO: [08.21-lurgi] 현재 모달이 여러개를 컨트롤 할 수 없는 관계로 새로 렌더링 합니다. + 추후에 Modal에 id값을 부여하여 여러개의 모달을 컨트롤 할 수 있게 변경해야합니다. 파일 맨 첫줄 주석도 삭제해야합니다. */} {currentMenu === '지원자 관리' && ( @@ -90,6 +91,10 @@ export default function Dashboard() { /> + + + +