+const AudienceBarChart: React.FC<{ data: { label: string; percentage: number }[]; delay?: number }> = ({ data, delay = 0 }) => {
+ const [isAnimated, setIsAnimated] = useState(false);
+
+ useEffect(() => {
+ const timer = setTimeout(() => setIsAnimated(true), delay);
+ return () => clearTimeout(timer);
+ }, [delay]);
+
+ return (
+
-
Mon
-
Tue
-
Wed
-
Thu
-
Fri
-
Sat
-
Sun
+const GenderChart: React.FC<{ male: number; female: number; delay?: number }> = ({ male, female, delay = 0 }) => {
+ const [isAnimated, setIsAnimated] = useState(false);
+
+ useEffect(() => {
+ const timer = setTimeout(() => setIsAnimated(true), delay);
+ return () => clearTimeout(timer);
+ }, [delay]);
+
+ return (
+
+
+
+ {male}%
+
+ {female}%
+
+
+
+ 남성
+ 여성
);
};
+// =====================================================
+// Main Component
+// =====================================================
+
+const DashboardContent: React.FC = () => {
+ const [selectedPlatform, setSelectedPlatform] = useState<'youtube' | 'instagram'>('youtube');
+
+ const currentPlatformData = PLATFORM_DATA.find(p => p.platform === selectedPlatform);
+ const lastUpdated = new Date().toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+
+ return (
+
+ {/* Header */}
+
+
+
+
대시보드
+
실시간 마케팅 퍼포먼스를 확인하세요.
+
+
마지막 업데이트: {lastUpdated}
+
+
+
+ {/* Content Performance Section */}
+
+ 콘텐츠 성과
+
+ {CONTENT_METRICS.map((metric, index) => (
+
+
+
+ ))}
+
+
+
+ {/* Two Column Layout */}
+
+ {/* Year over Year Growth Section */}
+
+
+
+
+
+
+
+ {MONTHLY_DATA.map(d => (
+ {d.month}
+ ))}
+
+
+
+
+ {/* Top Content Section */}
+
+
+
인기 콘텐츠
+
+ {TOP_CONTENT.map((content, index) => (
+
+
+
+ ))}
+
+
+
+
+
+ {/* Audience Insights Section */}
+
+ 오디언스 인사이트
+
+
+
+
+
+
+
성별 분포
+
+
+
+
+
+
상위 지역
+
({ label: r.region, percentage: r.percentage }))} delay={1400} />
+
+
+
+
+
+ {/* Platform Metrics Section */}
+
+
+
+
플랫폼별 지표
+
+
+
+
+
+
+ {currentPlatformData?.metrics.map((metric, index) => (
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
export default DashboardContent;
diff --git a/src/pages/Dashboard/GenerationFlow.tsx b/src/pages/Dashboard/GenerationFlow.tsx
index d7be301..7e80197 100755
--- a/src/pages/Dashboard/GenerationFlow.tsx
+++ b/src/pages/Dashboard/GenerationFlow.tsx
@@ -22,6 +22,7 @@ const ANALYSIS_DATA_KEY = 'castad_analysis_data';
// 다른 컴포넌트에서 사용하는 storage key들 (초기화용)
const SONG_GENERATION_KEY = 'castad_song_generation';
const VIDEO_GENERATION_KEY = 'castad_video_generation';
+const VIDEO_COMPLETE_KEY = 'castad_video_complete'; // 완료된 영상 정보
// 모든 프로젝트 관련 localStorage 초기화
const clearAllProjectStorage = () => {
@@ -30,6 +31,7 @@ const clearAllProjectStorage = () => {
localStorage.removeItem(IMAGE_TASK_ID_KEY);
localStorage.removeItem(SONG_GENERATION_KEY);
localStorage.removeItem(VIDEO_GENERATION_KEY);
+ localStorage.removeItem(VIDEO_COMPLETE_KEY);
};
interface BusinessInfo {
@@ -291,6 +293,7 @@ const GenerationFlow: React.FC
= ({
onNext={(taskId: string) => {
// Clear video generation state to start fresh
localStorage.removeItem(VIDEO_GENERATION_KEY);
+ localStorage.removeItem(VIDEO_COMPLETE_KEY);
setVideoGenerationStatus('idle');
setVideoGenerationProgress(0);
@@ -321,7 +324,15 @@ const GenerationFlow: React.FC = ({
case 3:
return (
goToWizardStep(2)}
+ onBack={() => {
+ // 뒤로가기 시 비디오 생성 상태 초기화
+ // 새 노래 생성 후 다시 영상 생성할 수 있도록
+ localStorage.removeItem(VIDEO_GENERATION_KEY);
+ localStorage.removeItem(VIDEO_COMPLETE_KEY);
+ setVideoGenerationStatus('idle');
+ setVideoGenerationProgress(0);
+ goToWizardStep(2);
+ }}
songTaskId={songTaskId}
onVideoStatusChange={setVideoGenerationStatus}
onVideoProgressChange={setVideoGenerationProgress}
diff --git a/src/pages/Social/SocialConnectError.tsx b/src/pages/Social/SocialConnectError.tsx
new file mode 100644
index 0000000..e174aad
--- /dev/null
+++ b/src/pages/Social/SocialConnectError.tsx
@@ -0,0 +1,48 @@
+
+import React from 'react';
+
+const SocialConnectError: React.FC = () => {
+ // URL 파라미터 파싱
+ const searchParams = new URLSearchParams(window.location.search);
+ const platform = searchParams.get('platform') || 'unknown';
+ const errorMessage = searchParams.get('error') || '알 수 없는 오류가 발생했습니다.';
+
+ const platformName = platform === 'youtube' ? 'YouTube' :
+ platform === 'instagram' ? 'Instagram' :
+ platform === 'facebook' ? 'Facebook' : platform;
+
+ const navigateToHome = () => {
+ window.location.href = '/';
+ };
+
+ return (
+
+
+
+
+
+
{platformName} 연결 실패
+
+ {errorMessage}
+
+
+ 다시 시도해주세요. 문제가 지속되면 고객 지원에 문의해주세요.
+
+
+
+
+
+
+ );
+};
+
+export default SocialConnectError;
diff --git a/src/pages/Social/SocialConnectSuccess.tsx b/src/pages/Social/SocialConnectSuccess.tsx
new file mode 100644
index 0000000..737938f
--- /dev/null
+++ b/src/pages/Social/SocialConnectSuccess.tsx
@@ -0,0 +1,121 @@
+
+import React, { useEffect, useState } from 'react';
+
+// 연결된 소셜 계정 정보를 저장하는 localStorage 키
+const SOCIAL_CONNECTED_KEY = 'castad_social_connected';
+
+interface ConnectedSocialAccount {
+ platform: string;
+ account_id: string;
+ channel_name: string;
+ profile_image: string | null;
+ connected_at: string;
+}
+
+const SocialConnectSuccess: React.FC = () => {
+ const [countdown, setCountdown] = useState(3);
+
+ // URL 파라미터 파싱
+ const searchParams = new URLSearchParams(window.location.search);
+ const platform = searchParams.get('platform') || 'unknown';
+ const accountId = searchParams.get('account_id') || '';
+ const channelName = searchParams.get('channel_name') || '';
+ const profileImage = searchParams.get('profile_image') || null;
+
+ const platformName = platform === 'youtube' ? 'YouTube' :
+ platform === 'instagram' ? 'Instagram' :
+ platform === 'facebook' ? 'Facebook' : platform;
+
+ // 연결된 계정 정보를 localStorage에 저장
+ useEffect(() => {
+ if (platform && accountId) {
+ const connectedAccount: ConnectedSocialAccount = {
+ platform,
+ account_id: accountId,
+ channel_name: channelName,
+ profile_image: profileImage,
+ connected_at: new Date().toISOString(),
+ };
+ localStorage.setItem(SOCIAL_CONNECTED_KEY, JSON.stringify(connectedAccount));
+ console.log('[Social Connect] Account saved:', connectedAccount);
+ }
+ }, [platform, accountId, channelName, profileImage]);
+
+ const navigateToHome = () => {
+ // URL을 루트로 변경하고 페이지 새로고침하여 generation_flow로 이동
+ window.location.href = '/';
+ };
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCountdown(prev => {
+ if (prev <= 1) {
+ clearInterval(timer);
+ navigateToHome();
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, []);
+
+ return (
+
+
+ {/* 프로필 이미지가 있으면 표시 */}
+ {profileImage ? (
+

+ ) : (
+
+ )}
+
+
+ {channelName || platformName} 연결 완료
+
+
+
+ {channelName ? (
+ <>{platformName} 계정이 성공적으로 연결되었습니다.>
+ ) : (
+ <>계정이 성공적으로 연결되었습니다.>
+ )}
+
+
+ {channelName && (
+
+ )}
+
+
+ {countdown}초 후 자동으로 이동합니다...
+
+
+
+
+
+ );
+};
+
+export default SocialConnectSuccess;
diff --git a/src/types/api.ts b/src/types/api.ts
index 51d9e07..98877f0 100644
--- a/src/types/api.ts
+++ b/src/types/api.ts
@@ -263,3 +263,43 @@ export interface VideosListResponse {
has_prev: boolean;
}
+// ============================================
+// Social OAuth Types
+// ============================================
+
+// YouTube OAuth 연결 응답
+export interface YouTubeConnectResponse {
+ auth_url: string;
+}
+
+// 연결된 소셜 계정 정보 (서버 응답 형식)
+export interface SocialAccount {
+ id: number;
+ platform: 'youtube' | 'instagram' | 'facebook';
+ platform_user_id: string;
+ platform_username: string;
+ display_name: string;
+ is_active: boolean;
+ connected_at: string;
+ // OAuth 콜백에서 전달받는 추가 정보 (optional)
+ profile_image?: string | null;
+}
+
+// 소셜 계정 목록 응답
+export interface SocialAccountsResponse {
+ accounts: SocialAccount[];
+ total: number;
+}
+
+// 소셜 계정 단일 조회 응답
+export interface SocialAccountResponse {
+ account: SocialAccount | null;
+ connected: boolean;
+}
+
+// 소셜 계정 연결 해제 응답
+export interface SocialDisconnectResponse {
+ success: boolean;
+ message: string;
+}
+
diff --git a/src/utils/api.ts b/src/utils/api.ts
index 1a54f5a..1ae8245 100644
--- a/src/utils/api.ts
+++ b/src/utils/api.ts
@@ -18,6 +18,10 @@ import {
KakaoCallbackResponse,
TokenRefreshResponse,
UserMeResponse,
+ YouTubeConnectResponse,
+ SocialAccountsResponse,
+ SocialAccountResponse,
+ SocialDisconnectResponse,
} from '../types/api';
const API_URL = import.meta.env.VITE_API_URL || 'http://40.82.133.44';
@@ -665,3 +669,59 @@ export async function autocomplete(request: AutocompleteRequest): Promise {
+ const response = await authenticatedFetch(`${API_URL}/social/oauth/youtube/connect`, {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return response.json();
+}
+
+// 연결된 소셜 계정 목록 조회
+export async function getSocialAccounts(): Promise {
+ const response = await authenticatedFetch(`${API_URL}/social/oauth/accounts`, {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return response.json();
+}
+
+// 특정 플랫폼 계정 조회
+export async function getSocialAccountByPlatform(platform: 'youtube' | 'instagram' | 'facebook'): Promise {
+ const response = await authenticatedFetch(`${API_URL}/social/oauth/accounts/${platform}`, {
+ method: 'GET',
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return response.json();
+}
+
+// 소셜 계정 연결 해제
+export async function disconnectSocialAccount(platform: 'youtube' | 'instagram' | 'facebook'): Promise {
+ const response = await authenticatedFetch(`${API_URL}/social/oauth/${platform}/disconnect`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return response.json();
+}