임시 저장
parent
bdd52ed992
commit
82dcda0038
16
index.css
16
index.css
|
|
@ -10620,7 +10620,21 @@
|
||||||
padding: 1.25rem 1.5rem;
|
padding: 1.25rem 1.5rem;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress-calendar-link {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress-calendar-link:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-progress-btn {
|
.upload-progress-btn {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface SocialPostingModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
video: VideoListItem | null;
|
video: VideoListItem | null;
|
||||||
|
onGoToCalendar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PrivacyType = 'public' | 'unlisted' | 'private';
|
type PrivacyType = 'public' | 'unlisted' | 'private';
|
||||||
|
|
@ -116,7 +117,8 @@ const MiniCalendar: React.FC<{
|
||||||
const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
video
|
video,
|
||||||
|
onGoToCalendar,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const tutorial = useTutorial();
|
const tutorial = useTutorial();
|
||||||
|
|
@ -150,6 +152,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
// 업로드 정보 (모달이 닫힌 후에도 유지)
|
// 업로드 정보 (모달이 닫힌 후에도 유지)
|
||||||
const [uploadVideoTitle, setUploadVideoTitle] = useState<string>('');
|
const [uploadVideoTitle, setUploadVideoTitle] = useState<string>('');
|
||||||
const [uploadChannelName, setUploadChannelName] = useState<string>('');
|
const [uploadChannelName, setUploadChannelName] = useState<string>('');
|
||||||
|
const [uploadIsScheduled, setUploadIsScheduled] = useState(false);
|
||||||
|
|
||||||
// 드롭다운 외부 클릭 시 닫기
|
// 드롭다운 외부 클릭 시 닫기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -300,6 +303,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
// 업로드 정보 저장 (모달이 닫힌 후에도 유지)
|
// 업로드 정보 저장 (모달이 닫힌 후에도 유지)
|
||||||
setUploadVideoTitle(title.trim());
|
setUploadVideoTitle(title.trim());
|
||||||
setUploadChannelName(selectedAcc.display_name);
|
setUploadChannelName(selectedAcc.display_name);
|
||||||
|
setUploadIsScheduled(publishTime === 'schedule');
|
||||||
setShowUploadProgress(true);
|
setShowUploadProgress(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -401,6 +405,7 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
setUploadErrorMessage(undefined);
|
setUploadErrorMessage(undefined);
|
||||||
setUploadVideoTitle('');
|
setUploadVideoTitle('');
|
||||||
setUploadChannelName('');
|
setUploadChannelName('');
|
||||||
|
setUploadIsScheduled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
|
@ -429,7 +434,8 @@ const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
|
||||||
channelName={uploadChannelName || selectedAccount?.display_name || ''}
|
channelName={uploadChannelName || selectedAccount?.display_name || ''}
|
||||||
youtubeUrl={uploadYoutubeUrl}
|
youtubeUrl={uploadYoutubeUrl}
|
||||||
errorMessage={uploadErrorMessage}
|
errorMessage={uploadErrorMessage}
|
||||||
isScheduled={publishTime === 'schedule'}
|
isScheduled={uploadIsScheduled}
|
||||||
|
onGoToCalendar={onGoToCalendar}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ interface UploadProgressModalProps {
|
||||||
youtubeUrl?: string;
|
youtubeUrl?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
isScheduled?: boolean;
|
isScheduled?: boolean;
|
||||||
|
onGoToCalendar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadProgressModal: React.FC<UploadProgressModalProps> = ({
|
const UploadProgressModal: React.FC<UploadProgressModalProps> = ({
|
||||||
|
|
@ -26,6 +27,7 @@ const UploadProgressModal: React.FC<UploadProgressModalProps> = ({
|
||||||
youtubeUrl,
|
youtubeUrl,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isScheduled = false,
|
isScheduled = false,
|
||||||
|
onGoToCalendar,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
@ -151,9 +153,16 @@ const UploadProgressModal: React.FC<UploadProgressModalProps> = ({
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="upload-progress-footer">
|
<div className="upload-progress-footer">
|
||||||
{canClose ? (
|
{canClose ? (
|
||||||
<button className="upload-progress-btn primary" onClick={onClose}>
|
<>
|
||||||
{status === 'completed' ? t('upload.confirm') : t('upload.close')}
|
<button className="upload-progress-btn primary" onClick={onClose}>
|
||||||
</button>
|
{status === 'completed' ? t('upload.confirm') : t('upload.close')}
|
||||||
|
</button>
|
||||||
|
{status === 'completed' && onGoToCalendar && (
|
||||||
|
<span className="upload-progress-calendar-link" onClick={() => { onClose(); onGoToCalendar(); }}>
|
||||||
|
{t('upload.goToCalendar')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="upload-progress-note">{t('upload.doNotClose')}</p>
|
<p className="upload-progress-note">{t('upload.doNotClose')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,8 @@
|
||||||
"viewOnYoutube": "View on YouTube",
|
"viewOnYoutube": "View on YouTube",
|
||||||
"confirm": "OK",
|
"confirm": "OK",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"doNotClose": "Upload is in progress. Do not close this window."
|
"doNotClose": "Upload is in progress. Do not close this window.",
|
||||||
|
"goToCalendar": "View in Calendar"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,8 @@
|
||||||
"viewOnYoutube": "YouTube에서 보기",
|
"viewOnYoutube": "YouTube에서 보기",
|
||||||
"confirm": "확인",
|
"confirm": "확인",
|
||||||
"close": "닫기",
|
"close": "닫기",
|
||||||
"doNotClose": "업로드가 진행 중입니다. 창을 닫지 마세요."
|
"doNotClose": "업로드가 진행 중입니다. 창을 닫지 마세요.",
|
||||||
|
"goToCalendar": "캘린더에서 확인"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"hero": {
|
"hero": {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ import SocialPostingModal from '../../components/SocialPostingModal';
|
||||||
|
|
||||||
interface ADO2ContentsPageProps {
|
interface ADO2ContentsPageProps {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
onNavigate?: (item: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADO2ContentsPage: React.FC<ADO2ContentsPageProps> = ({ onBack }) => {
|
const ADO2ContentsPage: React.FC<ADO2ContentsPageProps> = ({ onBack, onNavigate }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [videos, setVideos] = useState<VideoListItem[]>([]);
|
const [videos, setVideos] = useState<VideoListItem[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|
@ -295,6 +296,7 @@ const ADO2ContentsPage: React.FC<ADO2ContentsPageProps> = ({ onBack }) => {
|
||||||
isOpen={uploadModalOpen}
|
isOpen={uploadModalOpen}
|
||||||
onClose={handleUploadModalClose}
|
onClose={handleUploadModalClose}
|
||||||
video={uploadTargetVideo}
|
video={uploadTargetVideo}
|
||||||
|
onGoToCalendar={onNavigate ? () => onNavigate('콘텐츠 캘린더') : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ interface CompletionContentProps {
|
||||||
onVideoStatusChange?: (status: 'idle' | 'generating' | 'complete' | 'error') => void;
|
onVideoStatusChange?: (status: 'idle' | 'generating' | 'complete' | 'error') => void;
|
||||||
onVideoProgressChange?: (progress: number) => void;
|
onVideoProgressChange?: (progress: number) => void;
|
||||||
onVideoComplete?: () => void;
|
onVideoComplete?: () => void;
|
||||||
|
onGoToCalendar?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoStatus = 'idle' | 'generating' | 'polling' | 'complete' | 'error';
|
type VideoStatus = 'idle' | 'generating' | 'polling' | 'complete' | 'error';
|
||||||
|
|
@ -35,6 +36,7 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
onVideoStatusChange,
|
onVideoStatusChange,
|
||||||
onVideoProgressChange,
|
onVideoProgressChange,
|
||||||
onVideoComplete,
|
onVideoComplete,
|
||||||
|
onGoToCalendar,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [videoStatus, setVideoStatus] = useState<VideoStatus>('idle');
|
const [videoStatus, setVideoStatus] = useState<VideoStatus>('idle');
|
||||||
|
|
@ -47,18 +49,20 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
const displayIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const displayIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
const tutorial = useTutorial();
|
const tutorial = useTutorial();
|
||||||
|
const tutorialIsActiveRef = useRef(tutorial.isActive);
|
||||||
|
tutorialIsActiveRef.current = tutorial.isActive;
|
||||||
|
|
||||||
// 영상 생성 중 튜토리얼 트리거 (생성 상태 안내 -> 콘텐츠 정보 -> 내 정보 이동)
|
// 영상 생성 중 튜토리얼 트리거 (생성 상태 안내 -> 콘텐츠 정보 -> 내 정보 이동)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isComplete = videoStatus === 'complete';
|
const isComplete = videoStatus === 'complete';
|
||||||
const isProcessing = videoStatus === 'generating' || videoStatus === 'polling';
|
const isProcessing = videoStatus === 'generating' || videoStatus === 'polling';
|
||||||
|
|
||||||
if (isProcessing && !tutorial.isActive && !tutorial.hasSeen(TUTORIAL_KEYS.GENERATING)) {
|
if (isProcessing && !tutorialIsActiveRef.current && !tutorial.hasSeen(TUTORIAL_KEYS.GENERATING)) {
|
||||||
tutorial.startTutorial(TUTORIAL_KEYS.GENERATING);
|
tutorial.startTutorial(TUTORIAL_KEYS.GENERATING);
|
||||||
} else if (isComplete && !tutorial.isActive && !tutorial.hasSeen(TUTORIAL_KEYS.COMPLETION)) {
|
} else if (isComplete && !tutorialIsActiveRef.current && !tutorial.hasSeen(TUTORIAL_KEYS.COMPLETION)) {
|
||||||
tutorial.startTutorial(TUTORIAL_KEYS.COMPLETION);
|
tutorial.startTutorial(TUTORIAL_KEYS.COMPLETION);
|
||||||
}
|
}
|
||||||
}, [videoStatus, tutorial, tutorial.isActive]);
|
}, [videoStatus]);
|
||||||
|
|
||||||
// 소셜 미디어 포스팅 모달
|
// 소셜 미디어 포스팅 모달
|
||||||
const [showSocialModal, setShowSocialModal] = useState(false);
|
const [showSocialModal, setShowSocialModal] = useState(false);
|
||||||
|
|
@ -513,6 +517,7 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
<SocialPostingModal
|
<SocialPostingModal
|
||||||
isOpen={showSocialModal}
|
isOpen={showSocialModal}
|
||||||
onClose={handleCloseSocialConnect}
|
onClose={handleCloseSocialConnect}
|
||||||
|
onGoToCalendar={onGoToCalendar}
|
||||||
video={videoUrl && videoDbId ? {
|
video={videoUrl && videoDbId ? {
|
||||||
video_id: videoDbId,
|
video_id: videoDbId,
|
||||||
store_name: songCompletionData?.businessName || '',
|
store_name: songCompletionData?.businessName || '',
|
||||||
|
|
|
||||||
|
|
@ -548,19 +548,28 @@ const ContentCalendarContent: React.FC<ContentCalendarContentProps> = ({ onNavig
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 채널 아이콘 + 제목 */}
|
{/* 채널 아이콘 + 채널명 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<PlatformIcon platform={item.platform} size={18} />
|
<PlatformIcon platform={item.platform} size={16} />
|
||||||
<span style={{
|
<span style={{
|
||||||
fontFamily: 'Pretendard, sans-serif', fontWeight: 600, fontSize: 14,
|
fontFamily: 'Pretendard, sans-serif', fontWeight: 500, fontSize: 12,
|
||||||
color: '#e5f1f2', lineHeight: 1.4,
|
color: '#9bcacc', lineHeight: 1,
|
||||||
overflow: 'hidden', display: '-webkit-box',
|
overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
|
||||||
WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
|
|
||||||
}}>
|
}}>
|
||||||
{item.title}
|
{item.platform_username || item.platform_user_id || item.channel_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 제목 */}
|
||||||
|
<span style={{
|
||||||
|
fontFamily: 'Pretendard, sans-serif', fontWeight: 600, fontSize: 14,
|
||||||
|
color: '#e5f1f2', lineHeight: 1.4,
|
||||||
|
overflow: 'hidden', display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
|
||||||
|
}}>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
|
||||||
{/* 실패 메시지 */}
|
{/* 실패 메시지 */}
|
||||||
{isFailed && item.error_message && (
|
{isFailed && item.error_message && (
|
||||||
<p style={{
|
<p style={{
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
setAnalysisError(null);
|
setAnalysisError(null);
|
||||||
}
|
}
|
||||||
setActiveItem(item);
|
setActiveItem(item);
|
||||||
|
refreshCredits();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 새 프로젝트 만들기 - 단계별 컨텐츠 렌더링
|
// 새 프로젝트 만들기 - 단계별 컨텐츠 렌더링
|
||||||
|
|
@ -479,6 +480,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
onVideoStatusChange={setVideoGenerationStatus}
|
onVideoStatusChange={setVideoGenerationStatus}
|
||||||
onVideoProgressChange={setVideoGenerationProgress}
|
onVideoProgressChange={setVideoGenerationProgress}
|
||||||
onVideoComplete={refreshCredits}
|
onVideoComplete={refreshCredits}
|
||||||
|
onGoToCalendar={() => handleNavigate('콘텐츠 캘린더')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
@ -496,6 +498,7 @@ const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
||||||
return (
|
return (
|
||||||
<ADO2ContentsPage
|
<ADO2ContentsPage
|
||||||
onBack={() => setActiveItem('새 프로젝트 만들기')}
|
onBack={() => setActiveItem('새 프로젝트 만들기')}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case '콘텐츠 캘린더':
|
case '콘텐츠 캘린더':
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,8 @@ export interface UploadHistoryItem {
|
||||||
status: 'pending' | 'uploading' | 'completed' | 'failed' | 'scheduled' | 'cancelled';
|
status: 'pending' | 'uploading' | 'completed' | 'failed' | 'scheduled' | 'cancelled';
|
||||||
title: string;
|
title: string;
|
||||||
channel_name: string;
|
channel_name: string;
|
||||||
|
platform_user_id: string | null;
|
||||||
|
platform_username: string | null;
|
||||||
scheduled_at: string | null;
|
scheduled_at: string | null;
|
||||||
uploaded_at: string | null;
|
uploaded_at: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue