feat: 앨범 삭제 기능 - 삭제 다이얼로그, Toast 알림
This commit is contained in:
parent
40fa94f9f5
commit
d4bbf592d5
1 changed files with 111 additions and 4 deletions
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
Plus, Search, Edit2, Trash2, Image, Music,
|
Plus, Search, Edit2, Trash2, Image, Music,
|
||||||
Home, ChevronRight, LogOut, Calendar
|
Home, ChevronRight, LogOut, Calendar, AlertTriangle, X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import Toast from '../../../components/Toast';
|
||||||
|
|
||||||
function AdminAlbums() {
|
function AdminAlbums() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -12,6 +13,17 @@ function AdminAlbums() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
|
const [toast, setToast] = useState(null);
|
||||||
|
const [deleteDialog, setDeleteDialog] = useState({ show: false, album: null });
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
|
// Toast 자동 숨김
|
||||||
|
useEffect(() => {
|
||||||
|
if (toast) {
|
||||||
|
const timer = setTimeout(() => setToast(null), 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 로그인 확인
|
// 로그인 확인
|
||||||
|
|
@ -24,8 +36,10 @@ function AdminAlbums() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setUser(JSON.parse(userData));
|
setUser(JSON.parse(userData));
|
||||||
|
fetchAlbums();
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
// 앨범 목록 로드
|
const fetchAlbums = () => {
|
||||||
fetch('/api/albums')
|
fetch('/api/albums')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
|
@ -36,7 +50,7 @@ function AdminAlbums() {
|
||||||
console.error('앨범 로드 오류:', error);
|
console.error('앨범 로드 오류:', error);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, [navigate]);
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('adminToken');
|
localStorage.removeItem('adminToken');
|
||||||
|
|
@ -44,6 +58,34 @@ function AdminAlbums() {
|
||||||
navigate('/admin');
|
navigate('/admin');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!deleteDialog.album) return;
|
||||||
|
|
||||||
|
setDeleting(true);
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('adminToken');
|
||||||
|
const response = await fetch(`/api/admin/albums/${deleteDialog.album.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('삭제 실패');
|
||||||
|
}
|
||||||
|
|
||||||
|
setToast({ message: `"${deleteDialog.album.title}" 앨범이 삭제되었습니다.`, type: 'success' });
|
||||||
|
setDeleteDialog({ show: false, album: null });
|
||||||
|
fetchAlbums(); // 목록 새로고침
|
||||||
|
} catch (error) {
|
||||||
|
console.error('삭제 오류:', error);
|
||||||
|
setToast({ message: '앨범 삭제 중 오류가 발생했습니다.', type: 'error' });
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 날짜 포맷팅
|
// 날짜 포맷팅
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
|
|
@ -58,6 +100,70 @@ function AdminAlbums() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Toast */}
|
||||||
|
<Toast toast={toast} onClose={() => setToast(null)} />
|
||||||
|
|
||||||
|
{/* 삭제 확인 다이얼로그 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{deleteDialog.show && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
|
onClick={() => !deleting && setDeleteDialog({ show: false, album: null })}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.9, opacity: 0 }}
|
||||||
|
className="bg-white rounded-2xl p-6 max-w-md w-full mx-4 shadow-xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
|
||||||
|
<AlertTriangle className="text-red-500" size={20} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900">앨범 삭제</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
<span className="font-medium text-gray-900">"{deleteDialog.album?.title}"</span> 앨범을 삭제하시겠습니까?
|
||||||
|
<br />
|
||||||
|
<span className="text-sm text-red-500">이 작업은 되돌릴 수 없으며, 모든 트랙과 커버 이미지가 함께 삭제됩니다.</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteDialog({ show: false, album: null })}
|
||||||
|
disabled={deleting}
|
||||||
|
className="px-4 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{deleting ? (
|
||||||
|
<>
|
||||||
|
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
삭제 중...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
삭제
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<header className="bg-white shadow-sm border-b border-gray-100">
|
<header className="bg-white shadow-sm border-b border-gray-100">
|
||||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||||
|
|
@ -196,6 +302,7 @@ function AdminAlbums() {
|
||||||
<Edit2 size={18} />
|
<Edit2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
onClick={() => setDeleteDialog({ show: true, album })}
|
||||||
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
title="삭제"
|
title="삭제"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue