diff --git a/index.css b/index.css index 0bad73e..b0eaec6 100644 --- a/index.css +++ b/index.css @@ -7577,6 +7577,47 @@ background-color: rgba(255, 255, 255, 0.1); } +.social-connect-spinner { + width: 64px; + height: 64px; + margin: 0 auto 1.5rem; +} + +.social-spinner-svg { + width: 100%; + height: 100%; + animation: social-spinner-rotate 1.5s linear infinite; +} + +.social-spinner-svg circle { + stroke: #2dd4bf; + stroke-linecap: round; + stroke-dasharray: 90, 150; + stroke-dashoffset: 0; + animation: social-spinner-dash 1.5s ease-in-out infinite; +} + +@keyframes social-spinner-rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes social-spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} + /* ============================================ My Info Page (내 정보) ============================================ */ diff --git a/src/App.tsx b/src/App.tsx index 1b55437..98ba4fd 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import LoginSection from './pages/Login/LoginSection'; import GenerationFlow from './pages/Dashboard/GenerationFlow'; import SocialConnectSuccess from './pages/Social/SocialConnectSuccess'; import SocialConnectError from './pages/Social/SocialConnectError'; +import YouTubeOAuthCallback from './pages/Social/YouTubeOAuthCallback'; import { crawlUrl, autocomplete, kakaoCallback, isLoggedIn, saveTokens, AutocompleteRequest } from './utils/api'; import { CrawlingResponse } from './types/api'; @@ -102,14 +103,19 @@ const App: React.FC = () => { const accessToken = urlParams.get('access_token'); const refreshToken = urlParams.get('refresh_token'); const code = urlParams.get('code'); + const currentPath = window.location.pathname; + + // Social OAuth 콜백 경로는 여기서 처리하지 않음 (백엔드에서 처리) + const isSocialOAuthCallback = currentPath.includes('/social/oauth/') || + currentPath.includes('/social/connect/'); // 백엔드에서 토큰을 URL 파라미터로 전달한 경우 if (accessToken && refreshToken && !isProcessingCallback) { setIsProcessingCallback(true); handleTokenCallback(accessToken, refreshToken); } - // 기존 code 방식 (Redirect URI가 프론트엔드인 경우) - else if (code && !isProcessingCallback) { + // 기존 code 방식 (Redirect URI가 프론트엔드인 경우) - Social OAuth는 제외 + else if (code && !isProcessingCallback && !isSocialOAuthCallback) { setIsProcessingCallback(true); handleKakaoCallback(code); } @@ -333,6 +339,11 @@ const App: React.FC = () => { // Social OAuth 콜백 페이지 처리 const pathname = window.location.pathname; + // YouTube OAuth 콜백 처리 (Google에서 리다이렉트) + if (pathname === '/social/oauth/youtube/callback') { + return ; + } + if (pathname === '/social/connect/success') { return ; } diff --git a/src/pages/Social/YouTubeOAuthCallback.tsx b/src/pages/Social/YouTubeOAuthCallback.tsx new file mode 100644 index 0000000..8c266c8 --- /dev/null +++ b/src/pages/Social/YouTubeOAuthCallback.tsx @@ -0,0 +1,90 @@ + +import React, { useEffect, useState } from 'react'; + +const API_URL = import.meta.env.VITE_API_URL || 'http://40.82.133.44'; + +const YouTubeOAuthCallback: React.FC = () => { + const [status, setStatus] = useState<'processing' | 'error'>('processing'); + const [errorMessage, setErrorMessage] = useState(''); + + useEffect(() => { + const handleCallback = async () => { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + const state = urlParams.get('state'); + const error = urlParams.get('error'); + + // OAuth 에러 체크 + if (error) { + console.error('[YouTube OAuth] Error from Google:', error); + setStatus('error'); + setErrorMessage('Google 인증이 취소되었거나 실패했습니다.'); + setTimeout(() => { + window.location.href = '/social/connect/error?platform=youtube&error=' + encodeURIComponent(error); + }, 1500); + return; + } + + if (!code) { + console.error('[YouTube OAuth] No code received'); + setStatus('error'); + setErrorMessage('인증 코드를 받지 못했습니다.'); + setTimeout(() => { + window.location.href = '/social/connect/error?platform=youtube&error=no_code'; + }, 1500); + return; + } + + try { + // 백엔드의 YouTube OAuth 콜백 엔드포인트로 code 전달 + const callbackUrl = `${API_URL}/social/oauth/youtube/callback?code=${encodeURIComponent(code)}${state ? `&state=${encodeURIComponent(state)}` : ''}`; + + console.log('[YouTube OAuth] Forwarding to backend:', callbackUrl); + + // 백엔드로 리다이렉트 (백엔드가 처리 후 /social/connect/success 또는 /social/connect/error로 리다이렉트) + window.location.href = callbackUrl; + } catch (err) { + console.error('[YouTube OAuth] Failed to process callback:', err); + setStatus('error'); + setErrorMessage('YouTube 연결 처리 중 오류가 발생했습니다.'); + setTimeout(() => { + window.location.href = '/social/connect/error?platform=youtube&error=processing_failed'; + }, 1500); + } + }; + + handleCallback(); + }, []); + + return ( + + + {status === 'processing' ? ( + <> + + + + + + YouTube 연결 중... + 잠시만 기다려주세요. + > + ) : ( + <> + + + + + + + + 연결 실패 + {errorMessage} + > + )} + + + ); +}; + +export default YouTubeOAuthCallback;
잠시만 기다려주세요.
{errorMessage}