diff --git a/public/assets/images/logo512.png b/public/assets/images/logo512.png deleted file mode 100644 index a4e47a65..00000000 Binary files a/public/assets/images/logo512.png and /dev/null differ diff --git a/public/assets/images/shelterIcon.png b/public/assets/images/shelterIcon.png new file mode 100644 index 00000000..0055f093 Binary files /dev/null and b/public/assets/images/shelterIcon.png differ diff --git a/public/assets/pet.png b/public/assets/pet.png deleted file mode 100644 index 81f0d359..00000000 Binary files a/public/assets/pet.png and /dev/null differ diff --git a/src/App.tsx b/src/App.tsx index 82de16ef..9fd30ab0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import UrgentListPage from 'pages/profileList/urgentList/UrgentListPage'; import UpdatePage from 'pages/update/UpdatePage'; import HomePage from 'pages/home/HomePage'; import ValidateCheckLayout from 'layouts/ValidateCheckLayout'; +import EditProfilePage from 'pages/editProfile/EditProfilePage'; const queryClient = new QueryClient({ defaultOptions: { @@ -37,6 +38,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/src/commons/InputGroup.tsx b/src/commons/InputGroup.tsx index 0f93463d..fa286913 100644 --- a/src/commons/InputGroup.tsx +++ b/src/commons/InputGroup.tsx @@ -2,14 +2,12 @@ import React from 'react'; import Container from 'commons/Container'; import Input from 'commons/Input'; -export interface InputGroupProps { - id: string; +export interface InputGroupProps + extends React.HTMLAttributes { name: string; type: string; - placeholder: string; - onChange: (e: React.ChangeEvent) => void; + onChange?: (e: React.ChangeEvent) => void; autocomplete?: string; - defaultValue?: string; } const InputGroup = ({ diff --git a/src/commons/UserDropdownBox.tsx b/src/commons/UserDropdownBox.tsx new file mode 100644 index 00000000..0f0a17a7 --- /dev/null +++ b/src/commons/UserDropdownBox.tsx @@ -0,0 +1,105 @@ +import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; +import { getCookie, removeCookie, setCookie } from './cookie/cookie'; + +// 로그인 되었을 때 상태를 보여주는 SelectBox 제작 +const UserDropdownBox = () => { + const token = getCookie('loginToken'); + const options = ['My 정보 변경', 'My 보호소 이동', '로그아웃']; + const [isDropdownOpen, setIsDropdownOpen] = useState(false); // 드롭다운 메뉴 열림/닫힘 상태 + const navigate = useNavigate(); + const shelterId = getCookie('userAccountInfo'); + const id = shelterId ? shelterId.split(' ')[1] : ''; + + const toggleDropdown = () => { + setIsDropdownOpen(!isDropdownOpen); + }; + + const removeToken = () => { + removeCookie('loginToken'); + removeCookie('userAccountInfo'); + }; + + const handleOptionClick = (option: string) => { + switch (option) { + case 'My 정보 변경': + navigate(`/shelter/${id}/edit`); // 아직 회원정보 수정 페이지 구현 안됨 + break; + case 'My 보호소 이동': + navigate(`/shelter/${id}/1`); + break; + case '로그아웃': + removeToken(); + setCookie('userAccountInfo', 'Not Login'); + window.location.reload(); + break; + default: + break; + } + setIsDropdownOpen(false); + }; + + if (!token) { + return ( +
+ + +
+ ); + } + + return ( +
+
+ +
+ + {isDropdownOpen && ( +
+
+ {options.map((option, index) => ( +
handleOptionClick(option)} + role="menuitem" + > + {option} +
+ ))} +
+
+ )} +
+ ); +}; + +export default UserDropdownBox; diff --git a/src/commons/cookie/getUser.ts b/src/commons/cookie/getUser.ts deleted file mode 100644 index 84c1c2c8..00000000 --- a/src/commons/cookie/getUser.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getCookie, removeCookie } from './cookie'; - -export const getLoginState = () => { - const token = getCookie('loginToken'); - if (token) { - return '로그아웃'; - } - return '로그인'; -}; - -export const validateExpiredToken = () => { - const now = new Date(); - const expiredDate = new Date(getCookie('expiredDate')); - console.log('만료 검사'); - - // 토큰만료 검사 후 삭제 - if (now > expiredDate) { - removeCookie('loginToken'); - removeCookie('expiredDate'); - removeCookie('userAccountInfo'); - console.log('토큰이 만료되어 삭제되었습니다.'); - } -}; - -export const removeToken = () => { - removeCookie('loginToken'); -}; diff --git a/src/commons/modals/LoginGuideModal.tsx b/src/commons/modals/LoginGuideModal.tsx new file mode 100644 index 00000000..8da0cc54 --- /dev/null +++ b/src/commons/modals/LoginGuideModal.tsx @@ -0,0 +1,49 @@ +import { setCookie } from 'commons/cookie/cookie'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { tokenCheckState } from 'recoil/shelterState'; + +const LoginGuideModal = () => { + const navigate = useNavigate(); + const [isLogined, setIsLogined] = useRecoilState(tokenCheckState); // default : true + + if (isLogined) { + return null; + } + + return ( + +
+
+
로그인이 만료되었습니다.
+
다시 로그인 하시겠습니까?
+
+
+ + +
+
+
+ ); +}; + +export default LoginGuideModal; diff --git a/src/commons/modals/RegisterModal.tsx b/src/commons/modals/RegisterModal.tsx index 4508f63a..bfae1a46 100644 --- a/src/commons/modals/RegisterModal.tsx +++ b/src/commons/modals/RegisterModal.tsx @@ -9,6 +9,7 @@ export interface RegisterModalProps { isError: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any; + errorText: string; modalString: string; } @@ -22,9 +23,9 @@ const RegisterModal = ({ isSuccess, isError, data, + errorText, modalString, }: RegisterModalProps) => { - console.log(data); if (isError || data?.success === false) { return (
- - {modalString}에 실패했습니다 - -
{data?.error?.message}
+ {errorText}
- - {modalString}중입니다... - + 등록중입니다... hourglass ); } - return
문제가 생겼습니다
; + return
{errorText}
; }; export default RegisterModal; diff --git a/src/layouts/VGNB.tsx b/src/layouts/VGNB.tsx index c211afdd..d8230836 100644 --- a/src/layouts/VGNB.tsx +++ b/src/layouts/VGNB.tsx @@ -1,5 +1,5 @@ import LogoButton from 'commons/LogoButton'; -import { getLoginState, removeToken } from 'commons/cookie/getUser'; +import UserSelectBox from 'commons/UserDropdownBox'; import { Link } from 'react-router-dom'; export interface VGNBProps { @@ -36,19 +36,8 @@ const VGNB = (props: VGNBProps) => {
- - - - - - + {/* 여기서 변경 */} +
    diff --git a/src/layouts/VLargeGNB.tsx b/src/layouts/VLargeGNB.tsx index 8a3f7737..9e603832 100644 --- a/src/layouts/VLargeGNB.tsx +++ b/src/layouts/VLargeGNB.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; import LogoButton from 'commons/LogoButton'; -import { getLoginState, removeToken } from 'commons/cookie/getUser'; +import UserSelectBox from 'commons/UserDropdownBox'; export interface VLargeGNBProps { handleCategoryButtonClick: () => void; @@ -59,22 +59,8 @@ const VLargeGNB = (props: VLargeGNBProps) => { 등록하기
- -
- - - - - - -
+ {/* 여기서 변경 */} +
); diff --git a/src/layouts/ValidateCheckLayout.tsx b/src/layouts/ValidateCheckLayout.tsx index da146e86..101d136b 100644 --- a/src/layouts/ValidateCheckLayout.tsx +++ b/src/layouts/ValidateCheckLayout.tsx @@ -1,20 +1,44 @@ -import { validateExpiredToken } from 'commons/cookie/getUser'; +import { getCookie } from 'commons/cookie/cookie'; +import LoginGuideModal from 'commons/modals/LoginGuideModal'; import React, { useEffect } from 'react'; -import { Routes } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; +import { tokenCheckState } from 'recoil/shelterState'; interface LayoutProps { children: React.ReactNode; } const ValidateCheckLayout: React.FC = ({ children }) => { - useEffect(() => { - validateExpiredToken(); - const intervalId = setInterval(validateExpiredToken, 60000 * 30); // 30분에 한 번 + const setIsLogined = useSetRecoilState(tokenCheckState); + const loginToken = getCookie('loginToken'); + const userAccount = getCookie('userAccountInfo'); - return () => clearInterval(intervalId); - }, []); + useEffect(() => { + if (!loginToken && !userAccount) { + // loginToken이 없으면 모달 열기 + setIsLogined(false); + } + console.log('token 로직 동작'); + }, [loginToken, userAccount]); - return {children}; + return ( +
+ + } />; + } />; + } />; + } />; + } />; + } />; + } />; + } />; + } />; + + {/* */} + {children} +
+ ); }; export default ValidateCheckLayout; diff --git a/src/pages/detailPet/DetailPetData.tsx b/src/pages/detailPet/DetailPetData.tsx index 795696cb..f0cc55d2 100644 --- a/src/pages/detailPet/DetailPetData.tsx +++ b/src/pages/detailPet/DetailPetData.tsx @@ -19,7 +19,7 @@ const DetailPetData = () => { }, }); if (isLoading) return
로딩중
; - const labels = ['귀여움', '침착함', '유머감각', '외모', '의젓함']; + const labels = ['영리함', '친화력', '운동신경', '적응력', '활발함']; const radarChartProps: RadarChartProps = { setCanvas, diff --git a/src/pages/editProfile/EditAddressInputGroup.tsx b/src/pages/editProfile/EditAddressInputGroup.tsx new file mode 100644 index 00000000..eae9e4db --- /dev/null +++ b/src/pages/editProfile/EditAddressInputGroup.tsx @@ -0,0 +1,26 @@ +import VAddressInputGroup from 'pages/signUp/VAddressInputGroup'; +import { useRecoilState } from 'recoil'; +import { shelterSignupState } from 'recoil/shelterState'; + +const EditAddressInputGroup = () => { + const [shelterInfo, setShelterInfo] = useRecoilState(shelterSignupState); + + const handleChange = (e: React.ChangeEvent) => { + const target = e.target as HTMLInputElement; + setShelterInfo({ + ...shelterInfo, + address: { + ...shelterInfo.address, + detail: target.value, + }, + }); + }; + + const AddressInputGroupProps = { + handleChange, + shelterInfo, + }; + return ; +}; + +export default EditAddressInputGroup; diff --git a/src/pages/editProfile/EditProfilePage.tsx b/src/pages/editProfile/EditProfilePage.tsx new file mode 100644 index 00000000..1da601a8 --- /dev/null +++ b/src/pages/editProfile/EditProfilePage.tsx @@ -0,0 +1,143 @@ +import { useQuery } from '@tanstack/react-query'; +import Banner from 'commons/Banner'; +import InputGroup from 'commons/InputGroup'; +import GNB from 'layouts/GNB'; +import { useParams } from 'react-router-dom'; +import { ClipLoader } from 'react-spinners'; +import { useRecoilState } from 'recoil'; +import { shelterSignupState } from 'recoil/shelterState'; +import { useEffect } from 'react'; +import { getCookie } from 'commons/cookie/cookie'; +import EditAddressInputGroup from './EditAddressInputGroup'; + +const EditProfilePage = () => { + const params = useParams(); + const shelterId = params.id; + const [shelterInfo, setShelterInfo] = useRecoilState(shelterSignupState); + + const getProfileInfo = async () => { + const response = await fetch( + `${process.env.REACT_APP_URI}/shelter/${shelterId}?page=1`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!response.ok) { + if (response.status === 500) { + console.error('서버측 에러'); + } else { + console.error('내부 에러 : 알 수 없음'); + } + } + + return response.json(); + }; + + const shelterFetch = async () => { + const token = getCookie('loginToken'); + const response = await fetch( + `${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, + }), + }, + ); + + if (!response.ok) { + if (response.status === 500) { + console.error('서버측 에러'); + } else { + console.error('내부 에러 : 알 수 없음'); + } + } + + return response.json(); + }; + + const handleSubmit = () => { + shelterFetch(); + }; + + const { data } = useQuery({ + queryKey: ['editProfile', shelterId], + queryFn: getProfileInfo, + }); + + useEffect(() => { + setShelterInfo({ + ...shelterInfo, + name: data?.response.shelter.name, + contact: data?.response.shelter.contact, + address: { + ...data?.response.shelter.address, + }, + }); + }, [data]); + + return ( +
+ +
+ My 보호소 정보 수정 + +
+ {}} + autocomplete="off" + defaultValue={data?.response.shelter.name} + /> + {}} + autocomplete="off" + defaultValue={data?.response.shelter.contact} + /> + {/* defaultValue=data.shelter.address */}{' '} + {/* 내부 구조 바꾸는 작업 필요 => props 받는 방식을 바꾸면 될 듯 */} + + + +
+
+ ); +}; + +export default EditProfilePage; diff --git a/src/pages/login/LoginInputForm.tsx b/src/pages/login/LoginInputForm.tsx index bda64252..17388116 100644 --- a/src/pages/login/LoginInputForm.tsx +++ b/src/pages/login/LoginInputForm.tsx @@ -11,6 +11,7 @@ const LoginInputForm = () => { const [errors, setErrors] = useState>({}); const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); + const currentDate = new Date(); const validationSchema = Yup.object().shape({ email: Yup.string() @@ -20,6 +21,7 @@ const LoginInputForm = () => { }); const userfetch = () => { + let token: string; fetch(`${process.env.REACT_APP_URI}/account/login`, { method: 'POST', headers: { @@ -30,35 +32,38 @@ const LoginInputForm = () => { email: userInfo.email, password: userInfo.password, }), - }) - .then((res) => { - const jwtToken = res.headers.get('Authorization'); - if (jwtToken) { - const slicedToken = jwtToken.split(' ')[1]; - setCookie('loginToken', slicedToken); - } else { - console.log('로그인 실패로 token이 Null'); - } - return res.json(); - }) - - .then((data) => { - console.log('data: ', data); - if (data.success) { - const { accountInfo, tokenExpirationDateTime } = data.response; - const { id, role } = accountInfo; - const expiredDate = new Date(tokenExpirationDateTime); + }).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('userAccountInfo', `${role} ${id}`); - setCookie('expiredDate', String(expiredDate)); - setCookie('accountInfo', data.response.accountInfo); - navigate('/'); - } else { - // 형식은 맞지만 입력된 값이 가입되지 않은 계정일 때 - alert(data.error.message); - } - setIsLoading(false); - }); + setCookie('userAccountInfo', `${role} ${id}`, { + expires: tokenExpirationDate, + maxAge: timeDifferenceseconds, + }); + setCookie('loginToken', token, { + expires: tokenExpirationDate, + maxAge: timeDifferenceseconds, + }); + navigate('/'); + } else { + // 형식은 맞지만 입력된 값이 가입되지 않은 계정일 때 + alert(data.error.message); + } + setIsLoading(false); + }); }; const validateCheck = () => { diff --git a/src/pages/register/RegisterHeader.tsx b/src/pages/register/RegisterHeader.tsx index 287cb567..efbddaaf 100644 --- a/src/pages/register/RegisterHeader.tsx +++ b/src/pages/register/RegisterHeader.tsx @@ -13,6 +13,7 @@ import ImageVideoInput from './ImageVideoInput'; const RegisterHeader = () => { const [selectedImageFile, setSelectedImageFile] = useState(null); const [selectedVideoFile, setSelectedVideoFile] = useState(null); + const [errorText, setErrorText] = useState(''); const registerPetData = useRecoilValue(registerState); const imageRef = useRef(null); const videoRef = useRef(null); @@ -30,20 +31,44 @@ const RegisterHeader = () => { }; // 등록하기 관련 + // eslint-disable-next-line consistent-return const postPet = async (formData: FormData) => { const loginToken = getCookie('loginToken'); - const res = await fetch(`${process.env.REACT_APP_URI}/pet`, { + console.log('token', loginToken); + + const response = await fetch(`${process.env.REACT_APP_URI}/pet`, { method: 'POST', body: formData, headers: { Authorization: `Bearer ${loginToken}`, }, }); - return res.json(); + if (!response.ok) { + console.log(response.status); + // 로그인 화면으로 이동하기 위해 텍스트 바꿔주는 것 필요 + switch (response.status) { + case 400: + setErrorText('이미지, 비디오 형식이 잘못되었습니다.'); // 취소 + break; + case 401: + case 403: + setErrorText('로그인 정보가 만료되었습니다.'); // 로그인 페이지로 이동 / 취소 + break; + case 404: + setErrorText('보호소를 찾을 수 없습니다.'); // 로그인 페이지로 이동 / 취소 + break; + case 500: + setErrorText('서버에 문제가 발생했습니다.'); // 다시하기 / 취소 + break; + default: + setErrorText('등록 정보의 형식이 잘못되었습니다.'); // 취소 + break; + } + } + return response.json(); }; const { data, mutate, isError, isLoading, isSuccess } = useMutation(postPet); const handleRegisterButtonClick = async () => { - console.log(registerPetData); if (!selectedImageFile || !selectedVideoFile || !registerPetData.isComplete) return; const formData = new FormData(); @@ -93,13 +118,14 @@ const RegisterHeader = () => { isSuccess, isError, data, + errorText, modalString: '등록', + // 등록 글자 필요 }; return ( <>
-
-

등록하기

+