-
Notifications
You must be signed in to change notification settings - Fork 0
feat#42 kakao login #46
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
Changes from all commits
26fc979
c60de91
1887b70
fc11de6
8f8873e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export * from './getAuthTicket'; | ||
| export * from './useKakaoLogin'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { useMutation, useQuery } from '@tanstack/react-query'; | ||
| import { toast } from 'sonner'; | ||
|
|
||
| import { BASE_URL } from '@/shared'; | ||
|
|
||
| import { getTicketApi } from '../apis'; | ||
|
|
||
| // --- 1. "๋ก๊ทธ์ธ ์์"์ ์ํ useMutation --- | ||
| // ์ด ํ ์ API๋ฅผ ํธ์ถํ์ง ์์. ์ค์ง ์นด์นด์ค ๋ก๊ทธ์ธ ํ์ด์ง๋ก '์ด๋'์ํค๋ ์ก์ ๋ง ์ฑ ์์ ธ. | ||
| export const useKakaoLogin = () => { | ||
| return useMutation({ | ||
| mutationFn: () => { | ||
| // ์ค์ ๋ก๋ Promise๋ฅผ ๋ฐํํ ํ์๋ ์์ง๋ง, mutationFn์ Promise๋ฅผ ๊ธฐ๋ํด. | ||
| window.location.href = `${BASE_URL}/oauth2/authorization/kakao | ||
| `; | ||
| return Promise.resolve(); | ||
| }, | ||
| onError: () => { | ||
| toast.error('์นด์นด์ค ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๋ ๋ฐ ์คํจํ์ต๋๋ค.'); | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| // --- 2. "์ฝ๋ฐฑ ์ฒ๋ฆฌ"๋ฅผ ์ํ useQuery --- | ||
| export const KakaoCallbackQueryKey = { | ||
| callback: (ticket: string) => ['kakaoCallback', ticket], | ||
| }; | ||
|
|
||
| export const useKakaoCallback = (ticket: string) => { | ||
| return useQuery({ | ||
| queryKey: KakaoCallbackQueryKey.callback(ticket), | ||
| // code๊ฐ ์์ ๋๋ง ์ด ์ฟผ๋ฆฌ๋ฅผ ์คํํด. | ||
| queryFn: () => getTicketApi(ticket), | ||
| // ์ด ์ฟผ๋ฆฌ๋ ํ ๋ฒ๋ง ์คํ๋๋ฉด ๋๋ฏ๋ก, ์ฌ์๋๋ ์บ์ ๊ด๋ จ ์ต์ ์ ์กฐ์ ํ ์ ์์ด. | ||
| retry: 0, | ||
| staleTime: Infinity, | ||
| }); | ||
|
Comment on lines
+30
to
+37
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 useQuery({
queryKey: KakaoCallbackQueryKey.callback(ticket),
// ticket์ด ์์ ๋๋ง ์ด ์ฟผ๋ฆฌ๋ฅผ ์คํํด.
queryFn: () => getTicketApi(ticket),
enabled: !!ticket,
// ์ด ์ฟผ๋ฆฌ๋ ํ ๋ฒ๋ง ์คํ๋๋ฉด ๋๋ฏ๋ก, ์ฌ์๋๋ ์บ์ ๊ด๋ จ ์ต์
์ ์กฐ์ ํ ์ ์์ด.
retry: 0,
staleTime: Infinity,
}); |
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,19 @@ | ||
| import { BASE_URL, Button } from '@/shared'; | ||
| import { useKakaoLogin } from '@/entities'; | ||
| import { Button } from '@/shared'; | ||
|
|
||
| import KakaoSymbol from '../../../_assets/kakao-symbol.webp'; | ||
|
|
||
| export const KakaoLoginButton = () => { | ||
| const kakaoLogin = () => { | ||
| // OAuth ์๋ฒ๋ก ๋ฆฌ๋ค์ด๋ ํธํ๋, ๋ฆฌ๋ค์ด๋ ํธ URL์ ๋ช ์์ ์ผ๋ก ์ง์ | ||
| const redirectUri = encodeURIComponent(`${window.location.origin}/oauth/redirect`); | ||
| window.location.href = `${BASE_URL}/oauth2/authorization/kakao?redirect_uri=${redirectUri}`; | ||
| }; | ||
|
|
||
| const { mutate: startKakaoLogin, isPending } = useKakaoLogin(); | ||
| return ( | ||
| <Button | ||
| onClick={kakaoLogin} | ||
| onClick={() => startKakaoLogin()} | ||
| className='flex h-11 w-90 items-center gap-3 rounded-lg bg-kakao text-black hover:bg-kakao/80 active:bg-kakao/80' | ||
| > | ||
| <img src={KakaoSymbol} className='size-4' width={16} height={16} alt='kakao' /> | ||
| <span className='text-sm font-semibold'>์นด์นด์ค ๊ณ์ ์ผ๋ก ๊ณ์ํ๊ธฐ</span> | ||
| <span className='text-sm font-semibold'> | ||
| {isPending ? '์ด๋์ค' : '์นด์นด์ค ๊ณ์ ์ผ๋ก ๊ณ์ํ๊ธฐ'} | ||
| </span> | ||
| </Button> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,104 +1,33 @@ | ||
| import { useEffect, useState } from 'react'; | ||
| import { useEffect } from 'react'; | ||
| import { useNavigate, useSearchParams } from 'react-router-dom'; | ||
|
|
||
| import { useGetAuthTicket } from '@/entities/auth'; | ||
| import { toast } from 'sonner'; | ||
|
|
||
| import { ROUTER_PATH } from '@/shared'; | ||
| import { useKakaoCallback } from '@/entities'; | ||
| import { ROUTER_PATH, authStorage } from '@/shared'; | ||
|
|
||
| export default function OAuthRedirectPage() { | ||
| const [searchParams] = useSearchParams(); | ||
|
|
||
| const navigate = useNavigate(); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| // URL์์ ticket ํ๋ผ๋ฏธํฐ ์ถ์ถ | ||
| const ticket = searchParams.get('ticket'); | ||
| const oauthError = searchParams.get('error'); | ||
| const errorDescription = searchParams.get('error_description'); | ||
|
|
||
| useEffect(() => { | ||
| if (oauthError) { | ||
| setError(errorDescription || `OAuth ์ธ์ฆ ์ค๋ฅ: ${oauthError}`); | ||
| } else if (!ticket) { | ||
| setError('์ธ์ฆ ํ ํฐ์ด ์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.'); | ||
| } | ||
| }, [oauthError, errorDescription, ticket]); | ||
|
|
||
| // ticket์ด ์๊ณ OAuth ์๋ฌ๊ฐ ์์ ๋๋ง API ํธ์ถ | ||
| const shouldCallApi = !!ticket && !oauthError; | ||
| const { | ||
| data: authData, | ||
| error: apiError, | ||
| isLoading, | ||
| } = useGetAuthTicket(shouldCallApi ? ticket : ''); | ||
|
|
||
| // API ์๋ฌ ์ฒ๋ฆฌ | ||
| useEffect(() => { | ||
| if (apiError) { | ||
| console.error('API ํธ์ถ ์ค ์ค๋ฅ:', apiError); | ||
| setError('์ธ์ฆ ๊ฒ์ฆ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.'); | ||
| } | ||
| }, [apiError]); | ||
| const { data, isSuccess, isError } = useKakaoCallback(ticket ?? ''); | ||
|
|
||
| // ์ฑ๊ณต ์ ์ฒ๋ฆฌ | ||
| useEffect(() => { | ||
| if (authData && !isLoading && !apiError && shouldCallApi) { | ||
| console.log('OAuth ์ธ์ฆ ์ฑ๊ณต!', authData); | ||
|
|
||
| // refreshToken์ localStorage์ ์ ์ฅ | ||
| localStorage.setItem('refreshToken', authData.refreshToken); | ||
| if (isSuccess && data) { | ||
| authStorage.refreshToken.set(data.refreshToken); | ||
|
|
||
| // ๋ฉ์ธ ํ์ด์ง๋ก ์ด๋ | ||
| navigate(ROUTER_PATH.MAIN, { replace: true }); | ||
| navigate(ROUTER_PATH.ROOT); | ||
| } | ||
| }, [authData, isLoading, apiError, navigate, shouldCallApi]); | ||
|
|
||
| if (isLoading && shouldCallApi) { | ||
| return ( | ||
| <div className='flex min-h-screen items-center justify-center'> | ||
| <div className='text-center'> | ||
| <div className='mb-4'> | ||
| <div className='mx-auto h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent'></div> | ||
| </div> | ||
| <p className='text-lg font-medium text-gray-700'>์ธ์ฆ ์ฒ๋ฆฌ ์ค...</p> | ||
| <p className='mt-2 text-sm text-gray-500'>์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์.</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <div className='flex min-h-screen items-center justify-center'> | ||
| <div className='text-center'> | ||
| <div className='mb-4'> | ||
| <div className='mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100'> | ||
| <svg | ||
| className='h-6 w-6 text-red-600' | ||
| fill='none' | ||
| viewBox='0 0 24 24' | ||
| stroke='currentColor' | ||
| > | ||
| <path | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| strokeWidth={2} | ||
| d='M6 18L18 6M6 6l12 12' | ||
| /> | ||
| </svg> | ||
| </div> | ||
| </div> | ||
| <h2 className='mb-2 text-xl font-semibold text-gray-900'>์ธ์ฆ ์คํจ</h2> | ||
| <p className='mb-4 text-gray-600'>{error}</p> | ||
| <button | ||
| onClick={() => navigate(ROUTER_PATH.LOGIN)} | ||
| className='rounded-lg bg-blue-500 px-4 py-2 text-white transition-colors hover:bg-blue-600' | ||
| > | ||
| ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋์๊ฐ๊ธฐ | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| if (isError) { | ||
| // ์คํจ ์, ์๋ฌ ์ฒ๋ฆฌ ํ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ | ||
| toast.error('์นด์นด์ค ๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค.'); | ||
| navigate('/login'); | ||
| } | ||
| }, [isSuccess, isError, data, navigate]); | ||
|
Comment on lines
18
to
30
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 null; | ||
| return <div>๋ก๊ทธ์ธ ์ค์ ๋๋ค...</div>; | ||
| } | ||
|
Comment on lines
9
to
33
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. ํ์ฌ ๊ตฌํ์ OAuth ์ธ์ฆ ์คํจ ์ ์นด์นด์ค์์
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| type AuthStorageKey = { | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| }; | ||
|
|
||
| const initStorage = <T extends keyof AuthStorageKey>(key: T, storage: Storage) => { | ||
| const storageKey = `${key}`; | ||
|
|
||
| const get = (): AuthStorageKey[T] => { | ||
| const value = storage.getItem(storageKey); | ||
|
|
||
| return JSON.parse(value as string); | ||
| }; | ||
|
Comment on lines
+9
to
+13
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 get = (): AuthStorageKey[T] | null => {
const value = storage.getItem(storageKey);
if (value === null) {
return null;
}
try {
return JSON.parse(value);
} catch (e) {
console.error('Failed to parse auth storage value:', e);
return null;
}
}; |
||
|
|
||
| const set = (value: AuthStorageKey[T]) => { | ||
| if (value === undefined || value === null) { | ||
| return storage.removeItem(storageKey); | ||
| } | ||
|
|
||
| const stringifiedValue = JSON.stringify(value); | ||
|
|
||
| storage.setItem(storageKey, stringifiedValue); | ||
| }; | ||
|
Comment on lines
+15
to
+23
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 set = (value: AuthStorageKey[T] | null | undefined) => {
if (value === undefined || value === null) {
return storage.removeItem(storageKey);
}
const stringifiedValue = JSON.stringify(value);
storage.setItem(storageKey, stringifiedValue);
}; |
||
|
|
||
| return { get, set }; | ||
| }; | ||
|
|
||
| export const authStorage = { | ||
| accessToken: initStorage('accessToken', localStorage), | ||
| refreshToken: initStorage('refreshToken', localStorage), | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './auth-storage'; |
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.
URL ๋ฌธ์์ด ๋์ ๋ถํ์ํ ๊ฐํ ๋ฌธ์๊ฐ ํฌํจ๋์ด ์์ต๋๋ค. ์ด๋ก ์ธํด URL์ด ๋น์ ์์ ์ผ๋ก ์ธ์ฝ๋ฉ(์:
%0A์ถ๊ฐ)๋์ด ์๊ธฐ์น ์์ ๋์์ด ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก ์ ๊ฑฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.