diff --git a/frontend/src/components/dashboard/DashboardCreate/Recruitment/index.tsx b/frontend/src/components/dashboard/DashboardCreate/Recruitment/index.tsx index da7defc86..d4b40d7a6 100644 --- a/frontend/src/components/dashboard/DashboardCreate/Recruitment/index.tsx +++ b/frontend/src/components/dashboard/DashboardCreate/Recruitment/index.tsx @@ -29,8 +29,8 @@ export default function Recruitment({ recruitmentInfoState, setRecruitmentInfoSt const startDateText = startDate ? formatDate(startDate) : ''; const endDateText = endDate ? formatDate(endDate) : ''; - const { register, errors } = useForm({ - initialValues: { title: '', startDate: '', endDate: '', postingContent: '' }, + const { register, errors } = useForm>({ + initialValues: { title: recruitmentInfoState.title }, }); useEffect(() => { diff --git a/frontend/src/components/recruitmentPost/ApplyForm/index.tsx b/frontend/src/components/recruitmentPost/ApplyForm/index.tsx index 70400a708..1e44a35a4 100644 --- a/frontend/src/components/recruitmentPost/ApplyForm/index.tsx +++ b/frontend/src/components/recruitmentPost/ApplyForm/index.tsx @@ -27,6 +27,7 @@ export default function ApplyForm({ questions, isClosed }: ApplyFormProps) { const { data: recruitmentPost } = applyQueries.useGetRecruitmentPost({ applyFormId: applyFormId ?? '' }); const { mutate: apply } = applyMutations.useApply(applyFormId, recruitmentPost?.title ?? ''); + // TODO: useForm은 input으로 initialValues제공해야 한다. 따라서 SideEffect를 피하기 위해선 useForm외부에서 localStorage를 별도로 저장해야 한다. const { formData: applicant, register, @@ -34,6 +35,7 @@ export default function ApplyForm({ questions, isClosed }: ApplyFormProps) { } = useForm({ initialValues: { name: '', email: '', phone: '' }, }); + const { answers, changeHandler, isRequiredFieldsIncomplete } = useAnswers(questions); const [personalDataCollection, setPersonalDataCollection] = useState(false); diff --git a/frontend/src/hooks/useDashboardCreateForm/index.tsx b/frontend/src/hooks/useDashboardCreateForm/index.tsx index 51c68aa82..2d7f217d3 100644 --- a/frontend/src/hooks/useDashboardCreateForm/index.tsx +++ b/frontend/src/hooks/useDashboardCreateForm/index.tsx @@ -2,6 +2,7 @@ import dashboardApis from '@api/domain/dashboard'; import { DEFAULT_QUESTIONS } from '@constants/constants'; import type { Question, QuestionOptionValue, RecruitmentInfoState, StepState } from '@customTypes/dashboard'; import useClubId from '@hooks/service/useClubId'; +import useLocalStorageState from '@hooks/useLocalStorageState'; import { useMutation, UseMutationResult } from '@tanstack/react-query'; import { useState } from 'react'; @@ -40,14 +41,49 @@ const initialRecruitmentInfoState: RecruitmentInfoState = { }; export default function useDashboardCreateForm(): UseDashboardCreateFormReturn { - const [stepState, setStepState] = useState('recruitmentForm'); - const [recruitmentInfoState, setRecruitmentInfoState] = useState(initialRecruitmentInfoState); - const [applyState, setApplyState] = useState(DEFAULT_QUESTIONS); + const clubId = useClubId().getClubId() || ''; + const LOCALSTORAGE_KEYS = { + STEP: `${clubId}-step`, + INFO: `${clubId}-info`, + APPLY: `${clubId}-apply`, + } as const; + + const [enableStorage] = useState(() => { + const Step = window.localStorage.getItem(LOCALSTORAGE_KEYS.STEP); + const Info = window.localStorage.getItem(LOCALSTORAGE_KEYS.INFO); + const Apply = window.localStorage.getItem(LOCALSTORAGE_KEYS.APPLY); + + if (Step || Info || Apply) { + return window.confirm('이전 작성중인 공고가 있습니다. 이어서 진행하시겠습니까?'); + } + return false; + }); + + const resetStorage = () => { + window.localStorage.removeItem(LOCALSTORAGE_KEYS.STEP); + window.localStorage.removeItem(LOCALSTORAGE_KEYS.INFO); + window.localStorage.removeItem(LOCALSTORAGE_KEYS.APPLY); + }; + + const [stepState, setStepState] = useLocalStorageState('recruitmentForm', { + key: LOCALSTORAGE_KEYS.STEP, + enableStorage, + }); + const [recruitmentInfoState, setRecruitmentInfoState] = useLocalStorageState( + initialRecruitmentInfoState, + { + key: LOCALSTORAGE_KEYS.INFO, + enableStorage, + }, + ); + const [applyState, setApplyState] = useLocalStorageState(DEFAULT_QUESTIONS, { + key: LOCALSTORAGE_KEYS.APPLY, + enableStorage, + }); + const [finishResJson, setFinishResJson] = useState(null); const [uniqueId, setUniqueId] = useState(DEFAULT_QUESTIONS.length); - const clubId = useClubId().getClubId() || ''; - const submitMutator = useMutation({ mutationFn: () => dashboardApis.create({ @@ -62,7 +98,7 @@ export default function useDashboardCreateForm(): UseDashboardCreateFormReturn { }), onSuccess: async (data) => { setStepState('finished'); - // TODO: Suspence 작업 해야함. + resetStorage(); setFinishResJson(data); }, }); diff --git a/frontend/src/hooks/useDashboardCreateForm/useDashboardCreateForm.test.tsx b/frontend/src/hooks/useDashboardCreateForm/useDashboardCreateForm.test.tsx index ec8b97d26..4d92ec393 100644 --- a/frontend/src/hooks/useDashboardCreateForm/useDashboardCreateForm.test.tsx +++ b/frontend/src/hooks/useDashboardCreateForm/useDashboardCreateForm.test.tsx @@ -10,12 +10,13 @@ jest.mock('react-router-dom', () => ({ })); describe('useDashboardCreateForm', () => { - beforeAll(() => { + beforeEach(() => { localStorage.setItem('clubId', '1'); }); - afterAll(() => { - localStorage.removeItem('clubId'); + afterEach(() => { + localStorage.clear(); + jest.restoreAllMocks(); }); const createWrapper = () => { @@ -76,21 +77,23 @@ describe('useDashboardCreateForm', () => { expect(result.current.applyState).toEqual(initialQuestions); }); - it('인덱스가 1에서 4인 질문은 prev할 수 없다.', () => { + it('인덱스가 0에서 3인 질문은 prev할 수 없다.', () => { const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); - const initialQuestions = result.current.applyState; + + act(() => result.current.addQuestion()); + const expectQuestions = result.current.applyState; act(() => { + result.current.setQuestionPrev(0)(); result.current.setQuestionPrev(1)(); result.current.setQuestionPrev(2)(); result.current.setQuestionPrev(3)(); - result.current.setQuestionPrev(4)(); }); - expect(result.current.applyState).toEqual(initialQuestions); + expect(result.current.applyState).toEqual(expectQuestions); }); - it('deleteQuestion을 호출하면 인덱스가 3 이상인 경우에만 해당 인덱스의 질문이 삭제된다.', () => { + it('deleteQuestion을 호출하면 인덱스가 3 이상인 경우에만 해당 인덱스의 질문이 삭제된다.', async () => { const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); // 인덱스가 3 미만인 경우 삭제되지 않아야 한다. @@ -110,4 +113,98 @@ describe('useDashboardCreateForm', () => { expect(result.current.applyState).toHaveLength(3); }); + + it('localStorage에 값이 있으면 confirm을 1번 띄운다.', () => { + jest.spyOn(window, 'confirm').mockImplementation(() => true); + + // Arrange + localStorage.setItem('1-step', 'recruitmentForm'); + localStorage.setItem( + '1-info', + JSON.stringify({ + startDate: '2024-12-01', + endDate: '2024-12-31', + title: 'Test Title', + postingContent: 'This is a test content', + }), + ); + + const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); + + // Act + expect(window.confirm).toHaveBeenCalledTimes(1); + + // Assert + expect(result.current.stepState).toBe('recruitmentForm'); + expect(result.current.recruitmentInfoState).toEqual({ + startDate: '2024-12-01', + endDate: '2024-12-31', + title: 'Test Title', + postingContent: 'This is a test content', + }); + }); + + it('필수 정보를 채운 후 새로고침 시 localStorage를 복원하고 공고를 성공적으로 생성한다.', async () => { + jest.spyOn(window, 'confirm').mockImplementation(() => true); + + const { result, rerender } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); + + // [1]Act + act(() => { + result.current.setRecruitmentInfoState((prev) => ({ + ...prev, + startDate: '2024-12-01', + endDate: '2024-12-31', + title: 'Test Title', + postingContent: 'This is a test content.', + })); + result.current.nextStep(); + result.current.addQuestion(); + }); + act(() => result.current.setQuestionTitle(3)('Test Question')); + + // [1]Assert + expect(result.current.stepState).toBe('applyForm'); + + // [2]Arrange + rerender(); // 새로고침과 같은 역할 + + expect(result.current.stepState).toBe('applyForm'); + expect(result.current.recruitmentInfoState).toEqual({ + startDate: '2024-12-01', + endDate: '2024-12-31', + title: 'Test Title', + postingContent: 'This is a test content.', + }); + + // [2]Act + await act(async () => { + result.current.nextStep(); + }); + + // [2]Assert + expect(result.current.submitMutator.isSuccess).toBe(true); + }); + + it('confirm 입력으로 false를 입력하면 localStorage가 비워진다.', () => { + jest.spyOn(window, 'confirm').mockImplementation(() => false); + + localStorage.setItem('1-step', 'recruitmentForm'); + localStorage.setItem( + '1-info', + JSON.stringify({ + startDate: '2024-12-01', + endDate: '2024-12-31', + title: 'Test Title', + postingContent: 'This is a test content', + }), + ); + localStorage.setItem('1-apply', JSON.stringify([{ question: 'Test Question', type: 'SHORT_ANSWER' }])); + + renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); + + expect(localStorage.getItem('1-step')).toBeNull(); + expect(localStorage.getItem('1-info')).toBeNull(); + expect(localStorage.getItem('1-apply')).toBeNull(); + }); }); diff --git a/frontend/src/hooks/useLocalStorageState/index.ts b/frontend/src/hooks/useLocalStorageState/index.ts index 947159523..4917ed5af 100644 --- a/frontend/src/hooks/useLocalStorageState/index.ts +++ b/frontend/src/hooks/useLocalStorageState/index.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; interface OptionProp { key: string; @@ -36,6 +36,12 @@ const safeStringifyJSON = (value: T): string | null => { function useLocalStorageState(initialValue: T, option: OptionProp): [T, (value: T | ((prev: T) => T)) => void] { const { key, enableStorage = true } = option; + useEffect(() => { + if (!enableStorage) { + localStorage.removeItem(key); + } + }, []); + const [state, _setState] = useState(() => { if (!enableStorage) return initialValue; diff --git a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx index 4cbee9dc3..e5a1a4b1f 100644 --- a/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx +++ b/frontend/src/hooks/useLocalStorageState/useLocalStorageState.test.tsx @@ -56,7 +56,7 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 const { result } = renderHook(() => useLocalStorageState(0, { key: 'primitiveKey', enableStorage: false })); expect(result.current[0]).toBe(0); - expect(window.localStorage.getItem('primitiveKey')).toBe('10'); + expect(window.localStorage.getItem('primitiveKey')).toBeNull(); }); }); @@ -116,7 +116,7 @@ describe('useLocalStorageState의 값이 원시값인 경우에 대한 테스트 ); expect(result.current[0]).toEqual(initialObject); - expect(window.localStorage.getItem('objectKey')).toBe(storedObject); + expect(window.localStorage.getItem('objectKey')).toBeNull(); }); }); }); diff --git a/frontend/src/mocks/handlers/dashboardHandlers.ts b/frontend/src/mocks/handlers/dashboardHandlers.ts index 56336097c..bf4c4cab2 100644 --- a/frontend/src/mocks/handlers/dashboardHandlers.ts +++ b/frontend/src/mocks/handlers/dashboardHandlers.ts @@ -10,7 +10,7 @@ const dashboardHandlers = [ const clubId = url.searchParams.get('clubId'); const body = (await request.json()) as DashboardFormInfo; - if (!body.startDate || !body.endDate || !body.postingContent || !body.title || clubId) { + if (!body.startDate || !body.endDate || !body.postingContent || !body.title || !clubId) { return new Response(null, { status: 400, statusText: 'The request body is missing required information.', diff --git a/frontend/src/router/AppRouter.tsx b/frontend/src/router/AppRouter.tsx index d68dcbecd..a5b6c6c22 100644 --- a/frontend/src/router/AppRouter.tsx +++ b/frontend/src/router/AppRouter.tsx @@ -18,7 +18,9 @@ const RecruitmentPost = lazy(() => import(/* webpackChunkName: "RecruitmentPost" const ConfirmApply = lazy(() => import(/* webpackChunkName: "SignConfirmApplyUp" */ '@pages/ConfirmApply')); const DashboardLayout = lazy(() => import(/* webpackChunkName: "DashboardLayout" */ '@pages/DashboardLayout')); const DashboardList = lazy(() => import(/* webpackChunkName: "DashBoardList" */ '@pages/DashBoardList')); -const DashboardCreate = lazy(() => import(/* webpackChunkName: "DashboardCreate" */ '@pages/DashboardCreate')); +// const DashboardCreate = lazy(() => import(/* webpackChunkName: "DashboardCreate" */ '@pages/DashboardCreate')); +// eslint-disable-next-line import/first, prettier/prettier, no-trailing-spaces, import/order +import DashboardCreate from '@pages/DashboardCreate'; // TODO: [lurgi] - 24.12.24 중복 렌더링에 따른 동적 임포트 임시 제거 const router = createBrowserRouter( [