diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 819aefe9..b213104e 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -54,7 +54,16 @@ "no-confusing-arrow": 0, "indent": 0, "consistent-return": "off", - "function-paren-newline": "off" + "function-paren-newline": "off", + "@typescript-eslint/no-use-before-define": [ + "error", + { + "functions": false, + "classes": true, + "variables": true + } + ], + "no-use-before-define": "off" }, "overrides": [ { diff --git a/frontend/src/components/recruitmentPost/ApplyForm/index.tsx b/frontend/src/components/recruitmentPost/ApplyForm/index.tsx index cbce35d7..7295eecb 100644 --- a/frontend/src/components/recruitmentPost/ApplyForm/index.tsx +++ b/frontend/src/components/recruitmentPost/ApplyForm/index.tsx @@ -1,7 +1,8 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import Button from '@components/_common/atoms/Button'; import InputField from '@components/_common/molecules/InputField'; import CustomQuestion from '@components/recruitmentPost/CustomQuestion'; -import { FormEventHandler, useState } from 'react'; +import { FormEventHandler, useEffect, useState } from 'react'; import { validateEmail, validateName, validatePhoneNumber } from '@domain/validations/apply'; @@ -32,6 +33,7 @@ export default function ApplyForm({ questions, isClosed }: ApplyFormProps) { const { formData: applicant, register, + errors, hasErrors, } = useForm({ initialValues, @@ -81,69 +83,30 @@ export default function ApplyForm({ questions, isClosed }: ApplyFormProps) { setPersonalDataCollection(checked); }; - const { onChange: onNameChange, ...nameRegister } = register('name', { - validate: { onBlur: validateName.onBlur, onChange: validateName.onChange }, - }); - - const { onChange: onEmailChange, ...emailRegister } = register('email', { - validate: { onBlur: validateEmail.onBlur }, - }); - - const { onChange: onPhoneChange, ...phoneRegister } = register('phone', { - validate: { - onBlur: validatePhoneNumber.onBlur, - onChange: validatePhoneNumber.onChange, - }, - }); - return ( - - { - onNameChange(e); - baseInfoHandlers.handleName(e); - }} - /> - - - - { - onEmailChange(e); - baseInfoHandlers.handleEmail(e); - }} - /> - - - - { - onPhoneChange(e); - baseInfoHandlers.handlePhone(e); - }} - /> - + + + {questions.map((question, index) => ( ); } + +interface BaseInputProps { + questionCount: number; + register: any; + baseInfoHandlers: { + handleName: (value: string) => void; + handleEmail: (value: string) => void; + handlePhone: (value: string) => void; + }; +} + +interface NameInputProps extends BaseInputProps { + value: string; + error: string; +} + +interface EmailInputProps extends BaseInputProps { + value: string; + error: string; +} + +interface PhoneInputProps extends BaseInputProps { + value: string; + error: string; +} + +function NameInput({ questionCount, register, value, error, baseInfoHandlers }: NameInputProps) { + const nameRegister = register('name', { + validate: { onBlur: validateName.onBlur, onChange: validateName.onChange }, + }); + + useEffect(() => { + if (error) baseInfoHandlers.handleName(''); + else baseInfoHandlers.handleName(value); + }, [error, value]); + + return ( + + + + ); +} + +function EmailInput({ questionCount, register, value, error, baseInfoHandlers }: EmailInputProps) { + const emailRegister = register('email', { + validate: { onBlur: validateEmail.onBlur }, + }); + + useEffect(() => { + if (error) baseInfoHandlers.handleEmail(''); + else baseInfoHandlers.handleEmail(value); + }, [error, value]); + + return ( + + + + ); +} + +function PhoneInput({ questionCount, register, value, error, baseInfoHandlers }: PhoneInputProps) { + const phoneRegister = register('phone', { + validate: { + onBlur: validatePhoneNumber.onBlur, + onChange: validatePhoneNumber.onChange, + }, + }); + + useEffect(() => { + if (error) baseInfoHandlers.handlePhone(''); + else baseInfoHandlers.handlePhone(value); + }, [error, value]); + + return ( + + + + ); +} diff --git a/frontend/src/components/recruitmentPost/ApplyForm/useAnswers.ts b/frontend/src/components/recruitmentPost/ApplyForm/useAnswers.ts index e87ab603..9b12802a 100644 --- a/frontend/src/components/recruitmentPost/ApplyForm/useAnswers.ts +++ b/frontend/src/components/recruitmentPost/ApplyForm/useAnswers.ts @@ -1,29 +1,11 @@ import { Question } from '@customTypes/apply'; import useLocalStorageState from '@hooks/useLocalStorageState'; -import { useState } from 'react'; interface AnswerFormData { [key: string]: string[]; } -export const useAnswers = (questions: Question[], applyFormId: string) => { - const LOCALSTORAGE_KEY = `${applyFormId}-apply-form`; - - const [enableStorage] = useState(() => { - const prevSavedAnswer = window.localStorage.getItem(LOCALSTORAGE_KEY); - if (!prevSavedAnswer) return false; - - const prevSavedAnswerIds = Object.keys(JSON.parse(prevSavedAnswer)); - if (prevSavedAnswerIds.every((id) => questions.some(({ questionId }) => questionId === id))) { - return window.confirm('이전 작성중인 지원서가 있습니다. 이어서 진행하시겠습니까?'); - } - return false; - }); - - const resetAnswerStorage = () => { - window.localStorage.removeItem(LOCALSTORAGE_KEY); - }; - +export const useAnswers = (questions: Question[], LOCALSTORAGE_KEY: string, enableStorage: boolean) => { const [answers, setAnswers] = useLocalStorageState( (() => questions.reduce((acc, question) => ({ ...acc, [question.questionId]: [] }), {} as AnswerFormData))(), { @@ -62,6 +44,5 @@ export const useAnswers = (questions: Question[], applyFormId: string) => { SINGLE_CHOICE: handleRadio, }, isRequiredFieldsIncomplete, - resetAnswerStorage, }; }; diff --git a/frontend/src/components/recruitmentPost/RecruitmentPostTab/index.tsx b/frontend/src/components/recruitmentPost/RecruitmentPostTab/index.tsx index c2bcb625..4af51f8c 100644 --- a/frontend/src/components/recruitmentPost/RecruitmentPostTab/index.tsx +++ b/frontend/src/components/recruitmentPost/RecruitmentPostTab/index.tsx @@ -12,7 +12,7 @@ import ApplyForm from '../ApplyForm'; export type RecruitmentPostTabItems = '모집 공고' | '지원하기'; export default function RecruitmentPostTab() { - const { currentMenu, moveTab, moveTabByParam } = useTab({ defaultValue: '모집 공고' }); + const { currentMenu, moveTab } = useTab({ defaultValue: '모집 공고' }); const { applyFormId } = useParams<{ applyFormId: string }>() as { applyFormId: string }; const { data: questions } = applyQueries.useGetApplyForm({ applyFormId: applyFormId ?? '' }); @@ -47,7 +47,6 @@ export default function RecruitmentPostTab() { ; - handleEmail: ChangeEventHandler; - handlePhone: ChangeEventHandler; + handleName: (value: string) => void; + handleEmail: (value: string) => void; + handlePhone: (value: string) => void; }; resetStorage: () => void; answers: { @@ -48,63 +39,83 @@ const ApplyAnswerContext = createContext(null); interface ApplyAnswerContextProps extends PropsWithChildren { questions: Question[]; applyFormId: string; - moveTabByParam: (value: RecruitmentPostTabItems) => void; } const ExecutionTracker = createExecutionTracker(); -export function ApplyAnswerProvider({ questions, applyFormId, moveTabByParam, children }: ApplyAnswerContextProps) { - const LOCALSTORAGE_KEY = `${applyFormId}-initial-values`; +export function ApplyAnswerProvider({ questions, applyFormId, children }: ApplyAnswerContextProps) { + const LOCALSTORAGE_BASE_INFO_KEY = `${applyFormId}-base-info`; + const LOCALSTORAGE_ANSWER_KEY = `${applyFormId}-apply-form`; const [enableStorage] = useState(() => { - if (!ExecutionTracker.executeIfFirst()) return true; - if (window.localStorage.getItem(LOCALSTORAGE_KEY)) { + if (ExecutionTracker.getHasExecuted()) return true; + + const prevBaseInfo = window.localStorage.getItem(LOCALSTORAGE_BASE_INFO_KEY); + const prevAnswer = window.localStorage.getItem(LOCALSTORAGE_ANSWER_KEY); + if (prevBaseInfo || prevAnswer) { + if (prevBaseInfo && !isValidBaseInfo(prevBaseInfo)) { + return false; + } + + if (prevAnswer && !isValidAnswers(prevAnswer, questions)) { + return false; + } + if (window.confirm('이전 작성중인 지원서가 있습니다. 이어서 진행하시겠습니까?')) { - moveTabByParam('지원하기'); return true; } } return false; }); + useEffect(() => { + if (!ExecutionTracker.getHasExecuted()) { + ExecutionTracker.executet(); + } + }, []); + const [initialValues, setInitialValues] = useLocalStorageState( { name: '', email: '', phone: '' }, { - key: LOCALSTORAGE_KEY, + key: LOCALSTORAGE_BASE_INFO_KEY, enableStorage, }, ); const baseInfoHandlers = useMemo( () => ({ - handleName: (e: React.ChangeEvent) => { + handleName: (value: string) => { setInitialValues((prev) => ({ ...prev, - name: e.target.value, + name: value, })); }, - handleEmail: (e: React.ChangeEvent) => { + handleEmail: (value: string) => { setInitialValues((prev) => ({ ...prev, - email: e.target.value, + email: value, })); }, - handlePhone: (e: React.ChangeEvent) => { + handlePhone: (value: string) => { setInitialValues((prev) => ({ ...prev, - phone: e.target.value, + phone: value, })); }, }), [setInitialValues], ); - const { answers, changeHandler, isRequiredFieldsIncomplete, resetAnswerStorage } = useAnswers(questions, applyFormId); + const { answers, changeHandler, isRequiredFieldsIncomplete } = useAnswers( + questions, + LOCALSTORAGE_ANSWER_KEY, + enableStorage, + ); const resetStorage = useCallback(() => { - window.localStorage.removeItem(LOCALSTORAGE_KEY); - resetAnswerStorage(); - }, [LOCALSTORAGE_KEY, resetAnswerStorage]); + window.localStorage.removeItem(LOCALSTORAGE_BASE_INFO_KEY); + window.localStorage.removeItem(LOCALSTORAGE_ANSWER_KEY); + }, [LOCALSTORAGE_BASE_INFO_KEY, LOCALSTORAGE_ANSWER_KEY]); const valueObj = useMemo( () => ({ @@ -128,3 +139,23 @@ export const useApplyAnswer = () => { } return context; }; + +function isValidBaseInfo(prevSavedAnswer: string) { + const prevSavedAnswerKeys = Object.keys(JSON.parse(prevSavedAnswer)); + const prevSavedAnswerValues = Object.values(JSON.parse(prevSavedAnswer)); + + return ( + prevSavedAnswerKeys.every((key) => ['name', 'email', 'phone'].includes(key)) && + prevSavedAnswerValues.some((value) => value !== '') + ); +} + +function isValidAnswers(prevSavedAnswer: string, questions: Question[]) { + const prevSavedAnswerValues = Object.values(JSON.parse(prevSavedAnswer)); + const prevSavedAnswerKeys = Object.keys(JSON.parse(prevSavedAnswer)); + + return ( + prevSavedAnswerKeys.every((key) => questions.some(({ questionId }) => questionId === key)) && + prevSavedAnswerValues.some((value) => value !== '') + ); +} diff --git a/frontend/src/utils/createExecutionTracker.ts b/frontend/src/utils/createExecutionTracker.ts index 8c7e50c0..61d7f4bd 100644 --- a/frontend/src/utils/createExecutionTracker.ts +++ b/frontend/src/utils/createExecutionTracker.ts @@ -2,16 +2,9 @@ export function createExecutionTracker() { let hasExecuted = false; return { - /** - * 초기 실행 여부를 확인하고, 상태를 갱신합니다. - * - 처음 호출 시 true를 반환하고, 이후 호출 시 false를 반환합니다. - */ - executeIfFirst: () => { - if (!hasExecuted) { - hasExecuted = true; - return true; - } - return false; + executet: () => { + hasExecuted = true; }, + getHasExecuted: () => hasExecuted, }; }