Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat-fe: 대시보드 내 공고 수정 컴포넌트 및 기능 구현 #609

Merged
merged 12 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/src/api/domain/apply/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
161 changes: 161 additions & 0 deletions frontend/src/components/postManagement/PostManageBoard/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null);
const quillRef = useRef<ReactQuill | null>(null);

const { isLoading, postState, setPostState, modifyPostMutator } = usePostManagement({ postId });
const startDateText = postState ? formatDate(postState.startDate) : '';
const endDateText = postState ? formatDate(postState.endDate) : '';
const [contentText, setContentText] = useState<string | undefined>('');

const { register, errors } = useForm<RecruitmentInfoState>({ initialValues: postState });

useEffect(() => {
setContentText(quillRef.current?.unprivilegedEditor?.getText());
}, [quillRef]);

useEffect(() => {
if (wrapperRef.current && !isLoading) {
wrapperRef.current.scrollTop = 0;
}
}, [isLoading]);

if (isLoading || !postState) {
return <div>로딩 중입니다...</div>;
}

const isModifyButtonValid = !!(
postState &&
postState.startDate &&
postState.endDate &&
postState.title.trim() &&
contentText?.trim() &&
!Object.values(errors).some((error) => error)
);

const handleStartDate = (e: React.ChangeEvent<HTMLInputElement>) => {
setPostState((prev) => ({
...prev,
startDate: new Date(e.target.value).toISOString(),
}));
};

const handleEndDate = (e: React.ChangeEvent<HTMLInputElement>) => {
setPostState((prev) => ({
...prev,
endDate: new Date(e.target.value).toISOString(),
}));
};

const handleTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLButtonElement>) => {
event.preventDefault();
modifyPostMutator.mutate();
};

return (
<S.Wrapper ref={wrapperRef}>
<S.Section>
<S.SectionTitleContainer>
<h2>모집일자</h2>
<span>정해진 날짜가 지나면 자동으로 공고가 마감되며 비활성화 처리됩니다.</span>
</S.SectionTitleContainer>
<S.DatePickerContainer>
<S.DatePickerBox>
<DateInput
width="22rem"
label="시작일"
min={postState.startDate.split('T')[0]}
max={postState.endDate.split('T')[0]}
innerText={startDateText}
onChange={handleStartDate}
/>
</S.DatePickerBox>
<S.DatePickerBox>
<DateInput
width="22rem"
label="종료일"
min={postState.startDate.split('T')[0]}
disabled={!startDateText}
innerText={endDateText}
onChange={handleEndDate}
/>
</S.DatePickerBox>
</S.DatePickerContainer>
</S.Section>

<S.RecruitTitleContainer>
<S.SectionTitleContainer>
<h2>공고 제목</h2>
</S.SectionTitleContainer>
<InputField
{...register('title', {
validate: { onBlur: validateTitle, onChange: validateTitle },
placeholder: '공고 제목을 입력해 주세요',
maxLength: 32,
onChange: handleTitle,
})}
value={postState.title}
/>
</S.RecruitTitleContainer>

<S.RecruitDetailContainer>
<S.SectionTitleContainer>
<h2>상세 정보</h2>
</S.SectionTitleContainer>
<TextEditor
quillRef={quillRef}
value={postState.postingContent}
onChange={handlePostingContentChange}
/>
</S.RecruitDetailContainer>

<S.Section>
<S.ModifyButtonContainer>
<Button
type="button"
color="primary"
size="fillContainer"
disabled={!isModifyButtonValid}
onClick={handleSubmit}
>
수정하기
</Button>
</S.ModifyButtonContainer>
</S.Section>
</S.Wrapper>
);
}
83 changes: 83 additions & 0 deletions frontend/src/components/postManagement/PostManageBoard/style.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions frontend/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const DASHBOARD_TAB_MENUS: Record<string, DashboardTabItems> = {
applicant: '지원자 관리',
rejected: '불합격자 관리',
process: '모집 과정 관리',
post: '공고 관리',
apply: '지원서 관리',
} as const;

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/useApplyManagement/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default function useApplyManagement({ postId }: UseApplyManagementProps):
toast.success('지원서의 사전 질문 항목 수정에 성공했습니다.');
},
onError: () => {
toast.success('지원서의 사전 질문 항목 수정에 실패했습니다.');
toast.error('지원서의 사전 질문 항목 수정에 실패했습니다.');
},
});

Expand Down
51 changes: 51 additions & 0 deletions frontend/src/hooks/usePostManagement/index.ts
Original file line number Diff line number Diff line change
@@ -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<RecruitmentInfoState>(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,
};
}
11 changes: 11 additions & 0 deletions frontend/src/mocks/handlers/applyHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
11 changes: 8 additions & 3 deletions frontend/src/pages/Dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 };
Expand Down Expand Up @@ -56,8 +57,8 @@ export default function Dashboard() {
))}
</Tab>

{/* TODO: [08.21-lurgi] 현재 모달이 여러개를 컨트롤 할 수 없는 관계로 새로 렌더링 합니다.
추후에 Modal에 id값을 부여하여 여러개의 모달을 컨트롤 할 수 있게 변경해야합니다.
{/* TODO: [08.21-lurgi] 현재 모달이 여러개를 컨트롤 할 수 없는 관계로 새로 렌더링 합니다.
추후에 Modal에 id값을 부여하여 여러개의 모달을 컨트롤 할 수 있게 변경해야합니다.
파일 맨 첫줄 주석도 삭제해야합니다. */}
{currentMenu === '지원자 관리' && (
<Tab.TabPanel isVisible={currentMenu === '지원자 관리'}>
Expand Down Expand Up @@ -90,6 +91,10 @@ export default function Dashboard() {
/>
</Tab.TabPanel>

<Tab.TabPanel isVisible={currentMenu === '공고 관리'}>
<PostManageBoard postId={postId} />
</Tab.TabPanel>

<Tab.TabPanel isVisible={currentMenu === '지원서 관리'}>
<ApplyManagement isVisible={currentMenu === '지원서 관리'} />
</Tab.TabPanel>
Expand Down
Loading