Skip to content

Commit

Permalink
feat-fe: 공고 페이지에서 사용할 useDashboardCreateForm훅 구현 (#278)
Browse files Browse the repository at this point in the history
Co-authored-by: Jeongwoo Park <[email protected]>
  • Loading branch information
github-actions[bot] and lurgi authored Aug 7, 2024
1 parent 7f4ebc1 commit 8e257ca
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 1 deletion.
28 changes: 28 additions & 0 deletions frontend/src/api/dashboard.ts
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;
2 changes: 2 additions & 0 deletions frontend/src/api/endPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const PROCESSES = `${BASE_URL}/processes`;
export const APPLICANTS = `${BASE_URL}/applicants`;

export const EVALUATIONS = `${BASE_URL}/evaluations`;

export const DASHBOARDS = `${BASE_URL}/dashboards`;
2 changes: 2 additions & 0 deletions frontend/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ export const PROCESS = {
},
},
} as const;

export const CLUB_ID = 1; // TODO: 수정해야 합니다.
151 changes: 151 additions & 0 deletions frontend/src/hooks/useDashboardCreateForm/index.tsx
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,
};
}
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);
});
});
26 changes: 26 additions & 0 deletions frontend/src/mocks/handlers/dashboardHandlers.ts
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;
3 changes: 2 additions & 1 deletion frontend/src/mocks/handlers/index.ts
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;
25 changes: 25 additions & 0 deletions frontend/src/types/dashboard.ts
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;
}

0 comments on commit 8e257ca

Please sign in to comment.