diff --git a/frontend/src/components/common/LongReviewItem/styles.ts b/frontend/src/components/common/LongReviewItem/styles.ts index 0462ff15a..d06a800aa 100644 --- a/frontend/src/components/common/LongReviewItem/styles.ts +++ b/frontend/src/components/common/LongReviewItem/styles.ts @@ -20,6 +20,7 @@ export const Textarea = styled.textarea` 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; diff --git a/frontend/src/hooks/review/writingCardForm/index.ts b/frontend/src/hooks/review/writingCardForm/index.ts index df86c2af0..923a310b7 100644 --- a/frontend/src/hooks/review/writingCardForm/index.ts +++ b/frontend/src/hooks/review/writingCardForm/index.ts @@ -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'; diff --git a/frontend/src/hooks/review/writingCardForm/multiplceChoice/useAboveSelectionLimit.ts b/frontend/src/hooks/review/writingCardForm/multiplceChoice/useAboveSelectionLimit.ts new file mode 100644 index 000000000..1deefd9e3 --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/multiplceChoice/useAboveSelectionLimit.ts @@ -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; diff --git a/frontend/src/hooks/review/writingCardForm/multiplceChoice/useCancelAnsweredCategory.ts b/frontend/src/hooks/review/writingCardForm/multiplceChoice/useCancelAnsweredCategory.ts new file mode 100644 index 000000000..59055c572 --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/multiplceChoice/useCancelAnsweredCategory.ts @@ -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; diff --git a/frontend/src/hooks/review/writingCardForm/multiplceChoice/useMultipleChoice.ts b/frontend/src/hooks/review/writingCardForm/multiplceChoice/useMultipleChoice.ts new file mode 100644 index 000000000..d10f52370 --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/multiplceChoice/useMultipleChoice.ts @@ -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(null); + + const { isAnsweredCategoryChanged } = useCancelAnsweredCategory({ question }); + + const { selectedOptionList, updateAnswerState } = useUpdateMultipleChoiceAnswer({ question }); + + const { isOpenLimitGuide, isSelectedCheckbox, isAboveSelectionLimit, handleLimitGuideOpen } = useAboveSelectionLimit({ + question, + selectedOptionList, + }); + + const handleCheckboxChange = (event: React.ChangeEvent) => { + 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; diff --git a/frontend/src/hooks/review/writingCardForm/multiplceChoice/useUpdateMultipleChoiceAnswer.ts b/frontend/src/hooks/review/writingCardForm/multiplceChoice/useUpdateMultipleChoiceAnswer.ts new file mode 100644 index 000000000..6a1009114 --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/multiplceChoice/useUpdateMultipleChoiceAnswer.ts @@ -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([]); + + 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; diff --git a/frontend/src/hooks/review/writingCardForm/useCardSectionList.ts b/frontend/src/hooks/review/writingCardForm/useCardSectionList.ts new file mode 100644 index 000000000..edbd49c3b --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/useCardSectionList.ts @@ -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; diff --git a/frontend/src/hooks/review/writingCardForm/useCheckNextStepAvailability.ts b/frontend/src/hooks/review/writingCardForm/useCheckNextStepAvailability.ts new file mode 100644 index 000000000..41ec5dc96 --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/useCheckNextStepAvailability.ts @@ -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; diff --git a/frontend/src/hooks/review/writingCardForm/useMultipleChoice.ts b/frontend/src/hooks/review/writingCardForm/useMultipleChoice.ts deleted file mode 100644 index 472b326f9..000000000 --- a/frontend/src/hooks/review/writingCardForm/useMultipleChoice.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { useState } from 'react'; - -import { ReviewWritingAnswer, ReviewWritingCardQuestion } from '@/types'; - -interface UseMultipleChoiceProps { - question: ReviewWritingCardQuestion; - updateAnswerMap: (answer: ReviewWritingAnswer) => void; - updateAnswerValidationMap: (answer: ReviewWritingAnswer, isValidatedAnswer: boolean) => void; -} - -const useMultipleChoice = ({ question, updateAnswerMap, updateAnswerValidationMap }: UseMultipleChoiceProps) => { - const [selectedOptionList, setSelectedOptionList] = useState([]); - const [isOpenLimitGuide, setIsOpenLimitGuide] = useState(false); - - const handleCheckboxChange = (event: React.ChangeEvent) => { - const { id } = event.currentTarget; - // max를 넘어서는 선택하려 할 때 - if (isAboveSelectionLimit(Number(id))) { - return setIsOpenLimitGuide(true); - } - // max를 넘어서는 선택을 하지 않은 경우 - setIsOpenLimitGuide(false); - - const newSelectedOptionList = makeNewSelectedOptionList(event); - 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); - }; - - /** - * checkbox의 change 이벤트에 따라 새로운 selectedOptionList를 반환하는 함수 - */ - const makeNewSelectedOptionList = (event: React.ChangeEvent) => { - const { id, checked } = event.currentTarget; - const optionId = Number(id); - - 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 isMaxCheckedNumber = () => { - if (!question.optionGroup) return false; - return selectedOptionList.length >= question.optionGroup.maxCount; - }; - - /** - * 선택 가능한 문항 수를 넘어서 문항을 선택하려 하는지 여부 - */ - const isAboveSelectionLimit = (optionId: number) => !!(isMaxCheckedNumber() && !isSelectedCheckbox(optionId)); - - const isSelectedCheckbox = (optionId: number) => { - return selectedOptionList.includes(optionId); - }; - - return { - isOpenLimitGuide, - handleCheckboxChange, - isSelectedCheckbox, - }; -}; -export default useMultipleChoice; diff --git a/frontend/src/hooks/review/writingCardForm/useMutateReview/index.ts b/frontend/src/hooks/review/writingCardForm/useMutateReview/index.ts index 64cb038c6..686ab889c 100644 --- a/frontend/src/hooks/review/writingCardForm/useMutateReview/index.ts +++ b/frontend/src/hooks/review/writingCardForm/useMutateReview/index.ts @@ -4,13 +4,17 @@ import { postReviewApi } from '@/apis/review'; import { REVIEW_QUERY_KEYS } from '@/constants'; import { ReviewWritingFormResult } from '@/types'; -const useMutateReview = () => { +interface UseMutateReviewProps { + executeAfterMutateSuccess: () => void; +} +const useMutateReview = ({ executeAfterMutateSuccess }: UseMutateReviewProps) => { const queryClient = useQueryClient(); const reviewMutation = useMutation({ mutationFn: (formResult: ReviewWritingFormResult) => postReviewApi(formResult), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [REVIEW_QUERY_KEYS.postReview] }); + executeAfterMutateSuccess(); }, }); diff --git a/frontend/src/hooks/review/writingCardForm/useMutateReview/test.tsx b/frontend/src/hooks/review/writingCardForm/useMutateReview/test.tsx index 189c068a3..483a2e1cc 100644 --- a/frontend/src/hooks/review/writingCardForm/useMutateReview/test.tsx +++ b/frontend/src/hooks/review/writingCardForm/useMutateReview/test.tsx @@ -8,7 +8,9 @@ import useMutateReview from '.'; describe('리뷰 생성 요청 테스트', () => { test('성공적으로 리뷰를 생성한다.', async () => { - const { result } = renderHook(() => useMutateReview(), { wrapper: QueryClientWrapper }); + const { result } = renderHook(() => useMutateReview({ executeAfterMutateSuccess: () => {} }), { + wrapper: QueryClientWrapper, + }); act(() => { result.current.postReview(REVIEW_FORM_RESULT_DATA); diff --git a/frontend/src/hooks/review/writingCardForm/useQuestionList.ts b/frontend/src/hooks/review/writingCardForm/useQuestionList.ts deleted file mode 100644 index 23debddfe..000000000 --- a/frontend/src/hooks/review/writingCardForm/useQuestionList.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { ReviewWritingCardSection } from '@/types'; - -interface UseQuestionListProps { - questionListSectionsData: ReviewWritingCardSection[]; -} - -const useQuestionList = ({ questionListSectionsData }: UseQuestionListProps) => { - const [questionList, setQuestionList] = useState(null); - const [selectedCategory, setSelectedCategory] = useState(null); - - const updatedSelectedCategory = (newSelectedCategory: number[]) => { - setSelectedCategory(newSelectedCategory); - }; - - const updateQuestionList = () => { - const newQuestionList = questionListSectionsData.filter((data) => { - // 공통 질문 추출 - if (data.visible === 'ALWAYS') return true; - // 선택된 카테고리 답변과 data.onSelectedOptionId를 비교 - if (!data.onSelectedOptionId) return false; - return !!selectedCategory?.includes(data.onSelectedOptionId); - }); - setQuestionList(newQuestionList); - }; - - useEffect(() => { - updateQuestionList(); - }, [selectedCategory]); - - return { - questionList, - updatedSelectedCategory, - }; -}; -export default useQuestionList; diff --git a/frontend/src/hooks/review/writingCardForm/useResetFormRecoil.ts b/frontend/src/hooks/review/writingCardForm/useResetFormRecoil.ts new file mode 100644 index 000000000..eeff1783c --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/useResetFormRecoil.ts @@ -0,0 +1,21 @@ +import { useResetRecoilState } from 'recoil'; + +import { answerMapAtom, answerValidationMapAtom, selectedCategoryAtom } from '@/recoil'; +/** + * 리뷰 작성 페이지에서 사용하는 recoil 상태들을 초기화하는 훅 + */ +const useResetFormRecoil = () => { + const resetSelectedCategoryAtom = useResetRecoilState(selectedCategoryAtom); + const resetAnswerMapAtom = useResetRecoilState(answerMapAtom); + const resetAnswerValidationMapAtom = useResetRecoilState(answerValidationMapAtom); + + const resetFormRecoil = () => { + resetSelectedCategoryAtom(); + resetAnswerMapAtom(); + resetAnswerValidationMapAtom(); + }; + + return { resetFormRecoil }; +}; + +export default useResetFormRecoil; diff --git a/frontend/src/hooks/review/writingCardForm/useReviewerAnswer.ts b/frontend/src/hooks/review/writingCardForm/useReviewerAnswer.ts deleted file mode 100644 index b43595b39..000000000 --- a/frontend/src/hooks/review/writingCardForm/useReviewerAnswer.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { ReviewWritingAnswer, ReviewWritingCardSection } from '@/types'; - -interface UseReviewerAnswerProps { - currentCardIndex: number; - questionList: ReviewWritingCardSection[] | null; - updatedSelectedCategory: (newSelectedCategory: number[]) => void; -} - -const useReviewerAnswer = ({ currentCardIndex, questionList, updatedSelectedCategory }: UseReviewerAnswerProps) => { - const [answerMap, setAnswerMap] = useState>(); - const [answerValidationMap, SetAnswerValidationMap] = useState>(); - - useEffect(() => { - const newAnswerMap: Map = new Map(); - const newAnswerValidationMap: Map = new Map(); - questionList?.forEach((section) => { - section.questions.forEach((question) => { - const answer = answerMap?.get(question.questionId); - newAnswerMap.set(question.questionId, { - questionId: question.questionId, - selectedOptionIds: question.questionType === 'CHECKBOX' ? (answer?.selectedOptionIds ?? []) : null, - text: question.questionType === 'TEXT' ? (answer?.text ?? '') : null, - }); - newAnswerValidationMap.set( - question.questionId, - answerValidationMap?.get(question.questionId) ?? !question.required, - ); - }); - }); - setAnswerMap(newAnswerMap); - SetAnswerValidationMap(newAnswerValidationMap); - }, [questionList]); - - const [isAbleNextStep, setIsAbleNextStep] = useState(false); - - const isCategoryAnswer = (answer: ReviewWritingAnswer) => - answer.questionId === questionList?.[0].questions[0].questionId; - - const updateAnswerMap = (answer: ReviewWritingAnswer) => { - const newAnswerMap = new Map(answerMap); - newAnswerMap.set(answer.questionId, answer); - setAnswerMap(newAnswerMap); - - if (isCategoryAnswer(answer)) { - updatedSelectedCategory(answer.selectedOptionIds ?? []); - } - }; - - const updateAnswerValidationMap = (answer: ReviewWritingAnswer, isValidatedAnswer: boolean) => { - const newAnswerValidationMap = new Map(answerValidationMap); - newAnswerValidationMap.set(answer.questionId, isValidatedAnswer); - SetAnswerValidationMap(newAnswerValidationMap); - }; - - const isValidateAnswerList = () => { - if (!questionList) return false; - - return questionList[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 { - answerMap, - isAbleNextStep, - updateAnswerMap, - updateAnswerValidationMap, - }; -}; - -export default useReviewerAnswer; diff --git a/frontend/src/hooks/review/writingCardForm/useTextAnswer.ts b/frontend/src/hooks/review/writingCardForm/useTextAnswer.ts index 6143f3d6b..33a6d0f06 100644 --- a/frontend/src/hooks/review/writingCardForm/useTextAnswer.ts +++ b/frontend/src/hooks/review/writingCardForm/useTextAnswer.ts @@ -2,6 +2,8 @@ import { useState } from 'react'; import { ReviewWritingAnswer, ReviewWritingCardQuestion } from '@/types'; +import useUpdateReviewerAnswer from './useUpdateReviewerAnswer'; + const TEXT_ANSWER_LENGTH = { min: 20, max: 1000, @@ -9,10 +11,13 @@ const TEXT_ANSWER_LENGTH = { interface UseTextAnswerProps { question: ReviewWritingCardQuestion; - updateAnswerMap: (answer: ReviewWritingAnswer) => void; - updateAnswerValidationMap: (answer: ReviewWritingAnswer, isValidatedAnswer: boolean) => void; } -const useTextAnswer = ({ question, updateAnswerMap, updateAnswerValidationMap }: UseTextAnswerProps) => { +/** + * 하나의 주관식 질문에서 답변을 관리하는 훅 + */ +const useTextAnswer = ({ question }: UseTextAnswerProps) => { + const { updateAnswerMap, updateAnswerValidationMap } = useUpdateReviewerAnswer(); + const [textAnswer, setTextAnswer] = useState(''); // NOTE: change 시 마다 상태 변경되어서, 디바운스를 적용할 지 고민... diff --git a/frontend/src/hooks/review/writingCardForm/useUpdateDefaultAnswers.ts b/frontend/src/hooks/review/writingCardForm/useUpdateDefaultAnswers.ts new file mode 100644 index 000000000..1b6efae67 --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/useUpdateDefaultAnswers.ts @@ -0,0 +1,110 @@ +import { useEffect } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { answerMapAtom, answerValidationMapAtom, cardSectionListSelector } from '@/recoil'; +import { ReviewWritingAnswer, ReviewWritingCardQuestion } from '@/types'; + +const DEFAULT_VALUE = { + checkbox: [], + text: '', +}; +/** + * cardSectionListSelector(=리뷰 작성 페이지에서 리뷰이가 작성해야하는 질문지)가 변경되었을때, 이에 맞추어서 답변(answerMap)과 답변들의 유효성 여부(answerValidationMap)을 변경하는 훅 + */ +const useUpdateDefaultAnswers = () => { + const cardSectionList = useRecoilValue(cardSectionListSelector); + // NOTE : answerMap - 질문에 대한 답변들 , number : questionId + const [answerMap, setAnswerMap] = useRecoilState(answerMapAtom); + // NOTE : answerValidationMap -질문의 단볍들의 유효성 여부 ,number: questionId + const [answerValidationMap, setAnswerValidationMap] = useRecoilState(answerValidationMapAtom); + /* NOTE: 질문 변경 시, answerMap 변경 케이스 정리 + case1. 이전에 작성한 답이 있는 경우 : answerMap , answerValidation에 이를 반영 + case2. 이전에 작성한 답이 없는 경우 : answerMap, answerValidation에 기본값 설정 + - 서술형:'', 객관식 :[]*/ + /** + * 질문지 변경 시, 질문지에 맞는 새로운 answerMap, answerValidationMap을 반환하는 함수 + */ + const makeNewAnswerAndValidationMaps = () => { + const newAnswerMap: Map = new Map(); + const newAnswerValidationMap: Map = new Map(); + cardSectionList?.forEach((section) => { + section.questions.forEach((question) => { + updateNewAnswerMaps({ newAnswerMap, question }); + updateNewAnswerValidationMap({ newAnswerValidationMap, question }); + }); + }); + + return { newAnswerMap, newAnswerValidationMap }; + }; + interface UpdateAnswerValidationMapParams { + newAnswerValidationMap: Map; + question: ReviewWritingCardQuestion; + } + /** + * 변경된 질문지에 따라 생성될 새로운 answerValidationMap의 값을 변경하는 함수 + */ + const updateNewAnswerValidationMap = ({ newAnswerValidationMap, question }: UpdateAnswerValidationMapParams) => { + newAnswerValidationMap.set( + question.questionId, + answerValidationMap?.get(question.questionId) ?? !question.required, + ); + }; + + interface UpdateAnswerMapParams { + newAnswerMap: Map; + question: ReviewWritingCardQuestion; + } + /** + * 변경된 질문지에 따라 생성될 새로운 answerMap의 값을 변경하는 함수 + */ + const updateNewAnswerMaps = ({ newAnswerMap, question }: UpdateAnswerMapParams) => { + const answer = answerMap?.get(question.questionId); + if (answer) { + reflectExistingAnswerInMaps({ answer, newAnswerMap, question }); + return; + } + setInitialAnswerMap({ newAnswerMap, question }); + }; + /** + * 변경된 질문지에 대해 이전에 작성한 답변이 없는 경우, 답변 유형(객관식/서술형)에 따라 답변에 기본값을 설정하는 함수 + */ + const setInitialAnswerMap = ({ newAnswerMap, question }: UpdateAnswerMapParams) => { + newAnswerMap.set(question.questionId, { + questionId: question.questionId, + selectedOptionIds: question.questionType === 'CHECKBOX' ? DEFAULT_VALUE.checkbox : null, + text: question.questionType === 'TEXT' ? DEFAULT_VALUE.text : null, + }); + }; + interface ReflectExistingAnswerInMapsParams extends UpdateAnswerMapParams { + answer: ReviewWritingAnswer; + } + /** + * 변경된 질문지에 대해 이전에 작성한 답변이 있는 경우, 답변 유형(객관식/서술형)에 따라 답변에 기본값을 설정하는 함수 + */ + const reflectExistingAnswerInMaps = ({ answer, newAnswerMap, question }: ReflectExistingAnswerInMapsParams) => { + // 객관식 + if (question.questionType === 'CHECKBOX') { + newAnswerMap.set(question.questionId, { + questionId: question.questionId, + selectedOptionIds: answer.selectedOptionIds ?? DEFAULT_VALUE.checkbox, + text: null, + }); + } + // 서술형 + if (question.questionType === 'TEXT') { + newAnswerMap.set(question.questionId, { + questionId: question.questionId, + selectedOptionIds: null, + text: answer.text ?? DEFAULT_VALUE.text, + }); + } + }; + + useEffect(() => { + const { newAnswerMap, newAnswerValidationMap } = makeNewAnswerAndValidationMaps(); + setAnswerMap(newAnswerMap); + setAnswerValidationMap(newAnswerValidationMap); + }, [cardSectionList]); +}; + +export default useUpdateDefaultAnswers; diff --git a/frontend/src/hooks/review/writingCardForm/useUpdateReviewerAnswer.ts b/frontend/src/hooks/review/writingCardForm/useUpdateReviewerAnswer.ts new file mode 100644 index 000000000..f568bf266 --- /dev/null +++ b/frontend/src/hooks/review/writingCardForm/useUpdateReviewerAnswer.ts @@ -0,0 +1,49 @@ +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; + +import { answerMapAtom, answerValidationMapAtom, cardSectionListSelector, selectedCategoryAtom } from '@/recoil'; +import { ReviewWritingAnswer } from '@/types'; + +/** + * 리뷰어가 작성한 답변에 따라 answerMap ,answerValidationMap의 상태를 변경하는 핸들러는 반환하는 훅 + */ +const useUpdateReviewerAnswer = () => { + const cardSectionList = useRecoilValue(cardSectionListSelector); + const setSelectedCategory = useSetRecoilState(selectedCategoryAtom); + + const [answerMap, setAnswerMap] = useRecoilState(answerMapAtom); + const [answerValidationMap, setAnswerValidationMap] = useRecoilState(answerValidationMapAtom); + + const isCategoryAnswer = (answer: ReviewWritingAnswer) => + answer.questionId === cardSectionList?.[0].questions[0].questionId; + + /** + * 생성,수정,삭제된 답변을 answerMap에 반영하는 함수 + * @param answer : 반영한 답변 + */ + const updateAnswerMap = (answer: ReviewWritingAnswer) => { + const newAnswerMap = new Map(answerMap); + newAnswerMap.set(answer.questionId, answer); + setAnswerMap(newAnswerMap); + + if (isCategoryAnswer(answer)) { + setSelectedCategory(answer.selectedOptionIds ?? []); + } + }; + /** + * 변경된 답변에 따라 답변의 유효성 여부를 판단해 answerValidationMap에 반영하는 함수 + * @param answer : 변경된 답변 + * @param isValidatedAnswer: 변경된 답변의 유효성 + */ + const updateAnswerValidationMap = (answer: ReviewWritingAnswer, isValidatedAnswer: boolean) => { + const newAnswerValidationMap = new Map(answerValidationMap); + newAnswerValidationMap.set(answer.questionId, isValidatedAnswer); + setAnswerValidationMap(newAnswerValidationMap); + }; + + return { + updateAnswerMap, + updateAnswerValidationMap, + }; +}; + +export default useUpdateReviewerAnswer; diff --git a/frontend/src/mocks/mockData/writingCardForm/reviewWritingCardFormData.ts b/frontend/src/mocks/mockData/writingCardForm/reviewWritingCardFormData.ts index 9d791ac6a..b10ac05c7 100644 --- a/frontend/src/mocks/mockData/writingCardForm/reviewWritingCardFormData.ts +++ b/frontend/src/mocks/mockData/writingCardForm/reviewWritingCardFormData.ts @@ -3,317 +3,276 @@ import { ReviewWritingFrom } from '@/types'; export const REVIEW_REQUEST_CODE = 'ABCD1234'; export const REVIEW_WRITING_FORM_CARD_DATA: ReviewWritingFrom = { - formId: '1', - revieweeName: '김리뷰', - projectName: '리뷰미 프로젝트', + formId: 1, + revieweeName: 'bada', + projectName: 'bada', sections: [ - // 항상 보이는 Section { sectionId: 1, visible: 'ALWAYS', onSelectedOptionId: null, - header: '기억을 떠올려볼게요.', // 최종: `💡${REVIEWEE}와 함께 한 기억을 떠올려볼게요.`, + header: 'bada와 함께 한 기억을 떠올려볼게요.', questions: [ { questionId: 1, required: true, + content: '프로젝트 기간 동안, bada의 강점이 드러났던 순간을 선택해주세요.', questionType: 'CHECKBOX', - content: '프로젝트 기간동안 강점이 드러난 순간들을 골라주세요', - hasGuideline: false, - guideline: null, optionGroup: { optionGroupId: 1, minCount: 1, maxCount: 2, options: [ - { optionId: 1, content: '🗣️ 커뮤니케이션, 협업 능력 (ex: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, + { optionId: 1, content: '🗣️커뮤니케이션, 협업 능력 (ex: 팀원간의 원활한 정보 공유, 명확한 의사소통)' }, { optionId: 2, - content: '💡 문제 해결 능력 (ex: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)', + content: '💡문제 해결 능력 (ex: 프로젝트 중 만난 버그/오류를 분석하고 이를 해결하는 능력)', }, - { optionId: 3, content: '⏰ 시간 관리 능력 (ex: 일정과 마감 기한 준수, 업무의 우선 순위 분배)' }, - { optionId: 4, content: '🤓 기술적 역량, 전문 지식 (ex: 요구 사항을 이해하고 이를 구현하는 능력)' }, + { optionId: 3, content: '⏰시간 관리 능력 (ex: 일정과 마감 기한 준수, 업무의 우선 순위 분배)' }, + { optionId: 4, content: '💻기술적 역량, 전문 지식 (ex: 요구 사항을 이해하고 이를 구현하는 능력)' }, { optionId: 5, content: - '🌱 성장 마인드셋 (ex: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', + '🌱성장 마인드셋 (ex: 새로운 분야나 잘 모르는 분야에 도전하는 마음, 꾸준한 노력으로 프로젝트 이전보다 성장하는 모습)', }, ], }, + hasGuideline: false, + guideline: null, }, ], }, { - sectionId: 2, // 커뮤니케이션 + sectionId: 2, visible: 'CONDITIONAL', onSelectedOptionId: 1, - header: '이제 선택한 순간을 바탕으로 리뷰를 작성해볼게요', + header: '이제, 선택한 순간들을 바탕으로 bada에 대한 리뷰를 작성해볼게요.', questions: [ { questionId: 2, required: true, + content: '커뮤니케이션, 협업 능력에서 어떤 부분이 인상 깊었는지 선택해주세요.', questionType: 'CHECKBOX', - content: '어떤 커뮤니케이션/협업 능력이 인상깊었나요?', - hasGuideline: false, - guideline: null, optionGroup: { optionGroupId: 2, minCount: 1, - maxCount: 3, + maxCount: 7, options: [ { optionId: 6, content: '반대 의견을 내더라도 듣는 사람이 기분 나쁘지 않게 이야기해요.' }, { optionId: 7, content: '팀원들의 의견을 잘 모아서 회의가 매끄럽게 진행되도록 해요.' }, { optionId: 8, content: '팀의 분위기를 주도해요.' }, { optionId: 9, content: '주장을 이야기할 때에는 합당한 근거가 뒤따라요.' }, - { - optionId: 10, - content: '팀에게 필요한 것과 그렇지 않은 것을 잘 구분해요.', - }, - { - optionId: 11, - content: '팀 내 주어진 요구사항에 우선순위를 잘 매겨요.', - }, + { optionId: 10, content: '팀에게 필요한 것과 그렇지 않은 것을 잘 구분해요.' }, + { optionId: 11, content: '팀 내 주어진 요구사항에 우선순위를 잘 매겨요. (커뮤니케이션 능력을 특화하자)' }, { optionId: 12, content: '서로 다른 분야간의 소통도 중요하게 생각해요.' }, ], }, + hasGuideline: false, + guideline: null, }, { questionId: 3, required: true, + content: '위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요.', questionType: 'TEXT', - content: '인상깊은 상황을 이야기해주세요', optionGroup: null, hasGuideline: true, - guideline: `상황을 자세하게 기록할수록 리뷰에 도움이 돼요. 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요.`, + guideline: + '상황을 자세하게 기록할수록 bada에게 도움이 돼요. bada 덕분에 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요.', }, ], }, { - sectionId: 3, // 문제해결 능력 + sectionId: 3, visible: 'CONDITIONAL', onSelectedOptionId: 2, - header: '이제 선택한 순간을 바탕으로 리뷰를 작성해볼게요', + header: '이제, 선택한 순간들을 바탕으로 bada에 대한 리뷰를 작성해볼게요.', questions: [ { questionId: 4, required: true, + content: '문제해결 능력에서 어느 부분이 인상 깊었는지 선택해주세요.', questionType: 'CHECKBOX', - content: ' 어떤 문제 해결 능력이 인상 깊었는지 선택해주세요.', - hasGuideline: false, - guideline: null, optionGroup: { - optionGroupId: 2, + optionGroupId: 3, minCount: 1, - maxCount: 3, + maxCount: 8, options: [ - { optionId: 6, content: '큰 문제를 작은 단위로 쪼개서 단계별로 해결해나가요.' }, - { optionId: 7, content: '낯선 문제를 만나도 당황하지 않고 차분하게 풀어나가요.' }, - { optionId: 8, content: '문제 해결을 위해 GPT등의 자원을 적극적으로 활용해요.' }, + { optionId: 13, content: '큰 문제를 작은 단위로 쪼개서 단계별로 해결해나가요.' }, + { optionId: 14, content: '낯선 문제를 만나도 당황하지 않고 차분하게 풀어나가요.' }, + { optionId: 15, content: '문제 해결을 위해 GPT등의 자원을 적극적으로 활용해요.' }, { - optionId: 9, + optionId: 16, content: '문제를 해결한 뒤에도 재발 방지를 위한 노력을 기울여요. (예: 문서화, 테스트 케이스 추가 등)', }, + { optionId: 17, content: '문제의 원인을 적극적으로 탐구하고 해결해요. (예: 디버깅 툴의 적극적 활용 등)' }, + { optionId: 18, content: '어려운 문제를 만나도 피하지 않고 도전해요.' }, { - optionId: 10, - content: '문제의 원인을 적극적으로 탐구하고 해결해요. (예: 디버깅 툴의 적극적 활용 등)', - }, - { - optionId: 11, - content: '어려운 문제를 만나도 피하지 않고 도전해요.', - }, - { - optionId: 12, + optionId: 19, content: '문제를 해결하기 위해 타인과 의사소통을 할 수 있어요. (예: 팀원과 이슈 공유, 문제 상황 설명 등)', }, - { optionId: 13, content: '문제 원인과 해결책에 대한 가설을 세우고 직접 실험해봐요.' }, + { optionId: 20, content: '문제 원인과 해결책에 대한 가설을 세우고 직접 실험해봐요.' }, ], }, + hasGuideline: false, + guideline: null, }, { questionId: 5, required: true, + content: '위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요.', questionType: 'TEXT', - content: '인상깊은 상황을 이야기해주세요', optionGroup: null, hasGuideline: true, - guideline: `상황을 자세하게 기록할수록 리뷰에 도움이 돼요. 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요.`, + guideline: + '상황을 자세하게 기록할수록 bada에게 도움이 돼요. 어떤 문제 상황이 발생했고, bada이/가 어떻게 해결했는지 그 과정을 떠올려 보세요.', }, ], }, { - sectionId: 4, // 시간 관리 능력 + sectionId: 4, visible: 'CONDITIONAL', onSelectedOptionId: 3, - header: '이제 선택한 순간을 바탕으로 리뷰를 작성해볼게요', + header: '이제, 선택한 순간들을 바탕으로 bada에 대한 리뷰를 작성해볼게요.', questions: [ { questionId: 6, required: true, + content: '시간 관리 능력에서 어느 부분이 인상 깊었는지 선택해주세요.', questionType: 'CHECKBOX', - content: '어떤 시간 관리 능력이 인상 깊었는지 선택해주세요.', - hasGuideline: false, - guideline: null, optionGroup: { - optionGroupId: 2, + optionGroupId: 4, minCount: 1, - maxCount: 3, + maxCount: 5, options: [ - { optionId: 6, content: '큰 문제를 작은 단위로 쪼개서 단계별로 해결해나가요.' }, - { optionId: 7, content: '낯선 문제를 만나도 당황하지 않고 차분하게 풀어나가요.' }, - { optionId: 8, content: '문제 해결을 위해 GPT등의 자원을 적극적으로 활용해요.' }, - { - optionId: 9, - content: '문제를 해결한 뒤에도 재발 방지를 위한 노력을 기울여요. (예: 문서화, 테스트 케이스 추가 등)', - }, + { optionId: 21, content: '프로젝트의 일정과 주요 마일스톤을 설정하여 체계적으로 일정을 관리해요.' }, + { optionId: 22, content: '일정에 따라 마감 기한을 잘 지켜요.' }, { - optionId: 10, - content: '문제의 원인을 적극적으로 탐구하고 해결해요. (예: 디버깅 툴의 적극적 활용 등)', + optionId: 23, + content: '업무의 중요도와 긴급성을 고려하여 우선 순위를 정하고, 그에 따라 작업을 분배해요.', }, - { - optionId: 11, - content: '어려운 문제를 만나도 피하지 않고 도전해요.', - }, - { - optionId: 12, - content: - '문제를 해결하기 위해 타인과 의사소통을 할 수 있어요. (예: 팀원과 이슈 공유, 문제 상황 설명 등)', - }, - { optionId: 13, content: '문제 원인과 해결책에 대한 가설을 세우고 직접 실험해봐요.' }, + { optionId: 24, content: '예기치 않은 일정 변경에도 유연하게 대처해요.' }, + { optionId: 25, content: '회의 시간과 같은 약속된 시간을 잘 지켜요.' }, ], }, + hasGuideline: false, + guideline: null, }, { questionId: 7, required: true, + content: '위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요.', questionType: 'TEXT', - content: '인상깊은 상황을 이야기해주세요', optionGroup: null, hasGuideline: true, - guideline: `상황을 자세하게 기록할수록 리뷰에 도움이 돼요. 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요.`, + guideline: + '상황을 자세하게 기록할수록 bada에게 도움이 돼요. bada 덕분에 팀이 효율적으로 시간관리를 할 수 있었는지 떠올려 보세요.', }, ], }, { - sectionId: 5, // 기술적 역량, 전문 지식 능력 + sectionId: 5, visible: 'CONDITIONAL', onSelectedOptionId: 4, - header: '이제 선택한 순간을 바탕으로 리뷰를 작성해볼게요', + header: '이제, 선택한 순간들을 바탕으로 bada에 대한 리뷰를 작성해볼게요.', questions: [ { questionId: 8, required: true, + content: '기술 역량, 전문 지식에서 어떤 부분이 인상 깊었는지 선택해주세요.', questionType: 'CHECKBOX', - content: '어떤 기술적 능력이 인상 깊었는지 선택해주세요.', - hasGuideline: false, - guideline: null, optionGroup: { - optionGroupId: 2, + optionGroupId: 5, minCount: 1, - maxCount: 3, + maxCount: 12, options: [ - { optionId: 6, content: '큰 문제를 작은 단위로 쪼개서 단계별로 해결해나가요.' }, - { optionId: 7, content: '낯선 문제를 만나도 당황하지 않고 차분하게 풀어나가요.' }, - { optionId: 8, content: '문제 해결을 위해 GPT등의 자원을 적극적으로 활용해요.' }, - { - optionId: 9, - content: '문제를 해결한 뒤에도 재발 방지를 위한 노력을 기울여요. (예: 문서화, 테스트 케이스 추가 등)', - }, - { - optionId: 10, - content: '문제의 원인을 적극적으로 탐구하고 해결해요. (예: 디버깅 툴의 적극적 활용 등)', - }, - { - optionId: 11, - content: '어려운 문제를 만나도 피하지 않고 도전해요.', - }, - { - optionId: 12, - content: - '문제를 해결하기 위해 타인과 의사소통을 할 수 있어요. (예: 팀원과 이슈 공유, 문제 상황 설명 등)', - }, - { optionId: 13, content: '문제 원인과 해결책에 대한 가설을 세우고 직접 실험해봐요.' }, + { optionId: 26, content: '관련 언어 / 라이브러리 / 프레임워크 지식이 풍부해요.' }, + { optionId: 27, content: '인프라 지식이 풍부해요.' }, + { optionId: 28, content: 'CS 지식이 풍부해요.' }, + { optionId: 29, content: '코드 리뷰에서 중요한 개선점을 제안했어요.' }, + { optionId: 30, content: '리팩토링을 통해 전체 코드의 품질을 향상시켰어요.' }, + { optionId: 31, content: '복잡한 버그를 신속하게 찾고 해결했어요.' }, + { optionId: 32, content: '꼼꼼하게 테스트를 작성했어요.' }, + { optionId: 33, content: '처음 보는 기술을 빠르게 습득하여 팀 프로젝트에 적용했어요.' }, + { optionId: 34, content: '명확하고 자세한 기술 문서를 작성하여 팀의 이해를 도왔어요.' }, + { optionId: 35, content: '컨벤션을 잘 지키면서 클린 코드를 작성하려고 노력했어요.' }, + { optionId: 36, content: '성능 최적화에 기여했어요.' }, + { optionId: 37, content: '지속적인 학습과 공유를 통해 팀의 기술 수준을 높였어요.' }, ], }, + hasGuideline: false, + guideline: null, }, { questionId: 9, required: true, + content: '위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요.', questionType: 'TEXT', - content: '인상깊은 상황을 이야기해주세요', optionGroup: null, hasGuideline: true, - guideline: `상황을 자세하게 기록할수록 리뷰에 도움이 돼요. 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요.`, + guideline: + '상황을 자세하게 기록할수록 bada에게 도움이 돼요. bada 덕분에 기술적 역량, 전문 지식적으로 도움을 받은 경험을 떠올려 보세요.', }, ], }, { - sectionId: 6, // 성장 마인드 셋 + sectionId: 6, visible: 'CONDITIONAL', onSelectedOptionId: 5, - header: '이제 선택한 순간을 바탕으로 리뷰를 작성해볼게요', + header: '이제, 선택한 순간들을 바탕으로 bada에 대한 리뷰를 작성해볼게요.', questions: [ { questionId: 10, required: true, + content: '성장 마인드셋에서 어떤 부분이 인상 깊었는지 선택해주세요.', questionType: 'CHECKBOX', - content: '어떤 성장 마인드셋이 인상 깊었는지 선택해주세요.', - hasGuideline: false, - guideline: null, optionGroup: { - optionGroupId: 2, + optionGroupId: 6, minCount: 1, - maxCount: 3, + maxCount: 10, options: [ - { optionId: 6, content: '큰 문제를 작은 단위로 쪼개서 단계별로 해결해나가요.' }, - { optionId: 7, content: '낯선 문제를 만나도 당황하지 않고 차분하게 풀어나가요.' }, - { optionId: 8, content: '문제 해결을 위해 GPT등의 자원을 적극적으로 활용해요.' }, - { - optionId: 9, - content: '문제를 해결한 뒤에도 재발 방지를 위한 노력을 기울여요. (예: 문서화, 테스트 케이스 추가 등)', - }, - { - optionId: 10, - content: '문제의 원인을 적극적으로 탐구하고 해결해요. (예: 디버깅 툴의 적극적 활용 등)', - }, - { - optionId: 11, - content: '어려운 문제를 만나도 피하지 않고 도전해요.', - }, - { - optionId: 12, - content: - '문제를 해결하기 위해 타인과 의사소통을 할 수 있어요. (예: 팀원과 이슈 공유, 문제 상황 설명 등)', - }, - { optionId: 13, content: '문제 원인과 해결책에 대한 가설을 세우고 직접 실험해봐요.' }, + { optionId: 38, content: '어떤 상황에도 긍정적인 태도로 임해요.' }, + { optionId: 39, content: '주변 사람들한테 질문하는 것을 부끄러워하지 않아요.' }, + { optionId: 40, content: '어려움이 있어도 끝까지 해내요.' }, + { optionId: 41, content: '함께 성장하기 위해, 배운 내용을 다른 사람과 공유해요.' }, + { optionId: 42, content: '새로운 것을 두려워하지 않고 적극적으로 배워나가요.' }, + { optionId: 43, content: '이론적 학습에서 그치지 않고 직접 적용하려 노력해요.' }, + { optionId: 44, content: '다른 사람들과 비교하지 않고 본인만의 속도로 성장하는 법을 알고 있어요.' }, + { optionId: 45, content: '받은 피드백을 빠르게 수용해요.' }, + { optionId: 46, content: '회고를 통해 성장할 수 있는 방법을 스스로 탐색해요.' }, + { optionId: 47, content: '새로운 아이디어를 시도하고, 기존의 틀을 깨는 것을 두려워하지 않아요.' }, ], }, + hasGuideline: false, + guideline: null, }, { questionId: 11, required: true, + content: '위에서 선택한 사항에 대해 조금 더 자세히 설명해주세요.', questionType: 'TEXT', - content: '인상깊은 상황을 이야기해주세요', optionGroup: null, hasGuideline: true, - guideline: `상황을 자세하게 기록할수록 리뷰에 도움이 돼요. 팀이 원활한 소통을 이뤘거나, 함께 일하면서 배울 점이 있었는지 떠올려 보세요.`, + guideline: + '상황을 자세하게 기록할수록 bada에게 도움이 돼요. 인상깊었던 bada의 성장 마인드셋을 떠올려 보세요.', }, ], }, { sectionId: 7, visible: 'ALWAYS', - onSelectedOptionId: 6, - header: `성장을 위한 다음 목표를 설정해볼게요.`, + onSelectedOptionId: null, + header: 'bada의 성장을 도와주세요!', questions: [ { questionId: 12, required: true, + content: '앞으로의 성장을 위해서 bada이/가 어떤 목표를 설정하면 좋을까요?', questionType: 'TEXT', - content: '앞으로의 성장을 위해서 어떤 목표를 설정하면 좋을까요?', optionGroup: null, - hasGuideline: false, - guideline: null, + hasGuideline: true, + guideline: "어떤 점을 보완하면 좋을지와 함께 '이렇게 해보면 어떨까?'하는 간단한 솔루션을 제안해봐요.", }, ], }, @@ -321,13 +280,13 @@ export const REVIEW_WRITING_FORM_CARD_DATA: ReviewWritingFrom = { sectionId: 8, visible: 'ALWAYS', onSelectedOptionId: null, - header: `아직 전하지 못한 리뷰/응원을 적어보세요.`, + header: '리뷰를 더 하고 싶은 리뷰어를 위한 추가 리뷰!', questions: [ { questionId: 13, required: false, + content: 'bada에게 전하고 싶은 다른 리뷰가 있거나 응원의 말이 있다면 적어주세요.', questionType: 'TEXT', - content: '전하고 싶은 다른 리뷰가 있거나 응원의 말이 있다면 적어주세요.', optionGroup: null, hasGuideline: false, guideline: null, diff --git a/frontend/src/pages/LandingPage/components/ReviewGroupDataModal/index.tsx b/frontend/src/pages/LandingPage/components/ReviewGroupDataModal/index.tsx index c52621814..29a358edf 100644 --- a/frontend/src/pages/LandingPage/components/ReviewGroupDataModal/index.tsx +++ b/frontend/src/pages/LandingPage/components/ReviewGroupDataModal/index.tsx @@ -50,7 +50,7 @@ const ReviewGroupDataModal = ({ reviewRequestCode, closeModal }: URLModalProps) URL과 코드를 다른 곳에 저장해두었어요! diff --git a/frontend/src/pages/ReviewWritingCardFromPage/QnABox/index.tsx b/frontend/src/pages/ReviewWritingCardFromPage/QnABox/index.tsx deleted file mode 100644 index 544ff6740..000000000 --- a/frontend/src/pages/ReviewWritingCardFromPage/QnABox/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { CheckboxItem, LongReviewItem } from '@/components'; -import { useMultipleChoice, useTextAnswer } from '@/hooks'; -import { ReviewWritingAnswer, ReviewWritingCardQuestion } from '@/types'; - -import * as S from './style'; - -interface QnABoxProps { - question: ReviewWritingCardQuestion; - updateAnswerMap: (answer: ReviewWritingAnswer) => void; - updateAnswerValidationMap: (answer: ReviewWritingAnswer, isValidatedAnswer: boolean) => void; -} - -const QnABox = ({ question, updateAnswerMap, updateAnswerValidationMap }: QnABoxProps) => { - const { isOpenLimitGuide, handleCheckboxChange, isSelectedCheckbox } = useMultipleChoice({ - question, - updateAnswerMap, - updateAnswerValidationMap, - }); - - const { textAnswer, handleTextAnswerChange, TEXT_ANSWER_LENGTH } = useTextAnswer({ - question, - updateAnswerMap, - updateAnswerValidationMap, - }); - - const multipleGuideline = (() => { - const { optionGroup } = question; - if (!optionGroup) return; - - const { minCount, maxCount } = optionGroup; - - const isAllSelectAvailable = maxCount === optionGroup.options.length; - if (!maxCount || isAllSelectAvailable) return `(최소 ${minCount}개 이상)`; - - return `(${minCount}개 ~ ${maxCount}개)`; - })(); - - return ( - - - {question.content} - {question.required && *} - {question.questionType === 'TEXT' && ` (최소 ${TEXT_ANSWER_LENGTH.min}자 이상)`} - {multipleGuideline ?? ''} - - {question.guideline && {question.guideline}} - {/*객관식*/} - {question.questionType === 'CHECKBOX' && ( - <> - {question.optionGroup?.options.map((option) => ( - - ))} - - {isOpenLimitGuide &&

😅 최대 {question.optionGroup?.maxCount}개까지 선택가능해요.

} -
- - )} - - {/*서술형*/} - {question.questionType === 'TEXT' && ( - - )} -
- ); -}; - -export default QnABox; diff --git a/frontend/src/pages/ReviewWritingCardFromPage/CardForm/index.tsx b/frontend/src/pages/ReviewWritingCardFromPage/components/CardForm/index.tsx similarity index 70% rename from frontend/src/pages/ReviewWritingCardFromPage/CardForm/index.tsx rename to frontend/src/pages/ReviewWritingCardFromPage/components/CardForm/index.tsx index 80c14b951..adc7a66a0 100644 --- a/frontend/src/pages/ReviewWritingCardFromPage/CardForm/index.tsx +++ b/frontend/src/pages/ReviewWritingCardFromPage/components/CardForm/index.tsx @@ -1,24 +1,27 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router'; +import { useRecoilValue } from 'recoil'; -import { ConfirmModal, ProjectImg, AnswerListRecheckModal } from '@/components'; +import { ConfirmModal, AnswerListRecheckModal } from '@/components'; import { useCurrentCardIndex, useGetDataToWrite, useMutateReview, - useQuestionList, - useReviewerAnswer, + useCardSectionList, + useResetFormRecoil, useSearchParamAndQuery, useSlideWidthAndHeight, + useUpdateDefaultAnswers, } from '@/hooks'; import useModals from '@/hooks/useModals'; +import { answerMapAtom } from '@/recoil'; import { ReviewWritingFormResult } from '@/types'; import ReviewWritingCard from '../ReviewWritingCard'; import * as S from './styles'; -const PROJECT_IMAGE_SIZE = '5rem'; +// const PROJECT_IMAGE_SIZE = '5rem'; const INDEX_OFFSET = 1; const MODAL_KEYS = { confirm: 'CONFIRM', @@ -30,38 +33,38 @@ const CardForm = () => { paramKey: 'reviewRequestCode', }); - const { data } = useGetDataToWrite({ reviewRequestCode }); - const { revieweeName, projectName } = data; - const { currentCardIndex, handleCurrentCardIndex } = useCurrentCardIndex(); const { wrapperRef, slideWidth, slideHeight, makeId } = useSlideWidthAndHeight({ currentCardIndex }); + // 질문지 생성 + const { data } = useGetDataToWrite({ reviewRequestCode }); + const { revieweeName, projectName } = data; + const { cardSectionList } = useCardSectionList({ cardSectionListData: data.sections }); - const { questionList, updatedSelectedCategory } = useQuestionList({ questionListSectionsData: data.sections }); + // 답변 + // 생성된 질문지를 바탕으로 답변 기본값 및 답변의 유효성 기본값 설정 + useUpdateDefaultAnswers(); + const answerMap = useRecoilValue(answerMapAtom); + + const { resetFormRecoil } = useResetFormRecoil(); - const { answerMap, isAbleNextStep, updateAnswerMap, updateAnswerValidationMap } = useReviewerAnswer({ - currentCardIndex, - questionList, - updatedSelectedCategory, - }); const { isOpen, openModal, closeModal } = useModals(); + const navigate = useNavigate(); - const { postReview, isSuccess } = useMutateReview(); + const executeAfterMutateSuccess = () => { + navigate('/user/review-writing-complete'); + closeModal(MODAL_KEYS.confirm); + }; + const { postReview } = useMutateReview({ executeAfterMutateSuccess }); const handleSubmitButtonClick = () => { openModal(MODAL_KEYS.confirm); }; - useEffect(() => { - if (isSuccess) { - navigate('/user/review-writing-complete'); - closeModal(MODAL_KEYS.confirm); - } - }, [isSuccess]); - const submitAnswer = async () => { if (!answerMap || !reviewRequestCode) return; + const result: ReviewWritingFormResult = { reviewRequestCode: reviewRequestCode, answers: Array.from(answerMap.values()), @@ -73,11 +76,18 @@ const CardForm = () => { openModal(MODAL_KEYS.recheck); }; + useEffect(() => { + return () => { + // 페이지 나갈때 관련 recoil 상태 초기화 + resetFormRecoil(); + }; + }, []); + return ( <> - + {/* 현재 프로젝트가 깃헙 연동이 아니라서 주석 처리 */} {projectName}

@@ -86,17 +96,14 @@ const CardForm = () => { - {questionList?.map((section, index) => ( + {cardSectionList?.map((section, index) => ( @@ -117,9 +124,9 @@ const CardForm = () => { )} - {isOpen(MODAL_KEYS.recheck) && questionList && answerMap && ( + {isOpen(MODAL_KEYS.recheck) && cardSectionList && answerMap && ( closeModal(MODAL_KEYS.recheck)} /> diff --git a/frontend/src/pages/ReviewWritingCardFromPage/CardForm/styles.ts b/frontend/src/pages/ReviewWritingCardFromPage/components/CardForm/styles.ts similarity index 98% rename from frontend/src/pages/ReviewWritingCardFromPage/CardForm/styles.ts rename to frontend/src/pages/ReviewWritingCardFromPage/components/CardForm/styles.ts index 05e7fc47b..317f31dbe 100644 --- a/frontend/src/pages/ReviewWritingCardFromPage/CardForm/styles.ts +++ b/frontend/src/pages/ReviewWritingCardFromPage/components/CardForm/styles.ts @@ -4,6 +4,7 @@ export const CardForm = styled.div` position: relative; overflow: hidden; width: ${({ theme }) => theme.formWidth}; + overflow-wrap: break-word; `; interface SlideContainerProps { diff --git a/frontend/src/pages/ReviewWritingCardFromPage/CardSliderController/index.tsx b/frontend/src/pages/ReviewWritingCardFromPage/components/CardSliderController/index.tsx similarity index 85% rename from frontend/src/pages/ReviewWritingCardFromPage/CardSliderController/index.tsx rename to frontend/src/pages/ReviewWritingCardFromPage/components/CardSliderController/index.tsx index ccf22f413..896e43981 100644 --- a/frontend/src/pages/ReviewWritingCardFromPage/CardSliderController/index.tsx +++ b/frontend/src/pages/ReviewWritingCardFromPage/components/CardSliderController/index.tsx @@ -54,13 +54,15 @@ const SubmitButton = ({ isAbleNextStep, handleSubmitButtonClick }: SubmitButtonP ); }; interface RecheckButton { + isAbleNextStep: boolean; handleRecheckButtonClick: () => void; } -const RecheckButton = ({ handleRecheckButtonClick }: RecheckButton) => { +const RecheckButton = ({ isAbleNextStep, handleRecheckButtonClick }: RecheckButton) => { + const styledType: ButtonStyleType = isAbleNextStep ? 'secondary' : 'disabled'; return ( - ); }; diff --git a/frontend/src/pages/ReviewWritingCardFromPage/components/MultipleChoiceQuestion/index.tsx b/frontend/src/pages/ReviewWritingCardFromPage/components/MultipleChoiceQuestion/index.tsx new file mode 100644 index 000000000..07f23664f --- /dev/null +++ b/frontend/src/pages/ReviewWritingCardFromPage/components/MultipleChoiceQuestion/index.tsx @@ -0,0 +1,62 @@ +import { CheckboxItem } from '@/components'; +import { useMultipleChoice } from '@/hooks'; +import useModals from '@/hooks/useModals'; +import { ReviewWritingCardQuestion } from '@/types'; + +import * as S from './style'; + +interface MultipleChoiceQuestionProps { + question: ReviewWritingCardQuestion; +} + +const MODAL_KEY = { + confirm: 'CONFIRM', +}; + +const MultipleChoiceQuestion = ({ question }: MultipleChoiceQuestionProps) => { + const { isOpen, openModal, closeModal } = useModals(); + + const handleModalOpen = (isOpen: boolean) => { + isOpen ? openModal(MODAL_KEY.confirm) : closeModal(MODAL_KEY.confirm); + }; + + const { isOpenLimitGuide, handleCheckboxChange, isSelectedCheckbox, unCheckTargetOption } = useMultipleChoice({ + question, + handleModalOpen, + }); + + const handleModalCancelButtonClick = () => { + closeModal(MODAL_KEY.confirm); + }; + + const handleModalConfirmButtonClick = () => { + unCheckTargetOption(); + closeModal(MODAL_KEY.confirm); + }; + + return ( + <> + {question.optionGroup?.options.map((option) => ( + + ))} + + {isOpenLimitGuide &&

😅 최대 {question.optionGroup?.maxCount}개까지 선택가능해요.

} + + {isOpen(MODAL_KEY.confirm) && ( +
+ + +
+ )} + + ); +}; + +export default MultipleChoiceQuestion; diff --git a/frontend/src/pages/ReviewWritingCardFromPage/components/MultipleChoiceQuestion/style.ts b/frontend/src/pages/ReviewWritingCardFromPage/components/MultipleChoiceQuestion/style.ts new file mode 100644 index 000000000..b5d5a1e1e --- /dev/null +++ b/frontend/src/pages/ReviewWritingCardFromPage/components/MultipleChoiceQuestion/style.ts @@ -0,0 +1,12 @@ +import styled from '@emotion/styled'; + +export const LimitGuideMessage = styled.div` + width: fit-content; + height: 2.4rem; + margin-top: 2.5rem; + p { + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + color: ${({ theme }) => theme.colors.primary}; + border-radius: ${({ theme }) => theme.borderRadius.basic}; + } +`; diff --git a/frontend/src/pages/ReviewWritingCardFromPage/components/QnABox/index.tsx b/frontend/src/pages/ReviewWritingCardFromPage/components/QnABox/index.tsx new file mode 100644 index 000000000..18b3b3b81 --- /dev/null +++ b/frontend/src/pages/ReviewWritingCardFromPage/components/QnABox/index.tsx @@ -0,0 +1,60 @@ +import { LongReviewItem } from '@/components'; +import { useTextAnswer } from '@/hooks'; +import { ReviewWritingCardQuestion } from '@/types'; + +import MultipleChoiceQuestion from '../MultipleChoiceQuestion'; + +import * as S from './style'; + +interface QnABoxProps { + question: ReviewWritingCardQuestion; +} +/** + * 하나의 질문과 그에 대한 답을 관리 + */ + +const QnABox = ({ question }: QnABoxProps) => { + /** + * 객관식 문항의 최소,최대 개수에 대한 안내 문구 + */ + const multipleLGuideline = (() => { + const { optionGroup } = question; + if (!optionGroup) return; + + const { minCount, maxCount } = optionGroup; + + const isAllSelectAvailable = maxCount === optionGroup.options.length; + if (!maxCount || isAllSelectAvailable) return `(최소 ${minCount}개 이상)`; + + return `(${minCount}개 ~ ${maxCount}개)`; + })(); + + const { textAnswer, handleTextAnswerChange, TEXT_ANSWER_LENGTH } = useTextAnswer({ + question, + }); + + return ( + + + {question.content} + {question.required ? * : (선택) } + {multipleLGuideline ?? ''} + + {question.guideline && {question.guideline}} + {/*객관식*/} + {question.questionType === 'CHECKBOX' && } + {/*서술형*/} + {question.questionType === 'TEXT' && ( + + )} + + ); +}; + +export default QnABox; diff --git a/frontend/src/pages/ReviewWritingCardFromPage/QnABox/style.ts b/frontend/src/pages/ReviewWritingCardFromPage/components/QnABox/style.ts similarity index 71% rename from frontend/src/pages/ReviewWritingCardFromPage/QnABox/style.ts rename to frontend/src/pages/ReviewWritingCardFromPage/components/QnABox/style.ts index cb1b4d94e..691334593 100644 --- a/frontend/src/pages/ReviewWritingCardFromPage/QnABox/style.ts +++ b/frontend/src/pages/ReviewWritingCardFromPage/components/QnABox/style.ts @@ -3,16 +3,6 @@ import styled from '@emotion/styled'; export const QnASection = styled.section` margin-bottom: 2rem; `; -export const LimitGuideMessage = styled.div` - width: fit-content; - height: 2.4rem; - margin-top: 2.5rem; - p { - font-weight: ${({ theme }) => theme.fontWeight.semibold}; - color: ${({ theme }) => theme.colors.primary}; - border-radius: ${({ theme }) => theme.borderRadius.basic}; - } -`; export const QuestionGuideline = styled.p` margin-bottom: 2rem; diff --git a/frontend/src/pages/ReviewWritingCardFromPage/ReviewWritingCard/index.tsx b/frontend/src/pages/ReviewWritingCardFromPage/components/ReviewWritingCard/index.tsx similarity index 67% rename from frontend/src/pages/ReviewWritingCardFromPage/ReviewWritingCard/index.tsx rename to frontend/src/pages/ReviewWritingCardFromPage/components/ReviewWritingCard/index.tsx index 37bd7bc71..916091cbc 100644 --- a/frontend/src/pages/ReviewWritingCardFromPage/ReviewWritingCard/index.tsx +++ b/frontend/src/pages/ReviewWritingCardFromPage/components/ReviewWritingCard/index.tsx @@ -1,16 +1,15 @@ -import { ReviewWritingAnswer, ReviewWritingCardSection } from '@/types'; +import { useCheckNextStepAvailability } from '@/hooks'; +import { ReviewWritingCardSection } from '@/types'; import QnABox from '../QnABox'; import CardSliderController, { CardSliderControllerProps } from './../CardSliderController/index'; import * as S from './style'; -interface ReviewWritingCardProps extends CardSliderControllerProps { +interface ReviewWritingCardProps extends Omit { cardIndex: number; isLastCard: boolean; cardSection: ReviewWritingCardSection; - updateAnswerMap: (answer: ReviewWritingAnswer) => void; - updateAnswerValidationMap: (answer: ReviewWritingAnswer, isValidatedAnswer: boolean) => void; } const ReviewWritingCard = ({ @@ -18,24 +17,17 @@ const ReviewWritingCard = ({ currentCardIndex, isLastCard, cardSection, - isAbleNextStep, handleCurrentCardIndex, - updateAnswerMap, - updateAnswerValidationMap, handleRecheckButtonClick, handleSubmitButtonClick, }: ReviewWritingCardProps) => { + const { isAbleNextStep } = useCheckNextStepAvailability({ currentCardIndex }); return ( {cardSection.header} {cardSection.questions.map((question) => ( - + ))} @@ -47,7 +39,10 @@ const ReviewWritingCard = ({ )} {isLastCard ? ( <> - + theme.fontSize.mediumSmall}; diff --git a/frontend/src/pages/ReviewWritingCardFromPage/index.tsx b/frontend/src/pages/ReviewWritingCardFromPage/index.tsx index cebf6f7fe..2bf9cca99 100644 --- a/frontend/src/pages/ReviewWritingCardFromPage/index.tsx +++ b/frontend/src/pages/ReviewWritingCardFromPage/index.tsx @@ -1,7 +1,7 @@ import { ErrorSuspenseContainer } from '@/components'; -import CardForm from './CardForm'; -// TODO : API 연결 후 ReviewWriting 폴더 삭제 +import CardForm from './components/CardForm'; + const ReviewWritingCardFormPage = () => { return ( diff --git a/frontend/src/recoil/index.ts b/frontend/src/recoil/index.ts index c66b16060..5bc85b72a 100644 --- a/frontend/src/recoil/index.ts +++ b/frontend/src/recoil/index.ts @@ -1,2 +1,3 @@ export * from './groupAccessCode'; export * from './keys'; +export * from './reviewWritingForm'; diff --git a/frontend/src/recoil/keys/index.ts b/frontend/src/recoil/keys/index.ts index c326469fe..b8e0489f8 100644 --- a/frontend/src/recoil/keys/index.ts +++ b/frontend/src/recoil/keys/index.ts @@ -1,3 +1,15 @@ export const ATOM_KEY = { groupAccessCodeAtom: 'groupAccessCodeAtom', + reviewWritingForm: { + sectionList: 'reviewWritingFormSectionList', + selectedCategoryAtom: 'selectedCategoryAtom', + answerMapAtom: 'answerMapAtom', + answerValidationMapAtom: 'answerValidationMapAtom', + }, +}; + +export const SELECTOR_KEY = { + reviewWritingForm: { + cardSectionListSelector: 'cardSectionListSelector', + }, }; diff --git a/frontend/src/recoil/reviewWritingForm/atom.ts b/frontend/src/recoil/reviewWritingForm/atom.ts new file mode 100644 index 000000000..b9cd73dc5 --- /dev/null +++ b/frontend/src/recoil/reviewWritingForm/atom.ts @@ -0,0 +1,38 @@ +import { atom } from 'recoil'; + +import { ReviewWritingAnswer, ReviewWritingCardSection } from '@/types'; + +import { ATOM_KEY } from '../keys'; +/** + * 서버에서 내려준 리뷰 작성 폼 데이터 중, sections + */ +export const reviewWritingFormSectionListAtom = atom({ + key: ATOM_KEY.reviewWritingForm.sectionList, + default: [], // 초기 상태는 빈 배열로 설정합니다. +}); +/** + * 카테고리 질문에서 선택한 객관식 문항의 optionId + */ +export const selectedCategoryAtom = atom({ + key: ATOM_KEY.reviewWritingForm.selectedCategoryAtom, + default: [], +}); + +/** + * 질문에 대한 답변들 + * number : questionId + */ + +export const answerMapAtom = atom | null>({ + key: ATOM_KEY.reviewWritingForm.answerMapAtom, + default: null, +}); + +/** + * 질문의 단볍들의 유효성 여부 + * number: questionId + */ +export const answerValidationMapAtom = atom | null>({ + key: ATOM_KEY.reviewWritingForm.answerValidationMapAtom, + default: null, +}); diff --git a/frontend/src/recoil/reviewWritingForm/index.ts b/frontend/src/recoil/reviewWritingForm/index.ts new file mode 100644 index 000000000..2d949952a --- /dev/null +++ b/frontend/src/recoil/reviewWritingForm/index.ts @@ -0,0 +1,2 @@ +export * from './atom'; +export * from './selector'; diff --git a/frontend/src/recoil/reviewWritingForm/selector.ts b/frontend/src/recoil/reviewWritingForm/selector.ts new file mode 100644 index 000000000..726a89776 --- /dev/null +++ b/frontend/src/recoil/reviewWritingForm/selector.ts @@ -0,0 +1,26 @@ +import { selector } from 'recoil'; + +import { ReviewWritingCardSection } from '@/types'; + +import { SELECTOR_KEY } from '../keys'; + +import { reviewWritingFormSectionListAtom, selectedCategoryAtom } from './atom'; +/** + * 서버에서 내려준 질문지 데이터에서, 공통 질문과 카테고리 선택된 질문을 뽑아서 만든 질문지 + * */ +export const cardSectionListSelector = selector({ + key: SELECTOR_KEY.reviewWritingForm.cardSectionListSelector, + get: ({ get }) => { + const sectionList = get(reviewWritingFormSectionListAtom); + const selectedCategory = get(selectedCategoryAtom); + + return sectionList.filter((data) => { + // 공통 질문 차출 + if (data.visible === 'ALWAYS') return true; + // 카테고리에서 선택된 질문 차출 + if (!data.onSelectedOptionId) return false; + + return !!selectedCategory?.includes(data.onSelectedOptionId); + }); + }, +}); diff --git a/frontend/src/types/review.ts b/frontend/src/types/review.ts index 2da2de958..6bd5b317e 100644 --- a/frontend/src/types/review.ts +++ b/frontend/src/types/review.ts @@ -90,7 +90,7 @@ export interface Category { // 리뷰 작성 카드 관련 타입들 export interface ReviewWritingFrom { - formId: string; + formId: number; revieweeName: string; projectName: string; sections: ReviewWritingCardSection[];