social 계정 재연동 로직 추가 .

main
hbyang 2026-02-09 16:50:30 +09:00
parent 3eedb8dc17
commit 29fdb7e65c
5 changed files with 106 additions and 27 deletions

View File

@ -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<SocialPostingModalProps> = ({
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<SocialPostingModalProps> = ({
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 : '업로드에 실패했습니다.');

View File

@ -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<CompletionContentProps> = ({
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<CompletionContentProps> = ({
// 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<CompletionContentProps> = ({
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('연결 해제에 실패했습니다.');
}

View File

@ -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);
}
};

View File

@ -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;
}

View File

@ -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<Crawli
}
}
// ============================================
// Social OAuth TOKEN_EXPIRED 처리
// ============================================
// YouTube 등 소셜 플랫폼 토큰 만료 에러 클래스
export class TokenExpiredError extends Error {
platform: string;
reconnectUrl: string;
constructor(response: TokenExpiredErrorResponse) {
super(response.detail);
this.name = 'TokenExpiredError';
this.platform = response.platform;
this.reconnectUrl = response.reconnect_url;
}
}
// Social API 응답에서 TOKEN_EXPIRED 에러를 감지하고 처리
async function handleSocialResponse(response: Response): Promise<void> {
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<void> {
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<YouTubeConnectResponse> {
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<SocialAccountsResponse> {
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<Social
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
await handleSocialResponse(response);
return response.json();
}
@ -765,10 +801,7 @@ export async function uploadToSocial(request: SocialUploadRequest): Promise<Soci
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
await handleSocialResponse(response);
return response.json();
}
@ -778,10 +811,7 @@ export async function getUploadStatus(uploadId: string): Promise<SocialUploadSta
method: 'GET',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
await handleSocialResponse(response);
return response.json();
}