From 29fdb7e65c965348495716a9d06c37a227f45b5a Mon Sep 17 00:00:00 2001 From: hbyang Date: Mon, 9 Feb 2026 16:50:30 +0900 Subject: [PATCH] =?UTF-8?q?social=20=EA=B3=84=EC=A0=95=20=EC=9E=AC?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SocialPostingModal.tsx | 13 +++- src/pages/Dashboard/CompletionContent.tsx | 17 ++++- src/pages/Dashboard/MyInfoContent.tsx | 17 ++++- src/types/api.ts | 8 +++ src/utils/api.ts | 78 ++++++++++++++++------- 5 files changed, 106 insertions(+), 27 deletions(-) diff --git a/src/components/SocialPostingModal.tsx b/src/components/SocialPostingModal.tsx index 2ff87c2..377663b 100644 --- a/src/components/SocialPostingModal.tsx +++ b/src/components/SocialPostingModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; -import { getSocialAccounts, uploadToSocial, waitForUploadComplete } from '../utils/api'; +import { getSocialAccounts, uploadToSocial, waitForUploadComplete, TokenExpiredError, handleSocialReconnect } from '../utils/api'; import { SocialAccount, VideoListItem, SocialUploadStatusResponse } from '../types/api'; import UploadProgressModal, { UploadStatus } from './UploadProgressModal'; @@ -92,6 +92,11 @@ const SocialPostingModal: React.FC = ({ setSelectedChannel(activeAccounts[0].platform_user_id); } } catch (error) { + if (error instanceof TokenExpiredError) { + alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.'); + handleSocialReconnect(error.reconnectUrl); + return; + } console.error('Failed to load social accounts:', error); } finally { setIsLoadingAccounts(false); @@ -173,6 +178,12 @@ const SocialPostingModal: React.FC = ({ onClose(); resetForm(); } catch (error) { + if (error instanceof TokenExpiredError) { + setShowUploadProgress(false); + alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.'); + handleSocialReconnect(error.reconnectUrl); + return; + } console.error('Upload failed:', error); setUploadStatus('failed'); setUploadErrorMessage(error instanceof Error ? error.message : '업로드에 실패했습니다.'); diff --git a/src/pages/Dashboard/CompletionContent.tsx b/src/pages/Dashboard/CompletionContent.tsx index 2b3260c..61e2a6f 100755 --- a/src/pages/Dashboard/CompletionContent.tsx +++ b/src/pages/Dashboard/CompletionContent.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; -import { generateVideo, waitForVideoComplete, getYouTubeConnectUrl, getSocialAccounts, disconnectSocialAccount } from '../../utils/api'; +import { generateVideo, waitForVideoComplete, getYouTubeConnectUrl, getSocialAccounts, disconnectSocialAccount, TokenExpiredError, handleSocialReconnect } from '../../utils/api'; import { SocialAccount } from '../../types/api'; interface CompletionContentProps { @@ -317,6 +317,11 @@ const CompletionContent: React.FC = ({ console.log('[YouTube] No YouTube account connected'); } } catch (error) { + if (error instanceof TokenExpiredError) { + alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.'); + handleSocialReconnect(error.reconnectUrl); + return; + } console.error('[YouTube] Failed to check connection:', error); // API 에러 시에도 localStorage에서 확인한 값 유지 } @@ -351,6 +356,11 @@ const CompletionContent: React.FC = ({ // OAuth URL로 리다이렉트 window.location.href = response.auth_url; } catch (error) { + if (error instanceof TokenExpiredError) { + alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.'); + handleSocialReconnect(error.reconnectUrl); + return; + } console.error('YouTube connect failed:', error); setYoutubeError('YouTube 연결에 실패했습니다.'); setIsYoutubeConnecting(false); @@ -366,6 +376,11 @@ const CompletionContent: React.FC = ({ setYoutubeAccount(null); setSelectedSocials(prev => prev.filter(s => s !== 'Youtube')); } catch (error) { + if (error instanceof TokenExpiredError) { + alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.'); + handleSocialReconnect(error.reconnectUrl); + return; + } console.error('YouTube disconnect failed:', error); setYoutubeError('연결 해제에 실패했습니다.'); } diff --git a/src/pages/Dashboard/MyInfoContent.tsx b/src/pages/Dashboard/MyInfoContent.tsx index 4d80a62..2be07e3 100644 --- a/src/pages/Dashboard/MyInfoContent.tsx +++ b/src/pages/Dashboard/MyInfoContent.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { getSocialAccounts, getYouTubeConnectUrl, disconnectSocialAccount } from '../../utils/api'; +import { getSocialAccounts, getYouTubeConnectUrl, disconnectSocialAccount, TokenExpiredError, handleSocialReconnect } from '../../utils/api'; import { SocialAccount } from '../../types/api'; type TabType = 'basic' | 'payment' | 'business'; @@ -23,6 +23,11 @@ const MyInfoContent: React.FC = () => { const response = await getSocialAccounts(); setSocialAccounts(response.accounts || []); } catch (error) { + if (error instanceof TokenExpiredError) { + alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.'); + handleSocialReconnect(error.reconnectUrl); + return; + } console.error('Failed to load social accounts:', error); } finally { setIsLoadingAccounts(false); @@ -36,6 +41,11 @@ const MyInfoContent: React.FC = () => { const response = await getYouTubeConnectUrl(); window.location.href = response.auth_url; } catch (error) { + if (error instanceof TokenExpiredError) { + alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.'); + handleSocialReconnect(error.reconnectUrl); + return; + } console.error('Failed to get YouTube connect URL:', error); setIsConnecting(null); } @@ -47,6 +57,11 @@ const MyInfoContent: React.FC = () => { await disconnectSocialAccount(accountId); setSocialAccounts(prev => prev.filter(acc => acc.id !== accountId)); } catch (error) { + if (error instanceof TokenExpiredError) { + alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.'); + handleSocialReconnect(error.reconnectUrl); + return; + } console.error('Failed to disconnect:', error); } }; diff --git a/src/types/api.ts b/src/types/api.ts index 229bf91..fa4d8f0 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -350,3 +350,11 @@ export interface SocialUploadStatusResponse { uploaded_at?: string; } +// Social OAuth 토큰 만료 에러 응답 +export interface TokenExpiredErrorResponse { + detail: string; + code: 'TOKEN_EXPIRED'; + platform: string; + reconnect_url: string; +} + diff --git a/src/utils/api.ts b/src/utils/api.ts index a0b6757..bcd74f5 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -25,6 +25,7 @@ import { SocialUploadRequest, SocialUploadResponse, SocialUploadStatusResponse, + TokenExpiredErrorResponse, } from '../types/api'; const API_URL = import.meta.env.VITE_API_URL || 'http://40.82.133.44'; @@ -695,6 +696,53 @@ export async function autocomplete(request: AutocompleteRequest): Promise { + if (!response.ok) { + const body = await response.json().catch(() => null); + if (body && body.code === 'TOKEN_EXPIRED') { + throw new TokenExpiredError(body as TokenExpiredErrorResponse); + } + throw new Error(body?.detail || `HTTP error! status: ${response.status}`); + } +} + +// TOKEN_EXPIRED 발생 시 재연동 플로우 실행 +export async function handleSocialReconnect(reconnectUrl: string): Promise { + try { + const response = await authenticatedFetch(`${API_URL}${reconnectUrl}`, { + method: 'GET', + }); + + if (!response.ok) { + throw new Error(`재연동 요청 실패: ${response.status}`); + } + + const data: { auth_url: string } = await response.json(); + window.location.href = data.auth_url; + } catch (error) { + console.error('[Social] 재연동 처리 실패:', error); + throw error; + } +} + // ============================================ // Social OAuth API (YouTube, Instagram, Facebook) // ============================================ @@ -705,10 +753,7 @@ export async function getYouTubeConnectUrl(): Promise { method: 'GET', }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - + await handleSocialResponse(response); return response.json(); } @@ -718,10 +763,7 @@ export async function getSocialAccounts(): Promise { method: 'GET', }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - + await handleSocialResponse(response); return response.json(); } @@ -731,10 +773,7 @@ export async function getSocialAccountByPlatform(platform: 'youtube' | 'instagra method: 'GET', }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - + await handleSocialResponse(response); return response.json(); } @@ -744,10 +783,7 @@ export async function disconnectSocialAccount(accountId: number): Promise