social 계정 재연동 로직 추가 .
parent
3eedb8dc17
commit
29fdb7e65c
|
|
@ -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 : '업로드에 실패했습니다.');
|
||||
|
|
|
|||
|
|
@ -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('연결 해제에 실패했습니다.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue