castad-pre-v0.3/castad-data/components/ResultList.tsx

157 lines
7.6 KiB
TypeScript

import React from 'react';
import { GeneratedAssets } from '../types';
import { Play, Clock, Music, Mic, Video, Image as ImageIcon, Download } from 'lucide-react';
/**
* ResultList 컴포넌트의 Props 정의
* @interface ResultListProps
* @property {GeneratedAssets[]} history - 생성된 에셋들의 배열 (기록)
* @property {(asset: GeneratedAssets) => void} onSelect - 목록에서 에셋 선택 시 호출될 콜백 함수
* @property {string} [currentId] - 현재 선택된 에셋의 ID (선택 사항, UI에서 강조 표시용)
*/
interface ResultListProps {
history: GeneratedAssets[];
onSelect: (asset: GeneratedAssets) => void;
currentId?: string;
}
/**
* 생성 기록 목록 컴포넌트
* 이전에 생성된 AI 광고 영상/포스터 목록을 보여주고, 클릭 시 해당 에셋을 다시 볼 수 있도록 합니다.
*/
const ResultList: React.FC<ResultListProps> = ({ history, onSelect, currentId }) => {
// 파일 강제 다운로드 핸들러
const handleDirectDownload = async (e: React.MouseEvent, url: string, filename: string) => {
e.stopPropagation();
e.preventDefault();
try {
const response = await fetch(url);
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(blobUrl);
a.remove();
} catch (err) {
console.error("다운로드 실패:", err);
window.open(url, '_blank');
}
};
if (history.length === 0) return null;
return (
<div className="w-full max-w-5xl mx-auto mt-12 mb-20 animate-in slide-in-from-bottom-10 duration-700">
<div className="flex items-center gap-3 mb-6 px-4">
<Clock className="w-5 h-5 text-purple-400" />
<h3 className="text-xl font-bold text-white"> (History)</h3>
<span className="px-2 py-0.5 bg-purple-500/20 text-purple-300 text-xs rounded-full font-mono">{history.length}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 px-4">
{history.map((asset) => (
<div
key={asset.id}
onClick={() => onSelect(asset)}
className={`group relative bg-white/5 border rounded-2xl overflow-hidden cursor-pointer transition-all hover:scale-[1.02] hover:shadow-xl hover:shadow-purple-500/10
${currentId === asset.id ? 'border-purple-500 ring-1 ring-purple-500 bg-white/10' : 'border-white/10 hover:border-purple-500/50'}
`}
>
{/* 포스터 썸네일 */}
<div className="aspect-video bg-black relative overflow-hidden">
{asset.posterUrl ? (
<img
src={asset.posterUrl}
alt={asset.businessName}
className="w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-opacity"
onError={(e) => {
e.currentTarget.style.display = 'none'; // 이미지 숨김
e.currentTarget.nextElementSibling?.classList.remove('hidden'); // 대체 div 표시 (구조상 별도 처리 필요하지만 간단히 숨김 처리)
// 부모 요소에 배경색이 있으므로 이미지가 숨겨지면 배경색이 보임.
// 더 완벽하게 하려면 state로 관리해야 하나, 여기선 간단히 처리.
}}
/>
) : (
<div className="w-full h-full bg-gray-800 flex items-center justify-center text-gray-500"><ImageIcon /></div>
)}
{/* 이미지 로드 실패 시 보여줄 폴백 (JS로 제어하기보다, 위 onError에서 이미지 숨기면 아래 배경이 보임) */}
<div className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity backdrop-blur-[2px]">
<div className="p-3 rounded-full bg-white/20 backdrop-blur-sm border border-white/30">
<Play className="w-6 h-6 text-white fill-white" />
</div>
</div>
<div className="absolute top-2 left-2 flex gap-1 flex-wrap">
{asset.audioMode === 'Song' ? (
<div className="p-1 bg-purple-600/90 rounded text-white shadow-sm"><Music className="w-3 h-3" /></div>
) : (
<div className="p-1 bg-blue-600/90 rounded text-white shadow-sm"><Mic className="w-3 h-3" /></div>
)}
<div className="px-2 py-0.5 bg-black/60 backdrop-blur-sm rounded text-[10px] text-white font-bold border border-white/10 flex items-center">
{asset.textEffect}
</div>
</div>
</div>
{/* 정보 영역 */}
<div className="p-4">
<h4 className="text-white font-bold truncate mb-1 text-base">{asset.businessName}</h4>
{asset.sourceUrl && (
<a href={asset.sourceUrl} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} className="text-xs text-blue-400 hover:underline truncate block mb-2 opacity-70 hover:opacity-100">
{asset.sourceUrl}
</a>
)}
<div className="flex items-center justify-between text-xs text-gray-400">
{/* 생성 날짜 및 시간 */}
<span>
{new Date(asset.createdAt).toLocaleDateString()}
</span>
{/* 장르 또는 비디오 타입 표시 */}
<span className="flex items-center gap-1 bg-white/5 px-1.5 py-0.5 rounded">
{asset.audioMode === 'Song' ? (
<>{asset.musicGenre || 'Music'}</>
) : (
<><Mic className="w-3 h-3" /> Narration</>
)}
</span>
</div>
</div>
{/* 하단 버튼 영역 */}
<div className="px-4 pb-4 pt-0 flex gap-2">
{asset.finalVideoPath ? (
<button
onClick={(e) => handleDirectDownload(e, asset.finalVideoPath!, `CastAD_${asset.businessName}_Final.mp4`)}
className="flex-1 py-2 bg-green-600 hover:bg-green-500 rounded-lg text-sm font-bold text-white transition-colors flex items-center justify-center gap-2"
>
<Download className="w-4 h-4" />
</button>
) : null}
<button
onClick={(e) => {
e.stopPropagation();
onSelect(asset);
}}
className={`flex-1 py-2 bg-purple-600 hover:bg-purple-500 rounded-lg text-sm font-bold text-white transition-colors flex items-center justify-center gap-2 ${asset.finalVideoPath ? 'bg-gray-700 hover:bg-gray-600' : ''}`}
>
<Play className="w-4 h-4" /> {asset.finalVideoPath ? '수정/재생성' : '결과 보기 / 다운로드'}
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default ResultList;