Skip to content

Commit

Permalink
fix-fe: 공고, 지원서 페이지 QA (#385)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kim Da Eun <[email protected]>
  • Loading branch information
2 people authored and seongjinme committed Aug 23, 2024
1 parent 45ad220 commit a05ce58
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 43 deletions.
5 changes: 5 additions & 0 deletions frontend/src/components/common/Tab/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ const TabButton = styled.button<{ isActive: boolean }>`
&:hover {
color: ${({ theme }) => theme.baseColors.grayscale[950]};
}
&:disabled {
color: ${({ theme }) => theme.baseColors.grayscale[500]};
cursor: not-allowed;
}
`;

const TabPanel = styled.div<{ isVisible: boolean }>`
Expand Down
51 changes: 34 additions & 17 deletions frontend/src/components/recruitmentPost/ApplyForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,58 @@ import { useAnswers } from './useAnswers';

interface ApplyFormProps {
questions: Question[];
isClosed: boolean;
}

export default function ApplyForm({ questions }: ApplyFormProps) {
export default function ApplyForm({ questions, isClosed }: ApplyFormProps) {
const { postId } = useParams<{ postId: string }>() as { postId: string };

const { data: recruitmentPost } = applyQueries.useGetRecruitmentPost({ postId: postId ?? '' });
const { mutate: apply } = applyMutations.useApply(postId, recruitmentPost?.title ?? '');

const { formData: applicant, register } = useForm<ApplicantData>({
const {
formData: applicant,
register,
errors,
} = useForm<ApplicantData>({
initialValues: { name: '', email: '', phone: '' },
});

const { answers, changeHandler } = useAnswers(questions);
const [personalDataCollection, setPersonalDataCollection] = useState(false);

const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();

const applyData: ApplyRequestBody = {
applicant: {
...applicant,
},
answers: Object.entries(answers).map(([questionId, answer]) => ({
questionId,
replies: [...answer],
})),
personalDataCollection,
};

if (Object.values(answers).some((answer) => answer.length === 0)) {
if (!window.confirm('작성하지 않은 질문이 있습니다. 제출하시겠습니까?')) {
return;
}
}

if (Object.values(errors).some((error) => error)) {
window.alert('지원자 정보를 확인해주세요.');
return;
}

if (!personalDataCollection) {
window.alert('개인정보 수집 및 이용 동의에 체크해주세요.');
return;
}

apply({ body: applyData });
apply({
body: {
applicant: {
...applicant,
phone: applicant.phone.replace(/-/g, ''),
},
answers: Object.entries(answers).map(([questionId, answer]) => ({
questionId,
replies: [...answer],
})),
personalDataCollection,
} as ApplyRequestBody,
});
};

const handlePersonalDataCollection = (checked: boolean) => {
Expand All @@ -67,10 +80,11 @@ export default function ApplyForm({ questions }: ApplyFormProps) {
<C.ContentContainer>
<S.Form onSubmit={handleSubmit}>
<InputField
{...register('name', { validate: { onBlur: validateName.onBlur } })}
{...register('name', { validate: { onBlur: validateName.onBlur, onChange: validateName.onChange } })}
name="name"
label="이름"
placeholder="이름을 입력해 주세요."
maxLength={32}
required
/>
<InputField
Expand All @@ -83,6 +97,7 @@ export default function ApplyForm({ questions }: ApplyFormProps) {
{...register('phone', {
validate: {
onBlur: validatePhoneNumber.onBlur,
onChange: validatePhoneNumber.onChange,
},
formatter: formatPhoneNumber,
})}
Expand All @@ -103,13 +118,14 @@ export default function ApplyForm({ questions }: ApplyFormProps) {
))}

<S.Divider />
{/* TODO: CheckBoxField를 만들어 보기 */}
<S.CheckBoxContainer>
<S.CheckBoxOption>
<CheckBox
isChecked={personalDataCollection}
onToggle={handlePersonalDataCollection}
/>
<S.CheckBoxLabel>개인정보 수집 및 이용 동의</S.CheckBoxLabel>
<S.CheckBoxLabel required>개인정보 수집 및 이용 동의</S.CheckBoxLabel>
</S.CheckBoxOption>

<S.PersonalDataCollectionDescription>
Expand All @@ -122,8 +138,9 @@ export default function ApplyForm({ questions }: ApplyFormProps) {
type="submit"
color="primary"
size="fillContainer"
disabled={isClosed}
>
지원하기
제출하기
</Button>
</C.ButtonContainer>
</S.Form>
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/recruitmentPost/ApplyForm/style.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';

const Form = styled.form`
Expand All @@ -22,11 +23,22 @@ const CheckBoxOption = styled.div`
gap: 1.2rem;
`;

const CheckBoxLabel = styled.label`
const CheckBoxLabel = styled.label<{ required: boolean }>`
${({ theme }) => theme.typography.common.large}
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
${({ required, theme }) =>
required &&
css`
&::after {
content: '*';
color: ${theme.colors.feedback.error};
font-size: ${theme.typography.heading[500]};
margin-left: 0.4rem;
}
`}
`;

const PersonalDataCollectionDescription = styled.p`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default function CustomQuestion({ question, value = [], onChange = () =>
onChange={handleChange}
label={label}
name={questionId}
maxLength={50}
/>
);
}
Expand All @@ -42,6 +43,7 @@ export default function CustomQuestion({ question, value = [], onChange = () =>
onChange={handleChange}
resize={false}
style={{ height: 'calc(2.4rem * 10 + 1.2rem)' }}
maxLength={5000}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import Button from '@components/common/Button';
import TextEditor from '@components/common/TextEditor';
import { applyQueries } from '@hooks/apply';
import { useParams } from 'react-router-dom';
import { RecruitmentPost } from '@customTypes/apply';
import C from '../style';

interface RecruitmentPostDetailProps {
recruitmentPost?: RecruitmentPost;
isClosed: boolean;
moveTab: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

export default function RecruitmentPostDetail({ moveTab }: RecruitmentPostDetailProps) {
// TODO: smell
const { postId } = useParams<{ postId: string }>();
const { data: recruitmentPost, isClosed } = applyQueries.useGetRecruitmentPost({ postId: postId ?? '' });

export default function RecruitmentPostDetail({ recruitmentPost, isClosed, moveTab }: RecruitmentPostDetailProps) {
return (
<C.ContentContainer>
<TextEditor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function RecruitmentPostTab() {

const { postId } = useParams<{ postId: string }>() as { postId: string };
const { data: questions } = applyQueries.useGetApplyForm({ postId: postId ?? '' });
const { data: recruitmentPost, isClosed } = applyQueries.useGetRecruitmentPost({ postId: postId ?? '' });

return (
<>
Expand All @@ -26,15 +27,23 @@ export default function RecruitmentPostTab() {
name={label}
isActive={currentMenu === label}
handleClickTabItem={moveTab}
disabled={label === '지원하기' && isClosed}
/>
))}
</Tab>

<Tab.TabPanel isVisible={currentMenu === '모집 공고'}>
<RecruitmentPostDetail moveTab={moveTab} />
<RecruitmentPostDetail
recruitmentPost={recruitmentPost}
isClosed={isClosed}
moveTab={moveTab}
/>
</Tab.TabPanel>
<Tab.TabPanel isVisible={currentMenu === '지원하기'}>
<ApplyForm questions={questions ?? ([] as Question[])} />
<ApplyForm
isClosed={isClosed}
questions={questions ?? ([] as Question[])}
/>
</Tab.TabPanel>
</>
);
Expand Down
39 changes: 27 additions & 12 deletions frontend/src/domain/validations/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,40 @@ import { Branded } from '@customTypes/utilTypes';
import ValidationError from '@utils/errors/ValidationError';
import { isEmptyString, isNumber } from './common';

type EmailAddress = Branded<string, 'EmailAddress'>;
const isValidEmail = (email: string): email is EmailAddress => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
const regex = {
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
phone: /^\d{3}-\d{3,4}-\d{4}$/,
name: /[^ㄱ-ㅎ가-힣a-zA-Z\s-]/,
};

type EmailAddress = Branded<string, 'EmailAddress'>;
const isValidEmail = (email: string): email is EmailAddress => regex.email.test(email);

type PhoneNumber = Branded<string, 'PhoneNumber'>;
const isValidPhoneNumber = (phone: string): phone is PhoneNumber => {
const phoneRegex = /^\d{3}-\d{3,4}-\d{4}$/;
return phoneRegex.test(phone);
};
const isValidPhoneNumber = (phone: string): phone is PhoneNumber => regex.phone.test(phone);

export const validateName = {
onBlur: (name: string) => {
if (regex.name.test(name)) {
throw new ValidationError({ inputName: 'name', message: '한글, 영문, 공백, - 만 입력해 주세요.' });
}

if (isEmptyString(name)) {
throw new ValidationError({ inputName: 'name', message: '이름을 입력해주세요.' });
throw new ValidationError({ inputName: 'name', message: '이름을 입력해 주세요.' });
}
},

onChange: (name: string) => {
if (regex.name.test(name)) {
throw new ValidationError({ inputName: 'name', message: '한글, 영문, 공백, - 만 입력해 주세요.' });
}
},
};

export const validateEmail = {
onBlur: (email: string) => {
if (isEmptyString(email)) {
throw new ValidationError({ inputName: 'email', message: '이메일을 입력해주세요.' });
throw new ValidationError({ inputName: 'email', message: '이메일을 입력해 주세요.' });
}

if (!isValidEmail(email)) {
Expand All @@ -37,16 +47,21 @@ export const validateEmail = {
export const validatePhoneNumber = {
onBlur: (phone: string) => {
if (isEmptyString(phone)) {
throw new ValidationError({ inputName: 'phone', message: '번호를 입력해주세요.' });
throw new ValidationError({ inputName: 'phone', message: '전화번호를 입력해 주세요.' });
}

if (!isNumber(phone.replace(/-/g, ''))) {
throw new ValidationError({ inputName: 'phone', message: '숫자만 입력해 주세요.' });
}

if (!isValidPhoneNumber(phone)) {
throw new ValidationError({ inputName: 'phone', message: '전화번호를 확인해 주세요.' });
}
},

onChange: (phone: string) => {
if (!isNumber(phone.replace(/-/g, ''))) {
throw new ValidationError({ inputName: 'phone', message: '숫자만 입력해주세요.' });
throw new ValidationError({ inputName: 'phone', message: '숫자만 입력해 주세요.' });
}
},
};
9 changes: 7 additions & 2 deletions frontend/src/hooks/apply.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import applyApis from '@api/apply';
import { useMutation, useQuery } from '@tanstack/react-query';
import { ApplyRequestBody } from '@customTypes/apply';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import QUERY_KEYS from './queryKeys';

Expand All @@ -18,7 +18,12 @@ export const applyQueries = {
useGetRecruitmentPost: ({ postId }: { postId: string }) => {
const { data, ...restQueryObj } = useGetRecruitmentInfo({ postId });

const isClosed = data ? data.recruitmentPost.endDate < new Date().toISOString() : true;
const { startDate, endDate } = data?.recruitmentPost ?? { startDate: '', endDate: '' };
const start = new Date(startDate as string).getTime();
const end = new Date(endDate as string).getTime();
const now = new Date().getTime();

const isClosed = !data || end < now || start > now;

return {
isClosed,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/utils/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,5 @@ export default function useForm<TFieldData>({ initialValues }: UseFormProps<TFie
};
};

return { register, formData, handleSubmit, validateAndSetErrors };
return { register, formData, handleSubmit, validateAndSetErrors, errors };
}
2 changes: 1 addition & 1 deletion frontend/src/mocks/applyForm.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"title": "🪐 우아한테크코스 2024 신입생 모집",
"postingContent": "<h1>🗓️ 2024 신입생 (6기) 선발 일정</h1><br/><ol><li><b>서류접수: </b>2023년 10월 6일(금) 오후 3시 ~ 10월 16일(월) 오전 10시</li><li><b>프리코스: </b>2023년 10월 19일(목) ~ 11월 15일(수)</li><li><b>1차 합격자 발표: </b>2023년 12월 11일(월) 오후 3시, 개별 E-mail 통보</li><li><b>최종 코딩 테스트: </b>2023년 12월 16일(토)</li><li><b>최종 합격자 발표: </b>2023년 12월 27일(수) 오후 3시, 개별 E-mail 통보</li></ol>",
"startDate": "2024-08-07T00:00",
"startDate": "2024-08-12T16:00",
"endDate": "2024-10-31T23:59",
"questions": [
{
Expand Down

0 comments on commit a05ce58

Please sign in to comment.