diff --git a/frontend/src/hooks/useAlertModal.ts b/frontend/src/hooks/useAlertModal.ts new file mode 100644 index 000000000..aaa276031 --- /dev/null +++ b/frontend/src/hooks/useAlertModal.ts @@ -0,0 +1,16 @@ +import { useState } from 'react'; + +const useAlertModal = () => { + const [isAlertModalOpen, setIsAlertModalOpen] = useState(false); + + const openAlertModal = () => setIsAlertModalOpen(true); + const closeAlertModal = () => setIsAlertModalOpen(false); + + return { + isAlertModalOpen, + openAlertModal, + closeAlertModal, + }; +}; + +export default useAlertModal; diff --git a/frontend/src/hooks/useConfirmModal.ts b/frontend/src/hooks/useConfirmModal.ts new file mode 100644 index 000000000..172b337e3 --- /dev/null +++ b/frontend/src/hooks/useConfirmModal.ts @@ -0,0 +1,16 @@ +import { useState } from 'react'; + +const useConfirmModal = () => { + const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); + + const openConfirmModal = () => setIsConfirmModalOpen(true); + const closeConfirmModal = () => setIsConfirmModalOpen(false); + + return { + isConfirmModalOpen, + openConfirmModal, + closeConfirmModal, + }; +}; + +export default useConfirmModal; diff --git a/frontend/src/hooks/useErrorModal.ts b/frontend/src/hooks/useErrorModal.ts new file mode 100644 index 000000000..84d1e9aac --- /dev/null +++ b/frontend/src/hooks/useErrorModal.ts @@ -0,0 +1,25 @@ +import { useState } from 'react'; + +const useErrorModal = () => { + const [isErrorModalOpen, setIsErrorModalOpen] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const openErrorModal = (errorMessage: string) => { + setErrorMessage(errorMessage); + setIsErrorModalOpen(true); + }; + + const closeErrorModal = () => { + setErrorMessage(''); + setIsErrorModalOpen(false); + }; + + return { + isErrorModalOpen, + errorMessage, + openErrorModal, + closeErrorModal, + }; +}; + +export default useErrorModal; diff --git a/frontend/src/hooks/useReviewForm.ts b/frontend/src/hooks/useReviewForm.ts new file mode 100644 index 000000000..adf0d819b --- /dev/null +++ b/frontend/src/hooks/useReviewForm.ts @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; + +import { getDataToWriteReviewApi } from '@/apis/review'; +import { REVIEW } from '@/constants'; +import { Keyword, ReviewContent, WritingReviewInfoData } from '@/types'; + +interface UseReviewFormProps { + reviewRequestCode: string; + openErrorModal: (errorMessage: string) => void; +} + +const useReviewForm = ({ reviewRequestCode, openErrorModal }: UseReviewFormProps) => { + const [dataToWrite, setDataToWrite] = useState(null); + const [answers, setAnswers] = useState([]); + const [selectedKeywords, setSelectedKeywords] = useState([]); + + const isValidAnswersLength = !answers.some((id) => id.answer.length < REVIEW.answerMinLength); + const isValidKeywordSelection = + selectedKeywords.length >= REVIEW.keywordMinCount && selectedKeywords.length <= REVIEW.keywordMaxCount; + const isValidForm = isValidAnswersLength && isValidKeywordSelection; + + useEffect(() => { + const getDataToWrite = async () => { + const data = await getDataToWriteReviewApi(reviewRequestCode); + setDataToWrite(data); + setAnswers(data.questions.map((question) => ({ questionId: question.id, answer: '' }))); + }; + + getDataToWrite(); + }, [reviewRequestCode]); + + const handleAnswerChange = (questionId: number, value: string) => { + setAnswers((prev) => + prev.map((answer) => (answer.questionId === questionId ? { ...answer, answer: value } : answer)), + ); + }; + + const handleKeywordButtonClick = (keyword: Keyword) => { + if (selectedKeywords.length === REVIEW.keywordMaxCount && !selectedKeywords.includes(keyword.id)) { + openErrorModal('키워드는 최대 5개까지 선택할 수 있어요.'); + return; + } + + setSelectedKeywords((prev) => + prev.includes(keyword.id) ? selectedKeywords.filter((id) => id !== keyword.id) : [...prev, keyword.id], + ); + }; + + return { + dataToWrite, + answers, + selectedKeywords, + isValidForm, + handleAnswerChange, + handleKeywordButtonClick, + }; +}; + +export default useReviewForm; diff --git a/frontend/src/hooks/useReviewRequestCode.ts b/frontend/src/hooks/useReviewRequestCode.ts new file mode 100644 index 000000000..69a61d270 --- /dev/null +++ b/frontend/src/hooks/useReviewRequestCode.ts @@ -0,0 +1,11 @@ +import { useLocation } from 'react-router'; + +const useReviewRequestCode = () => { + const location = useLocation(); + const params = location.pathname.split('/'); + const reviewRequestCode = params.slice(-1).toString(); + + return { reviewRequestCode }; +}; + +export default useReviewRequestCode; diff --git a/frontend/src/pages/ReviewWriting/index.tsx b/frontend/src/pages/ReviewWriting/index.tsx index daaec753f..05e37c665 100644 --- a/frontend/src/pages/ReviewWriting/index.tsx +++ b/frontend/src/pages/ReviewWriting/index.tsx @@ -1,13 +1,15 @@ -import { useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router'; +import { useNavigate } from 'react-router'; -import { getDataToWriteReviewApi, postReviewApi } from '@/apis/review'; +import { postReviewApi } from '@/apis/review'; // import ClockLogo from '@/assets/clock.svg'; import GithubLogoIcon from '@/assets/githubLogo.svg'; import { ConfirmModal, ErrorAlertModal } from '@/components'; import Button from '@/components/common/Button'; -import { REVIEW } from '@/constants/review'; -import { Keyword, ReviewContent, ReviewData, WritingReviewInfoData } from '@/types'; +import useConfirmModal from '@/hooks/useConfirmModal'; +import useErrorModal from '@/hooks/useErrorModal'; +import useReviewForm from '@/hooks/useReviewForm'; +import useReviewRequestCode from '@/hooks/useReviewRequestCode'; +import { ReviewData } from '@/types'; import LoadingPage from '../LoadingPage'; @@ -22,53 +24,17 @@ const SUBMIT_CONFIRM_MESSAGE = `리뷰를 제출할까요? const ReviewWritingPage = () => { const navigate = useNavigate(); - const [dataToWrite, setDataToWrite] = useState(null); - const [answers, setAnswers] = useState([]); - const [selectedKeywords, setSelectedKeywords] = useState([]); + const { reviewRequestCode } = useReviewRequestCode(); - const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); - const [isErrorModalOpen, setIsErrorModalOpen] = useState(false); + const { isConfirmModalOpen, openConfirmModal, closeConfirmModal } = useConfirmModal(); + const { isErrorModalOpen, errorMessage, openErrorModal, closeErrorModal } = useErrorModal(); - const isValidAnswersLength = !answers.some((id) => id.answer.length < REVIEW.answerMinLength); - const isValidKeywordSelection = - selectedKeywords.length >= REVIEW.keywordMinCount && selectedKeywords.length <= REVIEW.keywordMaxCount; - const isValidForm = isValidAnswersLength && isValidKeywordSelection; - - const location = useLocation(); - const params = location.pathname.split('/'); - const reviewRequestCode = params.slice(-1).toString(); - - useEffect(() => { - const getDataToWrite = async () => { - const data = await getDataToWriteReviewApi(reviewRequestCode); - setDataToWrite(data); - setAnswers(data.questions.map((question) => ({ questionId: question.id, answer: '' }))); - }; - - getDataToWrite(); - }, []); - - const handleAnswerChange = (questionId: number, value: string) => { - setAnswers((prev) => - prev.map((answer) => (answer.questionId === questionId ? { ...answer, answer: value } : answer)), - ); - }; - - const handleKeywordButtonClick = (keyword: Keyword) => { - if (selectedKeywords.length === REVIEW.keywordMaxCount && !selectedKeywords.includes(keyword.id)) { - alert('키워드는 최대 5개까지 선택할 수 있어요.'); - return; - } - - setSelectedKeywords((prev) => - prev.includes(keyword.id) ? selectedKeywords.filter((id) => id !== keyword.id) : [...prev, keyword.id], - ); - }; + const { dataToWrite, answers, selectedKeywords, isValidForm, handleAnswerChange, handleKeywordButtonClick } = + useReviewForm({ reviewRequestCode, openErrorModal }); const handleClickSubmitButton = async (event: React.FormEvent) => { event.preventDefault(); - - setIsConfirmModalOpen(true); + openConfirmModal(); }; const handleSubmitReview = async () => { @@ -80,11 +46,11 @@ const ReviewWritingPage = () => { try { await postReviewApi({ reviewData }); - setIsConfirmModalOpen(false); + closeConfirmModal(); navigate('/user/review-writing-complete', { replace: true }); } catch (error) { - setIsConfirmModalOpen(false); - setIsErrorModalOpen(true); + closeConfirmModal(); + openErrorModal('리뷰를 제출할 수 없어요.'); } }; @@ -149,8 +115,8 @@ const ReviewWritingPage = () => { {isConfirmModalOpen && ( setIsConfirmModalOpen(false) }} - handleClose={() => setIsConfirmModalOpen(false)} + cancelButton={{ type: 'secondary', text: '취소', handleClick: closeConfirmModal }} + handleClose={closeConfirmModal} isClosableOnBackground={true} > {SUBMIT_CONFIRM_MESSAGE} @@ -158,9 +124,9 @@ const ReviewWritingPage = () => { )} {isErrorModalOpen && ( setIsErrorModalOpen(false) }} - handleClose={() => setIsErrorModalOpen(false)} + errorText={errorMessage} + closeButton={{ content: '닫기', type: 'primary', handleClick: closeErrorModal }} + handleClose={closeErrorModal} /> )}