feat:생성 이력 체크박스 선택 삭제 기능 추가
- 이력 테이블 첫 열에 체크박스 추가 (전체 선택/해제) - 선택 시 상단에 빨간색 삭제 버튼 표시 - DELETE /video/veo3/history API 엔드포인트 추가 - 삭제 후 이력 목록 자동 새로고침 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -220,4 +220,24 @@ public function history(Request $request): JsonResponse
|
||||
'data' => $videos,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성 이력 삭제 (복수)
|
||||
*/
|
||||
public function destroy(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array|min:1',
|
||||
'ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$deleted = VideoGeneration::where('user_id', auth()->id())
|
||||
->whereIn('id', $request->input('ids'))
|
||||
->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'deleted' => $deleted,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,13 +532,51 @@ className="bg-indigo-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-in
|
||||
const HistoryTable = ({ onSelect }) => {
|
||||
const [history, setHistory] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [checked, setChecked] = useState(new Set());
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchHistory = () => {
|
||||
api('/video/veo3/history')
|
||||
.then(data => setHistory(data.data || []))
|
||||
.then(data => { setHistory(data.data || []); setChecked(new Set()); })
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchHistory(); }, []);
|
||||
|
||||
const toggleCheck = (id) => {
|
||||
setChecked(prev => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAll = () => {
|
||||
if (checked.size === history.length) {
|
||||
setChecked(new Set());
|
||||
} else {
|
||||
setChecked(new Set(history.map(h => h.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (checked.size === 0) return;
|
||||
if (!confirm(`선택한 ${checked.size}개 이력을 삭제하시겠습니까?`)) return;
|
||||
|
||||
setDeleting(true);
|
||||
try {
|
||||
await api('/video/veo3/history', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({ ids: Array.from(checked) }),
|
||||
});
|
||||
fetchHistory();
|
||||
} catch (err) {
|
||||
alert('삭제 실패: ' + err.message);
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="text-center py-4 text-gray-400">이력 로딩 중...</div>;
|
||||
if (history.length === 0) return null;
|
||||
@@ -562,11 +600,33 @@ className="bg-indigo-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-in
|
||||
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<h3 className="text-lg font-bold text-gray-700 mb-4">생성 이력</h3>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-bold text-gray-700">생성 이력</h3>
|
||||
{checked.size > 0 && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-red-500 text-white text-xs font-medium rounded-lg hover:bg-red-600 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{deleting ? '삭제 중...' : `${checked.size}개 삭제`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-gray-50">
|
||||
<th className="py-2 px-3 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={history.length > 0 && checked.size === history.length}
|
||||
onChange={toggleAll}
|
||||
className="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer"
|
||||
/>
|
||||
</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-gray-500">날짜</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-gray-500">키워드</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-gray-500">제목</th>
|
||||
@@ -577,7 +637,15 @@ className="bg-indigo-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-in
|
||||
</thead>
|
||||
<tbody>
|
||||
{history.map((item) => (
|
||||
<tr key={item.id} className="border-b hover:bg-gray-50">
|
||||
<tr key={item.id} className={`border-b hover:bg-gray-50 ${checked.has(item.id) ? 'bg-indigo-50' : ''}`}>
|
||||
<td className="py-2 px-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked.has(item.id)}
|
||||
onChange={() => toggleCheck(item.id)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-gray-600">{new Date(item.created_at).toLocaleDateString('ko-KR')}</td>
|
||||
<td className="py-2 px-3 font-medium">{item.keyword}</td>
|
||||
<td className="py-2 px-3 text-gray-600 truncate max-w-[200px]">{item.title || '-'}</td>
|
||||
|
||||
@@ -1471,6 +1471,7 @@
|
||||
Route::get('/download/{id}', [\App\Http\Controllers\Video\Veo3Controller::class, 'download'])->name('download');
|
||||
Route::get('/preview/{id}', [\App\Http\Controllers\Video\Veo3Controller::class, 'preview'])->name('preview');
|
||||
Route::get('/history', [\App\Http\Controllers\Video\Veo3Controller::class, 'history'])->name('history');
|
||||
Route::delete('/history', [\App\Http\Controllers\Video\Veo3Controller::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user