fromis_9/frontend/src/pages/pc/admin/dashboard/Dashboard.jsx
caadiq 01cf083da2 feat(admin): 활동 로그 라우트/메뉴 연결 및 UI 개선
- /admin/logs 라우트 등록, 대시보드 메뉴에 활동 로그 항목 추가
- 테이블 컬럼 비율 조정 (내용 컬럼 공간 확보)
- 날짜 선택기를 커스텀 DatePicker로 교체
- 행위자 드롭다운에 애니메이션 추가
- reorder 액션 제거

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:40:27 +09:00

176 lines
5.9 KiB
JavaScript

/**
* 관리자 대시보드 페이지
*/
import { Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { Disc3, Calendar, Users, Home, ChevronRight, ScrollText } from 'lucide-react';
import { AdminLayout } from '@/components/pc/admin';
import { useAdminAuth } from '@/hooks/pc/admin';
import { adminStatsApi } from '@/api/admin';
/**
* 슬롯머신 스타일 롤링 숫자 컴포넌트
*/
function AnimatedNumber({ value }) {
const formatted = value.toLocaleString();
const chars = formatted.split('');
return (
<span className="inline-flex overflow-hidden">
{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>
);
})}
</span>
);
}
function AdminDashboard() {
const { user, isAuthenticated } = useAdminAuth();
// 통계 조회
const { data: stats = { members: 0, albums: 0, photos: 0, schedules: 0 } } = useQuery({
queryKey: ['admin', 'stats'],
queryFn: adminStatsApi.getStats,
enabled: isAuthenticated,
staleTime: 30 * 1000,
});
// 메뉴 아이템
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',
},
{
icon: ScrollText,
label: '활동 로그',
description: '관리자 및 봇 활동 기록 조회',
path: '/admin/logs',
color: 'bg-gray-500',
},
];
return (
<AdminLayout user={user}>
<div 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">
<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>
<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.photos} />
</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.schedules} />
</p>
<p className="text-gray-500 text-sm"> 일정</p>
</div>
</div>
</div>
</div>
</AdminLayout>
);
}
export default AdminDashboard;