임시 저장

feature-credit
김성경 2026-04-30 14:58:10 +09:00
parent bdd52ed992
commit 82dcda0038
10 changed files with 73 additions and 21 deletions

View File

@ -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 {

View File

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

View File

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

View File

@ -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": {

View File

@ -150,7 +150,8 @@
"viewOnYoutube": "YouTube에서 보기", "viewOnYoutube": "YouTube에서 보기",
"confirm": "확인", "confirm": "확인",
"close": "닫기", "close": "닫기",
"doNotClose": "업로드가 진행 중입니다. 창을 닫지 마세요." "doNotClose": "업로드가 진행 중입니다. 창을 닫지 마세요.",
"goToCalendar": "캘린더에서 확인"
}, },
"landing": { "landing": {
"hero": { "hero": {

View File

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

View File

@ -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 || '',

View File

@ -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={{

View File

@ -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 '콘텐츠 캘린더':

View File

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