diff --git a/src/assets/github.svg b/src/assets/github.svg new file mode 100644 index 00000000..ccee5560 --- /dev/null +++ b/src/assets/github.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/google.svg b/src/assets/google.svg new file mode 100644 index 00000000..a92ac682 --- /dev/null +++ b/src/assets/google.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/kakao.svg b/src/assets/kakao.svg new file mode 100644 index 00000000..82884f87 --- /dev/null +++ b/src/assets/kakao.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/naver.svg b/src/assets/naver.svg new file mode 100644 index 00000000..28656bc3 --- /dev/null +++ b/src/assets/naver.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/constants/authConstants.ts b/src/constants/authConstants.ts index 8e5b0362..a9c4f04c 100644 --- a/src/constants/authConstants.ts +++ b/src/constants/authConstants.ts @@ -1,3 +1,8 @@ +import kakao from '../assets/kakao.svg'; +import naver from '../assets/naver.svg'; +import google from '../assets/google.svg'; +import github from '../assets/github.svg'; + export const ERROR_MESSAGES = { EMAIL_REQUIRED: '이메일을 입력해주세요.', INVALID_EMAIL: '유효한 이메일을 입력해주세요.', @@ -25,3 +30,26 @@ export const MY_STATUS = { ACCEPTED: 'ACCEPTED', REJECTED: 'REJECTED', } as const; + +export const OAUTH_PROVIDERS = [ + { + name: 'kakao', + url: import.meta.env.VITE_APP_BASE_URL_KAKAO, + icon: kakao, + }, + { + name: 'naver', + url: import.meta.env.VITE_APP_BASE_URL_NAVER, + icon: naver, + }, + { + name: 'google', + url: import.meta.env.VITE_APP_BASE_URL_GOOGLE, + icon: google, + }, + { + name: 'github', + url: import.meta.env.VITE_APP_BASE_URL_GITHUB, + icon: github, + }, +]; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index ee74a8bc..b4e78580 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -26,4 +26,5 @@ export const ROUTES = { FAQ: '/faq', notice: '/notice', inquiry: '/inquiry', + loginSuccess: '/login/oauth2/code', } as const; diff --git a/src/pages/login/Login.styled.ts b/src/pages/login/Login.styled.ts index 0decffe6..15c12402 100644 --- a/src/pages/login/Login.styled.ts +++ b/src/pages/login/Login.styled.ts @@ -97,3 +97,30 @@ export const ButtonWrapper = styled.div` display: flex; justify-content: center; `; + +export const OauthButtonWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +export const Button = styled.button` + display: block; + width: 5rem; + height: 4rem; + display: flex; + justify-content: center; + align-items: center; + + img { + width: 100%; + height: 100%; + } +`; + +export const LoginSuccessContainer = styled.div` + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx index 71571cb0..f562c985 100644 --- a/src/pages/login/Login.tsx +++ b/src/pages/login/Login.tsx @@ -8,7 +8,7 @@ import { z } from 'zod'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import Button from '../../components/common/Button/Button'; -import { ERROR_MESSAGES } from '../../constants/authConstants'; +import { ERROR_MESSAGES, OAUTH_PROVIDERS } from '../../constants/authConstants'; import { useAuth } from '../../hooks/useAuth'; import { ROUTES } from '../../constants/routes'; import { useModal } from '../../hooks/useModal'; @@ -27,6 +27,7 @@ export type loginFormValues = z.infer; const Login = () => { const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); const { userLogin } = useAuth(handleModalOpen); + const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; const { control, @@ -101,6 +102,20 @@ const Login = () => { 비밀번호 재설정 + + + {OAUTH_PROVIDERS.map((provider) => ( + + (window.location.href = `${BASE_URL}/${provider.url}`) + } + > + {`${provider.name} + + ))} + +

아직 DevPals 친구가 아니신가요? diff --git a/src/pages/login/LoginSuccess.tsx b/src/pages/login/LoginSuccess.tsx new file mode 100644 index 00000000..cfd2dbed --- /dev/null +++ b/src/pages/login/LoginSuccess.tsx @@ -0,0 +1,33 @@ +import { useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import useAuthStore from '../../store/authStore'; +import { ROUTES } from '../../constants/routes'; +import * as S from './Login.styled'; +import { Spinner } from '../../components/common/loadingSpinner/LoadingSpinner.styled'; + +function LoginSuccess() { + const [searchParams] = useSearchParams(); + const { storeLogin } = useAuthStore.getState(); + const navigate = useNavigate(); + + useEffect(() => { + const accessToken = searchParams.get('accessToken'); + + if (accessToken) { + storeLogin(accessToken); + localStorage.setItem('accessToken', accessToken); + navigate(ROUTES.main); + } else { + alert('로그인 토큰이 존재하지 않습니다.'); + navigate(ROUTES.login); + } + }, []); + + return ( + + + + ); +} + +export default LoginSuccess; diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index e2624297..8b23f9b5 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -14,6 +14,7 @@ import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; import QueryErrorBoundary from '../components/common/error/QueryErrorBoundary'; import { ToastProvider } from '../components/common/Toast/ToastProvider'; const Login = lazy(() => import('../pages/login/Login')); +const LoginSuccess = lazy(() => import('../pages/login/LoginSuccess')); const Register = lazy(() => import('../pages/register/Register')); const ChangePassword = lazy( () => import('../pages/changePassword/ChangePassword') @@ -97,6 +98,10 @@ const AppRoutes = () => { path: ROUTES.login, element: isLoggedIn ? : , }, + { + path: ROUTES.loginSuccess, + element: , + }, { path: ROUTES.signup, element: isLoggedIn ? ( @@ -343,6 +348,7 @@ const AppRoutes = () => { ), + children: [...newRouteList, { path: '*', element: }], }, ]); diff --git a/src/store/authStore.ts b/src/store/authStore.ts index c9b0b6ad..ff5f8d60 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -13,7 +13,7 @@ interface AuthState { userData: UserData | null; storeLogin: ( accessToken: string, - refreshToken: string, + refreshToken?: string, userData?: UserData ) => void; storeLogout: () => void; @@ -37,9 +37,11 @@ export const getTokens = () => { return { accessToken, refreshToken }; }; -const setTokens = (accessToken: string, refreshToken: string) => { +const setTokens = (accessToken: string, refreshToken?: string) => { localStorage.setItem('accessToken', accessToken); - localStorage.setItem('refreshToken', refreshToken); + if (refreshToken) { + localStorage.setItem('refreshToken', refreshToken); + } }; const removeTokens = () => { @@ -55,7 +57,7 @@ const useAuthStore = create( storeLogin: ( accessToken: string, - refreshToken: string, + refreshToken?: string, userData?: UserData ) => { setTokens(accessToken, refreshToken);