o2o-castad-frontend/src/pages/Dashboard/ADO2ContentsPage.tsx

304 lines
11 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { getVideosList, deleteVideo } from '../../utils/api';
import { VideoListItem } from '../../types/api';
import SocialPostingModal from '../../components/SocialPostingModal';
interface ADO2ContentsPageProps {
onBack: () => void;
}
const ADO2ContentsPage: React.FC<ADO2ContentsPageProps> = ({ onBack }) => {
const { t } = useTranslation();
const [videos, setVideos] = useState<VideoListItem[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasNext, setHasNext] = useState(false);
const [hasPrev, setHasPrev] = useState(false);
const [totalPages, setTotalPages] = useState(1);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [deleteTargetId, setDeleteTargetId] = useState<number | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [uploadModalOpen, setUploadModalOpen] = useState(false);
const [uploadTargetVideo, setUploadTargetVideo] = useState<VideoListItem | null>(null);
const pageSize = 12;
useEffect(() => {
fetchVideos();
}, [page]);
const fetchVideos = async () => {
setLoading(true);
setError(null);
try {
const response = await getVideosList(page, pageSize);
console.log('[ADO2] API response:', response);
console.log('[ADO2] First video item:', response.items[0]);
// result_movie_url이 있는 비디오만 필터링 (빈/더미 데이터 제외)
const validVideos = response.items.filter(video => video.result_movie_url && video.result_movie_url.trim() !== '');
setVideos(validVideos);
setTotal(response.total);
setTotalPages(response.total_pages);
setHasNext(response.has_next);
setHasPrev(response.has_prev);
} catch (err) {
console.error('Failed to fetch videos:', err);
setError(t('ado2Contents.loadFailed'));
} finally {
setLoading(false);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}.${month}.${day}${hours}:${minutes}`;
};
const formatTitle = (storeName: string, dateString: string) => {
const date = new Date(dateString);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${storeName} ${month}/${day} ${hours}:${minutes}`;
};
const handleDownload = async (videoUrl: string, storeName: string) => {
try {
const response = await fetch(videoUrl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${storeName}.mp4`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (err) {
console.error('Download failed:', err);
alert(t('ado2Contents.downloadFailed'));
}
};
const handleDeleteClick = (videoId: number) => {
setDeleteTargetId(videoId);
setDeleteModalOpen(true);
};
const handleUploadClick = (video: VideoListItem) => {
console.log('[ADO2] Upload clicked - video object:', video);
console.log('[ADO2] video.video_id:', video.video_id);
setUploadTargetVideo(video);
setUploadModalOpen(true);
};
const handleUploadModalClose = () => {
setUploadModalOpen(false);
setUploadTargetVideo(null);
};
const handleDeleteCancel = () => {
setDeleteModalOpen(false);
setDeleteTargetId(null);
};
const handleDeleteConfirm = async () => {
if (!deleteTargetId) return;
setIsDeleting(true);
try {
await deleteVideo(deleteTargetId);
// 삭제 성공 시 로컬 상태에서 즉시 제거 (UI 즉시 반영)
// fetchVideos()를 호출하지 않음 - 서버 캐시 또는 동기화 지연으로 인해
// 삭제된 항목이 다시 나타날 수 있기 때문
setVideos(prev => prev.filter(video => video.video_id !== deleteTargetId));
setTotal(prev => Math.max(0, prev - 1));
setDeleteModalOpen(false);
setDeleteTargetId(null);
} catch (err) {
console.error('Delete failed:', err);
alert(t('ado2Contents.deleteFailed'));
} finally {
setIsDeleting(false);
}
};
return (
<div className="ado2-contents-page">
{/* Header */}
<div className="ado2-contents-header">
<h1 className="ado2-contents-title">{t('ado2Contents.title')}</h1>
<span className="ado2-contents-count">{t('ado2Contents.totalCount', { count: total })}</span>
</div>
{/* Content Grid */}
{loading ? (
<div className="ado2-contents-loading">
<div className="loading-spinner"></div>
<p>{t('ado2Contents.loading')}</p>
</div>
) : error ? (
<div className="ado2-contents-error">
<p>{error}</p>
<button onClick={fetchVideos} className="retry-btn">{t('ado2Contents.retry')}</button>
</div>
) : videos.length === 0 ? (
<div className="ado2-contents-empty">
<p>{t('ado2Contents.noContent')}</p>
</div>
) : (
<>
<div className="ado2-contents-grid">
{videos.map((video) => (
<div key={video.task_id} className="ado2-content-card">
{/* Video Thumbnail */}
<div className="content-card-thumbnail">
{video.result_movie_url ? (
<video
src={video.result_movie_url}
className="content-video-preview"
muted
playsInline
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => {
e.currentTarget.pause();
e.currentTarget.currentTime = 0;
}}
/>
) : (
<div className="content-no-video">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/>
<line x1="7" y1="2" x2="7" y2="22"/>
<line x1="17" y1="2" x2="17" y2="22"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<line x1="2" y1="7" x2="7" y2="7"/>
<line x1="2" y1="17" x2="7" y2="17"/>
<line x1="17" y1="17" x2="22" y2="17"/>
<line x1="17" y1="7" x2="22" y2="7"/>
</svg>
</div>
)}
</div>
{/* Card Info */}
<div className="content-card-info">
<div className="content-card-text">
<h3 className="content-card-title">
{formatTitle(video.store_name, video.created_at)}
</h3>
<p className="content-card-date">
{formatDate(video.created_at)}
</p>
</div>
{/* Action Buttons */}
<div className="content-card-actions">
<button
className="content-download-btn"
onClick={() => handleDownload(video.result_movie_url, video.store_name)}
disabled={!video.result_movie_url}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M10 3v10M10 13l-4-4M10 13l4-4"/>
<path d="M3 15v2h14v-2"/>
</svg>
<span>{t('ado2Contents.download')}</span>
</button>
<button
className="content-upload-btn"
onClick={() => handleUploadClick(video)}
disabled={!video.result_movie_url}
title={t('ado2Contents.uploadToSocial')}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M10 13V3M10 3l-4 4M10 3l4 4"/>
<path d="M3 15v2h14v-2"/>
</svg>
</button>
<button
className="content-delete-btn"
onClick={() => handleDeleteClick(video.video_id)}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M3 5h14M8 5V3h4v2M6 5v12h8V5"/>
<line x1="8" y1="8" x2="8" y2="14"/>
<line x1="12" y1="8" x2="12" y2="14"/>
</svg>
</button>
</div>
</div>
</div>
))}
</div>
{/* Pagination - 항상 표시 */}
<div className="ado2-contents-pagination">
<button
className="pagination-btn"
onClick={() => setPage(p => p - 1)}
disabled={!hasPrev}
>
{t('ado2Contents.previous')}
</button>
<span className="pagination-info">{page} / {totalPages}</span>
<button
className="pagination-btn"
onClick={() => setPage(p => p + 1)}
disabled={!hasNext}
>
{t('ado2Contents.next')}
</button>
</div>
</>
)}
{/* 삭제 확인 모달 */}
{deleteModalOpen && (
<div className="delete-modal-overlay" onClick={handleDeleteCancel}>
<div className="delete-modal" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<h2 className="delete-modal-title">{t('ado2Contents.deleteConfirmTitle')}</h2>
<p className="delete-modal-description">{t('ado2Contents.deleteConfirmDesc')}</p>
<div className="delete-modal-actions">
<button
className="delete-modal-btn cancel"
onClick={handleDeleteCancel}
disabled={isDeleting}
>
{t('ado2Contents.cancel')}
</button>
<button
className="delete-modal-btn confirm"
onClick={handleDeleteConfirm}
disabled={isDeleting}
>
{isDeleting ? t('ado2Contents.deleting') : t('ado2Contents.delete')}
</button>
</div>
</div>
</div>
)}
{/* 소셜 미디어 업로드 모달 */}
<SocialPostingModal
isOpen={uploadModalOpen}
onClose={handleUploadModalClose}
video={uploadTargetVideo}
/>
</div>
);
};
export default ADO2ContentsPage;