social 계정 재연동 로직 추가 .
parent
3eedb8dc17
commit
29fdb7e65c
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
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 { SocialAccount, VideoListItem, SocialUploadStatusResponse } from '../types/api';
|
||||||
import UploadProgressModal, { UploadStatus } from './UploadProgressModal';
|
import UploadProgressModal, { UploadStatus } from './UploadProgressModal';
|
||||||
|
|
||||||
|
|
@ -92,6 +92,11 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
setSelectedChannel(activeAccounts[0].platform_user_id);
|
setSelectedChannel(activeAccounts[0].platform_user_id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.');
|
||||||
|
handleSocialReconnect(error.reconnectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Failed to load social accounts:', error);
|
console.error('Failed to load social accounts:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingAccounts(false);
|
setIsLoadingAccounts(false);
|
||||||
|
|
@ -173,6 +178,12 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
onClose();
|
onClose();
|
||||||
resetForm();
|
resetForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
setShowUploadProgress(false);
|
||||||
|
alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.');
|
||||||
|
handleSocialReconnect(error.reconnectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Upload failed:', error);
|
console.error('Upload failed:', error);
|
||||||
setUploadStatus('failed');
|
setUploadStatus('failed');
|
||||||
setUploadErrorMessage(error instanceof Error ? error.message : '업로드에 실패했습니다.');
|
setUploadErrorMessage(error instanceof Error ? error.message : '업로드에 실패했습니다.');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
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';
|
import { SocialAccount } from '../../types/api';
|
||||||
|
|
||||||
interface CompletionContentProps {
|
interface CompletionContentProps {
|
||||||
|
|
@ -317,6 +317,11 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
console.log('[YouTube] No YouTube account connected');
|
console.log('[YouTube] No YouTube account connected');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.');
|
||||||
|
handleSocialReconnect(error.reconnectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('[YouTube] Failed to check connection:', error);
|
console.error('[YouTube] Failed to check connection:', error);
|
||||||
// API 에러 시에도 localStorage에서 확인한 값 유지
|
// API 에러 시에도 localStorage에서 확인한 값 유지
|
||||||
}
|
}
|
||||||
|
|
@ -351,6 +356,11 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
// OAuth URL로 리다이렉트
|
// OAuth URL로 리다이렉트
|
||||||
window.location.href = response.auth_url;
|
window.location.href = response.auth_url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.');
|
||||||
|
handleSocialReconnect(error.reconnectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('YouTube connect failed:', error);
|
console.error('YouTube connect failed:', error);
|
||||||
setYoutubeError('YouTube 연결에 실패했습니다.');
|
setYoutubeError('YouTube 연결에 실패했습니다.');
|
||||||
setIsYoutubeConnecting(false);
|
setIsYoutubeConnecting(false);
|
||||||
|
|
@ -366,6 +376,11 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
setYoutubeAccount(null);
|
setYoutubeAccount(null);
|
||||||
setSelectedSocials(prev => prev.filter(s => s !== 'Youtube'));
|
setSelectedSocials(prev => prev.filter(s => s !== 'Youtube'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.');
|
||||||
|
handleSocialReconnect(error.reconnectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('YouTube disconnect failed:', error);
|
console.error('YouTube disconnect failed:', error);
|
||||||
setYoutubeError('연결 해제에 실패했습니다.');
|
setYoutubeError('연결 해제에 실패했습니다.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
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';
|
import { SocialAccount } from '../../types/api';
|
||||||
|
|
||||||
type TabType = 'basic' | 'payment' | 'business';
|
type TabType = 'basic' | 'payment' | 'business';
|
||||||
|
|
@ -23,6 +23,11 @@ const MyInfoContent: React.FC = () => {
|
||||||
const response = await getSocialAccounts();
|
const response = await getSocialAccounts();
|
||||||
setSocialAccounts(response.accounts || []);
|
setSocialAccounts(response.accounts || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.');
|
||||||
|
handleSocialReconnect(error.reconnectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Failed to load social accounts:', error);
|
console.error('Failed to load social accounts:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingAccounts(false);
|
setIsLoadingAccounts(false);
|
||||||
|
|
@ -36,6 +41,11 @@ const MyInfoContent: React.FC = () => {
|
||||||
const response = await getYouTubeConnectUrl();
|
const response = await getYouTubeConnectUrl();
|
||||||
window.location.href = response.auth_url;
|
window.location.href = response.auth_url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.');
|
||||||
|
handleSocialReconnect(error.reconnectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Failed to get YouTube connect URL:', error);
|
console.error('Failed to get YouTube connect URL:', error);
|
||||||
setIsConnecting(null);
|
setIsConnecting(null);
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +57,11 @@ const MyInfoContent: React.FC = () => {
|
||||||
await disconnectSocialAccount(accountId);
|
await disconnectSocialAccount(accountId);
|
||||||
setSocialAccounts(prev => prev.filter(acc => acc.id !== accountId));
|
setSocialAccounts(prev => prev.filter(acc => acc.id !== accountId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
alert('YouTube 인증이 만료되었습니다. 재연동 페이지로 이동합니다.');
|
||||||
|
handleSocialReconnect(error.reconnectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Failed to disconnect:', error);
|
console.error('Failed to disconnect:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -350,3 +350,11 @@ export interface SocialUploadStatusResponse {
|
||||||
uploaded_at?: string;
|
uploaded_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Social OAuth 토큰 만료 에러 응답
|
||||||
|
export interface TokenExpiredErrorResponse {
|
||||||
|
detail: string;
|
||||||
|
code: 'TOKEN_EXPIRED';
|
||||||
|
platform: string;
|
||||||
|
reconnect_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
SocialUploadRequest,
|
SocialUploadRequest,
|
||||||
SocialUploadResponse,
|
SocialUploadResponse,
|
||||||
SocialUploadStatusResponse,
|
SocialUploadStatusResponse,
|
||||||
|
TokenExpiredErrorResponse,
|
||||||
} from '../types/api';
|
} from '../types/api';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://40.82.133.44';
|
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)
|
// Social OAuth API (YouTube, Instagram, Facebook)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -705,10 +753,7 @@ export async function getYouTubeConnectUrl(): Promise<YouTubeConnectResponse> {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
await handleSocialResponse(response);
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -718,10 +763,7 @@ export async function getSocialAccounts(): Promise<SocialAccountsResponse> {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
await handleSocialResponse(response);
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -731,10 +773,7 @@ export async function getSocialAccountByPlatform(platform: 'youtube' | 'instagra
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
await handleSocialResponse(response);
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -744,10 +783,7 @@ export async function disconnectSocialAccount(accountId: number): Promise<Social
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
await handleSocialResponse(response);
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -765,10 +801,7 @@ export async function uploadToSocial(request: SocialUploadRequest): Promise<Soci
|
||||||
body: JSON.stringify(request),
|
body: JSON.stringify(request),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
await handleSocialResponse(response);
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -778,10 +811,7 @@ export async function getUploadStatus(uploadId: string): Promise<SocialUploadSta
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
await handleSocialResponse(response);
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue