165 lines
6.9 KiB
React
165 lines
6.9 KiB
React
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import { useNavigate, Link } from 'react-router-dom';
|
||
|
|
import { motion } from 'framer-motion';
|
||
|
|
import {
|
||
|
|
Disc3, Calendar, Users, LogOut,
|
||
|
|
Home, ChevronRight
|
||
|
|
} from 'lucide-react';
|
||
|
|
|
||
|
|
function AdminDashboard() {
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const [user, setUser] = useState(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
// 로그인 상태 확인
|
||
|
|
const token = localStorage.getItem('adminToken');
|
||
|
|
const userData = localStorage.getItem('adminUser');
|
||
|
|
|
||
|
|
if (!token || !userData) {
|
||
|
|
navigate('/admin');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setUser(JSON.parse(userData));
|
||
|
|
|
||
|
|
// 토큰 유효성 검증
|
||
|
|
fetch('/api/admin/verify', {
|
||
|
|
headers: { Authorization: `Bearer ${token}` }
|
||
|
|
})
|
||
|
|
.then(res => {
|
||
|
|
if (!res.ok) throw new Error('Invalid token');
|
||
|
|
return res.json();
|
||
|
|
})
|
||
|
|
.catch(() => {
|
||
|
|
localStorage.removeItem('adminToken');
|
||
|
|
localStorage.removeItem('adminUser');
|
||
|
|
navigate('/admin');
|
||
|
|
});
|
||
|
|
}, [navigate]);
|
||
|
|
|
||
|
|
const handleLogout = () => {
|
||
|
|
localStorage.removeItem('adminToken');
|
||
|
|
localStorage.removeItem('adminUser');
|
||
|
|
navigate('/admin');
|
||
|
|
};
|
||
|
|
|
||
|
|
// 메뉴 아이템
|
||
|
|
const menuItems = [
|
||
|
|
{
|
||
|
|
icon: Users,
|
||
|
|
label: '멤버 관리',
|
||
|
|
description: '멤버 정보 및 프로필 관리',
|
||
|
|
path: '/admin/members',
|
||
|
|
color: 'bg-primary'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
icon: Disc3,
|
||
|
|
label: '앨범 관리',
|
||
|
|
description: '앨범, 트랙, 사진 업로드 및 관리',
|
||
|
|
path: '/admin/albums',
|
||
|
|
color: 'bg-purple-500'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
icon: Calendar,
|
||
|
|
label: '일정 관리',
|
||
|
|
description: '스케줄 추가 및 관리',
|
||
|
|
path: '/admin/schedule',
|
||
|
|
color: 'bg-blue-500'
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="min-h-screen bg-gray-50">
|
||
|
|
{/* 헤더 */}
|
||
|
|
<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="flex items-center gap-4">
|
||
|
|
<Link to="/" className="text-2xl font-bold text-primary hover:opacity-80 transition-opacity">
|
||
|
|
fromis_9
|
||
|
|
</Link>
|
||
|
|
<span className="px-3 py-1 bg-primary/10 text-primary text-sm font-medium rounded-full">
|
||
|
|
Admin
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<span className="text-gray-500 text-sm">
|
||
|
|
안녕하세요, <span className="text-gray-900 font-medium">{user?.username}</span>님
|
||
|
|
</span>
|
||
|
|
<button
|
||
|
|
onClick={handleLogout}
|
||
|
|
className="flex items-center gap-2 px-4 py-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
||
|
|
>
|
||
|
|
<LogOut size={18} />
|
||
|
|
<span>로그아웃</span>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
{/* 메인 콘텐츠 */}
|
||
|
|
<main className="max-w-7xl mx-auto px-6 py-8">
|
||
|
|
{/* 브레드크럼 */}
|
||
|
|
<div className="flex items-center gap-2 text-sm text-gray-400 mb-8">
|
||
|
|
<Home size={16} />
|
||
|
|
<ChevronRight size={14} />
|
||
|
|
<span className="text-gray-700">관리자 대시보드</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 타이틀 */}
|
||
|
|
<div className="mb-8">
|
||
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">관리자 대시보드</h1>
|
||
|
|
<p className="text-gray-500">fromis_9 팬사이트를 관리하세요</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 메뉴 그리드 */}
|
||
|
|
<div className="grid grid-cols-3 gap-6">
|
||
|
|
{menuItems.map((item, index) => (
|
||
|
|
<motion.div
|
||
|
|
key={item.label}
|
||
|
|
initial={{ opacity: 0, y: 20 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ delay: index * 0.1 }}
|
||
|
|
>
|
||
|
|
<Link
|
||
|
|
to={item.path}
|
||
|
|
className="block bg-white rounded-2xl p-6 border border-gray-100 shadow-sm hover:shadow-lg hover:border-gray-200 transition-all group"
|
||
|
|
>
|
||
|
|
<div className={`w-12 h-12 ${item.color} rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`}>
|
||
|
|
<item.icon size={24} className="text-white" />
|
||
|
|
</div>
|
||
|
|
<h3 className="text-lg font-bold text-gray-900 mb-2">{item.label}</h3>
|
||
|
|
<p className="text-gray-500 text-sm">{item.description}</p>
|
||
|
|
</Link>
|
||
|
|
</motion.div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 빠른 통계 */}
|
||
|
|
<div className="mt-12">
|
||
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6">빠른 통계</h2>
|
||
|
|
<div className="grid grid-cols-4 gap-6">
|
||
|
|
<div className="bg-white rounded-xl p-6 border border-gray-100 shadow-sm">
|
||
|
|
<p className="text-3xl font-bold text-primary mb-1">-</p>
|
||
|
|
<p className="text-gray-500 text-sm">총 앨범</p>
|
||
|
|
</div>
|
||
|
|
<div className="bg-white rounded-xl p-6 border border-gray-100 shadow-sm">
|
||
|
|
<p className="text-3xl font-bold text-primary mb-1">-</p>
|
||
|
|
<p className="text-gray-500 text-sm">총 사진</p>
|
||
|
|
</div>
|
||
|
|
<div className="bg-white rounded-xl p-6 border border-gray-100 shadow-sm">
|
||
|
|
<p className="text-3xl font-bold text-primary mb-1">-</p>
|
||
|
|
<p className="text-gray-500 text-sm">총 일정</p>
|
||
|
|
</div>
|
||
|
|
<div className="bg-white rounded-xl p-6 border border-gray-100 shadow-sm">
|
||
|
|
<p className="text-3xl font-bold text-primary mb-1">5</p>
|
||
|
|
<p className="text-gray-500 text-sm">멤버</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default AdminDashboard;
|