2026-01-16 23:10:30 +09:00
|
|
|
import { Link } from 'react-router-dom';
|
2026-01-17 00:08:16 +09:00
|
|
|
import { useQuery } from '@tanstack/react-query';
|
2026-01-01 18:01:42 +09:00
|
|
|
import { motion } from 'framer-motion';
|
2026-01-16 23:10:30 +09:00
|
|
|
import { Disc3, Calendar, Users, Home, ChevronRight } from 'lucide-react';
|
2026-01-11 12:12:46 +09:00
|
|
|
import AdminLayout from '../../../components/admin/AdminLayout';
|
2026-01-16 23:10:30 +09:00
|
|
|
import useAdminAuth from '../../../hooks/useAdminAuth';
|
2026-01-17 00:08:16 +09:00
|
|
|
import { getStats } from '../../../api/admin/stats';
|
2026-01-01 18:01:42 +09:00
|
|
|
|
2026-01-17 00:08:16 +09:00
|
|
|
// 슬롯머신 스타일 롤링 숫자 컴포넌트 (아래에서 위로, 3자리 쉼표 포함)
|
2026-01-04 13:10:34 +09:00
|
|
|
function AnimatedNumber({ value }) {
|
2026-01-17 00:08:16 +09:00
|
|
|
// 3자리마다 쉼표가 들어갈 위치 계산
|
|
|
|
|
const formatted = value.toLocaleString();
|
|
|
|
|
const chars = formatted.split('');
|
|
|
|
|
|
2026-01-04 13:10:34 +09:00
|
|
|
return (
|
|
|
|
|
<span className="inline-flex overflow-hidden">
|
2026-01-17 00:08:16 +09:00
|
|
|
{chars.map((char, i) => {
|
|
|
|
|
// 쉼표도 애니메이션으로 표시
|
|
|
|
|
if (char === ',') {
|
|
|
|
|
return (
|
|
|
|
|
<motion.span
|
|
|
|
|
key={i}
|
|
|
|
|
className="h-[1.2em] flex items-center"
|
|
|
|
|
initial={{ y: '100%', opacity: 0 }}
|
|
|
|
|
animate={{ y: 0, opacity: 1 }}
|
|
|
|
|
transition={{
|
|
|
|
|
type: 'tween',
|
|
|
|
|
ease: 'easeOut',
|
|
|
|
|
duration: 0.8,
|
|
|
|
|
delay: i * 0.15
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
,
|
|
|
|
|
</motion.span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<span key={i} className="relative h-[1.2em] overflow-hidden">
|
|
|
|
|
<motion.span
|
|
|
|
|
className="flex flex-col"
|
|
|
|
|
initial={{ y: '100%' }}
|
|
|
|
|
animate={{ y: `-${parseInt(char) * 10}%` }}
|
|
|
|
|
transition={{
|
|
|
|
|
type: 'tween',
|
|
|
|
|
ease: 'easeOut',
|
|
|
|
|
duration: 0.8,
|
|
|
|
|
delay: i * 0.15
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => (
|
|
|
|
|
<span key={n} className="h-[1.2em] flex items-center justify-center">
|
|
|
|
|
{n}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</motion.span>
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-01-04 13:10:34 +09:00
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 18:01:42 +09:00
|
|
|
function AdminDashboard() {
|
2026-01-16 23:10:30 +09:00
|
|
|
const { user, isAuthenticated } = useAdminAuth();
|
2026-01-01 18:01:42 +09:00
|
|
|
|
2026-01-17 00:08:16 +09:00
|
|
|
// 통계 조회 (useQuery)
|
|
|
|
|
const { data: stats = { members: 0, albums: 0, photos: 0, schedules: 0 } } = useQuery({
|
|
|
|
|
queryKey: ['admin', 'stats'],
|
|
|
|
|
queryFn: getStats,
|
|
|
|
|
enabled: isAuthenticated,
|
|
|
|
|
staleTime: 30 * 1000, // 30초 캐시
|
|
|
|
|
});
|
2026-01-04 13:10:34 +09:00
|
|
|
|
2026-01-01 18:01:42 +09:00
|
|
|
// 메뉴 아이템
|
|
|
|
|
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: '일정 관리',
|
2026-01-03 14:30:30 +09:00
|
|
|
description: '일정 추가 및 관리',
|
2026-01-01 18:01:42 +09:00
|
|
|
path: '/admin/schedule',
|
|
|
|
|
color: 'bg-blue-500'
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-11 12:12:46 +09:00
|
|
|
<AdminLayout user={user}>
|
2026-01-01 18:01:42 +09:00
|
|
|
{/* 메인 콘텐츠 */}
|
2026-01-11 12:12:46 +09:00
|
|
|
<div className="max-w-7xl mx-auto px-6 py-8">
|
2026-01-01 18:01:42 +09:00
|
|
|
{/* 브레드크럼 */}
|
|
|
|
|
<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">
|
2026-01-04 13:10:34 +09:00
|
|
|
<p className="text-3xl font-bold text-primary mb-1"><AnimatedNumber value={stats.members} /></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"><AnimatedNumber value={stats.albums} /></p>
|
2026-01-01 18:01:42 +09:00
|
|
|
<p className="text-gray-500 text-sm">총 앨범</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-white rounded-xl p-6 border border-gray-100 shadow-sm">
|
2026-01-04 13:10:34 +09:00
|
|
|
<p className="text-3xl font-bold text-primary mb-1"><AnimatedNumber value={stats.photos} /></p>
|
2026-01-01 18:01:42 +09:00
|
|
|
<p className="text-gray-500 text-sm">총 사진</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-white rounded-xl p-6 border border-gray-100 shadow-sm">
|
2026-01-04 13:10:34 +09:00
|
|
|
<p className="text-3xl font-bold text-primary mb-1"><AnimatedNumber value={stats.schedules} /></p>
|
2026-01-01 18:01:42 +09:00
|
|
|
<p className="text-gray-500 text-sm">총 일정</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-11 12:12:46 +09:00
|
|
|
</div>
|
|
|
|
|
</AdminLayout>
|
2026-01-01 18:01:42 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default AdminDashboard;
|