diff --git a/packages/web/src/app/(admin)/admin/curation/items/page.tsx b/packages/web/src/app/(admin)/admin/curation/items/page.tsx index 08d2516..d536a64 100644 --- a/packages/web/src/app/(admin)/admin/curation/items/page.tsx +++ b/packages/web/src/app/(admin)/admin/curation/items/page.tsx @@ -13,6 +13,16 @@ import { import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; interface CurationItem { id: string; @@ -58,6 +68,9 @@ export default function CurationItemsPage() { const [loading, setLoading] = useState(true); const [deletingId, setDeletingId] = useState(null); + // Delete confirmation state + const [deleteTarget, setDeleteTarget] = useState(null); + // Filters const [category, setCategory] = useState('all'); const [sourceId, setSourceId] = useState('all'); @@ -101,21 +114,25 @@ export default function CurationItemsPage() { }; const handleDelete = async (item: CurationItem) => { - if (!confirm(`"${item.title}" 아이템을 삭제하시겠습니까?`)) return; + setDeleteTarget(item); + }; + + const confirmDelete = async () => { + if (!deleteTarget) return; try { - setDeletingId(item.id); - const response = await fetch(`/api/admin/curation/items/${item.id}`, { + setDeletingId(deleteTarget.id); + const response = await fetch(`/api/admin/curation/items/${deleteTarget.id}`, { method: 'DELETE', }); if (!response.ok) throw new Error('Failed to delete item'); + setDeleteTarget(null); // Refresh current page fetchItems(pagination.page, category, sourceId); } catch (err) { console.error('Error deleting item:', err); - alert('삭제에 실패했습니다.'); } finally { setDeletingId(null); } @@ -206,7 +223,10 @@ export default function CurationItemsPage() { className="font-medium text-sm hover:underline line-clamp-2 flex items-start gap-1 group" > {item.title} - + + {/* Source + Date */} @@ -280,6 +300,22 @@ export default function CurationItemsPage() { )} + + {/* Delete Confirmation Dialog */} + !open && setDeleteTarget(null)}> + + + 아이템 삭제 + + “{deleteTarget?.title}” 아이템을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. + + + + 취소 + 삭제 + + + ); } diff --git a/packages/web/src/app/(admin)/admin/curation/page.tsx b/packages/web/src/app/(admin)/admin/curation/page.tsx index b6baaaa..bb736e8 100644 --- a/packages/web/src/app/(admin)/admin/curation/page.tsx +++ b/packages/web/src/app/(admin)/admin/curation/page.tsx @@ -12,6 +12,17 @@ import { } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; import { AdminDashboardSkeleton, PageError } from '@/components/ui/page-state'; import { CrawlModal } from './crawl-modal'; import type { CrawlStatus, CrawlSourceResult, CrawlSummary } from './crawl-modal'; @@ -60,6 +71,9 @@ export default function AdminCurationPage() { const [editError, setEditError] = useState(null); const [saving, setSaving] = useState(false); + // Delete confirmation state + const [deleteTarget, setDeleteTarget] = useState(null); + // Crawl modal state const [crawlModalOpen, setCrawlModalOpen] = useState(false); const [crawlStatus, setCrawlStatus] = useState('idle'); @@ -185,17 +199,15 @@ export default function AdminCurationPage() { }; const handleDelete = async (source: CurationSource) => { - if ( - !confirm( - `"${source.name}" 소스를 삭제하시겠습니까?\n수집된 ${source.itemCount}개의 아이템도 함께 삭제됩니다.`, - ) - ) { - return; - } + setDeleteTarget(source); + }; + + const confirmDelete = async () => { + if (!deleteTarget) return; try { - setUpdatingId(source.id); - const response = await fetch(`/api/admin/curation/${source.id}`, { + setUpdatingId(deleteTarget.id); + const response = await fetch(`/api/admin/curation/${deleteTarget.id}`, { method: 'DELETE', }); @@ -203,6 +215,7 @@ export default function AdminCurationPage() { throw new Error('Failed to delete source'); } + setDeleteTarget(null); await fetchSources(); } catch (err) { console.error('Error deleting source:', err); @@ -361,6 +374,23 @@ export default function AdminCurationPage() { errorMessage={crawlErrorMessage} /> + {/* Delete Confirmation Dialog */} + !open && setDeleteTarget(null)}> + + + 소스 삭제 + + “{deleteTarget?.name}” 소스를 삭제하시겠습니까? 수집된 {deleteTarget?.itemCount}개의 + 아이템도 함께 삭제됩니다. 이 작업은 되돌릴 수 없습니다. + + + + 취소 + 삭제 + + + + {/* Stats Cards */}
@@ -443,8 +473,12 @@ export default function AdminCurationPage() { {filteredSources.length}개의 소스
- +
diff --git a/packages/web/src/app/(admin)/admin/members/page.tsx b/packages/web/src/app/(admin)/admin/members/page.tsx index 791da9c..d1612a7 100644 --- a/packages/web/src/app/(admin)/admin/members/page.tsx +++ b/packages/web/src/app/(admin)/admin/members/page.tsx @@ -18,6 +18,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { Table, TableBody, @@ -257,8 +258,12 @@ export default function AdminMembersPage() {
- +
- + + ))} @@ -299,10 +302,12 @@ function PostsContent() { href={post.url} target="_blank" rel="noopener noreferrer" - className="text-sm text-primary hover:underline underline-offset-4 line-clamp-1 font-medium" + className="text-sm text-primary hover:underline underline-offset-4 line-clamp-1 font-medium inline-flex items-center gap-1" onClick={() => trackPostView(post.id)} > {post.title} +