feat(social): 업로드 폼에 저장된 영상 SEO 메타데이터를 표시
값이 있으면 DB 결과를 바로 채우고, 없으면 video_id 기준으로 한 번만 생성한다.
This commit is contained in:
parent
63d60caabd
commit
bdb581459d
@ -142,10 +142,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||||||
const channelDropdownRef = useRef<HTMLDivElement>(null);
|
const channelDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const privacyDropdownRef = useRef<HTMLDivElement>(null);
|
const privacyDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const hasBeenOpenedRef = useRef(false);
|
const hasBeenOpenedRef = useRef(false);
|
||||||
const loadedForTaskIdRef = useRef<string | null>(null);
|
const loadedForVideoIdRef = useRef<number | null>(null);
|
||||||
const loadedAtRef = useRef<number>(0);
|
|
||||||
const seoCache = useRef<Map<string, { title: string; description: string; tags: string }>>(new Map());
|
|
||||||
const SEO_CACHE_TTL = 50 * 60 * 1000;
|
|
||||||
|
|
||||||
// Upload progress modal state
|
// Upload progress modal state
|
||||||
const [showUploadProgress, setShowUploadProgress] = useState(false);
|
const [showUploadProgress, setShowUploadProgress] = useState(false);
|
||||||
@ -212,28 +209,30 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||||||
|
|
||||||
// 소셜 계정 로드
|
// 소셜 계정 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) {
|
||||||
|
loadedForVideoIdRef.current = null;
|
||||||
const now = Date.now();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loadSocialAccounts();
|
loadSocialAccounts();
|
||||||
|
|
||||||
const taskId = video?.task_id ?? null;
|
const videoId = video?.video_id ?? null;
|
||||||
const expired = now - loadedAtRef.current > SEO_CACHE_TTL;
|
if (!videoId || videoId === loadedForVideoIdRef.current) {
|
||||||
|
return;
|
||||||
if (taskId && (taskId !== loadedForTaskIdRef.current || expired)) {
|
|
||||||
loadedForTaskIdRef.current = taskId;
|
|
||||||
loadedAtRef.current = now;
|
|
||||||
loadAutocomplete();
|
|
||||||
} else if (taskId) {
|
|
||||||
const cached = seoCache.current.get(taskId);
|
|
||||||
if (cached) {
|
|
||||||
setTitle(cached.title);
|
|
||||||
setDescription(cached.description);
|
|
||||||
setTags(cached.tags);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [isOpen, video?.task_id]);
|
|
||||||
|
loadedForVideoIdRef.current = videoId;
|
||||||
|
|
||||||
|
if (video?.title) {
|
||||||
|
setTitle(video.title);
|
||||||
|
setDescription(video.description || '');
|
||||||
|
setTags((video.hashtags || []).join(','));
|
||||||
|
setIsLoadingAutoDescription(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAutocomplete();
|
||||||
|
}, [isOpen, video?.video_id, video?.title, video?.description, video?.hashtags]);
|
||||||
|
|
||||||
const loadSocialAccounts = async () => {
|
const loadSocialAccounts = async () => {
|
||||||
setIsLoadingAccounts(true);
|
setIsLoadingAccounts(true);
|
||||||
@ -259,29 +258,20 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadAutocomplete = async () => {
|
const loadAutocomplete = async () => {
|
||||||
if (!video?.task_id) return;
|
if (!video?.video_id) return;
|
||||||
|
|
||||||
setIsLoadingAutoDescription(true);
|
setIsLoadingAutoDescription(true);
|
||||||
try {
|
try {
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
task_id : video.task_id,
|
video_id: video.video_id,
|
||||||
};
|
};
|
||||||
// Call autoSEO API
|
|
||||||
console.log('[Upload] Request payload:', requestPayload);
|
|
||||||
const autoSeoResponse = await getAutoSeoYoutube(requestPayload);
|
const autoSeoResponse = await getAutoSeoYoutube(requestPayload);
|
||||||
|
|
||||||
// 각 필드가 있을 때만 덮어씌움 (기존 값 보호)
|
|
||||||
if (autoSeoResponse.title) setTitle(autoSeoResponse.title);
|
if (autoSeoResponse.title) setTitle(autoSeoResponse.title);
|
||||||
if (autoSeoResponse.description) setDescription(autoSeoResponse.description);
|
if (autoSeoResponse.description) setDescription(autoSeoResponse.description);
|
||||||
if (autoSeoResponse.keywords) setTags(autoSeoResponse.keywords.join(','));
|
if (autoSeoResponse.keywords) setTags(autoSeoResponse.keywords.join(','));
|
||||||
seoCache.current.set(video.task_id, {
|
|
||||||
title: autoSeoResponse.title || '',
|
|
||||||
description: autoSeoResponse.description || '',
|
|
||||||
tags: autoSeoResponse.keywords?.join(',') || '',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load autocomplete:', error);
|
console.error('Failed to load autocomplete:', error);
|
||||||
// 실패해도 사용자에게 별도 알림 없이 조용히 처리
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingAutoDescription(false);
|
setIsLoadingAutoDescription(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -278,6 +278,9 @@ export interface VideoListItem {
|
|||||||
result_movie_url: string;
|
result_movie_url: string;
|
||||||
poster_url?: string | null;
|
poster_url?: string | null;
|
||||||
thumbnail_url?: string;
|
thumbnail_url?: string;
|
||||||
|
title?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
hashtags?: string[] | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
like_count: number;
|
like_count: number;
|
||||||
comment_count: number;
|
comment_count: number;
|
||||||
@ -394,7 +397,7 @@ export interface SocialDisconnectResponse {
|
|||||||
|
|
||||||
// 유튜브 SEO Description 자동완성 요청
|
// 유튜브 SEO Description 자동완성 요청
|
||||||
export interface YTAutoSeoRequest {
|
export interface YTAutoSeoRequest {
|
||||||
task_id: string; // 아카이브의 비디오 ID
|
video_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 유튜브 SEO Description 자동완성 응답
|
// 유튜브 SEO Description 자동완성 응답
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user