-
- {invitedTeamList.map((invite) => (
-
-
- -- team -
{invite.teamName}
+ <> + ++ {invitedTeamList && invitedTeamList.length > 0 ? ( + + + ))} + + ) : ( +-
+ {invitedTeamList.map((invite) => (
+
-
+ +-+ team ++
{invite.teamName}
++ head ++{invite.creator}
++ desc +{invite.content}
+- head --{invite.creator}
++ +-- desc --{invite.content}
-- - --
- ))}
-
- 현재 가입 대기중인 팀이 없습니다!- )} -
- 팀에 가입해 보세요 😄 -+ 현재 가입 대기중인 팀이 없습니다!+ )} + + > ); } diff --git a/src/pages/setting/TeamJoinedPage.tsx b/src/pages/setting/TeamJoinedPage.tsx index b12f3618..3892b81c 100644 --- a/src/pages/setting/TeamJoinedPage.tsx +++ b/src/pages/setting/TeamJoinedPage.tsx @@ -1,6 +1,6 @@ +import Meta from '@components/common/Meta'; import Spinner from '@components/common/Spinner'; import { useDeleteTeam, useLeaveTeam, useReadTeams } from '@hooks/query/useTeamQuery'; - import { useStore } from '@stores/useStore'; export default function JoinedTeamPage() { @@ -11,52 +11,55 @@ export default function JoinedTeamPage() { if (isLoading) return
+ 팀에 가입해 보세요 😄 +; return ( - - {joinedTeamList && joinedTeamList.length > 0 ? ( - -
- {joinedTeamList.map((team) => (
-
-
- -- team --
{team.teamName}
-- head --{team.creator}
+ <> + ++ {joinedTeamList && joinedTeamList.length > 0 ? ( + -
+ {joinedTeamList.map((team) => (
+
-
+ +-+ team ++
{team.teamName}
++ head ++{team.creator}
++ desc +{team.content}
+- desc --{team.content}
-
- {team.creatorId === userInfo.userId && ( ++ + ))} + + ) : ( ++ {team.creatorId === userInfo.userId && ( + + )} - )} - -- - ))} - - ) : ( -- 현재 가입된 팀이 없습니다!- )} - +
- 팀에 가입해 보세요 😄 -+ 현재 가입된 팀이 없습니다!+ )} + + > ); } diff --git a/src/pages/setting/UserAuthenticatePage.tsx b/src/pages/setting/UserAuthenticatePage.tsx index 7bd03079..2dbfc99e 100644 --- a/src/pages/setting/UserAuthenticatePage.tsx +++ b/src/pages/setting/UserAuthenticatePage.tsx @@ -3,6 +3,7 @@ import { AxiosError } from 'axios'; import { useForm } from 'react-hook-form'; import { USER_AUTH_VALIDATION_RULES } from '@constants/formValidationRules'; import useEmailVerification from '@hooks/useEmailVerification'; +import Meta from '@components/common/Meta'; import ValidationInput from '@components/common/ValidationInput'; import VerificationButton from '@components/user/auth-form/VerificationButton'; import useToast from '@hooks/useToast'; @@ -38,41 +39,44 @@ function UserAuthenticatePage() { }; return ( -
+ 팀에 가입해 보세요 😄 +--+ > ); } diff --git a/src/pages/setting/UserPasswordSettingPage.tsx b/src/pages/setting/UserPasswordSettingPage.tsx index b7b8cb92..9d538837 100644 --- a/src/pages/setting/UserPasswordSettingPage.tsx +++ b/src/pages/setting/UserPasswordSettingPage.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { AxiosError } from 'axios'; import { useForm } from 'react-hook-form'; +import Meta from '@components/common/Meta'; import ValidationInput from '@components/common/ValidationInput'; import { USER_AUTH_VALIDATION_RULES } from '@constants/formValidationRules'; import useToast from '@hooks/useToast'; @@ -43,38 +44,41 @@ export default function UserPasswordSettingPage() { }; return ( -- 개인정보 변경을 위한 이메일 인증 단계입니다. -
+ <> + +
- 인증요청 버튼 클릭 후, 이메일로 발송된 인증번호를 입력해주세요. -+-++ 개인정보 변경을 위한 이메일 인증 단계입니다. +
- + {isVerificationRequested && ( +
+ 인증요청 버튼 클릭 후, 이메일로 발송된 인증번호를 입력해주세요. ++ )} + + {/* 인증 요청 및 확인 버튼 */} + requestVerificationCode(watch('email')))} + expireVerificationCode={expireVerificationCode} + buttonLabel="확인" + /> + + - ++ > ); } diff --git a/src/pages/setting/UserSettingPage.tsx b/src/pages/setting/UserSettingPage.tsx index edad2b27..caef0dd1 100644 --- a/src/pages/setting/UserSettingPage.tsx +++ b/src/pages/setting/UserSettingPage.tsx @@ -1,5 +1,6 @@ import { FormProvider, useForm } from 'react-hook-form'; import { USER_AUTH_VALIDATION_RULES } from '@constants/formValidationRules'; +import Meta from '@components/common/Meta'; import ValidationInput from '@components/common/ValidationInput'; import ProfileImageContainer from '@components/user/auth-form/ProfileImageContainer'; import LinkContainer from '@components/user/auth-form/LinkContainer'; @@ -48,74 +49,77 @@ export default function UserSettingPage() { }; return ( -- -- {/* 프로필 이미지 */} - setValue('profileImageName', url)} - /> - - {/* 아이디 */} - + <> + + + + + > ); } diff --git a/src/pages/team/TeamPage.tsx b/src/pages/team/TeamPage.tsx index b0bf499f..42d53c65 100644 --- a/src/pages/team/TeamPage.tsx +++ b/src/pages/team/TeamPage.tsx @@ -3,6 +3,7 @@ import CreateModalProject from '@components/modal/project/CreateModalProject'; import { useStore } from '@stores/useStore'; import useModal from '@hooks/useModal'; import useToast from '@hooks/useToast'; +import Meta from '@components/common/Meta'; import { useReadProjects } from '@hooks/query/useProjectQuery'; import { useReadTeamCoworkers, useReadTeams } from '@hooks/query/useTeamQuery'; import Spinner from '@components/common/Spinner'; @@ -40,30 +41,33 @@ export default function TeamPage() { } return ( -+-+ {/* 프로필 이미지 */} + -setValue('profileImageName', url)} + /> - {/* 이메일 */} - + {/* 아이디 */} + - {/* 닉네임, 중복 확인 */} - + {/* 이메일 */} + - {/* 자기소개 */} - - - - - {/* 개인정보 수정 버튼 */} - ---
+ {/* 자기소개 */} ++ + + + + {/* 개인정보 수정 버튼 */} + + ++
- {/* 링크 */} -+ {/* 링크 */} + + - - + <> + +- team - {teamName} -- -+ + > ); } diff --git a/src/pages/user/SearchIdPage.tsx b/src/pages/user/SearchIdPage.tsx index 5507b300..623bbdc6 100644 --- a/src/pages/user/SearchIdPage.tsx +++ b/src/pages/user/SearchIdPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { AxiosError } from 'axios'; import { useForm, FormProvider } from 'react-hook-form'; +import Meta from '@components/common/Meta'; import Spinner from '@components/common/Spinner'; import SearchResultSection from '@components/user/auth-form/SearchResultSection'; import SearchDataForm from '@components/user/auth-form/SearchDataForm'; @@ -59,16 +60,19 @@ export default function SearchIdPage() { }; return ( -+ -+ team + {teamName} ++ +- {projectList.length > 0 ? ( - - {showProjectModal &&- ) : ( - - )} + + {projectList.length > 0 ? ( + + {showProjectModal &&+ ) : ( + + )} + } } - - + > ); } diff --git a/src/pages/user/SearchPasswordPage.tsx b/src/pages/user/SearchPasswordPage.tsx index 6cd75075..0dad6390 100644 --- a/src/pages/user/SearchPasswordPage.tsx +++ b/src/pages/user/SearchPasswordPage.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { AxiosError } from 'axios'; import { FormProvider, useForm } from 'react-hook-form'; +import Meta from '@components/common/Meta'; import Spinner from '@components/common/Spinner'; import SearchResultSection from '@components/user/auth-form/SearchResultSection'; import SearchDataForm from '@components/user/auth-form/SearchDataForm'; @@ -59,14 +60,17 @@ export default function SearchPasswordPage() { }; return ( -- {loading && +} + <> + + + + {!loading && !searchIdResult &&+ {loading && -} - {!loading && searchIdResult && ( - - )} + {!loading && searchIdResult && ( + + )} - {!loading && !searchIdResult && } - } + - + > ); } diff --git a/src/pages/user/SignInPage.tsx b/src/pages/user/SignInPage.tsx index d2c20d1b..abd5ffc4 100644 --- a/src/pages/user/SignInPage.tsx +++ b/src/pages/user/SignInPage.tsx @@ -1,6 +1,7 @@ import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { AxiosError } from 'axios'; +import Meta from '@components/common/Meta'; import ValidationInput from '@components/common/ValidationInput'; import FooterLinks from '@components/user/auth-form/FooterLinks'; import SocialButton from '@components/user/auth-form/SocialButton'; @@ -69,6 +70,7 @@ export default function SignInPage() { return ( <> +- {loading && +} + <> + + + + {!loading && !tempPassword &&+ {loading && -} - {!loading && tempPassword && } + {!loading && tempPassword && } - {!loading && !tempPassword && } - } + {/* 아이디 */} - - {/* 아이디 */} - + + > ); } diff --git a/src/routes/AfterLoginRoute.tsx b/src/routes/AfterLoginRoute.tsx index e3c91b49..f2d5e406 100644 --- a/src/routes/AfterLoginRoute.tsx +++ b/src/routes/AfterLoginRoute.tsx @@ -1,11 +1,38 @@ +import { useEffect, type PropsWithChildren } from 'react'; +import { Navigate, Outlet, useNavigate } from 'react-router-dom'; +import useToast from '@hooks/useToast'; import useStore from '@stores/useStore'; -import type { PropsWithChildren } from 'react'; -import { Navigate, Outlet } from 'react-router-dom'; +import { getAccessToken } from '@services/authService'; export default function AfterLoginRoute({ children }: PropsWithChildren) { - const { isAuthenticated, userInfo } = useStore(); + const { toastError } = useToast(); + const navigate = useNavigate(); + const { onLogout, onLogin, clearUserInfo, accessToken, isAuthenticated } = useStore(); - if (!isAuthenticated && !userInfo.userId) return- - {/* 이메일 */} - + <> + + + + {/* 링크 */} ++ {/* 아이디 */} + -- {isVerificationRequested && ( + {/* 이메일 */} - )} - {/* 닉네임, 중복 확인 */} - + {isVerificationRequested && ( + + )} - {/* 비밀번호 */} - + {/* 닉네임, 중복 확인 */} + - {/* 비밀번호 확인 */} - + {/* 비밀번호 */} + - {/* 자기소개 */} - - - - - {/* 링크 */} -+ {/* 자기소개 */} + + + + - {/* 인증 요청 및 확인 버튼 */} ---- - 로그인 페이지로 돌아가기 - - + + {/* 인증 요청 및 확인 버튼 */} + +++ + 로그인 페이지로 돌아가기 + + ; + useEffect(() => { + const autoRefreshToken = async () => { + try { + const refreshResponse = await getAccessToken(); + const newAccessToken = refreshResponse.headers.authorization; + + if (!newAccessToken) throw new Error('토큰 발급에 실패했습니다.'); + + onLogin(newAccessToken.split(' ')[1]); + } catch (error) { + // 토큰 갱신 실패 시 처리 + toastError('로그인 정보가 만료되었습니다. 다시 로그인 해주세요.'); + setTimeout(() => { + onLogout(); + clearUserInfo(); + navigate('/signin', { replace: true }); + }, 2000); + } + }; + + if (isAuthenticated && !accessToken) autoRefreshToken(); + }, [accessToken]); + + if (!isAuthenticated) return ; return children || ; } diff --git a/src/routes/BeforeLoginRoute.tsx b/src/routes/BeforeLoginRoute.tsx index 6f50b7e9..7aacd36b 100644 --- a/src/routes/BeforeLoginRoute.tsx +++ b/src/routes/BeforeLoginRoute.tsx @@ -3,9 +3,9 @@ import type { PropsWithChildren } from 'react'; import { Navigate, Outlet } from 'react-router-dom'; export default function BeforeLoginRoute({ children }: PropsWithChildren) { - const { isAuthenticated, userInfo } = useStore(); + const { isAuthenticated } = useStore(); - if (isAuthenticated || userInfo.userId) return ; + if (isAuthenticated) return ; return children || ; } diff --git a/src/services/authService.ts b/src/services/authService.ts index b03368db..1b57510d 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -101,7 +101,7 @@ export async function getUserInfo(axiosConfig: AxiosRequestConfig = {}) { * @returns {Promise >} */ export async function logout(axiosConfig: AxiosRequestConfig = {}) { - return defaultAxios.post('user/logout', null, { ...axiosConfig, withCredentials: true }); + return authAxios.post('user/logout', null, { ...axiosConfig, withCredentials: true }); } /** diff --git a/src/services/axiosProvider.ts b/src/services/axiosProvider.ts index 503363be..8ec803d6 100644 --- a/src/services/axiosProvider.ts +++ b/src/services/axiosProvider.ts @@ -11,9 +11,9 @@ type InterceptorProps = { children: ReactNode; }; -const BASE_URL = import.meta.env.VITE_BASE_URL; +const API_URL = import.meta.env.VITE_API_URL; const defaultConfigOptions: AxiosRequestConfig = { - baseURL: BASE_URL, + baseURL: API_URL, timeout: 10 * SECOND, headers: { 'Content-Type': 'application/json' }, }; diff --git a/src/stores/useStore.ts b/src/stores/useStore.ts index e5be59fa..573d73c1 100644 --- a/src/stores/useStore.ts +++ b/src/stores/useStore.ts @@ -46,7 +46,6 @@ const createAuthSlice: StateCreator = (set) => ({ setTimeout(() => { set({ - isAuthenticated: false, accessToken: null, }); }, AUTH_SETTINGS.ACCESS_TOKEN_EXPIRATION); @@ -136,6 +135,8 @@ export const useStore = create ()( })), partialize: (state) => ({ userInfo: state.userInfo, + isAuthenticated: state.isAuthenticated, + isVerified: state.isVerified, }), }, ), diff --git a/yarn.lock b/yarn.lock index 73b0f386..0237c2bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4850,6 +4850,20 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" +react-fast-compare@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + +react-helmet-async@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-2.0.5.tgz#cfc70cd7bb32df7883a8ed55502a1513747223ec" + integrity sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg== + dependencies: + invariant "^2.2.4" + react-fast-compare "^3.2.2" + shallowequal "^1.1.0" + react-hook-form@^7.51.4: version "7.51.4" resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz" @@ -5309,6 +5323,11 @@ set-function-name@^2.0.1, set-function-name@^2.0.2: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" -
+
-
+