From e0befc5fcf0a186cac7ef10fee05d992e16d8c7f Mon Sep 17 00:00:00 2001 From: JeonDoGyun Date: Tue, 7 Nov 2023 13:39:48 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20kakao=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=B0=BE=EC=A7=80=20=EB=AA=BB=ED=95=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useMap에서 kakao 객체 못 찾는 에러 있어서 변수로 하나 만들어줬습니다. 안 쓰는 TestMap.tsx 삭제했습니다. --- src/pages/map/TestMap.tsx | 136 -------------------------------------- src/pages/map/useMap.ts | 1 + 2 files changed, 1 insertion(+), 136 deletions(-) delete mode 100644 src/pages/map/TestMap.tsx diff --git a/src/pages/map/TestMap.tsx b/src/pages/map/TestMap.tsx deleted file mode 100644 index bbc8a89c..00000000 --- a/src/pages/map/TestMap.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { - useState, - useEffect, - Dispatch, - SetStateAction, - useRef, - RefObject, -} from 'react'; -import { Map, MapMarker } from 'react-kakao-maps-sdk'; - -type Coordinate = { - lat: number; - lng: number; -}; - -type PlaceType = { - address_name: string; - category_group_code: string; - category_group_name: string; - category_name: string; - distance: string; - id: string; - place_name: string; - place_url: string; - road_address_name: string; - x: string; - y: string; -}; - -type FilteredPlaceType = { - address_name: string; - place_name: string; - lat: string; - lng: string; -}; - -const TestMap = () => { - const [currentPosition, setCurrentPosition] = useState({ - lat: 35.1759293, - lng: 126.9149701, - }); - const [markers, setMarkers] = useState>([]); - const [searchedPlaces, setSearchedPlaces] = useState>([]); - const [isExecuted, setIsExecuted] = useState(false); - const mapRef = useRef(); - const markerRef = useRef(); - - // 보호소 키워드로 검색한 후 저장 - const searchKeywordPlace = ( - state: PlaceType[], - setState: React.Dispatch>, - ) => { - const ps = new kakao.maps.services.Places(); - - ps.keywordSearch( - '동물 보호소', - (data: any, status: kakao.maps.services.Status) => { - if (status === kakao.maps.services.Status.OK && data) { - setState(data); - setIsExecuted(true); - } else if (status === kakao.maps.services.Status.ERROR) { - // 검색 결과가 없을 때 -> 이 부분은 나중에 어떻게 표현할지 회의 필요 - alert('검색 결과가 없습니다.'); - setIsExecuted(false); - } - }, - ); - }; - - // 필요한 값만 새로운 객체로 저장 - useEffect(() => { - if (!isExecuted) searchKeywordPlace(searchedPlaces, setSearchedPlaces); - console.log('filteredPlaces: ', searchedPlaces); - console.log('MapRef: ', mapRef.current); - }, [searchedPlaces]); - - return ( -
- - {/* */} - {/* Marker 표시 */} - {searchedPlaces.map((searchedPlace) => { - console.log('마커 작동여부 확인'); - return ( - - ); - })} - -
- {isExecuted && searchedPlaces.length > 0 ? ( - searchedPlaces.map((searchedPlace, index) => ( - - {searchedPlace.place_name} - {searchedPlace.address_name} - - )) - ) : ( -
검색 결과가 없습니다.
- )} -
-
- ); -}; - -export default TestMap; diff --git a/src/pages/map/useMap.ts b/src/pages/map/useMap.ts index 5886eea8..09315632 100644 --- a/src/pages/map/useMap.ts +++ b/src/pages/map/useMap.ts @@ -6,6 +6,7 @@ import displayMarker from './displayMarker'; function useMap( containerRef: RefObject, ) { + const { kakao } = window; const [map, setMap] = useState(); const [searchedPlace, setSearchedPlace] = useState([]); const boundRef = useRef(); From 5554b578c11b34624d76b760ad2f500b1ec2d141 Mon Sep 17 00:00:00 2001 From: JeonDoGyun Date: Tue, 7 Nov 2023 14:35:54 +0900 Subject: [PATCH 2/7] =?UTF-8?q?refactor:=20handleChange=20=EB=82=B4=20getI?= =?UTF-8?q?nputValue=20=ED=95=A8=EC=88=98=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit target.value에 접근하는 방식에서 멘토링 조언 수용하여 data-set 속성 활용하는 것으로 수정 input, inputGroup에도 해당 속성 사용할 수 있도록 nullable로 추가했습니다. --- src/commons/Input.tsx | 2 ++ src/commons/InputGroup.tsx | 3 +++ src/pages/signUp/SignupInputForm.tsx | 19 +++++-------------- src/pages/signUp/VSignupInputForm.tsx | 5 +++++ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/commons/Input.tsx b/src/commons/Input.tsx index 7b4bd176..8bf9bebe 100644 --- a/src/commons/Input.tsx +++ b/src/commons/Input.tsx @@ -8,11 +8,13 @@ const Input = ({ onChange, autocomplete, defaultValue, + dataInputType, }: InputGroupProps) => { return ( ) => void; autocomplete?: string; + dataInputType?: string; } const InputGroup = ({ @@ -18,6 +19,7 @@ const InputGroup = ({ onChange, autocomplete, defaultValue, + dataInputType, }: InputGroupProps) => { return ( @@ -26,6 +28,7 @@ const InputGroup = ({ { }; const getInputValue = (target: HTMLInputElement) => { - switch (target.id) { - case 'email': - setShelterInfo((prev) => ({ ...prev, email: target.value })); - break; - case 'password': - setShelterInfo((prev) => ({ ...prev, password: target.value })); - break; - case 'shelter': - setShelterInfo((prev) => ({ ...prev, name: target.value })); - break; - case 'shelter-contact': - setShelterInfo((prev) => ({ ...prev, contact: target.value })); - break; - // 비밀번호 일치하지 않는 경우, 표시하기 위해 해당 부분 구현 + const inputKey = target.dataset.inputType as string; + switch (inputKey) { + // 비밀번호 일치하지 않는 경우 에러 텍스트 표시 case 'password-confirm': if (target.value !== shelterInfo.password) { setPasswordConfirm(false); @@ -182,7 +171,9 @@ const SignupInputForm = () => { setPasswordConfirm(true); } break; + // 나머지 case의 경우, input value를 저장하는 용도로만 사용하기 때문에 default로 설정 default: + setShelterInfo((prev) => ({ ...prev, [inputKey]: target.value })); break; } }; diff --git a/src/pages/signUp/VSignupInputForm.tsx b/src/pages/signUp/VSignupInputForm.tsx index 45ffa5c8..a8853c32 100644 --- a/src/pages/signUp/VSignupInputForm.tsx +++ b/src/pages/signUp/VSignupInputForm.tsx @@ -42,6 +42,7 @@ const VSignupInputForm = ({
Date: Tue, 7 Nov 2023 14:37:50 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20useMutation=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20userFetch=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 데이터를 패칭하는 부분만 userFetch로 가져오고, return data를 다루는 부분을 분리하도록 useMutation을 사용했습니다. --- src/pages/login/LoginInputForm.tsx | 72 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/pages/login/LoginInputForm.tsx b/src/pages/login/LoginInputForm.tsx index 47a83dd3..f540652e 100644 --- a/src/pages/login/LoginInputForm.tsx +++ b/src/pages/login/LoginInputForm.tsx @@ -3,6 +3,7 @@ import { useRecoilState } from 'recoil'; import { useNavigate } from 'react-router-dom'; import { ShelterLoginType, shelterLoginState } from 'recoil/shelterState'; import * as Yup from 'yup'; +import { useMutation } from '@tanstack/react-query'; import VLoginInputForm from './VLoginInputForm'; import { setCookie } from '../../commons/cookie/cookie'; @@ -20,9 +21,8 @@ const LoginInputForm = () => { password: Yup.string().required('비밀번호를 입력해주세요.'), }); - const userfetch = () => { - let token: string; - fetch(`${process.env.REACT_APP_URI}/account/login`, { + const userFetch = async () => { + const res = await fetch(`${process.env.REACT_APP_URI}/account/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -32,38 +32,19 @@ const LoginInputForm = () => { email: userInfo.email, password: userInfo.password, }), - }).then(async (res) => { - const jwtToken = res.headers.get('Authorization'); - if (jwtToken) { - // eslint-disable-next-line prefer-destructuring - token = jwtToken.split(' ')[1]; - } else { - console.log('로그인 실패로 token이 Null'); - } - const data = await res.json(); - if (data.success && token) { - const { accountInfo, tokenExpirationDateTime } = data.response; - const { id, role } = accountInfo; - const tokenExpirationDate = new Date(tokenExpirationDateTime); - const timeDifferenceseconds = Math.floor( - (Number(tokenExpirationDate) - Number(currentDate)) / 1000, - ); - - setCookie('accountInfo', `${role} ${id}`, { - expires: tokenExpirationDate, - maxAge: timeDifferenceseconds, - }); - setCookie('loginToken', token, { - expires: tokenExpirationDate, - maxAge: timeDifferenceseconds, - }); - navigate('/'); - } else { - // 형식은 맞지만 입력된 값이 가입되지 않은 계정일 때 - alert(data.error.message); - } - setIsLoading(false); }); + + if (!res.ok) { + // error 발생 시 처리는 status 값에 따라 하는 것으로 변경 필요 + const errorData = await res.json(); + console.error('userFetchError: ', errorData); + } + + const response = await res.json(); + const jwtToken = res.headers.get('Authorization'); + const token = jwtToken ? jwtToken.split(' ')[1] : ''; + + return { response, token }; }; const validateCheck = () => { @@ -83,11 +64,32 @@ const LoginInputForm = () => { }); }; + const mutation = useMutation(userFetch, { + onSuccess: (data) => { + const { accountInfo, tokenExpirationDateTime } = data.response.response; + const { id, role } = accountInfo; + const tokenExpirationDate = new Date(tokenExpirationDateTime); + const timeDifferenceseconds = Math.floor( + (Number(tokenExpirationDate) - Number(currentDate)) / 1000, + ); + + setCookie('accountInfo', `${role} ${id}`, { + expires: tokenExpirationDate, + maxAge: timeDifferenceseconds, + }); + setCookie('loginToken', data.token, { + expires: tokenExpirationDate, + maxAge: timeDifferenceseconds, + }); + navigate('/'); + }, + }); + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); validateCheck(); - userfetch(); + mutation.mutate(); }; const handleChange = (event: React.ChangeEvent) => { From 983a80ff8a0e64d710b456bc460b026c4d39a596 Mon Sep 17 00:00:00 2001 From: JeonDoGyun Date: Tue, 7 Nov 2023 15:42:45 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20=EB=A1=9C=EB=8D=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=95=A8=EC=88=98=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 중복 확인 버튼에 Loader를 추가하면서 중복되는 state를 하나로 관리하도록 합쳤습니다. 함수에 param 포함 기능에 대한 주석 달았습니다. type, interface를 VAC 패턴에서 dependency cycle이 발생하지 않도록 따로 파일 관리하도록 했습니다. --- src/pages/signUp/SignUpType.ts | 15 +++++++ src/pages/signUp/SignupInputForm.tsx | 65 +++++++++++++++------------ src/pages/signUp/VSignupInputForm.tsx | 21 +++++++-- 3 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 src/pages/signUp/SignUpType.ts diff --git a/src/pages/signUp/SignUpType.ts b/src/pages/signUp/SignUpType.ts new file mode 100644 index 00000000..f7dbd366 --- /dev/null +++ b/src/pages/signUp/SignUpType.ts @@ -0,0 +1,15 @@ +export interface EmailConfirmProps { + isValid: boolean; + checked: boolean; +} + +export interface LoadingProps { + submitIsLoading: boolean; + duplicateCheckIsLoading: boolean; +} + +export interface EmailValidationProps { + validText: string; + inValidText: string; + emailConfirmObj: EmailConfirmProps; +} diff --git a/src/pages/signUp/SignupInputForm.tsx b/src/pages/signUp/SignupInputForm.tsx index 96a46559..21087353 100644 --- a/src/pages/signUp/SignupInputForm.tsx +++ b/src/pages/signUp/SignupInputForm.tsx @@ -1,28 +1,19 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useRecoilState } from 'recoil'; -import { ShelterSignupType, shelterSignupState } from 'recoil/shelterState'; import * as Yup from 'yup'; +import { ShelterSignupType, shelterSignupState } from 'recoil/shelterState'; +import { + EmailConfirmProps, + EmailValidationProps, + LoadingProps, +} from './SignUpType'; import VSignupInputForm from './VSignupInputForm'; -export interface EmailConfirmProps { - isValid: boolean; - checked: boolean; -} - -interface EmailValidProps { - validText: string; - inValidText: string; - emailConfirmObj: EmailConfirmProps; -} - const SignupInputForm = () => { const [shelterInfo, setShelterInfo] = useRecoilState(shelterSignupState); - // confirm state의 경우, 일치하지 않을 때 false - // isValid: 중복검사 통과했는가? - // checked: 이메일 중복 검사를 했는가? const [emailConfirm, setEmailConfirm] = useState({ - isValid: true, + isValid: false, checked: false, }); const [passwordConfirm, setPasswordConfirm] = useState(true); @@ -30,7 +21,10 @@ const SignupInputForm = () => { const [emailInValidText, setEmailInValidText] = useState(''); const [errors, setErrors] = useState>({}); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState({ + submitIsLoading: false, + duplicateCheckIsLoading: false, + }); const navigate = useNavigate(); @@ -66,19 +60,27 @@ const SignupInputForm = () => { ), }); + /** 회원가입 전 이메일 중복 검사를 완료했는지 / 검사 결과 적합했는지 확인 + * @param {string} validText : 적합한 경우 화면에 나타날 Text + * @param {string} inValidText : 부적합한 경우 화면에 나타날 Text + * @param {object} emailConfirmObj : 중복 검사 시행 확인 / 적합, 부적합을 판단하는 객체 + * @param {boolean} emailConfirmObj.isValid 적합(true), 부적합(false)을 판단 + * @param {boolean} emailConfirmObj.checked 중복 검사 시행(true) 확인 + */ const getEmailValidText = ({ validText, inValidText, emailConfirmObj, - }: EmailValidProps) => { + }: EmailValidationProps) => { setEmailValidText(validText); setEmailInValidText(inValidText); setEmailConfirm(emailConfirmObj); }; - const duplicateCheck = () => { - // shelterInfo.email - fetch(`${process.env.REACT_APP_URI}/account/email`, { + // 이메일 중복 검사 api + const duplicateCheck = async () => { + setIsLoading((prev) => ({ ...prev, duplicateCheckIsLoading: true })); + const response = await fetch(`${process.env.REACT_APP_URI}/account/email`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -121,13 +123,14 @@ const SignupInputForm = () => { }); } }); + setIsLoading((prev) => ({ ...prev, duplicateCheckIsLoading: false })); }; - const userfetch = () => { + const userFetch = () => { // 중복 확인이 되지 않았을 때 if (!emailConfirm.checked) { alert('이메일 중복을 확인해주세요'); - setIsLoading(false); + setIsLoading((prev) => ({ ...prev, submitIsLoading: false })); } // 제대로 확인되었을 때 if (emailConfirm.isValid && emailConfirm.checked) { @@ -156,14 +159,16 @@ const SignupInputForm = () => { navigate('/login'); } }); - setIsLoading(false); + setIsLoading((prev) => ({ ...prev, submitIsLoading: false })); } }; + // input에 들어가는 value에 따라 recoilState인 shelterInfo의 값 갱신 + // 비밀번호 일치하지 않는 경우 에러 텍스트 표시 + // 나머지 case의 경우, input value를 저장하는 용도로만 사용하기 때문에 default로 설정 const getInputValue = (target: HTMLInputElement) => { const inputKey = target.dataset.inputType as string; switch (inputKey) { - // 비밀번호 일치하지 않는 경우 에러 텍스트 표시 case 'password-confirm': if (target.value !== shelterInfo.password) { setPasswordConfirm(false); @@ -171,13 +176,13 @@ const SignupInputForm = () => { setPasswordConfirm(true); } break; - // 나머지 case의 경우, input value를 저장하는 용도로만 사용하기 때문에 default로 설정 default: setShelterInfo((prev) => ({ ...prev, [inputKey]: target.value })); break; } }; + // yup을 통해 input value의 validation check 후 errorText를 errors state에 저장 const validationCheck = () => { validationSchema .validate(shelterInfo, { abortEarly: false }) @@ -210,11 +215,15 @@ const SignupInputForm = () => { getInputValue(target); }; + /** 회원가입 버튼 onSubmit handler + * 유효성 검사 시행 + * userFetch가 동작하는 동안 Loader를 보여주기 위해 isLoading state 사용 + */ const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); validationCheck(); - setIsLoading(true); - userfetch(); + setIsLoading((prev) => ({ ...prev, submitIsLoading: true })); + userFetch(); }; const SignupInputFormProps = { diff --git a/src/pages/signUp/VSignupInputForm.tsx b/src/pages/signUp/VSignupInputForm.tsx index a8853c32..0aae3596 100644 --- a/src/pages/signUp/VSignupInputForm.tsx +++ b/src/pages/signUp/VSignupInputForm.tsx @@ -3,6 +3,7 @@ import InputGroup from 'commons/InputGroup'; import React from 'react'; import { ClipLoader } from 'react-spinners'; import { ShelterSignupType } from 'recoil/shelterState'; +import { LoadingProps } from './SignUpType'; interface VSignupInputProps { handleChange: (event: React.ChangeEvent) => void; @@ -12,7 +13,7 @@ interface VSignupInputProps { emailInValidText: string; passwordConfirm: boolean; errors: Partial; - isLoading: boolean; + isLoading: LoadingProps; } interface ValidationProps { @@ -54,7 +55,15 @@ const VSignupInputForm = ({ className="bg-brand-color text-white rounded min-w-[100px] min-h-[44px]" onClick={duplicateCheck} > - 중복 확인 + {isLoading.duplicateCheckIsLoading ? ( + + ) : ( + '중복 확인' + )}
@@ -104,8 +113,12 @@ const VSignupInputForm = ({