-
Notifications
You must be signed in to change notification settings - Fork 4
[✨ Feature] 카카오 간편 회원가입/ 로그인 연결 [✨ Feature] 로그인 회원가입 로직 alert 토스트 팝업으로 변경 #212
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
b18347a
73382cd
c5e14b9
9849cb9
32a9849
b3a726a
854063e
2c4c3ad
91554a3
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 |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // src/api/oauth.ts | ||
| import { apiFetch } from '@/config/client'; | ||
| import { OAuthAuthResponse } from '@/types/oauth'; | ||
|
|
||
| type OAuthProvider = 'kakao'; | ||
|
|
||
| type OAuthSignInParams = { | ||
| provider: OAuthProvider; | ||
| redirectUri: string; | ||
| token: string; | ||
| }; | ||
|
|
||
| type OAuthSignUpParams = { | ||
| provider: OAuthProvider; | ||
| redirectUri: string; | ||
| token: string; | ||
| nickname: string; | ||
| }; | ||
|
|
||
| /** | ||
| * OAuth 로그인 | ||
| * POST /oauth/sign-in/{provider} | ||
| */ | ||
| export function oauthSignIn({ | ||
| provider, | ||
| redirectUri, | ||
| token, | ||
| }: OAuthSignInParams) { | ||
| return apiFetch<OAuthAuthResponse>(`/oauth/sign-in/${provider}`, { | ||
| method: 'POST', | ||
| body: { | ||
| redirectUri, | ||
| token, | ||
| }, | ||
| skipAuthRefresh: true, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * OAuth 회원가입 | ||
| * POST /oauth/sign-up/{provider} | ||
| */ | ||
| export function oauthSignUp({ | ||
| provider, | ||
| redirectUri, | ||
| token, | ||
| nickname, | ||
| }: OAuthSignUpParams) { | ||
| return apiFetch<OAuthAuthResponse>(`/oauth/sign-up/${provider}`, { | ||
| method: 'POST', | ||
| body: { | ||
| redirectUri, | ||
| token, | ||
| nickname, | ||
| }, | ||
| skipAuthRefresh: true, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,8 @@ import { signup } from '@/api/users'; | |||||||||||||||||||||||||||||||||||||||||||
| import kakaoLogo from '@/assets/icons/auth/ic-kakao.svg'; | ||||||||||||||||||||||||||||||||||||||||||||
| import Button from '@/components/Button'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { TextInput, PasswordInput } from '@/components/Input'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { useToast } from '@/components/toast/useToast'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { KAKAO_REST_API_KEY, KAKAO_REDIRECT_URI } from '@/config/oauth'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { useGuestOnly } from '@/hooks/useGuestOnly'; | ||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||
| validateEmail, | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -35,6 +37,19 @@ export default function SignupPage() { | |||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||||||||
| const toast = useToast(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const handleKakaoSignup = () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const url = | ||||||||||||||||||||||||||||||||||||||||||||
| 'https://kauth.kakao.com/oauth/authorize' + | ||||||||||||||||||||||||||||||||||||||||||||
| `?client_id=${KAKAO_REST_API_KEY}` + | ||||||||||||||||||||||||||||||||||||||||||||
| `&redirect_uri=${encodeURIComponent(KAKAO_REDIRECT_URI)}` + | ||||||||||||||||||||||||||||||||||||||||||||
| '&response_type=code' + | ||||||||||||||||||||||||||||||||||||||||||||
| '&state=signup' + | ||||||||||||||||||||||||||||||||||||||||||||
| '&prompt=login'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = url; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+52
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. 카카오 회원가입 URL을 생성할 때 문자열을 직접 더하는 방식보다
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const handleChange = (key: keyof typeof form) => (value: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| setForm((prev) => ({ | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -71,36 +86,37 @@ export default function SignupPage() { | |||||||||||||||||||||||||||||||||||||||||||
| password: form.password, | ||||||||||||||||||||||||||||||||||||||||||||
| nickname: form.nickname, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| //TODO toast 팝업으로 교체 | ||||||||||||||||||||||||||||||||||||||||||||
| alert('회원가입이 완료되었습니다!'); | ||||||||||||||||||||||||||||||||||||||||||||
| toast.success('회원가입이 완료되었습니다!'); | ||||||||||||||||||||||||||||||||||||||||||||
| sessionStorage.setItem('signupEmail', form.email); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| router.push('/login'); | ||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||
| //TODO 에러처리 | ||||||||||||||||||||||||||||||||||||||||||||
| if (error instanceof Error) { | ||||||||||||||||||||||||||||||||||||||||||||
| if (error.message.includes('이메일')) { | ||||||||||||||||||||||||||||||||||||||||||||
| setErrors((prev) => ({ | ||||||||||||||||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||||||||||||||||
| email: error.message, | ||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||
| toast.error(error.message); | ||||||||||||||||||||||||||||||||||||||||||||
| } else if (error.message.includes('닉네임')) { | ||||||||||||||||||||||||||||||||||||||||||||
| setErrors((prev) => ({ | ||||||||||||||||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||||||||||||||||
| nickname: error.message, | ||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||
| toast.error(error.message); | ||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||
| setErrors((prev) => ({ | ||||||||||||||||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||||||||||||||||
| password: error.message, | ||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||
| toast.error(error.message); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
94
to
112
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. 에러 메시지 문자열( References
|
||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||
| // 예상치 못한 에러 | ||||||||||||||||||||||||||||||||||||||||||||
| setErrors((prev) => ({ | ||||||||||||||||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||||||||||||||||
| password: '알 수 없는 오류가 발생했습니다.', | ||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
115
to
118
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. 예상치 못한 에러가 발생했을 때, 에러 메시지를 References
|
||||||||||||||||||||||||||||||||||||||||||||
| toast.error('알 수 없는 오류가 발생했습니다.'); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
93
to
120
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 에러 메시지 문자열( 예시: {
"code": "DUPLICATE_EMAIL",
"message": "이미 사용 중인 이메일입니다.",
"field": "email"
}References
|
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -199,7 +215,12 @@ export default function SignupPage() { | |||||||||||||||||||||||||||||||||||||||||||
| <div className="h-px flex-1 bg-gray-100" /> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| <Button type="button" size="lg" variant="secondary" className="w-full"> | ||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||
| size="lg" | ||||||||||||||||||||||||||||||||||||||||||||
| variant="secondary" | ||||||||||||||||||||||||||||||||||||||||||||
| className="w-full" | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleKakaoSignup}> | ||||||||||||||||||||||||||||||||||||||||||||
| <Image src={kakaoLogo} alt="카카오로고" width={24} height={24} /> | ||||||||||||||||||||||||||||||||||||||||||||
| 카카오 회원가입 | ||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| 'use client'; | ||
|
|
||
| import Image from 'next/image'; | ||
| import { useRouter, useSearchParams } from 'next/navigation'; | ||
| import { useEffect, useRef } from 'react'; | ||
|
|
||
| import { oauthSignIn, oauthSignUp } from '@/api/oauth'; | ||
| import kakaoLogo from '@/assets/icons/auth/ic-kakao.svg'; | ||
| import { useToast } from '@/components/toast/useToast'; | ||
| import { KAKAO_REDIRECT_URI } from '@/config/oauth'; | ||
|
|
||
| // 랜덤 5자리 문자열 생성 함수 | ||
| function generateRandomNickname(length = 5) { | ||
| const chars = | ||
| 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | ||
| let result = ''; | ||
| for (let i = 0; i < length; i++) { | ||
| result += chars.charAt(Math.floor(Math.random() * chars.length)); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| export default function KakaoCallbackContent() { | ||
| const router = useRouter(); | ||
| const searchParams = useSearchParams(); | ||
| const hasRequested = useRef(false); | ||
| const toast = useToast(); | ||
|
|
||
| useEffect(() => { | ||
| const code = searchParams.get('code'); | ||
| const state = searchParams.get('state'); | ||
|
|
||
| if (!code || !state) { | ||
| router.replace('/login'); | ||
| return; | ||
| } | ||
|
|
||
| if (hasRequested.current) return; | ||
| hasRequested.current = true; | ||
|
|
||
| window.history.replaceState({}, '', '/oauth/kakao'); | ||
|
|
||
| const processOAuth = async () => { | ||
| try { | ||
| let res; | ||
| if (state === 'signup') { | ||
| res = await oauthSignUp({ | ||
| provider: 'kakao', | ||
| nickname: generateRandomNickname(), | ||
| redirectUri: KAKAO_REDIRECT_URI, | ||
| token: code, | ||
| }); | ||
| } else { | ||
| res = await oauthSignIn({ | ||
| provider: 'kakao', | ||
| redirectUri: KAKAO_REDIRECT_URI, | ||
| token: code, | ||
| }); | ||
| } | ||
|
|
||
| localStorage.setItem('accessToken', res.accessToken); | ||
| localStorage.setItem('refreshToken', res.refreshToken); | ||
|
Comment on lines
+61
to
+62
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. |
||
|
|
||
| router.replace('/'); | ||
| } catch (e) { | ||
| console.error('카카오 인증 실패', e); | ||
| if (e instanceof Error) { | ||
| if (state === 'signup' && e.message.includes('이미')) { | ||
| toast.warning('이미 가입된 계정입니다. 로그인을 진행해 주세요.'); | ||
| router.replace('/login'); | ||
| } else if (state === 'login' && e.message.includes('않은')) { | ||
| toast.warning( | ||
| '가입되지 않은 계정입니다. 회원가입을 진행해 주세요.' | ||
| ); | ||
| router.replace('/signup'); | ||
| } else { | ||
| toast.error('인증에 실패했습니다. 다시 시도해주세요.'); | ||
| router.replace(state === 'signup' ? '/signup' : '/login'); | ||
| } | ||
|
Comment on lines
+67
to
+79
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. 회원가입/로그인 실패 시 에러 메시지 문자열( References
Contributor
Author
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. /gemini 이렇게 테스트 해봤을때 아래처럼 떠서 글자 기준으로 분기했어 확인해줘 page.tsx:55_ 카카오 인증 실패 ApiError: 이미 등록된 사용자입니다. at apiFetch (client.ts:151:11) at async KakaoCallbackPage.useEffect.processOAuth (page.tsx:36:17) 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. eunji0124님, 디버깅 결과를 공유해주셔서 감사합니다. 하지만 여전히 이 방식은 백엔드에서 에러 메시지 내용이 변경될 경우 클라이언트 로직이 오작동할 수 있는 위험을 내포하고 있습니다. 예를 들어, '이미 등록된 사용자입니다.'라는 메시지가 '이미 가입된 회원입니다.' 등으로 변경되면 현재 장기적인 관점에서 더 안정적이고 유지보수하기 쉬운 에러 처리를 위해서는 백엔드 API에서 현재 상황에서는 이 로직이 최선일 수 있으나, 백엔드 팀과 협의하여 에러 응답 구조를 개선하는 방안을 고려해보시는 것이 좋겠습니다. |
||
| } | ||
| } | ||
| }; | ||
|
|
||
| processOAuth(); | ||
| }, [router, searchParams]); | ||
|
|
||
| return ( | ||
| <div className="flex min-h-screen flex-col items-center justify-center"> | ||
| <Image src={kakaoLogo} alt="카카오로고" width={100} height={100} /> | ||
| <p className="title-lg mt-5">카카오 인증 처리 중...</p> | ||
| <p className="title-sm mt-3">잠시만 기다려주세요</p> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| 'use client'; | ||
|
|
||
| import Image from 'next/image'; | ||
| import { Suspense } from 'react'; | ||
|
|
||
| import KakaoCallbackContent from './KakaoCallbackContent'; | ||
|
|
||
| import kakaoLogo from '@/assets/icons/auth/ic-kakao.svg'; | ||
|
|
||
| export default function KakaoCallbackPage() { | ||
| return ( | ||
| <Suspense | ||
| fallback={ | ||
| <div className="flex min-h-screen flex-col items-center justify-center"> | ||
| <Image src={kakaoLogo} alt="카카오로고" width={100} height={100} /> | ||
| <p className="title-lg mt-5">카카오 인증 처리 중...</p> | ||
| </div> | ||
| }> | ||
| <KakaoCallbackContent /> | ||
| </Suspense> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // src/config/oauth.ts | ||
| export const KAKAO_REST_API_KEY = process.env.NEXT_PUBLIC_KAKAO_REST_API_KEY!; | ||
|
|
||
| export const KAKAO_REDIRECT_URI = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!; | ||
|
|
||
| //redirectUri는 카카오 디벨로퍼 + 백엔드에 등록한 값과 동일해야 함 |
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을 생성할 때 문자열을 직접 더하는 방식보다
URLSearchParams객체를 사용하면 가독성과 유지보수성이 향상됩니다. 파라미터를 객체 형태로 관리할 수 있어 실수를 줄일 수 있습니다.