feat: 로그인 버튼 생성
This commit is contained in:
@@ -1,22 +1,25 @@
|
||||
'use client'; // 클라이언트 사이드 이벤트(onClick) 및 훅(useRouter)을 사용하므로 필수
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useSocialLogin } from '../hooks/useSocialLogin';
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import FacebookLogin from 'react-facebook-login/dist/facebook-login-render-props';
|
||||
|
||||
const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '';
|
||||
const FACEBOOK_APP_ID = process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || '';
|
||||
|
||||
/**
|
||||
* 로그인 화면 UI (View)
|
||||
* 비즈니스 로직(토큰 교환 등)을 전혀 모른 채, 오직 버튼만 그리고 이벤트를 훅(Hook)으로 위임합니다.
|
||||
* 실제 버튼들을 렌더링하고 커스텀 훅(useSocialLogin)을 호출하는 내부 컴포넌트입니다.
|
||||
* 이 컴포넌트는 반드시 GoogleOAuthProvider의 자식으로 존재해야 합니다.
|
||||
*/
|
||||
export const SocialLoginGroup = () => {
|
||||
// 로직과 뷰의 완벽한 분리: useSocialLogin 커스텀 훅에서 필요한 함수만 꺼내옵니다.
|
||||
const { loginWithGoogle, loginWithApple, loginWithFacebook, isLoading, error } = useSocialLogin();
|
||||
const SocialLoginButtons = () => {
|
||||
const { loginWithGoogle, loginWithApple, handleFacebookCallback, isLoading, error } = useSocialLogin();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* 1. Google 로그인 */}
|
||||
{/* 1. Google 로그인 (useGoogleLogin 훅 연동) */}
|
||||
<button
|
||||
onClick={loginWithGoogle}
|
||||
onClick={() => loginWithGoogle()}
|
||||
disabled={isLoading}
|
||||
className="w-full flex items-center justify-center gap-3 bg-white border border-brand-dark/20 text-brand-dark px-6 py-3.5 rounded-xl font-bold hover:bg-slate-50 transition-colors shadow-sm disabled:opacity-50"
|
||||
>
|
||||
@@ -29,7 +32,7 @@ export const SocialLoginGroup = () => {
|
||||
{isLoading ? '연결 중...' : 'Google로 계속하기'}
|
||||
</button>
|
||||
|
||||
{/* 2. Apple 로그인 */}
|
||||
{/* 2. Apple 로그인 (react-apple-signin-auth 연동) */}
|
||||
<button
|
||||
onClick={loginWithApple}
|
||||
disabled={isLoading}
|
||||
@@ -41,17 +44,23 @@ export const SocialLoginGroup = () => {
|
||||
{isLoading ? '연결 중...' : 'Apple로 계속하기'}
|
||||
</button>
|
||||
|
||||
{/* 3. Facebook 로그인 */}
|
||||
<button
|
||||
onClick={loginWithFacebook}
|
||||
disabled={isLoading}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#1877F2] text-white px-6 py-3.5 rounded-xl font-bold hover:bg-[#1864D9] transition-colors shadow-sm disabled:opacity-50"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
{isLoading ? '연결 중...' : 'Facebook으로 계속하기'}
|
||||
</button>
|
||||
{/* 3. Facebook 로그인 (react-facebook-login Render Props 연동) */}
|
||||
<FacebookLogin
|
||||
appId={FACEBOOK_APP_ID}
|
||||
callback={handleFacebookCallback}
|
||||
render={(renderProps: any) => (
|
||||
<button
|
||||
onClick={renderProps.onClick}
|
||||
disabled={isLoading || renderProps.isDisabled}
|
||||
className="w-full flex items-center justify-center gap-3 bg-[#1877F2] text-white px-6 py-3.5 rounded-xl font-bold hover:bg-[#1864D9] transition-colors shadow-sm disabled:opacity-50"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
{isLoading ? '연결 중...' : 'Facebook으로 계속하기'}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 백엔드 연동 에러 메시지 표시 */}
|
||||
{error && (
|
||||
@@ -62,3 +71,15 @@ export const SocialLoginGroup = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 로그인 화면 UI (View)
|
||||
* GoogleOAuthProvider로 감싸서 내부에서 useGoogleLogin 훅을 사용할 수 있게 합니다.
|
||||
*/
|
||||
export const SocialLoginGroup = () => {
|
||||
return (
|
||||
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
||||
<SocialLoginButtons />
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { authApi } from '../api/authApi';
|
||||
import { useGoogleLogin } from '@react-oauth/google';
|
||||
import appleAuthHelpers from 'react-apple-signin-auth';
|
||||
import { useAuthStore } from '@/store/useAuthStore';
|
||||
|
||||
export const useSocialLogin = () => {
|
||||
const router = useRouter();
|
||||
@@ -8,7 +11,8 @@ export const useSocialLogin = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* [비즈니스 로직 1] 플랫폼별 소셜 SDK에서 받은 토큰을 백엔드로 보내어 VibeRoom JWT를 얻어오는 공통 함수
|
||||
* [비즈니스 로직 1] 공통 토큰 교환 로직
|
||||
* 플랫폼별 SDK에서 획득한 토큰을 백엔드로 보내어 VibeRoom 전용 JWT로 교환합니다.
|
||||
*/
|
||||
const handleSocialLogin = async (provider: 'google' | 'apple' | 'facebook', socialToken: string) => {
|
||||
setIsLoading(true);
|
||||
@@ -16,16 +20,14 @@ export const useSocialLogin = () => {
|
||||
|
||||
try {
|
||||
// 1. 백엔드로 소셜 토큰 전송 (토큰 교환)
|
||||
const response = await authApi.loginWithSocial({
|
||||
provider,
|
||||
token: socialToken,
|
||||
});
|
||||
|
||||
// 2. 응답받은 VibeRoom 전용 토큰을 로컬에 저장 (TODO: 실제로는 Zustand/Cookie에 저장)
|
||||
const response = await authApi.loginWithSocial({ provider, token: socialToken });
|
||||
|
||||
console.log(`[${provider}] 백엔드 연동 성공! JWT:`, response.accessToken);
|
||||
// ex) useAuthStore.getState().setToken(response.accessToken);
|
||||
|
||||
// 3. 성공 후 메인 대시보드 화면으로 이동
|
||||
|
||||
// 2. 응답받은 VibeRoom 전용 토큰과 유저 정보를 전역 상태 및 쿠키에 저장
|
||||
useAuthStore.getState().setAuth(response);
|
||||
|
||||
// 3. 메인 대시보드 화면으로 이동
|
||||
router.push('/dashboard');
|
||||
} catch (err) {
|
||||
console.error(`[${provider}] 로그인 실패:`, err);
|
||||
@@ -36,32 +38,63 @@ export const useSocialLogin = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* [비즈니스 로직 2] UI 컴포넌트(View)에서 호출할 개별 플랫폼 로그인 함수들
|
||||
* 향후 여기에 구글/애플/페이스북의 실제 프론트엔드 SDK 코드가 들어갑니다.
|
||||
* [비즈니스 로직 2] Google Login
|
||||
* @react-oauth/google 라이브러리의 훅을 사용하여 구글 로그인 팝업을 호출합니다.
|
||||
*/
|
||||
const loginWithGoogle = async () => {
|
||||
// TODO: Google Identity Services(GSI) SDK 호출 코드 작성
|
||||
const mockGoogleToken = 'mock_google_token_123';
|
||||
await handleSocialLogin('google', mockGoogleToken);
|
||||
const loginWithGoogle = useGoogleLogin({
|
||||
onSuccess: (tokenResponse) => {
|
||||
handleSocialLogin('google', tokenResponse.access_token);
|
||||
},
|
||||
onError: () => {
|
||||
setError('구글 로그인에 실패했습니다. 팝업 차단 여부를 확인해 주세요.');
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* [비즈니스 로직 3] Apple Login
|
||||
* react-apple-signin-auth 라이브러리를 사용하여 애플 로그인을 팝업으로 호출합니다.
|
||||
*/
|
||||
const loginWithApple = () => {
|
||||
try {
|
||||
appleAuthHelpers.signIn({
|
||||
authOptions: {
|
||||
clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID || '',
|
||||
scope: 'email name',
|
||||
redirectURI: window.location.origin, // Apple 요구사항: 현재 도메인 입력 필요
|
||||
usePopup: true,
|
||||
},
|
||||
onSuccess: (response: any) => {
|
||||
if (response.authorization?.id_token) {
|
||||
handleSocialLogin('apple', response.authorization.id_token);
|
||||
}
|
||||
},
|
||||
onError: (err: any) => {
|
||||
console.error('Apple SignIn error:', err);
|
||||
setError('애플 로그인 중 오류가 발생했습니다.');
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('애플 로그인 초기화 실패');
|
||||
}
|
||||
};
|
||||
|
||||
const loginWithApple = async () => {
|
||||
// TODO: Sign in with Apple JS 호출 코드 작성
|
||||
const mockAppleToken = 'mock_apple_token_456';
|
||||
await handleSocialLogin('apple', mockAppleToken);
|
||||
/**
|
||||
* [비즈니스 로직 4] Facebook Callback
|
||||
* react-facebook-login 컴포넌트에서 콜백으로 받은 토큰을 처리합니다.
|
||||
*/
|
||||
const handleFacebookCallback = (response: any) => {
|
||||
if (response?.accessToken) {
|
||||
handleSocialLogin('facebook', response.accessToken);
|
||||
} else {
|
||||
setError('페이스북 로그인에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const loginWithFacebook = async () => {
|
||||
// TODO: Facebook Login SDK 호출 코드 작성
|
||||
const mockFacebookToken = 'mock_facebook_token_789';
|
||||
await handleSocialLogin('facebook', mockFacebookToken);
|
||||
};
|
||||
|
||||
// UI 컴포넌트에 노출할 상태와 함수들만 캡슐화하여 반환
|
||||
return {
|
||||
loginWithGoogle,
|
||||
loginWithApple,
|
||||
loginWithFacebook,
|
||||
handleFacebookCallback,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user