-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat-fe: 공고 페이지에서 사용할 useDashboardCreateForm훅 구현 (#278)
Co-authored-by: Jeongwoo Park <[email protected]>
- Loading branch information
1 parent
7f4ebc1
commit 8e257ca
Showing
8 changed files
with
333 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { DashboardFormInfo } from '@customTypes/dashboard'; | ||
import { DASHBOARDS } from './endPoint'; | ||
import { convertParamsToQueryString } from './utils'; | ||
import ApiError from './ApiError'; | ||
|
||
const dashboardApis = { | ||
create: async ({ clubId, dashboardFormInfo }: { clubId: number; dashboardFormInfo: DashboardFormInfo }) => { | ||
const queryParams = { | ||
clubId: String(clubId), | ||
}; | ||
|
||
const response = await fetch(`${DASHBOARDS}?${convertParamsToQueryString(queryParams)}`, { | ||
method: 'POST', | ||
headers: { | ||
'Content-type': 'application/json', | ||
}, | ||
body: JSON.stringify(dashboardFormInfo), | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new ApiError({ message: 'Network response was not ok', method: 'POST', statusCode: response.status }); | ||
} | ||
|
||
return response; | ||
}, | ||
}; | ||
|
||
export default dashboardApis; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,3 +20,5 @@ export const PROCESS = { | |
}, | ||
}, | ||
} as const; | ||
|
||
export const CLUB_ID = 1; // TODO: 수정해야 합니다. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import dashboardApis from '@api/dashboard'; | ||
import { CLUB_ID } from '@constants/constants'; | ||
import type { Question, RecruitmentInfoState, StepState } from '@customTypes/dashboard'; | ||
import { useMutation, UseMutationResult } from '@tanstack/react-query'; | ||
import { useState } from 'react'; | ||
|
||
interface Option { | ||
value: string; | ||
} | ||
interface UseDashboardCreateFormReturn { | ||
stepState: StepState; | ||
nextStep: () => void; | ||
|
||
recruitmentInfoState: RecruitmentInfoState; | ||
setRecruitmentInfoState: React.Dispatch<React.SetStateAction<RecruitmentInfoState>>; | ||
applyState: Question[]; | ||
|
||
addQuestion: () => void; | ||
setQuestionTitle: (index: number) => (title: string) => void; | ||
setQuestionType: (index: number) => (type: Question['type']) => void; | ||
setQuestionOptions: (index: number) => (Options: Option[]) => void; | ||
setQuestionRequiredToggle: (index: number) => () => void; | ||
setQuestionPrev: (index: number) => () => void; | ||
setQuestionNext: (index: number) => () => void; | ||
deleteQuestion: (index: number) => void; | ||
|
||
submitMutator: UseMutationResult<unknown, Error, void, unknown>; | ||
} | ||
|
||
const initialRecruitmentInfoState: RecruitmentInfoState = { | ||
startDate: '', | ||
endDate: '', | ||
title: '', | ||
postingContent: '', | ||
}; | ||
|
||
const initialApplyState: Question[] = [ | ||
{ type: 'SHORT_ANSWER', question: '이름', choices: [], required: true }, | ||
{ type: 'SHORT_ANSWER', question: '이메일', choices: [], required: true }, | ||
{ type: 'SHORT_ANSWER', question: '전화번호', choices: [], required: true }, | ||
]; | ||
|
||
export default function useDashboardCreateForm(): UseDashboardCreateFormReturn { | ||
const [stepState, setStepState] = useState<StepState>('recruitmentForm'); | ||
const [recruitmentInfoState, setRecruitmentInfoState] = useState<RecruitmentInfoState>(initialRecruitmentInfoState); | ||
const [applyState, setApplyState] = useState<Question[]>(initialApplyState); | ||
|
||
const nextStep = () => { | ||
if (stepState === 'recruitmentForm') setStepState('applyForm'); | ||
if (stepState === 'applyForm') setStepState('finished'); | ||
}; | ||
|
||
const addQuestion = () => { | ||
setApplyState((prev) => [...prev, { type: 'SHORT_ANSWER', question: '', choices: [], required: false }]); | ||
}; | ||
|
||
const setQuestionTitle = (index: number) => (string: string) => { | ||
setApplyState((prevState) => { | ||
const questionsCopy = [...prevState]; | ||
questionsCopy[index].question = string; | ||
return questionsCopy; | ||
}); | ||
}; | ||
|
||
const setQuestionType = (index: number) => (type: Question['type']) => { | ||
setApplyState((prevState) => { | ||
const questionsCopy = [...prevState]; | ||
questionsCopy[index].type = type; | ||
questionsCopy[index].choices = []; | ||
return questionsCopy; | ||
}); | ||
}; | ||
|
||
const setQuestionOptions = (index: number) => (options: Option[]) => { | ||
setApplyState((prevState) => { | ||
const questionsCopy = [...prevState]; | ||
questionsCopy[index].choices = options.map(({ value }, i) => ({ choice: value, orderIndex: i })); | ||
return questionsCopy; | ||
}); | ||
}; | ||
|
||
const setQuestionRequiredToggle = (index: number) => () => { | ||
setApplyState((prevState) => { | ||
const questionsCopy = [...prevState]; | ||
questionsCopy[index].required = !prevState[index].required; | ||
return questionsCopy; | ||
}); | ||
}; | ||
|
||
const setQuestionPrev = (index: number) => () => { | ||
setApplyState((prevState) => { | ||
if (index > initialApplyState.length) { | ||
const questionsCopy = [...prevState]; | ||
const temp = questionsCopy[index]; | ||
questionsCopy[index] = questionsCopy[index - 1]; | ||
questionsCopy[index - 1] = temp; | ||
return questionsCopy; | ||
} | ||
return prevState; | ||
}); | ||
}; | ||
|
||
const setQuestionNext = (index: number) => () => { | ||
setApplyState((prevState) => { | ||
if (index >= initialApplyState.length && index < prevState.length - 1) { | ||
const questionsCopy = [...prevState]; | ||
const temp = questionsCopy[index]; | ||
questionsCopy[index] = questionsCopy[index + 1]; | ||
questionsCopy[index + 1] = temp; | ||
return questionsCopy; | ||
} | ||
return prevState; | ||
}); | ||
}; | ||
|
||
const deleteQuestion = (index: number) => { | ||
if (index < initialApplyState.length) return; | ||
setApplyState((prevState) => prevState.filter((_, i) => i !== index)); | ||
}; | ||
|
||
const submitMutator = useMutation({ | ||
mutationFn: () => | ||
dashboardApis.create({ | ||
clubId: CLUB_ID, | ||
dashboardFormInfo: { | ||
...recruitmentInfoState, | ||
questions: applyState, | ||
}, | ||
}), | ||
}); | ||
|
||
return { | ||
stepState, | ||
nextStep, | ||
|
||
recruitmentInfoState, | ||
setRecruitmentInfoState, | ||
applyState, | ||
|
||
addQuestion, | ||
setQuestionTitle, | ||
setQuestionType, | ||
setQuestionOptions, | ||
setQuestionRequiredToggle, | ||
setQuestionPrev, | ||
setQuestionNext, | ||
deleteQuestion, | ||
|
||
submitMutator, | ||
}; | ||
} |
97 changes: 97 additions & 0 deletions
97
frontend/src/hooks/useDashboardCreateForm/useDashboardCreateForm.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { PropsWithChildren } from 'react'; | ||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||
import { act, renderHook } from '@testing-library/react'; | ||
import useDashboardCreateForm from '.'; | ||
|
||
describe('useDashboardCreateForm', () => { | ||
const createWrapper = () => { | ||
const queryClient = new QueryClient(); | ||
// eslint-disable-next-line react/function-component-definition | ||
return ({ children }: PropsWithChildren) => ( | ||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> | ||
); | ||
}; | ||
|
||
it('addQuestion을 호출하면 새로운 질문이 추가된다.', () => { | ||
const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); | ||
|
||
act(() => { | ||
result.current.addQuestion(); | ||
}); | ||
|
||
expect(result.current.applyState).toHaveLength(4); | ||
}); | ||
|
||
it('setQuestionType으로 질문 타입을 변경할 수 있다.', () => { | ||
const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); | ||
|
||
act(() => { | ||
result.current.setQuestionType(0)('LONG_ANSWER'); | ||
}); | ||
|
||
expect(result.current.applyState[0].type).toBe('LONG_ANSWER'); | ||
}); | ||
|
||
it('setQuestionOptions로 질문 옵션을 설정할 수 있다.', () => { | ||
const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); | ||
|
||
const options = [{ value: 'Option 1' }, { value: 'Option 2' }]; | ||
act(() => { | ||
result.current.setQuestionOptions(0)(options); | ||
}); | ||
|
||
expect(result.current.applyState[0].choices).toEqual([ | ||
{ choice: 'Option 1', orderIndex: 0 }, | ||
{ choice: 'Option 2', orderIndex: 1 }, | ||
]); | ||
}); | ||
|
||
it('인덱스가 1에서 3인 질문과 마지막 요소는 next할 수 없다.', () => { | ||
const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); | ||
const initialQuestions = result.current.applyState; | ||
|
||
act(() => { | ||
result.current.setQuestionNext(1)(); | ||
result.current.setQuestionNext(2)(); | ||
result.current.setQuestionNext(3)(); | ||
result.current.setQuestionNext(initialQuestions.length - 1)(); | ||
}); | ||
|
||
expect(result.current.applyState).toEqual(initialQuestions); | ||
}); | ||
|
||
it('인덱스가 1에서 4인 질문은 prev할 수 없다.', () => { | ||
const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); | ||
const initialQuestions = result.current.applyState; | ||
|
||
act(() => { | ||
result.current.setQuestionPrev(1)(); | ||
result.current.setQuestionPrev(2)(); | ||
result.current.setQuestionPrev(3)(); | ||
result.current.setQuestionPrev(4)(); | ||
}); | ||
|
||
expect(result.current.applyState).toEqual(initialQuestions); | ||
}); | ||
|
||
it('deleteQuestion을 호출하면 인덱스가 3 이상인 경우에만 해당 인덱스의 질문이 삭제된다.', () => { | ||
const { result } = renderHook(() => useDashboardCreateForm(), { wrapper: createWrapper() }); | ||
|
||
// 인덱스가 3 미만인 경우 삭제되지 않아야 한다. | ||
act(() => { | ||
result.current.deleteQuestion(0); | ||
result.current.deleteQuestion(1); | ||
result.current.deleteQuestion(2); | ||
}); | ||
|
||
expect(result.current.applyState).toHaveLength(3); | ||
|
||
// 인덱스가 3 이상인 경우 삭제되어야 한다. | ||
act(() => { | ||
result.current.addQuestion(); | ||
result.current.deleteQuestion(3); | ||
}); | ||
|
||
expect(result.current.applyState).toHaveLength(3); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { http } from 'msw'; | ||
|
||
import { DASHBOARDS } from '@api/endPoint'; | ||
import { DashboardFormInfo } from '@customTypes/dashboard'; | ||
|
||
const dashboardHandlers = [ | ||
http.post(DASHBOARDS, async ({ request }) => { | ||
const url = new URL(request.url); | ||
const clubId = url.searchParams.get('clubId'); | ||
const body = (await request.json()) as DashboardFormInfo; | ||
|
||
if (!body.startDate || !body.endDate || !body.postingContent || !body.title || clubId) { | ||
return new Response(null, { | ||
status: 400, | ||
statusText: 'The request body is missing required information.', | ||
}); | ||
} | ||
|
||
return new Response(null, { | ||
status: 201, | ||
statusText: 'Created', | ||
}); | ||
}), | ||
]; | ||
|
||
export default dashboardHandlers; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
import applicantHandlers from './applicantHandlers'; | ||
import processHandlers from './processHandlers'; | ||
import evaluationHandlers from './evaluationHandlers'; | ||
import dashboardHandlers from './dashboardHandlers'; | ||
|
||
const handlers = [...processHandlers, ...applicantHandlers, ...evaluationHandlers]; | ||
const handlers = [...processHandlers, ...applicantHandlers, ...evaluationHandlers, ...dashboardHandlers]; | ||
|
||
export default handlers; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
export interface Question { | ||
type: 'SHORT_ANSWER' | 'LONG_ANSWER' | 'MULTIPLE_CHOICE' | 'DROPDOWN'; | ||
question: string; | ||
choices: { | ||
choice: string; | ||
orderIndex: number; | ||
}[]; | ||
required: boolean; | ||
} | ||
|
||
export interface RecruitmentInfoState { | ||
startDate: string; | ||
endDate: string; | ||
title: string; | ||
postingContent: string; | ||
} | ||
|
||
export type DashboardFormInfo = { questions: Question[] } & RecruitmentInfoState; | ||
|
||
export type StepState = 'recruitmentForm' | 'applyForm' | 'finished'; | ||
|
||
export interface QuestionOption { | ||
choice: string; | ||
orderIndex: number; | ||
} |