Skip to content

Commit

Permalink
[FE] refactor,fix : 리뷰 작성 페이지 디자인 버그 수정 및 상태 관리 리팩토링 (#369)
Browse files Browse the repository at this point in the history
* chore: 깃허브 로고 주석 처리

* fix: Checkbox 의 onChange 오류 수정

- 오류 : handleChange로 props명이 변경되면서 구조분해할당으로 input에 onChange 이벤트가 들어가지 않게 됨
- 오류 수정; onChange에 handleChange를 직접 넣어주는 방식으로 수정

* chore: 서술형 질문에 글자 수 안내 문구 삭제, 선택 문항에 '(선택)' 문구 추

* refactor: multipleGuidline을 useMultipleChoice로 이동

* refactor: useMutateReview의 onSuccess에서 리뷰 생성 후 기능 실행하도록 수정

- mutate 성공 시, 실행할 executeAfterMutateSuccess를 useMutateReview의 props로 추가
- 리뷰 제출 성공 시, 모달 닫고 페이지 이동하는 코드를 onSuccess 에서 실행하도록 수정

* refactor: useMutateReview의 onSuccess에서 리뷰 생성 후 기능 실행하도록 수정

- mutate 성공 시, 실행할 executeAfterMutateSuccess를 useMutateReview의 props로 추가
- 리뷰 제출 성공 시, 모달 닫고 페이지 이동하는 코드를 onSuccess 에서 실행하도록 수정

* fix: Checkbox props명 오류 수정

- isDisabled -> disabled

* refactor : 리뷰 작성 질문들을 recoil로 관리하도록 리팩토링

- 컴포넌트간 props drilling을 줄이기 위해 recoil 상태관리를 사용
- 카테고리 선택 결과에 따라 리뷰 작성 질문지(questionList)가 동적으로 변할 수 있도록 atom, selector를 사용

* refactor: answerMap, answerValidationMap을 atom으로 변경

* refactor: useReviewAnswer를  3개의 훅으로 분리

<hook>
- 분리되어 생성된 훅 :useUpdateReviewerAnswer, useUpdateDefaultAnswers, useCheckStepAvailability

<components>
- QnABox: props 변경 및 useUpdateReviewerAnswer 사용
-  RevieWritingCard : props 변경 및 useCheckNextStepAvailability 사용

* feat: 리뷰 작성 폼 관련 recoil 상태 초기화하는 훅 생성 및 적용

* fix: 리뷰 작성 페이지 목 데이터에서 optionId  중복 수정

* chore: 코드 설명에 대한 주석 추가

* refactor: useUpdateDefaultAnswers 에서 객관식/서술형 기본값 상수 처리

* fix: 서술형에서 길에 한문장으로 작성 시 맥에서 가로 스크롤이 생기는 오류 수정

* fix :  ReviewWritingFrom의 formId 타입 변경 (string-> number)

* chore: REVIEW_WRITING_FORM_CARD_DATA 변경

* refactor: 훅 , selector 네이밍 변경

- questionList -> cardSectionList

* refactor: useMultipleChoice 리팩토링

- useAboveSelectionLimit , useUpdateMultipleChoiceAnswer로 분리

* feat :기존에 답변한 카테고리를 카테고리 질문에서 해제할 때에 대한 대응 추가

-  useCancelAnsweredCategory 추가
- 카테고리 질문에서 이미 답변을 작성한 카테고리 선택을 해제하는 지 판단
- 해제하려는 경우, answerMap, answerValidationMap의 상태 변경을 하지 않고 모달을 띄워줌
- 모달에서 확인 버튼 클릭하면, 해제되고 취소되면 기존의 선택이 유지

* refactor: ReviewWritingCardForm하위 컴포넌트 파일들을  components 폴더로 이동

* feat: QnABox에 답변한 카테고리 해제 시, 관련 기능 적용

- 리뷰 작성 페이지의 다른 모달들과 같은 컴포넌트에 위치하면 좋겠으나,
확인 버튼 클릭 시 사용자가 해제하려했던 카테고리 선택이 해제되어야해서 컴포넌트 구조상 QnABox에 관련 모달이 있어야한다고 판단함

* refactor: QnABox에서 MultipleChoiceQuestion 분리

* fix: 개수 제한 가이드 라인 표시 이전에 선택 오류 수정

개수 제한 가이드 라인 표시 이전에 최대 개수를 넘는 선택 시, 체크박스 선택되는 오류 수정

* chore: 필요없는 코드 삭제

* fix: 답변이 없는 카테고리 선택 취소 시, 모달 띄워지는 오류 수정

* fix: handleModalOpen 파라미터 오류 수정

* fix: 답변이 없는 카테고리 선택 취소 시 답변 기본값에 의한 오류 수정

- 답변의 기본값이 빈문자열 이나 빈 배열이여서 답변을 하지 않았음에도 모달이 띄워지는 오류가 있었음

* refactor: useTextAnswer에서 useUpdateReviewerAnswer사용

* design: 프로젝트 이름, 리뷰이의 이름이 길어지는 경우를 대비한 디자인 수정

* feat: 작성 내용 확인 버튼에 비활성화 기능 추가

- 모든 답변이 제출 가능한 상태여야 리뷰 작성 확인 버튼도 비활성화 됨
- 버튼의 text를 작성 내용 확인 버튼으로 수정

* fix: Checkbox 의 onChange 오류 수정

- 오류 : handleChange로 props명이 변경되면서 구조분해할당으로 input에 onChange 이벤트가 들어가지 않게 됨
- 오류 수정; onChange에 handleChange를 직접 넣어주는 방식으로 수정

* chore: 서술형 질문에 글자 수 안내 문구 삭제, 선택 문항에 '(선택)' 문구 추

* fix: Checkbox 의 onChange 오류 수정

- 오류 : handleChange로 props명이 변경되면서 구조분해할당으로 input에 onChange 이벤트가 들어가지 않게 됨
- 오류 수정; onChange에 handleChange를 직접 넣어주는 방식으로 수정

* fix: 서술형에서 길에 한문장으로 작성 시 맥에서 가로 스크롤이 생기는 오류 수정

* design: 프로젝트 이름, 리뷰이의 이름이 길어지는 경우를 대비한 디자인 수정

* chore: Checkbox 변경으로 인한 코드 수정
  • Loading branch information
BadaHertz52 authored Aug 16, 2024
1 parent ae79025 commit 0ed89fd
Show file tree
Hide file tree
Showing 36 changed files with 868 additions and 495 deletions.
1 change: 1 addition & 0 deletions frontend/src/components/common/LongReviewItem/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const Textarea = styled.textarea<TextareaProps>`
padding: 1.6rem;
font-weight: ${({ theme }) => theme.fontWeight.medium};
overflow-wrap: break-word;
border: 0.1rem solid ${({ $isError, theme }) => ($isError ? theme.colors.red : theme.colors.black)};
border-radius: 0.8rem;
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/hooks/review/writingCardForm/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export { default as useReviewerAnswer } from './useReviewerAnswer';
export { default as useSlideWidthAndHeight } from './useSlideWidthAndHeight';
export { default as useCheckNextStepAvailability } from './useCheckNextStepAvailability';
export { default as useCurrentCardIndex } from './useCurrentCardIndex';
export { default as useMultipleChoice } from './useMultipleChoice';
export { default as useTextAnswer } from './useTextAnswer';
export { default as useQuestionList } from './useQuestionList';
export { default as useGetDataToWrite } from './useGetDataToWrite';
export { default as useMutateReview } from './useMutateReview';
export { default as useMultipleChoice } from './multiplceChoice/useMultipleChoice';
export { default as useCardSectionList } from './useCardSectionList';
export { default as useResetFormRecoil } from './useResetFormRecoil';
export { default as useUpdateReviewerAnswer } from './useUpdateReviewerAnswer';
export { default as useSlideWidthAndHeight } from './useSlideWidthAndHeight';
export { default as useTextAnswer } from './useTextAnswer';
export { default as useUpdateDefaultAnswers } from './useUpdateDefaultAnswers';
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState } from 'react';

import { ReviewWritingCardQuestion } from '@/types';

interface UseAboveSelectionLimit {
question: ReviewWritingCardQuestion;
selectedOptionList: number[];
}
const useAboveSelectionLimit = ({ question, selectedOptionList }: UseAboveSelectionLimit) => {
const [isOpenLimitGuide, setIsOpenLimitGuide] = useState(false);

const isMaxCheckedNumber = () => {
if (!question.optionGroup) return false;
return selectedOptionList.length >= question.optionGroup.maxCount;
};

const isSelectedCheckbox = (optionId: number) => {
return selectedOptionList.includes(optionId);
};

/**
* 선택 가능한 문항 수를 넘어서 문항을 선택하려 하는지 여부
*/
const isAboveSelectionLimit = (optionId: number) => !!(isMaxCheckedNumber() && !isSelectedCheckbox(optionId));

/**
* 최대 문항 수를 넘어서 선택하려는지, 그럴 경우에 대한 핸들링
* @param id : 객관식 문항의 optionId
*/
const handleLimitGuideOpen = (isOpen: boolean) => {
setIsOpenLimitGuide(isOpen);
};

return {
isOpenLimitGuide,
isSelectedCheckbox,
isAboveSelectionLimit,
handleLimitGuideOpen,
};
};

export default useAboveSelectionLimit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useRecoilValue } from 'recoil';

import { answerMapAtom, cardSectionListSelector } from '@/recoil';
import { ReviewWritingCardQuestion } from '@/types';

interface UseCancelAnsweredCategoryProps {
question: ReviewWritingCardQuestion;
}
const useCancelAnsweredCategory = ({ question }: UseCancelAnsweredCategoryProps) => {
const cardSectionList = useRecoilValue(cardSectionListSelector);
const answerMap = useRecoilValue(answerMapAtom);
const isCategoryQuestion = () => {
return question.questionId === cardSectionList[0].questions[0].questionId;
};
// 이미 답변을 작성한 카테고리를 해제하는 경우
/**
* 카테고리 항목 선택일때, optionId에 해당하는 카테고리 찾기
*/
const getCategoryByOptionId = (categoryOptionId: number) => {
return cardSectionList.filter((section) => section.onSelectedOptionId === categoryOptionId)[0];
};

/**
* categoryOptionId로 찾은 카테고리에 답변이 달려있는 지 여부
* 답이 있기만 하다면 true
*/
const isSelectedCategoryAnswer = (categoryOptionId: number) => {
if (!answerMap) return false;
// 선택한 객관식에 해당하는 카테고리의 질문들 가져오기
const targetCategoryQuestionList = getCategoryByOptionId(categoryOptionId);

if (!targetCategoryQuestionList) return false;
//카테고리에 유효한 답변이 있는 지 판단
const questionIdList = targetCategoryQuestionList.questions.map((question) => question.questionId);
const questionId = questionIdList.find((id) => answerMap.has(id));

if (!questionId) return;

const answer = answerMap.get(questionId);

return !!answer?.selectedOptionIds?.length || !!answer?.text?.length;
};

/**
* 해제하기 위해 카테고리 문항을 선택한 경우, 이미 이에 대해 답변을 했는 지 여부
* @param optionId : 문항의 optionId
*/
const isAnsweredCategoryChanged = (optionId: number) => {
if (!isCategoryQuestion) return false;
return isSelectedCategoryAnswer(optionId);
};

return {
isAnsweredCategoryChanged,
};
};

export default useCancelAnsweredCategory;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState } from 'react';

import { ReviewWritingCardQuestion } from '@/types';

import useAboveSelectionLimit from './useAboveSelectionLimit';
import useCancelAnsweredCategory from './useCancelAnsweredCategory';
import useUpdateMultipleChoiceAnswer from './useUpdateMultipleChoiceAnswer';

interface UseMultipleChoiceProps {
question: ReviewWritingCardQuestion;
handleModalOpen: (isOpen: boolean) => void;
}
/**
* 하나의 객관식 질문에서 선택된 문항, 문항 선택 관리(최대를 넘는 문항 선택 시, 안내 문구 표시)등을 하는 훅
*/
const useMultipleChoice = ({ question, handleModalOpen }: UseMultipleChoiceProps) => {
const [unCheckTargetOptionId, setUnCheckTargetOptionId] = useState<number | null>(null);

const { isAnsweredCategoryChanged } = useCancelAnsweredCategory({ question });

const { selectedOptionList, updateAnswerState } = useUpdateMultipleChoiceAnswer({ question });

const { isOpenLimitGuide, isSelectedCheckbox, isAboveSelectionLimit, handleLimitGuideOpen } = useAboveSelectionLimit({
question,
selectedOptionList,
});

const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { id, checked } = event.currentTarget;
const optionId = Number(id);
if (isAboveSelectionLimit(optionId)) {
return handleLimitGuideOpen(true);
}
handleLimitGuideOpen(false);
// 답변이 달린 카테고리를 해제하려는 경우
const isUnCheckCategory = isAnsweredCategoryChanged(optionId);
setUnCheckTargetOptionId(isUnCheckCategory ? optionId : null);
handleModalOpen(!!isUnCheckCategory);

if (!isUnCheckCategory) {
updateAnswerState({ optionId, checked });
}
};

const unCheckTargetOption = () => {
if (unCheckTargetOptionId) {
updateAnswerState({ optionId: unCheckTargetOptionId, checked: false });
}
};
return {
isOpenLimitGuide,
handleCheckboxChange,
isSelectedCheckbox,
unCheckTargetOption,
};
};
export default useMultipleChoice;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState } from 'react';

import { ReviewWritingAnswer, ReviewWritingCardQuestion } from '@/types';

import useUpdateReviewerAnswer from '../useUpdateReviewerAnswer';

interface UseUpdateMultipleChoiceAnswerProps {
question: ReviewWritingCardQuestion;
}

const useUpdateMultipleChoiceAnswer = ({ question }: UseUpdateMultipleChoiceAnswerProps) => {
const [selectedOptionList, setSelectedOptionList] = useState<number[]>([]);

const { updateAnswerMap, updateAnswerValidationMap } = useUpdateReviewerAnswer();

interface MakeNewSelectedOptionList {
optionId: number;
checked: boolean;
}
/**
* checkbox의 change 이벤트에 따라 새로운 selectedOptionList를 반환하는 함수
*/
const makeNewSelectedOptionList = ({ optionId, checked }: MakeNewSelectedOptionList) => {
if (checked) {
return selectedOptionList.concat(optionId);
}

return selectedOptionList.filter((option) => option !== optionId);
};

const isValidatedChoice = (newSelectedOptionList: number[]) => {
if (!question.optionGroup) return false;

const { minCount, maxCount } = question.optionGroup;
const { length } = newSelectedOptionList;

return length >= minCount && length <= maxCount;
};

const updateAnswerState = ({ optionId, checked }: MakeNewSelectedOptionList) => {
const newSelectedOptionList = makeNewSelectedOptionList({ optionId, checked });
setSelectedOptionList(newSelectedOptionList);

// 유효한 선택(=객관식 문항의 최소,최대 개수를 지켰을 경우)인지에 따라 answer 변경
const isValidatedAnswer = isValidatedChoice(newSelectedOptionList);
const isNotRequiredEmptyAnswer = !question.required && newSelectedOptionList.length === 0;

const newAnswer: ReviewWritingAnswer = {
questionId: question.questionId,
selectedOptionIds: isValidatedAnswer ? newSelectedOptionList : [],
text: null,
};

updateAnswerMap(newAnswer);
updateAnswerValidationMap(newAnswer, isValidatedAnswer || isNotRequiredEmptyAnswer);
};

return {
selectedOptionList,
updateAnswerState,
};
};

export default useUpdateMultipleChoiceAnswer;
29 changes: 29 additions & 0 deletions frontend/src/hooks/review/writingCardForm/useCardSectionList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';

import { cardSectionListSelector, reviewWritingFormSectionListAtom } from '@/recoil';
import { ReviewWritingCardSection } from '@/types';

interface UseCardSectionListProps {
cardSectionListData: ReviewWritingCardSection[];
}
/**
* 서버에서 받아온 데이터를 바탕으로 리뷰 작성 폼에서 사용할 질문지(상태)를 변경하는 훅
* @param {ReviewWritingCardSection[]} cardSectionListData 서버에서 받아온 질문 데이터
* @returns
*/
const useCardSectionList = ({ cardSectionListData }: UseCardSectionListProps) => {
const setReviewWritingFormSectionList = useSetRecoilState(reviewWritingFormSectionListAtom);

const cardSectionList = useRecoilValue(cardSectionListSelector);

useEffect(() => {
setReviewWritingFormSectionList(cardSectionListData);
}, [cardSectionListData]);

return {
cardSectionList,
};
};

export default useCardSectionList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';

import { cardSectionListSelector, answerValidationMapAtom, answerMapAtom } from '@/recoil';

interface UseCheckNextStepAvailability {
currentCardIndex: number;
}
const useCheckNextStepAvailability = ({ currentCardIndex }: UseCheckNextStepAvailability) => {
const cardSectionList = useRecoilValue(cardSectionListSelector);
const answerValidationMap = useRecoilValue(answerValidationMapAtom);
const answerMap = useRecoilValue(answerMapAtom);

const [isAbleNextStep, setIsAbleNextStep] = useState(false);

const isValidateAnswerList = () => {
if (!cardSectionList.length) return false;

return cardSectionList[currentCardIndex].questions.every((question) => {
const { questionId, required } = question;
const answerValidation = answerValidationMap?.get(questionId);

if (!required && answerValidation) return true;
return !!answerValidation;
});
};

useEffect(() => {
const answerListValidation = isValidateAnswerList();
setIsAbleNextStep(answerListValidation);
}, [answerMap, currentCardIndex]);

return {
isAbleNextStep,
};
};

export default useCheckNextStepAvailability;
78 changes: 0 additions & 78 deletions frontend/src/hooks/review/writingCardForm/useMultipleChoice.ts

This file was deleted.

Loading

0 comments on commit 0ed89fd

Please sign in to comment.