304 lines
11 KiB
TypeScript
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;
|