diff --git a/public/index.html b/public/index.html index 9991b105..d0fdcb02 100644 --- a/public/index.html +++ b/public/index.html @@ -17,7 +17,10 @@ /> - + - + }> + + ); diff --git a/src/commons/apis/usePostFetch.ts b/src/commons/apis/usePostFetch.ts index 4ae61309..050aaf10 100644 --- a/src/commons/apis/usePostFetch.ts +++ b/src/commons/apis/usePostFetch.ts @@ -3,7 +3,7 @@ import { useState } from 'react'; const usePostFetch = (url: string, options: object) => { const [data, setData] = useState>(); const [error, setError] = useState>(); - const [postStatusCode, setStatusCode] = useState(); + const [postStatusCode, setStatusCode] = useState(200); const [postloading, setLoading] = useState(false); const postData = async () => { diff --git a/src/commons/modals/TextGuideModal.tsx b/src/commons/modals/TextGuideModal.tsx new file mode 100644 index 00000000..ea624087 --- /dev/null +++ b/src/commons/modals/TextGuideModal.tsx @@ -0,0 +1,27 @@ +interface TextGuideModalProps { + open: boolean; + text: string; + onClose: () => void; +} + +const TextGuideModal = ({ text, open, onClose }: TextGuideModalProps) => { + if (!open) { + return null; + } + + return ( + + + {text} + + 확인 + + + + ); +}; + +export default TextGuideModal; diff --git a/src/layouts/ErrorBoundary.tsx b/src/layouts/ErrorBoundary.tsx index d23515b7..40c46778 100644 --- a/src/layouts/ErrorBoundary.tsx +++ b/src/layouts/ErrorBoundary.tsx @@ -47,7 +47,7 @@ class ErrorBoundary extends Component { {this.state.error.message} 홈으로 @@ -64,7 +64,7 @@ class ErrorBoundary extends Component { 홈으로 diff --git a/src/layouts/VGNB.tsx b/src/layouts/VGNB.tsx index fc2aa8cb..b0d44d27 100644 --- a/src/layouts/VGNB.tsx +++ b/src/layouts/VGNB.tsx @@ -1,5 +1,6 @@ import LogoButton from 'commons/components/LogoButton'; import UserToggleBox from 'commons/components/UserToggleBox'; +import { getCookie } from 'commons/cookie/cookie'; import { Link } from 'react-router-dom'; export interface VGNBProps { @@ -21,6 +22,8 @@ const VGNB = (props: VGNBProps) => { handleToggleClick, } = props; + const token = getCookie('loginToken'); + return ( <> {/* 좁은 화면 */} @@ -66,14 +69,16 @@ const VGNB = (props: VGNBProps) => { > 내 주변 보호소 찾기 - - 등록하기 - + {token && ( + + 등록하기 + + )} diff --git a/src/layouts/VLargeGNB.tsx b/src/layouts/VLargeGNB.tsx index 9bce525c..017102eb 100644 --- a/src/layouts/VLargeGNB.tsx +++ b/src/layouts/VLargeGNB.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router-dom'; import LogoButton from 'commons/components/LogoButton'; import UserSelectBox from 'commons/components/UserDropdownBox'; +import { getCookie } from 'commons/cookie/cookie'; export interface VLargeGNBProps { handleCategoryButtonClick: () => void; @@ -16,6 +17,7 @@ const VLargeGNB = (props: VLargeGNBProps) => { isFindShelterPage, isRegisterPage, } = props; + const token = getCookie('loginToken'); return ( @@ -48,15 +50,17 @@ const VLargeGNB = (props: VLargeGNBProps) => { > 내 주변 보호소 찾기 - - 등록하기 - + {token && ( + + 등록하기 + + )} diff --git a/src/pages/editProfile/EditProfilePage.tsx b/src/pages/editProfile/EditProfilePage.tsx index 4341c339..2673f938 100644 --- a/src/pages/editProfile/EditProfilePage.tsx +++ b/src/pages/editProfile/EditProfilePage.tsx @@ -1,70 +1,13 @@ -import { useMutation } from '@tanstack/react-query'; -import { useParams } from 'react-router-dom'; -import { useRecoilState } from 'recoil'; -import { shelterSignupState } from 'recoil/shelterState'; -import { getCookie } from 'commons/cookie/cookie'; -import usePostFetch from 'commons/apis/usePostFetch'; -import useGetFetch from 'commons/apis/useGetFetch'; -import { useEffect } from 'react'; -import VEditProfilePage from './VEditProfilePage'; +import React from 'react'; +import ErrorBoundary from 'layouts/ErrorBoundary'; +import EditProfileTemplate from './components/EditProfileTemplate'; const EditProfilePage = () => { - const params = useParams(); - const shelterId = params.id; - const token = getCookie('loginToken'); - const [shelterInfo, setShelterInfo] = useRecoilState(shelterSignupState); - - const { getStatusCode, getLoading, getData } = useGetFetch( - `${process.env.REACT_APP_URI}/shelter/${shelterId}?page=1`, + return ( + + + ); - - const { postStatusCode, postloading, postData } = usePostFetch( - `${process.env.REACT_APP_URI}/shelter/${shelterId}`, - { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - - body: JSON.stringify({ - name: shelterInfo.name, - contact: shelterInfo.contact, - shelterAddressUpdateDto: shelterInfo.address, - }), - }, - ); - - const mutation = useMutation(getData, { - onSuccess: (info) => { - setShelterInfo({ - ...shelterInfo, - name: info.response.shelter.name, - contact: info.response.shelter.contact, - address: { - ...info.response.shelter.address, - }, - }); - }, - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - postData(); - }; - - useEffect(() => { - mutation.mutate(); - }, []); - - const EditProfileProps = { - getLoading, - postloading, - handleSubmit, - shelterInfo, - }; - - return ; }; export default EditProfilePage; diff --git a/src/pages/editProfile/components/EditProfileTemplate.tsx b/src/pages/editProfile/components/EditProfileTemplate.tsx new file mode 100644 index 00000000..0795ad0c --- /dev/null +++ b/src/pages/editProfile/components/EditProfileTemplate.tsx @@ -0,0 +1,115 @@ +import { useMutation } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; +import { useRecoilState } from 'recoil'; +import { shelterSignupState } from 'recoil/shelterState'; +import { getCookie } from 'commons/cookie/cookie'; +import usePostFetch from 'commons/apis/usePostFetch'; +import useGetFetch from 'commons/apis/useGetFetch'; +import { useEffect, useState } from 'react'; +import VEditProfilePage from './VEditProfileTemplate'; + +const EditProfileTemplate = () => { + const params = useParams(); + const shelterId = params.id; + const token = getCookie('loginToken'); + const [shelterInfo, setShelterInfo] = useRecoilState(shelterSignupState); + const [modalOpen, setModalOpen] = useState(false); + const [modalText, setModalText] = useState(''); + + const { getStatusCode, getLoading, getData } = useGetFetch( + `${process.env.REACT_APP_URI}/shelter/${shelterId}?page=1`, + ); + + const { postStatusCode, postloading, postData } = usePostFetch( + `${process.env.REACT_APP_URI}/shelter/${shelterId}`, + { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + + body: JSON.stringify({ + name: shelterInfo.name, + contact: shelterInfo.contact, + shelterAddressUpdateDto: shelterInfo.address, + }), + }, + ); + + const mutation = useMutation(getData, { + onSuccess: (info) => { + setShelterInfo({ + ...shelterInfo, + name: info.response.shelter.name, + contact: info.response.shelter.contact, + address: { + ...info.response.shelter.address, + }, + }); + }, + onError: () => { + if (getStatusCode === 404) { + setModalText('해당 보호소는 없는 보호소입니다.'); + setModalOpen(true); + } + if (getStatusCode === 500) { + setModalText('서버에 오류가 발생했습니다.'); + setModalOpen(true); + } + }, + }); + + const getInputValue = (target: HTMLInputElement) => { + const inputKey = target.dataset.inputType as string; + setShelterInfo((prev) => ({ ...prev, [inputKey]: target.value })); + }; + + const handleChange = (event: React.ChangeEvent) => { + const target = event.target as HTMLInputElement; + getInputValue(target); + }; + + const handleModalClose = () => { + setModalOpen(false); + }; + + const handlePutStatusCode = (status: number) => { + if (status === 403) { + setModalText('타 보호소 정보 수정에 대한 접근 권한이 없습니다.'); + setModalOpen(true); + } + if (status === 404) { + setModalText('존재하지 않는 보호소 계정입니다.'); + setModalOpen(true); + } + if (status === 500) { + setModalText('서버에 오류가 발생했습니다.'); + setModalOpen(true); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + postData(); + handlePutStatusCode(postStatusCode); + }; + + useEffect(() => { + mutation.mutate(); + }, []); + + const EditProfileProps = { + getLoading, + postloading, + modalOpen, + modalText, + handleSubmit, + handleChange, + handleModalClose, + }; + + return ; +}; + +export default EditProfileTemplate; diff --git a/src/pages/editProfile/VEditProfilePage.tsx b/src/pages/editProfile/components/VEditProfileTemplate.tsx similarity index 75% rename from src/pages/editProfile/VEditProfilePage.tsx rename to src/pages/editProfile/components/VEditProfileTemplate.tsx index afffcede..6f0ab40f 100644 --- a/src/pages/editProfile/VEditProfilePage.tsx +++ b/src/pages/editProfile/components/VEditProfileTemplate.tsx @@ -3,14 +3,19 @@ import InputGroup from 'commons/components/InputGroup'; import { ClipLoader } from 'react-spinners'; import { shelterSignupState } from 'recoil/shelterState'; import { useRecoilValue } from 'recoil'; -import EditProfilePageSkeleton from './components/EditProfilePageSkeleton'; -import EditAddressInputGroup from './components/EditAddressInputGroup'; -import { VEditProfileProps } from './editProfileType'; +import TextGuideModal from 'commons/modals/TextGuideModal'; +import EditProfilePageSkeleton from './EditProfilePageSkeleton'; +import EditAddressInputGroup from './EditAddressInputGroup'; +import { VEditProfileProps } from '../editProfileType'; -const VEditProfilePage = ({ +const VEditProfileTemplate = ({ getLoading, postloading, + modalOpen, + modalText, + handleModalClose, handleSubmit, + handleChange, }: VEditProfileProps) => { const shelterInfo = useRecoilValue(shelterSignupState); @@ -37,19 +42,23 @@ const VEditProfilePage = ({ > @@ -60,10 +69,16 @@ const VEditProfilePage = ({ )} + + )} ); }; -export default VEditProfilePage; +export default VEditProfileTemplate; diff --git a/src/pages/editProfile/editProfileType.d.ts b/src/pages/editProfile/editProfileType.d.ts index 0967c435..0f068284 100644 --- a/src/pages/editProfile/editProfileType.d.ts +++ b/src/pages/editProfile/editProfileType.d.ts @@ -1,5 +1,9 @@ export interface VEditProfileProps { getLoading: boolean; postloading: boolean; + modalOpen: boolean; + modalText: string; + handleModalClose: () => void; + handleChange: (event: React.ChangeEvent) => void; handleSubmit: (e: React.FormEvent) => void; } diff --git a/src/pages/login/components/LoginInputForm.tsx b/src/pages/login/components/LoginInputForm.tsx index 020eb858..953f3155 100644 --- a/src/pages/login/components/LoginInputForm.tsx +++ b/src/pages/login/components/LoginInputForm.tsx @@ -11,6 +11,8 @@ const LoginInputForm = () => { const [userInfo, setUserInfo] = useRecoilState(shelterLoginState); const [errors, setErrors] = useState>({}); const [isLoading, setIsLoading] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [modalText, setModalText] = useState(''); const navigate = useNavigate(); const currentDate = new Date(); @@ -44,8 +46,14 @@ const LoginInputForm = () => { if (!res.ok) { // error 발생 시 처리는 status 값에 따라 하는 것으로 변경 필요 - const errorData = await res.json(); - console.error('userFetchError: ', errorData); + if (res.status === 400) { + setModalText('이메일, 비밀번호를 확인해주세요.'); + setModalOpen(true); + } + if (res.status === 500) { + setModalText('서버에 오류가 발생했습니다.'); + setModalOpen(true); + } } const response = await res.json(); @@ -102,8 +110,11 @@ const LoginInputForm = () => { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); - validateCheck(); - mutation.mutate(); + try { + validateCheck(); + } finally { + mutation.mutate(); + } }; const handleChange = (event: React.ChangeEvent) => { @@ -115,11 +126,18 @@ const LoginInputForm = () => { } }; + const handleModalClose = () => { + setModalOpen(false); + }; + const LoginInputFormProps = { handleChange, handleSubmit, errors, isLoading, + modalOpen, + modalText, + handleModalClose, }; return ; diff --git a/src/pages/login/components/VLoginInputForm.tsx b/src/pages/login/components/VLoginInputForm.tsx index f221a9b8..df162b57 100644 --- a/src/pages/login/components/VLoginInputForm.tsx +++ b/src/pages/login/components/VLoginInputForm.tsx @@ -1,5 +1,6 @@ import InputGroup from 'commons/components/InputGroup'; import { ClipLoader } from 'react-spinners'; +import TextGuideModal from 'commons/modals/TextGuideModal'; import { LoginInputFormProps, ValidationTextProps } from '../loginType'; const ValidateText = ({ text }: ValidationTextProps) => { @@ -11,43 +12,54 @@ const VLoginInputForm = ({ isLoading, handleChange, handleSubmit, + modalOpen, + modalText, + handleModalClose, }: LoginInputFormProps) => { return ( - - { - handleChange(e); - }} - autocomplete="off" - /> - + <> + + { + handleChange(e); + }} + autocomplete="off" + /> + + + { + handleChange(e); + }} + autocomplete="off" + /> + + + {isLoading ? ( + + ) : ( + '로그인' + )} + + - { - handleChange(e); - }} - autocomplete="off" + - - - {isLoading ? ( - - ) : ( - '로그인' - )} - - + > ); }; diff --git a/src/pages/login/loginType.d.ts b/src/pages/login/loginType.d.ts index 387aa882..bead2d35 100644 --- a/src/pages/login/loginType.d.ts +++ b/src/pages/login/loginType.d.ts @@ -5,6 +5,9 @@ export interface LoginInputFormProps { isLoading: boolean; handleChange: (event: React.ChangeEvent) => void; handleSubmit: (e: React.FormEvent) => void; + modalOpen: boolean; + modalText: string; + handleModalClose: () => void; } export interface ValidationTextProps { diff --git a/src/pages/signUp/components/SignupInputForm.tsx b/src/pages/signUp/components/SignupInputForm.tsx index bef45f8c..24d3a10a 100644 --- a/src/pages/signUp/components/SignupInputForm.tsx +++ b/src/pages/signUp/components/SignupInputForm.tsx @@ -16,18 +16,19 @@ const SignupInputForm = () => { isValid: false, checked: false, }); - const [passwordConfirm, setPasswordConfirm] = useState(true); - const [emailValidText, setEmailValidText] = useState(''); - const [emailInValidText, setEmailInValidText] = useState(''); - + const [passwordConfirm, setPasswordConfirm] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [modalText, setModalText] = useState(''); + const [emailValidText, setEmailValidText] = useState(''); + const [emailInValidText, setEmailInValidText] = useState(''); + const navigate = useNavigate(); const [errors, setErrors] = useState>({}); const [isLoading, setIsLoading] = useState({ submitIsLoading: false, duplicateCheckIsLoading: false, }); - const navigate = useNavigate(); - + /** 프론트에서 유효성 검사 진행 */ const validationSchema = Yup.object().shape({ email: Yup.string() .email('이메일 형식에 맞게 입력해주세요.') @@ -77,64 +78,84 @@ const SignupInputForm = () => { setEmailConfirm(emailConfirmObj); }; - // 이메일 중복 검사 api + const handleModalClose = () => { + setModalOpen(false); + }; + + /** 이메일 중복 검사 api */ const duplicateCheck = async () => { setErrors({}); setIsLoading((prev) => ({ ...prev, duplicateCheckIsLoading: true })); - await fetch(`${process.env.REACT_APP_URI}/account/email`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: shelterInfo.email, - }), - }) - .then((res) => { - return res.json(); - }) - .then((data) => { - if (!data.success) { - getEmailValidText({ - validText: '', - inValidText: data.error.message, - emailConfirmObj: { - isValid: false, - checked: false, - }, - }); - } else if (!shelterInfo.email) { - getEmailValidText({ - validText: '', - inValidText: '', - // 안 넣으면 빈칸으로 공간 차지해서 이렇게 조건 넣어줌 - emailConfirmObj: { - isValid: false, - checked: true, - }, - }); - } else { - getEmailValidText({ - validText: '사용 가능한 이메일입니다.', - inValidText: '', - emailConfirmObj: { - isValid: true, - checked: true, - }, - }); - } - }); - setIsLoading((prev) => ({ ...prev, duplicateCheckIsLoading: false })); + try { + const response = await fetch( + `${process.env.REACT_APP_URI}/account/email`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: shelterInfo.email, + }), + }, + ); + + const data = await response.json(); + + if (!data.success) { + getEmailValidText({ + validText: '', + inValidText: data.error.message, + emailConfirmObj: { + isValid: false, + checked: false, + }, + }); + } else if (!shelterInfo.email) { + getEmailValidText({ + validText: '', + inValidText: '', + emailConfirmObj: { + isValid: false, + checked: true, + }, + }); + } else { + getEmailValidText({ + validText: '사용 가능한 이메일입니다.', + inValidText: '', + emailConfirmObj: { + isValid: true, + checked: true, + }, + }); + } + return data; + } finally { + setIsLoading((prev) => ({ ...prev, duplicateCheckIsLoading: false })); + } }; + /** 회원가입 api 요청 + * 이메일 중복 확인이 되었는지 확인 + * 필수 정보가 입력되었는지 확인 + * 정보가 제대로 입력되었을 때 api 요청 + */ const userFetch = () => { - // 중복 확인이 되지 않았을 때 + getEmailValidText({ + validText: '', + inValidText: '', + emailConfirmObj: { + isValid: false, + checked: false, + }, + }); if (!emailConfirm.checked) { - alert('이메일 중복을 확인해주세요'); + setErrors({}); + setModalText('이메일을 중복을 확인해주세요.'); setIsLoading((prev) => ({ ...prev, submitIsLoading: false })); - } - // 제대로 확인되었을 때 - if (emailConfirm.isValid && emailConfirm.checked) { + setModalOpen(true); + } else if (emailConfirm.isValid && emailConfirm.checked) { fetch(`${process.env.REACT_APP_URI}/account/shelter`, { method: 'POST', headers: { @@ -151,11 +172,28 @@ const SignupInputForm = () => { }), }) .then((res) => { + if (!res.ok) { + if (res.status === 400) { + setModalText('필수 정보를 확인해주세요.'); + setModalOpen(true); + } + if (res.status === 500) { + setModalText('서버에 오류가 발생했습니다.'); + setModalOpen(true); + } + } return res.json(); }) .then((data) => { if (!data.success) { - alert(data.error.message); + getEmailValidText({ + validText: '', + inValidText: '', + emailConfirmObj: { + isValid: false, + checked: false, + }, + }); } else { navigate('/login'); } @@ -164,9 +202,10 @@ const SignupInputForm = () => { } }; - // input에 들어가는 value에 따라 recoilState인 shelterInfo의 값 갱신 - // 비밀번호 일치하지 않는 경우 에러 텍스트 표시 - // 나머지 case의 경우, input value를 저장하는 용도로만 사용하기 때문에 default로 설정 + /** input에 들어가는 value에 따라 recoilState인 shelterInfo의 값 갱신 + * 비밀번호 일치하지 않는 경우 에러 텍스트 표시 + * 나머지 case의 경우, input value를 저장하는 용도로만 사용하기 때문에 default로 설정 + */ const getInputValue = (target: HTMLInputElement) => { const inputKey = target.dataset.inputType as string; switch (inputKey) { @@ -183,7 +222,7 @@ const SignupInputForm = () => { } }; - // yup을 통해 input value의 validation check 후 errorText를 errors state에 저장 + /** yup을 통해 input value의 validation check 후 errorText를 errors state에 저장 */ const validationCheck = () => { validationSchema .validate(shelterInfo, { abortEarly: false }) @@ -213,6 +252,7 @@ const SignupInputForm = () => { const handleChange = (event: React.ChangeEvent) => { const target = event.target as HTMLInputElement; + setErrors({}); getInputValue(target); }; @@ -222,20 +262,23 @@ const SignupInputForm = () => { */ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await validationCheck(); setIsLoading((prev) => ({ ...prev, submitIsLoading: true })); - await userFetch(); + validationCheck(); + userFetch(); }; const SignupInputFormProps = { handleChange, handleSubmit, + handleModalClose, duplicateCheck, emailValidText, emailInValidText, passwordConfirm, errors, isLoading, + modalOpen, + modalText, }; return ; diff --git a/src/pages/signUp/components/VSignupInputForm.tsx b/src/pages/signUp/components/VSignupInputForm.tsx index 26f2b69c..0dab9c71 100644 --- a/src/pages/signUp/components/VSignupInputForm.tsx +++ b/src/pages/signUp/components/VSignupInputForm.tsx @@ -3,9 +3,11 @@ import InputGroup from 'commons/components/InputGroup'; import { ClipLoader } from 'react-spinners'; import { VSignupInputProps } from '../signupType'; import ValidateText from './ValidateText'; +import TextGuideModal from '../../../commons/modals/TextGuideModal'; const VSignupInputForm = ({ handleChange, + handleModalClose, handleSubmit, duplicateCheck, emailValidText, @@ -13,96 +15,106 @@ const VSignupInputForm = ({ passwordConfirm, errors, isLoading, + modalOpen, + modalText, }: VSignupInputProps) => { return ( - - + <> + + + + onClick만 동작 + className="bg-brand-color text-white rounded min-w-[100px] min-h-[44px]" + onClick={duplicateCheck} + > + {isLoading.duplicateCheckIsLoading ? ( + + ) : ( + '중복 확인' + )} + + + + + + + + {!passwordConfirm && ( + 비밀번호가 일치하지 않습니다. + )} + - onClick만 동작 - className="bg-brand-color text-white rounded min-w-[100px] min-h-[44px]" - onClick={duplicateCheck} - > - {isLoading.duplicateCheckIsLoading ? ( + + + + + + {isLoading.submitIsLoading ? ( ) : ( - '중복 확인' + '회원가입' )} - - - - - - - - {!passwordConfirm && ( - 비밀번호가 일치하지 않습니다. - )} - - - + + - - - - {isLoading.submitIsLoading ? ( - - ) : ( - '회원가입' - )} - - + > ); }; diff --git a/src/pages/signUp/signupType.d.ts b/src/pages/signUp/signupType.d.ts index 239f8a88..c6b181e4 100644 --- a/src/pages/signUp/signupType.d.ts +++ b/src/pages/signUp/signupType.d.ts @@ -28,12 +28,15 @@ export interface ValidationProps { export interface VSignupInputProps { handleChange: (event: React.ChangeEvent) => void; handleSubmit: (e: React.FormEvent) => void; + handleModalClose: () => void; duplicateCheck: () => void; emailValidText: string; emailInValidText: string; passwordConfirm: boolean; errors: Partial; isLoading: LoadingProps; + modalOpen: boolean; + modalText: string; } export interface SignupPageProps {