diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index d4a54e8..4851b2f 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -1,7 +1,7 @@
'use client';
import Link from 'next/link';
import { signIn, signOut, useSession } from 'next-auth/react';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { BiFolder , BiCommentDetail } from 'react-icons/bi';
import { FaChartBar } from 'react-icons/fa';
import { FaBuffer } from 'react-icons/fa6';
@@ -18,8 +18,10 @@ import DecryptedText from '../entities/bits/DecryptedText';
const AdminDashboard = () => {
const { data: session } = useSession();
const toast = useToast();
+ const [mounted, setMounted] = useState(false);
useEffect(() => {
+ setMounted(true);
if (session) {
toast.success('관리자 페이지에 오신 것을 환영합니다.');
}
@@ -45,49 +47,49 @@ const AdminDashboard = () => {
title: '블로그 포스트 작성',
icon: ,
description: '새로운 글을 작성합니다.',
- bgColor: 'bg-blue-950/20', // 짙은 파란색의 투명도 적용
+ accent: 'border-l-brand-primary',
link: '/admin/write',
},
{
title: '프로젝트 관리',
icon: ,
description: '포트폴리오 프로젝트를 관리합니다.',
- bgColor: 'bg-yellow-950/20', // 짙은 노란색의 투명도 적용
+ accent: 'border-l-semantic-info',
link: '/admin/portfolio',
},
{
title: '게시글 수정/삭제',
icon: ,
description: '기존 게시글을 관리합니다.',
- bgColor: 'bg-green-950/20', // 짙은 초록색의 투명도 적용
+ accent: 'border-l-primary-bangladesh',
link: '/admin/posts',
},
{
title: '방문자 및 조회수 분석',
icon: ,
description: '블로그 통계를 확인합니다.',
- bgColor: 'bg-purple-950/20', // 짙은 보라색의 투명도 적용
+ accent: 'border-l-semantic-warning',
link: '/admin/analytics',
},
{
title: '시리즈 관리',
icon: ,
description: '블로그 시리즈를 관리합니다.',
- bgColor: 'bg-emerald-950/20', // 짙은 보라색의 투명도 적용
+ accent: 'border-l-brand-secondary',
link: '/admin/series',
},
{
title: '댓글 확인 및 관리',
icon: ,
description: '댓글을 관리합니다.',
- bgColor: 'bg-pink-950/20', // 짙은 분홍색의 투명도 적용
+ accent: 'border-l-semantic-error',
link: '/admin/comments',
},
{
title: '블로그 설정 관리',
icon: ,
description: '블로그 설정을 변경합니다.',
- bgColor: 'bg-gray-800/20', // 짙은 회색의 투명도 적용
+ accent: 'border-l-primary-mountain',
link: '/admin/settings',
},
];
@@ -121,28 +123,48 @@ const AdminDashboard = () => {
-
- {dashboardItems.map((item, index) => (
-
-
-
- {item.icon}
+
+
+
+
+ {!mounted ? (
+
+ {[...Array(7)].map((_, i) => (
+
-
{item.description}
-
- ))}
-
+ ))}
+
+ ) : (
+
+ {dashboardItems.map((item, index) => (
+
+
+
+ {item.icon}
+
+
{item.title}
+
+
{item.description}
+
+ ))}
+
+ )}
-
);
diff --git a/app/api/admin/stats/route.ts b/app/api/admin/stats/route.ts
index ced8d67..d2ef520 100644
--- a/app/api/admin/stats/route.ts
+++ b/app/api/admin/stats/route.ts
@@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
import dbConnect from '@/app/lib/dbConnect';
import Post from '@/app/models/Post';
import Series from '@/app/models/Series';
+import View from '@/app/models/View';
export const dynamic = 'force-dynamic';
@@ -18,12 +19,17 @@ export async function GET() {
await dbConnect();
- const [totalPosts, totalSeries, publicPosts, privatePosts] =
+ const todayStart = new Date();
+ todayStart.setHours(0, 0, 0, 0);
+
+ const [totalPosts, totalSeries, publicPosts, privatePosts, totalViews, todayViews] =
await Promise.all([
Post.countDocuments({}),
Series.countDocuments({}),
Post.countDocuments({ isPrivate: false }),
Post.countDocuments({ isPrivate: true }),
+ View.countDocuments({}),
+ View.countDocuments({ createdAt: { $gte: todayStart } }),
]);
return Response.json(
@@ -34,6 +40,8 @@ export async function GET() {
totalSeries,
publicPosts,
privatePosts,
+ totalViews,
+ todayViews,
},
},
{
diff --git a/app/entities/admin/dashboard/QuickStats.tsx b/app/entities/admin/dashboard/QuickStats.tsx
index 427089f..34d42bd 100644
--- a/app/entities/admin/dashboard/QuickStats.tsx
+++ b/app/entities/admin/dashboard/QuickStats.tsx
@@ -1,6 +1,32 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
+
+function useCountUp(target: number, duration = 1200) {
+ const [count, setCount] = useState(0);
+ const rafRef = useRef
(null);
+
+ useEffect(() => {
+ if (target === 0) return;
+ const start = performance.now();
+
+ const tick = (now: number) => {
+ const elapsed = now - start;
+ const progress = Math.min(elapsed / duration, 1);
+ // ease-out cubic
+ const eased = 1 - Math.pow(1 - progress, 3);
+ setCount(Math.round(eased * target));
+ if (progress < 1) rafRef.current = requestAnimationFrame(tick);
+ };
+
+ rafRef.current = requestAnimationFrame(tick);
+ return () => {
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
+ };
+ }, [target, duration]);
+
+ return count;
+}
interface Stats {
totalPosts: number;
@@ -8,6 +34,8 @@ interface Stats {
publicPosts: number;
privatePosts: number;
activeSubscribers: number;
+ totalViews: number;
+ todayViews: number;
}
const QuickStats = () => {
@@ -15,12 +43,14 @@ const QuickStats = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const totalViewsCount = useCountUp(stats?.totalViews ?? 0);
+ const todayViewsCount = useCountUp(stats?.todayViews ?? 0);
+
useEffect(() => {
const fetchStats = async () => {
try {
setLoading(true);
- // Fetch both stats in parallel
const [blogStatsRes, subscriberStatsRes] = await Promise.all([
fetch('/api/admin/stats'),
fetch('/api/admin/subscribers'),
@@ -54,55 +84,74 @@ const QuickStats = () => {
if (loading) {
return (
-
-
빠른 통계
-
로딩 중...
+
+
+
+ {[...Array(2)].map((_, i) => (
+
+ ))}
+
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
);
}
if (error || !stats) {
return (
-
-
빠른 통계
+
+
블로그 통계
{error || '통계를 불러올 수 없습니다.'}
);
}
+ const secondaryStats = [
+ { label: '전체 게시글', value: stats.totalPosts },
+ { label: '전체 시리즈', value: stats.totalSeries },
+ { label: '공개 게시글', value: stats.publicPosts },
+ { label: '비공개 게시글', value: stats.privatePosts },
+ { label: '활성 구독자', value: stats.activeSubscribers },
+ ];
+
return (
-
-
빠른 통계
-
-
-
전체 게시글
-
{stats.totalPosts}
-
-
-
전체 시리즈
-
- {stats.totalSeries}
-
-
-
-
공개 게시글
-
- {stats.publicPosts}
-
-
-
-
비공개 게시글
-
- {stats.privatePosts}
+
+
블로그 통계
+
+ {/* 조회수 강조 섹션 */}
+
+
+
전체 조회수
+
+ {totalViewsCount.toLocaleString()}
-
-
활성 구독자
-
- {stats.activeSubscribers}
+
+
오늘 조회수
+
+ {todayViewsCount.toLocaleString()}
+
+ {/* 기타 통계 */}
+
+ {secondaryStats.map(({ label, value }) => (
+
+
{label}
+
{value.toLocaleString()}
+
+ ))}
+
);
};
diff --git a/app/entities/admin/dashboard/RecentActivity.tsx b/app/entities/admin/dashboard/RecentActivity.tsx
index f800fa5..add70df 100644
--- a/app/entities/admin/dashboard/RecentActivity.tsx
+++ b/app/entities/admin/dashboard/RecentActivity.tsx
@@ -42,41 +42,51 @@ const RecentActivity = () => {
if (loading) {
return (
-
-
최근 활동
-
로딩 중...
+
+
+
+ {[...Array(5)].map((_, i) => (
+ -
+
+
+
+ ))}
+
);
}
if (error) {
return (
-
-
최근 활동
+
);
}
return (
-
-
최근 활동
+
+
최근 활동
{posts.length === 0 ? (
-
최근 게시글이 없습니다.
+
최근 게시글이 없습니다.
) : (
-
+
{posts.map((post) => (
-
{post.title}
-
+
{formatDate(post.date)}