feat:생성 이력 체크박스 선택 삭제 기능 추가

- 이력 테이블 첫 열에 체크박스 추가 (전체 선택/해제)
- 선택 시 상단에 빨간색 삭제 버튼 표시
- DELETE /video/veo3/history API 엔드포인트 추가
- 삭제 후 이력 목록 자동 새로고침

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-15 10:26:37 +09:00
parent bbedb67817
commit 8d8fa2da0a
3 changed files with 94 additions and 5 deletions

View File

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

View File

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

View File

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