main
hbyang 2026-02-02 18:51:43 +09:00
parent dc42d26e6b
commit 61892a5d93
3 changed files with 144 additions and 2 deletions

View File

@ -7577,6 +7577,47 @@
background-color: rgba(255, 255, 255, 0.1); 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 ( ) My Info Page ( )
============================================ */ ============================================ */

View File

@ -10,6 +10,7 @@ import LoginSection from './pages/Login/LoginSection';
import GenerationFlow from './pages/Dashboard/GenerationFlow'; import GenerationFlow from './pages/Dashboard/GenerationFlow';
import SocialConnectSuccess from './pages/Social/SocialConnectSuccess'; import SocialConnectSuccess from './pages/Social/SocialConnectSuccess';
import SocialConnectError from './pages/Social/SocialConnectError'; import SocialConnectError from './pages/Social/SocialConnectError';
import YouTubeOAuthCallback from './pages/Social/YouTubeOAuthCallback';
import { crawlUrl, autocomplete, kakaoCallback, isLoggedIn, saveTokens, AutocompleteRequest } from './utils/api'; import { crawlUrl, autocomplete, kakaoCallback, isLoggedIn, saveTokens, AutocompleteRequest } from './utils/api';
import { CrawlingResponse } from './types/api'; import { CrawlingResponse } from './types/api';
@ -102,14 +103,19 @@ const App: React.FC = () => {
const accessToken = urlParams.get('access_token'); const accessToken = urlParams.get('access_token');
const refreshToken = urlParams.get('refresh_token'); const refreshToken = urlParams.get('refresh_token');
const code = urlParams.get('code'); const code = urlParams.get('code');
const currentPath = window.location.pathname;
// Social OAuth 콜백 경로는 여기서 처리하지 않음 (백엔드에서 처리)
const isSocialOAuthCallback = currentPath.includes('/social/oauth/') ||
currentPath.includes('/social/connect/');
// 백엔드에서 토큰을 URL 파라미터로 전달한 경우 // 백엔드에서 토큰을 URL 파라미터로 전달한 경우
if (accessToken && refreshToken && !isProcessingCallback) { if (accessToken && refreshToken && !isProcessingCallback) {
setIsProcessingCallback(true); setIsProcessingCallback(true);
handleTokenCallback(accessToken, refreshToken); handleTokenCallback(accessToken, refreshToken);
} }
// 기존 code 방식 (Redirect URI가 프론트엔드인 경우) // 기존 code 방식 (Redirect URI가 프론트엔드인 경우) - Social OAuth는 제외
else if (code && !isProcessingCallback) { else if (code && !isProcessingCallback && !isSocialOAuthCallback) {
setIsProcessingCallback(true); setIsProcessingCallback(true);
handleKakaoCallback(code); handleKakaoCallback(code);
} }
@ -333,6 +339,11 @@ const App: React.FC = () => {
// Social OAuth 콜백 페이지 처리 // Social OAuth 콜백 페이지 처리
const pathname = window.location.pathname; const pathname = window.location.pathname;
// YouTube OAuth 콜백 처리 (Google에서 리다이렉트)
if (pathname === '/social/oauth/youtube/callback') {
return <YouTubeOAuthCallback />;
}
if (pathname === '/social/connect/success') { if (pathname === '/social/connect/success') {
return <SocialConnectSuccess />; return <SocialConnectSuccess />;
} }

View File

@ -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<string>('');
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 (
<div className="social-connect-page">
<div className="social-connect-card">
{status === 'processing' ? (
<>
<div className="social-connect-spinner">
<svg viewBox="0 0 50 50" className="social-spinner-svg">
<circle cx="25" cy="25" r="20" fill="none" strokeWidth="4" />
</svg>
</div>
<h1 className="social-connect-title">YouTube ...</h1>
<p className="social-connect-message"> .</p>
</>
) : (
<>
<div className="social-connect-icon error">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
</div>
<h1 className="social-connect-title"> </h1>
<p className="social-connect-message">{errorMessage}</p>
</>
)}
</div>
</div>
);
};
export default YouTubeOAuthCallback;