autoseo 추가
parent
9c975ad36a
commit
9df3fcbcd3
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getSocialAccounts, uploadToSocial, waitForUploadComplete, TokenExpiredError, handleSocialReconnect } from '../utils/api';
|
||||
import { getSocialAccounts, uploadToSocial, waitForUploadComplete, TokenExpiredError, handleSocialReconnect, getAutoSeoYoutube } from '../utils/api';
|
||||
import { SocialAccount, VideoListItem, SocialUploadStatusResponse } from '../types/api';
|
||||
import UploadProgressModal, { UploadStatus } from './UploadProgressModal';
|
||||
|
||||
|
|
@ -43,6 +43,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
const [isPosting, setIsPosting] = useState(false);
|
||||
const [isChannelDropdownOpen, setIsChannelDropdownOpen] = useState(false);
|
||||
const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false);
|
||||
const [isLoadingAutoDescription, setIsLoadingAutoDescription] = useState(false);
|
||||
const channelDropdownRef = useRef<HTMLDivElement>(null);
|
||||
const privacyDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -75,12 +76,13 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadSocialAccounts();
|
||||
// 비디오 정보로 기본 제목 설정
|
||||
if (video) {
|
||||
const date = new Date(video.created_at);
|
||||
const formattedDate = `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
|
||||
setTitle(`${video.store_name} ${formattedDate}`);
|
||||
}
|
||||
loadAutocomplete();
|
||||
// // 비디오 정보로 기본 제목 설정
|
||||
// if (video) {
|
||||
// const date = new Date(video.created_at);
|
||||
// const formattedDate = `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
|
||||
// setTitle(`${video.store_name} ${formattedDate}`);
|
||||
// }
|
||||
}
|
||||
}, [isOpen, video]);
|
||||
|
||||
|
|
@ -105,6 +107,30 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const loadAutocomplete = async () => {
|
||||
if (!video?.task_id) return;
|
||||
|
||||
setIsLoadingAutoDescription(true);
|
||||
try {
|
||||
const requestPayload = {
|
||||
task_id : video.task_id,
|
||||
};
|
||||
// Call autoSEO API
|
||||
console.log('[Upload] Request payload:', requestPayload);
|
||||
const autoSeoResponse = await getAutoSeoYoutube(requestPayload);
|
||||
|
||||
// 각 필드가 있을 때만 덮어씌움 (기존 값 보호)
|
||||
if (autoSeoResponse.title) setTitle(autoSeoResponse.title);
|
||||
if (autoSeoResponse.description) setDescription(autoSeoResponse.description);
|
||||
if (autoSeoResponse.keywords) setTags(autoSeoResponse.keywords.join(','));
|
||||
} catch (error) {
|
||||
console.error('Failed to load autocomplete:', error);
|
||||
// 실패해도 사용자에게 별도 알림 없이 조용히 처리
|
||||
} finally {
|
||||
setIsLoadingAutoDescription(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePost = async () => {
|
||||
if (!selectedChannel || !title.trim() || !video) {
|
||||
alert(t('social.channelAndTitleRequired'));
|
||||
|
|
@ -258,8 +284,8 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
<h2 className="social-posting-title">{t('social.title')}</h2>
|
||||
<button className="social-posting-close" onClick={handleClose}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -325,7 +351,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
)}
|
||||
</div>
|
||||
<svg className="social-posting-channel-arrow" viewBox="0 0 12 8" fill="none">
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
{isChannelDropdownOpen && (
|
||||
|
|
@ -363,9 +389,11 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={t('social.postTitlePlaceholder')}
|
||||
placeholder={isLoadingAutoDescription ? t('social.autoSeoTitle') : t('social.postTitlePlaceholder')}
|
||||
// placeholder={t('social.postTitlePlaceholder')}
|
||||
className="social-posting-input"
|
||||
maxLength={100}
|
||||
disabled={isLoadingAutoDescription}
|
||||
/>
|
||||
<span className="social-posting-char-count">{title.length}/100</span>
|
||||
</div>
|
||||
|
|
@ -376,10 +404,12 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t('social.postContentPlaceholder')}
|
||||
placeholder={t(isLoadingAutoDescription ? t('social.autoSeoDescription') : t('social.postContentPlaceholder'))}
|
||||
// placeholder={t('social.postContentPlaceholder')}
|
||||
className="social-posting-textarea"
|
||||
maxLength={5000}
|
||||
rows={4}
|
||||
disabled={isLoadingAutoDescription}
|
||||
/>
|
||||
<span className="social-posting-char-count">{description.length}/5,000</span>
|
||||
</div>
|
||||
|
|
@ -391,9 +421,11 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
type="text"
|
||||
value={tags}
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
placeholder={t('social.tagsPlaceholder')}
|
||||
placeholder={t(isLoadingAutoDescription ? t('social.autoSeoTags') : t('social.tagsPlaceholder'))}
|
||||
// placeholder={t('social.tagsPlaceholder')}
|
||||
className="social-posting-input"
|
||||
maxLength={500}
|
||||
disabled={isLoadingAutoDescription}
|
||||
/>
|
||||
<span className="social-posting-char-count">{tags.length}/500</span>
|
||||
</div>
|
||||
|
|
@ -413,7 +445,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
|||
<span>{selectedPrivacyOption?.label}</span>
|
||||
</div>
|
||||
<svg className="social-posting-channel-arrow" viewBox="0 0 12 8" fill="none">
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
{isPrivacyDropdownOpen && (
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@
|
|||
"selectChannel": "Please select a channel.",
|
||||
"invalidVideoInfo": "Video information is invalid. (missing video_id)",
|
||||
"uploadStartFailed": "Failed to start upload.",
|
||||
"uploadFailed": "Upload failed."
|
||||
"uploadFailed": "Upload failed.",
|
||||
"autoSeoTitle": "This will be automatically generated. please wait.",
|
||||
"autoSeoDescription": "This will be automatically generated. please wait.",
|
||||
"autoSeoTags": "This will be automatically generated. please wait."
|
||||
},
|
||||
"upload": {
|
||||
"title": "YouTube Upload",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@
|
|||
"selectChannel": "채널을 선택해주세요.",
|
||||
"invalidVideoInfo": "영상 정보가 올바르지 않습니다. (video_id 누락)",
|
||||
"uploadStartFailed": "업로드 시작에 실패했습니다.",
|
||||
"uploadFailed": "업로드에 실패했습니다."
|
||||
"uploadFailed": "업로드에 실패했습니다.",
|
||||
"autoSeoTitle": "자동으로 작성중입니다. 기다려주세요.",
|
||||
"autoSeoDescription": "자동으로 작성중입니다. 기다려주세요.",
|
||||
"autoSeoTags": "자동으로 작성중입니다. 기다려주세요."
|
||||
},
|
||||
"upload": {
|
||||
"title": "YouTube 업로드",
|
||||
|
|
|
|||
|
|
@ -314,6 +314,18 @@ export interface SocialDisconnectResponse {
|
|||
// Social Upload Types (YouTube Upload)
|
||||
// ============================================
|
||||
|
||||
// 유튜브 SEO Description 자동완성 요청
|
||||
export interface YTAutoSeoRequest {
|
||||
task_id: string; // 아카이브의 비디오 ID
|
||||
}
|
||||
|
||||
// 유튜브 SEO Description 자동완성 응답
|
||||
export interface YTAutoSeoResponse {
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
// 소셜 업로드 요청
|
||||
export interface SocialUploadRequest {
|
||||
video_id: number; // 아카이브의 비디오 ID
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import {
|
|||
SocialUploadResponse,
|
||||
SocialUploadStatusResponse,
|
||||
TokenExpiredErrorResponse,
|
||||
YTAutoSeoRequest,
|
||||
YTAutoSeoResponse,
|
||||
} from '../types/api';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://40.82.133.44';
|
||||
|
|
@ -791,6 +793,20 @@ export async function disconnectSocialAccount(accountId: number): Promise<Social
|
|||
// Social Upload API (YouTube Video Upload)
|
||||
// ============================================
|
||||
|
||||
// YouTube Description API
|
||||
export async function getAutoSeoYoutube(request: YTAutoSeoRequest): Promise<YTAutoSeoResponse> {
|
||||
const response = await authenticatedFetch(`${API_URL}/social/seo/youtube`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
await handleSocialResponse(response);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// YouTube 영상 업로드 시작
|
||||
export async function uploadToSocial(request: SocialUploadRequest): Promise<SocialUploadResponse> {
|
||||
const response = await authenticatedFetch(`${API_URL}/social/upload`, {
|
||||
|
|
|
|||
Loading…
Reference in New Issue