feat(social): 업로드 폼에 저장된 영상 SEO 메타데이터를 표시

값이 있으면 DB 결과를 바로 채우고, 없으면 video_id 기준으로 한 번만 생성한다.
This commit is contained in:
김성경 2026-08-19 10:19:54 +09:00
parent 63d60caabd
commit bdb581459d
2 changed files with 27 additions and 34 deletions

View File

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

View File

@ -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 자동완성 응답