feat: 구글 소셜로그인 백엔드 연결
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { apiClient } from '@/shared/lib/apiClient';
|
import { apiClient } from "@/shared/lib/apiClient";
|
||||||
import { SocialLoginRequest, AuthResponse } from '../types';
|
import { AuthResponse, SocialLoginRequest } from "../types";
|
||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
/**
|
/**
|
||||||
@@ -7,11 +7,11 @@ export const authApi = {
|
|||||||
* @param data 구글/애플/페이스북에서 발급받은 Provider 이름과 Token
|
* @param data 구글/애플/페이스북에서 발급받은 Provider 이름과 Token
|
||||||
*/
|
*/
|
||||||
loginWithSocial: async (data: SocialLoginRequest): Promise<AuthResponse> => {
|
loginWithSocial: async (data: SocialLoginRequest): Promise<AuthResponse> => {
|
||||||
return apiClient<AuthResponse>('/auth/social', {
|
return apiClient<AuthResponse>("api/v1/auth/social", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: 이후 필요 시 logout, refreshAccessToken 등 인증 관련 API 추가
|
// TODO: 이후 필요 시 logout, refreshAccessToken 등 인증 관련 API 추가
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,39 +1,59 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import { GoogleOAuthProvider } from "@react-oauth/google";
|
||||||
import { useSocialLogin } from '../hooks/useSocialLogin';
|
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 GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "";
|
||||||
const FACEBOOK_APP_ID = process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || '';
|
const FACEBOOK_APP_ID = process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 실제 버튼들을 렌더링하고 커스텀 훅(useSocialLogin)을 호출하는 내부 컴포넌트입니다.
|
* 실제 버튼들을 렌더링하고 커스텀 훅(useSocialLogin)을 호출하는 내부 컴포넌트입니다.
|
||||||
* 이 컴포넌트는 반드시 GoogleOAuthProvider의 자식으로 존재해야 합니다.
|
* 이 컴포넌트는 반드시 GoogleOAuthProvider의 자식으로 존재해야 합니다.
|
||||||
*/
|
*/
|
||||||
const SocialLoginButtons = () => {
|
const SocialLoginButtons = () => {
|
||||||
const { loginWithGoogle, loginWithApple, handleFacebookCallback, isLoading, error } = useSocialLogin();
|
const {
|
||||||
|
loginWithGoogle,
|
||||||
|
loginWithApple,
|
||||||
|
handleFacebookCallback,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useSocialLogin();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 1. Google 로그인 (useGoogleLogin 훅 연동) */}
|
{/* 1. Google 로그인 (useGoogleLogin 훅 연동) */}
|
||||||
<button
|
<button
|
||||||
onClick={() => loginWithGoogle()}
|
onClick={() => loginWithGoogle()}
|
||||||
disabled={isLoading}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
|
className="w-5 h-5"
|
||||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
viewBox="0 0 24 24"
|
||||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
>
|
||||||
|
<path
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||||
|
fill="#4285F4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||||
|
fill="#34A853"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||||
|
fill="#FBBC05"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||||
|
fill="#EA4335"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{isLoading ? '연결 중...' : 'Google로 계속하기'}
|
{isLoading ? "연결 중..." : "Google로 계속하기"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 2. Apple 로그인 (react-apple-signin-auth 연동) */}
|
{/* 2. Apple 로그인 (react-apple-signin-auth 연동) */}
|
||||||
<button
|
{/* <button
|
||||||
onClick={loginWithApple}
|
onClick={loginWithApple}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full flex items-center justify-center gap-3 bg-[#111111] text-white px-6 py-3.5 rounded-xl font-bold hover:bg-black transition-colors shadow-sm disabled:opacity-50"
|
className="w-full flex items-center justify-center gap-3 bg-[#111111] text-white px-6 py-3.5 rounded-xl font-bold hover:bg-black transition-colors shadow-sm disabled:opacity-50"
|
||||||
@@ -42,10 +62,10 @@ const SocialLoginButtons = () => {
|
|||||||
<path d="M16.365 21.438c-1.562 1.085-3.235 1.127-4.706.033-1.524-1.128-3.085-1.1-4.733.053-1.636 1.144-2.887 1.026-4.144-.393C1.516 19.882 0 16.516 0 12.872 0 8.783 2.502 6.075 5.753 5.92c1.46-.07 3.04.945 4.02.945 1.012 0 2.895-1.226 4.67-1.042 1.488.082 2.883.615 3.82 1.635-3.322 1.956-2.775 6.645.545 8.01-1.002 2.56-2.316 4.965-4.443 5.97zM11.95 5.567c-.204-2.618 1.914-5.004 4.544-5.26.335 2.723-2.074 5.213-4.544 5.26z"/>
|
<path d="M16.365 21.438c-1.562 1.085-3.235 1.127-4.706.033-1.524-1.128-3.085-1.1-4.733.053-1.636 1.144-2.887 1.026-4.144-.393C1.516 19.882 0 16.516 0 12.872 0 8.783 2.502 6.075 5.753 5.92c1.46-.07 3.04.945 4.02.945 1.012 0 2.895-1.226 4.67-1.042 1.488.082 2.883.615 3.82 1.635-3.322 1.956-2.775 6.645.545 8.01-1.002 2.56-2.316 4.965-4.443 5.97zM11.95 5.567c-.204-2.618 1.914-5.004 4.544-5.26.335 2.723-2.074 5.213-4.544 5.26z"/>
|
||||||
</svg>
|
</svg>
|
||||||
{isLoading ? '연결 중...' : 'Apple로 계속하기'}
|
{isLoading ? '연결 중...' : 'Apple로 계속하기'}
|
||||||
</button>
|
</button> */}
|
||||||
|
|
||||||
{/* 3. Facebook 로그인 (react-facebook-login Render Props 연동) */}
|
{/* 3. Facebook 로그인 (react-facebook-login Render Props 연동) */}
|
||||||
<FacebookLogin
|
{/* <FacebookLogin
|
||||||
appId={FACEBOOK_APP_ID}
|
appId={FACEBOOK_APP_ID}
|
||||||
callback={handleFacebookCallback}
|
callback={handleFacebookCallback}
|
||||||
render={(renderProps: any) => (
|
render={(renderProps: any) => (
|
||||||
@@ -60,7 +80,7 @@ const SocialLoginButtons = () => {
|
|||||||
{isLoading ? '연결 중...' : 'Facebook으로 계속하기'}
|
{isLoading ? '연결 중...' : 'Facebook으로 계속하기'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
{/* 백엔드 연동 에러 메시지 표시 */}
|
{/* 백엔드 연동 에러 메시지 표시 */}
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useAuthStore } from "@/store/useAuthStore";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useGoogleLogin } from "@react-oauth/google";
|
||||||
import { authApi } from '../api/authApi';
|
import { useRouter } from "next/navigation";
|
||||||
import { useGoogleLogin } from '@react-oauth/google';
|
import { useState } from "react";
|
||||||
import appleAuthHelpers from 'react-apple-signin-auth';
|
import appleAuthHelpers from "react-apple-signin-auth";
|
||||||
import { useAuthStore } from '@/store/useAuthStore';
|
import { authApi } from "../api/authApi";
|
||||||
|
|
||||||
export const useSocialLogin = () => {
|
export const useSocialLogin = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -14,24 +14,31 @@ export const useSocialLogin = () => {
|
|||||||
* [비즈니스 로직 1] 공통 토큰 교환 로직
|
* [비즈니스 로직 1] 공통 토큰 교환 로직
|
||||||
* 플랫폼별 SDK에서 획득한 토큰을 백엔드로 보내어 VibeRoom 전용 JWT로 교환합니다.
|
* 플랫폼별 SDK에서 획득한 토큰을 백엔드로 보내어 VibeRoom 전용 JWT로 교환합니다.
|
||||||
*/
|
*/
|
||||||
const handleSocialLogin = async (provider: 'google' | 'apple' | 'facebook', socialToken: string) => {
|
const handleSocialLogin = async (
|
||||||
|
provider: "google" | "apple" | "facebook",
|
||||||
|
socialToken: string,
|
||||||
|
) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
console.log("token:" + socialToken);
|
||||||
try {
|
try {
|
||||||
// 1. 백엔드로 소셜 토큰 전송 (토큰 교환)
|
// 1. 백엔드로 소셜 토큰 전송 (토큰 교환)
|
||||||
const response = await authApi.loginWithSocial({ provider, token: socialToken });
|
const response = await authApi.loginWithSocial({
|
||||||
|
provider,
|
||||||
|
token: socialToken,
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`[${provider}] 백엔드 연동 성공! JWT:`, response.accessToken);
|
console.log(`[${provider}] 백엔드 연동 성공! JWT:`, response.accessToken);
|
||||||
|
|
||||||
// 2. 응답받은 VibeRoom 전용 토큰과 유저 정보를 전역 상태 및 쿠키에 저장
|
// 2. 응답받은 VibeRoom 전용 토큰과 유저 정보를 전역 상태 및 쿠키에 저장
|
||||||
useAuthStore.getState().setAuth(response);
|
useAuthStore.getState().setAuth(response);
|
||||||
|
|
||||||
// 3. 메인 대시보드 화면으로 이동
|
// 3. 메인 대시보드 화면으로 이동
|
||||||
router.push('/dashboard');
|
router.push("/dashboard");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[${provider}] 로그인 실패:`, err);
|
console.error(`[${provider}] 로그인 실패:`, err);
|
||||||
setError('로그인에 실패했습니다. 다시 시도해 주세요.');
|
setError("로그인에 실패했습니다. 다시 시도해 주세요.");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -43,10 +50,10 @@ export const useSocialLogin = () => {
|
|||||||
*/
|
*/
|
||||||
const loginWithGoogle = useGoogleLogin({
|
const loginWithGoogle = useGoogleLogin({
|
||||||
onSuccess: (tokenResponse) => {
|
onSuccess: (tokenResponse) => {
|
||||||
handleSocialLogin('google', tokenResponse.access_token);
|
handleSocialLogin("google", tokenResponse.access_token);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setError('구글 로그인에 실패했습니다. 팝업 차단 여부를 확인해 주세요.');
|
setError("구글 로그인에 실패했습니다. 팝업 차단 여부를 확인해 주세요.");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,24 +65,24 @@ export const useSocialLogin = () => {
|
|||||||
try {
|
try {
|
||||||
appleAuthHelpers.signIn({
|
appleAuthHelpers.signIn({
|
||||||
authOptions: {
|
authOptions: {
|
||||||
clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID || '',
|
clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID || "",
|
||||||
scope: 'email name',
|
scope: "email name",
|
||||||
redirectURI: window.location.origin, // Apple 요구사항: 현재 도메인 입력 필요
|
redirectURI: window.location.origin, // Apple 요구사항: 현재 도메인 입력 필요
|
||||||
usePopup: true,
|
usePopup: true,
|
||||||
},
|
},
|
||||||
onSuccess: (response: any) => {
|
onSuccess: (response: any) => {
|
||||||
if (response.authorization?.id_token) {
|
if (response.authorization?.id_token) {
|
||||||
handleSocialLogin('apple', response.authorization.id_token);
|
handleSocialLogin("apple", response.authorization.id_token);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: any) => {
|
||||||
console.error('Apple SignIn error:', err);
|
console.error("Apple SignIn error:", err);
|
||||||
setError('애플 로그인 중 오류가 발생했습니다.');
|
setError("애플 로그인 중 오류가 발생했습니다.");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setError('애플 로그인 초기화 실패');
|
setError("애플 로그인 초기화 실패");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,9 +92,9 @@ export const useSocialLogin = () => {
|
|||||||
*/
|
*/
|
||||||
const handleFacebookCallback = (response: any) => {
|
const handleFacebookCallback = (response: any) => {
|
||||||
if (response?.accessToken) {
|
if (response?.accessToken) {
|
||||||
handleSocialLogin('facebook', response.accessToken);
|
handleSocialLogin("facebook", response.accessToken);
|
||||||
} else {
|
} else {
|
||||||
setError('페이스북 로그인에 실패했습니다.');
|
setError("페이스북 로그인에 실패했습니다.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,4 +105,4 @@ export const useSocialLogin = () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
export interface SocialLoginRequest {
|
export interface SocialLoginRequest {
|
||||||
provider: 'google' | 'apple' | 'facebook';
|
provider: "GOOGLE" | "apple" | "FACEBOOK";
|
||||||
token: string; // 소셜 프로바이더로부터 발급받은 id_token 또는 access_token
|
token: string; // 소셜 프로바이더로부터 발급받은 id_token 또는 access_token
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
accessToken: string; // VibeRoom 전용 JWT (API 요청 시 사용)
|
accessToken: string; // VibeRoom 전용 JWT (API 요청 시 사용)
|
||||||
refreshToken: string; // 토큰 갱신용
|
refreshToken: string; // 토큰 갱신용
|
||||||
user: {
|
}
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
profileImage?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* VibeRoom 공통 API 클라이언트
|
* VibeRoom 공통 API 클라이언트
|
||||||
* 환경 변수(NEXT_PUBLIC_API_BASE_URL)를 기반으로 자동으로 Base URL이 설정됩니다.
|
* 환경 변수(NEXT_PUBLIC_API_BASE_URL)를 기반으로 자동으로 Base URL이 설정됩니다.
|
||||||
*
|
*
|
||||||
* - npm run dev 실행 시: https://api-dev.viberoom.io (from .env.development)
|
* - npm run dev 실행 시: https://api-dev.viberoom.io (from .env.development)
|
||||||
* - 빌드 후 운영 환경: https://api.viberoom.io (from .env.production)
|
* - 빌드 후 운영 환경: https://api.viberoom.io (from .env.production)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
const API_BASE_URL =
|
||||||
|
process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080";
|
||||||
|
|
||||||
export const apiClient = async <T>(
|
export const apiClient = async <T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
options: RequestInit = {}
|
options: RequestInit = {},
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
// 엔드포인트 앞의 슬래시(/) 중복 방지
|
// 엔드포인트 앞의 슬래시(/) 중복 방지
|
||||||
const url = `${API_BASE_URL}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
const url = `${API_BASE_URL}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`;
|
||||||
|
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
|||||||
Reference in New Issue
Block a user