-
Notifications
You must be signed in to change notification settings - Fork 2
feat: 로그인 기능구현 #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 로그인 기능구현 #52
Changes from 13 commits
b725a79
ca23169
30bc2f2
6da8353
b8f7c22
eb6cc62
ab4688e
38e5f01
1a5240f
f9004af
f789e93
7401575
57debf8
cb6b529
c8bba52
cce466e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import useAuthStore from '@/stores/authStore'; | ||
|
|
||
| import client from './client'; | ||
|
|
||
| type LoginType = 'kakao' | 'naver' | 'google'; | ||
|
||
| export const socialLogin = (loginType: LoginType) => { | ||
| window.location.href = `http://13.209.132.150:8081/oauth2/authorization/${loginType}`; | ||
|
||
| }; | ||
|
|
||
| export const logout = async () => { | ||
| const { accessToken } = useAuthStore.getState(); | ||
|
|
||
| try { | ||
| const response = await client.post(`/api/logout`, { | ||
| Authorization: { token: `Bearer ${accessToken}` }, | ||
| withCredentials: true, | ||
|
||
| }); | ||
|
||
| if (!response) throw new Error('logout fail'); | ||
| return response; | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
|
|
||
| export const getUserToken = async (stateToken: string) => { | ||
| try { | ||
| const response = await client.get(`/api/auth/token?state=${stateToken}`); | ||
| if (!response) throw new Error('getUserToken: Error while fetching user token'); | ||
| const userInfo = response.data; | ||
| if (userInfo) { | ||
| return userInfo; | ||
| } | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
|
|
||
| export const postZipCode = async () => { | ||
| try { | ||
| const response = await client.post(`/api/members/zipCode`); | ||
| if(!response) throw new Error('fail to post ZipCode') | ||
| return response; | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
|
|
||
| export const getNewToken = async () => { | ||
| try { | ||
| const response = await client.get('/api/reissue', { withCredentials: true }); | ||
| if (!response) throw new Error('getNewToken: no response data'); | ||
| return response; | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
|
|
||
| export const getMydata = async () => { | ||
| try { | ||
| const response = await client.get('/api/members/me'); | ||
| if (!response) throw new Error('getNewTOken: no response data'); | ||
| return response; | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
|
|
||
| export const deleteUserInfo = async () => { | ||
| try { | ||
| const response = await client.delete('/api/members/me', { | ||
| withCredentials: true, | ||
| }); | ||
| if (!response) throw new Error('deleteUserInfo: no response'); | ||
| return response; | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
|
|
||
| export const postLogout = async () => { | ||
| try { | ||
| const response = await client.post('/api/logout', { withCredentials: true }); | ||
| if (!response) throw new Error('postLogout: failed to logout'); | ||
| return response; | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
|
Comment on lines
+62
to
+70
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오잉 로그아웃 요청 함수가 2개 있네용? |
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하나의 페이지에서 api를 여러개 호출 + 마침 토큰도 유효하지 않을 경우, 토큰 재발급 요청도 여러번 발생할 수 있을 것 같아요. 토큰 갱신이 한 번만 진행되도록 로직을 보완하면 어떨까요?! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,69 @@ | ||
| import axios from 'axios'; | ||
|
|
||
| import useAuthStore from '@/stores/authStore'; | ||
|
|
||
| import { getNewToken } from './auth'; | ||
|
|
||
| const client = axios.create({ | ||
| baseURL: import.meta.env.VITE_API_URL, | ||
| }); | ||
|
|
||
| // client.interceptors.request.use( | ||
| // (config) => { | ||
| // const token = localStorage.getItem('authToken'); | ||
| // if (token) { | ||
| // config.headers['Authorization'] = `Bearer ${token}`; | ||
| // } | ||
| // return config; | ||
| // }, | ||
| // (error) => { | ||
| // //TODO: 에러처리 | ||
| // return Promise.reject(error); | ||
| // }, | ||
| // ); | ||
| client.interceptors.request.use( | ||
| (config) => { | ||
| const { accessToken } = useAuthStore.getState(); | ||
| console.log(config.url); | ||
| console.log(accessToken); | ||
| if (config.url !== '/auth/reissue' && accessToken) { | ||
| config.headers.Authorization = `Bearer ${accessToken}`; | ||
| console.log('intercepter', config.headers); | ||
| } | ||
| return config | ||
| ; | ||
| }, | ||
| (error) => { | ||
| const { logout } = useAuthStore.getState(); | ||
| logout(); | ||
| window.location.replace('/login'); | ||
| return Promise.reject(error); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return문의 요 코드는 콘솔창에 에러 출력하는 코드인가용? |
||
| }, | ||
| ); | ||
|
|
||
| client.interceptors.response.use( | ||
| (response) => response, | ||
| async (error) => { | ||
| const { setAccessToken, logout } = useAuthStore.getState(); | ||
| const originalRequest = error.config; | ||
|
|
||
| if (!originalRequest) { | ||
| return Promise.reject(error); | ||
| } | ||
|
|
||
| if ( | ||
| (error.response.status === 401 || | ||
| error.response.status === 403 || | ||
| error.response.data.message === 'Unauthorized') && | ||
| !originalRequest._retry | ||
| ) { | ||
| originalRequest._retry = true; | ||
|
|
||
| try { | ||
| const response = await getNewToken(); | ||
| const newToken = response?.data.accessToken; | ||
|
|
||
| if (!newToken) throw new Error('Failed to Refresh Token'); | ||
|
|
||
| setAccessToken(newToken); | ||
| originalRequest.headers.Authorization = `Bearer ${newToken}`; | ||
|
|
||
| return client(originalRequest); | ||
| } catch (e) { | ||
| logout(); | ||
| window.location.replace('/login'); | ||
| return Promise.reject(e); | ||
| } | ||
| } | ||
| return Promise.reject(error); | ||
| }, | ||
| ); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기 파트가 엑세스토큰이 만료될 시에 리프레시 토큰으로 새 토큰을 가져오는 로직인가요?! |
||
|
|
||
| export default client; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,11 +15,11 @@ queryClient.setDefaultOptions({ | |
| }); | ||
|
|
||
| createRoot(document.getElementById('root')!).render( | ||
| <StrictMode> | ||
| <QueryClientProvider client={queryClient}> | ||
| <BrowserRouter> | ||
| <App /> | ||
| </BrowserRouter> | ||
| </QueryClientProvider> | ||
| </StrictMode>, | ||
| // <StrictMode> | ||
| <QueryClientProvider client={queryClient}> | ||
| <BrowserRouter> | ||
| <App /> | ||
| </BrowserRouter> | ||
| </QueryClientProvider>, | ||
| // </StrictMode>, | ||
|
Comment on lines
+18
to
+24
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오잉 strictMode는 임시로 끄신건가용? |
||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| /* eslint-disable @typescript-eslint/no-unused-expressions */ | ||
| import { useEffect } from 'react'; | ||
| import { useNavigate } from 'react-router'; | ||
|
|
||
| import { getUserToken, getMydata, deleteUserInfo, postZipCode } from '@/apis/auth'; | ||
| import useAuthStore from '@/stores/authStore'; | ||
|
|
||
| const AuthCallbackPage = () => { | ||
| const stateToken = new URLSearchParams(window.location.search).get('state'); | ||
| const redirectURL = new URLSearchParams(window.location.search).get('redirect'); | ||
|
Comment on lines
+9
to
+10
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 소셜로그인 후 로그인 페이지 리다이렉트 될때 이 코드를 사용해서 토큰값을 저장하는걸까용?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아닌가 유저 정보를 가져오는건가?!
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redirectUrl은 로그인 이후에 온보딩으로 갈지 홈으로 갈지 정해주는 쿼리파라미터값 같은데 어느 페이지에서 넘어올때 이런 값들이 담겨져서 오는건가용??
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오호라 redirect까지 넘겨주시는걸로 백엔드 분과 얘기가 된 건가요?? (영섭님이었나) 저는 개인적으로 이 방식의 경우, 프론트 측에서 언제든지 path 변경을 할 수도 있고, 그런 상황에서는 백엔드 측에서도 같이 수정을 하는 번거로움이 있을 것 같다는 단점이 있다고 생각이 되어요. 그래서 새로운 유저인지 아닌지만 구분해서 넘겨주시면 저희가 홈으로 보낼지 온보딩으로 보낼지 로직을 처리하는 게 더 나을 것 같다는 의견이긴 합니다 |
||
|
|
||
| const { setZipCode, setAccessToken, login } = useAuthStore(); | ||
|
|
||
| const navigate = useNavigate(); | ||
|
|
||
| const setUserInfo = async (stateToken: string) => { | ||
| try { | ||
| const response = await getUserToken(stateToken); | ||
| if (!response) throw new Error('Error Fetching userInfo'); | ||
|
|
||
| const userInfo = response.data; | ||
| if (userInfo) { | ||
| login(); | ||
| userInfo.accessToken && setAccessToken(userInfo.accessToken); | ||
|
|
||
| if (redirectURL == 'home') { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 비교 연산자가 ===가 아닌 ==인 이유가 있을까용? |
||
| const zipCodeResponse = await getMydata(); | ||
| if (!zipCodeResponse) throw new Error('Error Fetching userInfo'); | ||
| const zipCode = zipCodeResponse.data.data.zipCode; | ||
| zipCode && setZipCode(zipCode); | ||
|
|
||
| console.log( | ||
| 'isLoggedIn', | ||
| useAuthStore.getState().isLoggedIn, | ||
| 'access', | ||
| useAuthStore.getState().accessToken, | ||
| 'zipCode', | ||
| useAuthStore.getState().zipCode, | ||
| ); | ||
| } else if (redirectURL === 'onboarding') { | ||
| const createZipCodeResponse = await postZipCode(); | ||
| if (!createZipCodeResponse) throw new Error('Error creating ZipCode'); | ||
| const zipCode = createZipCodeResponse.data.data.zipCode; | ||
| console.log(createZipCodeResponse); | ||
| const newAccessToken = createZipCodeResponse.headers['Authorization']; | ||
| setZipCode(zipCode); | ||
| setAccessToken(newAccessToken); | ||
| console.log( | ||
| 'isLoggedIn', | ||
| useAuthStore.getState().isLoggedIn, | ||
| 'access', | ||
| useAuthStore.getState().accessToken, | ||
| 'zipCode', | ||
| useAuthStore.getState().zipCode, | ||
| ); | ||
| } | ||
| } else { | ||
| navigate('/login'); | ||
| } | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
|
|
||
| const redirection = () => { | ||
| if (redirectURL === 'onboarding') navigate('/onboarding'); | ||
| else if (redirectURL === 'home') navigate('/'); | ||
| else navigate('/notFound'); | ||
|
Comment on lines
+68
to
+70
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹시 이동된 페이지에서 뒤로가기 하면 여기로 다시 진입 가능한가욧?! |
||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (stateToken) { | ||
| setUserInfo(stateToken as string); | ||
| redirection(); | ||
| } else navigate('/notFound'); | ||
| }, []); | ||
|
|
||
| const handleLeave = async () => { | ||
| try { | ||
| const response = await deleteUserInfo(); | ||
| console.log(response); | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }; | ||
| return <button onClick={() => handleLeave()}>탈퇴</button>; | ||
nirii00 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
|
||
| export default AuthCallbackPage; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,14 @@ | ||
| import { socialLogin } from '@/apis/auth'; | ||
| import { GoogleIcon, KakaoIcon, NaverIcon, StampIcon } from '@/assets/icons'; | ||
|
|
||
| import Background from './components/Background'; | ||
|
|
||
| const LoginPage = () => { | ||
| type LoginType = 'kakao' | 'naver' | 'google'; | ||
|
||
|
|
||
| const handleLogin = (loginType: LoginType) => { | ||
| socialLogin(loginType); | ||
| }; | ||
| return ( | ||
| <> | ||
| <main className="mt-10 flex grow flex-col items-center justify-between"> | ||
|
|
@@ -22,20 +28,23 @@ const LoginPage = () => { | |
| type="button" | ||
| className="rounded-full bg-[#03C75A] p-3.5" | ||
| aria-label="네이버 로그인" | ||
| onClick={() => handleLogin('naver')} | ||
| > | ||
| <NaverIcon /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className="rounded-full bg-[#FEE500] p-3.5" | ||
| aria-label="카카오 로그인" | ||
| onClick={() => handleLogin('kakao')} | ||
| > | ||
| <KakaoIcon /> | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className="border-gray-5 rounded-full border bg-white p-3.5" | ||
| aria-label="구글 로그인" | ||
| onClick={() => handleLogin('google')} | ||
| > | ||
| <GoogleIcon /> | ||
| </button> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
immer는 어디에 사용되는지 알 수 있을까요?