Skip to content

Commit 5488c40

Browse files
Merge pull request #81 from ShipFriend0516/feature/view-entire
feat: 어드민페이지 조회수 보기 기능 추가 및 스타일 개선
2 parents 305772f + 44f24c3 commit 5488c40

File tree

4 files changed

+162
-73
lines changed

4 files changed

+162
-73
lines changed

app/admin/page.tsx

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import Link from 'next/link';
33
import { signIn, signOut, useSession } from 'next-auth/react';
4-
import { useEffect } from 'react';
4+
import { useEffect, useState } from 'react';
55
import { BiFolder , BiCommentDetail } from 'react-icons/bi';
66
import { FaChartBar } from 'react-icons/fa';
77
import { FaBuffer } from 'react-icons/fa6';
@@ -18,8 +18,10 @@ import DecryptedText from '../entities/bits/DecryptedText';
1818
const AdminDashboard = () => {
1919
const { data: session } = useSession();
2020
const toast = useToast();
21+
const [mounted, setMounted] = useState(false);
2122

2223
useEffect(() => {
24+
setMounted(true);
2325
if (session) {
2426
toast.success('관리자 페이지에 오신 것을 환영합니다.');
2527
}
@@ -45,49 +47,49 @@ const AdminDashboard = () => {
4547
title: '블로그 포스트 작성',
4648
icon: <RiFileTextLine />,
4749
description: '새로운 글을 작성합니다.',
48-
bgColor: 'bg-blue-950/20', // 짙은 파란색의 투명도 적용
50+
accent: 'border-l-brand-primary',
4951
link: '/admin/write',
5052
},
5153
{
5254
title: '프로젝트 관리',
5355
icon: <BiFolder />,
5456
description: '포트폴리오 프로젝트를 관리합니다.',
55-
bgColor: 'bg-yellow-950/20', // 짙은 노란색의 투명도 적용
57+
accent: 'border-l-semantic-info',
5658
link: '/admin/portfolio',
5759
},
5860
{
5961
title: '게시글 수정/삭제',
6062
icon: <HiBookOpen />,
6163
description: '기존 게시글을 관리합니다.',
62-
bgColor: 'bg-green-950/20', // 짙은 초록색의 투명도 적용
64+
accent: 'border-l-primary-bangladesh',
6365
link: '/admin/posts',
6466
},
6567
{
6668
title: '방문자 및 조회수 분석',
6769
icon: <FaChartBar />,
6870
description: '블로그 통계를 확인합니다.',
69-
bgColor: 'bg-purple-950/20', // 짙은 보라색의 투명도 적용
71+
accent: 'border-l-semantic-warning',
7072
link: '/admin/analytics',
7173
},
7274
{
7375
title: '시리즈 관리',
7476
icon: <FaBuffer />,
7577
description: '블로그 시리즈를 관리합니다.',
76-
bgColor: 'bg-emerald-950/20', // 짙은 보라색의 투명도 적용
78+
accent: 'border-l-brand-secondary',
7779
link: '/admin/series',
7880
},
7981
{
8082
title: '댓글 확인 및 관리',
8183
icon: <BiCommentDetail />,
8284
description: '댓글을 관리합니다.',
83-
bgColor: 'bg-pink-950/20', // 짙은 분홍색의 투명도 적용
85+
accent: 'border-l-semantic-error',
8486
link: '/admin/comments',
8587
},
8688
{
8789
title: '블로그 설정 관리',
8890
icon: <IoSettingsSharp />,
8991
description: '블로그 설정을 변경합니다.',
90-
bgColor: 'bg-gray-800/20', // 짙은 회색의 투명도 적용
92+
accent: 'border-l-primary-mountain',
9193
link: '/admin/settings',
9294
},
9395
];
@@ -121,28 +123,48 @@ const AdminDashboard = () => {
121123
</button>
122124
</header>
123125

124-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 ">
125-
{dashboardItems.map((item, index) => (
126-
<Link
127-
key={index}
128-
href={item.link}
129-
prefetch={false}
130-
className={`${item.bgColor} p-6 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-1`}
131-
>
132-
<div className="flex items-center mb-4">
133-
<div className="p-2 border text-weak rounded-lg shadow-sm">
134-
{item.icon}
126+
<div className="mb-8">
127+
<QuickStats />
128+
</div>
129+
130+
{!mounted ? (
131+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-pulse">
132+
{[...Array(7)].map((_, i) => (
133+
<div
134+
key={i}
135+
className="border border-gray-200 dark:border-gray-700 border-l-4 border-l-gray-300 dark:border-l-gray-600 rounded-lg p-6"
136+
>
137+
<div className="flex items-center mb-3">
138+
<div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded" />
139+
<div className="h-5 w-36 bg-gray-200 dark:bg-gray-700 rounded ml-2" />
135140
</div>
136-
<h2 className="text-xl font-semibold ml-3">{item.title}</h2>
141+
<div className="h-4 w-44 bg-gray-200 dark:bg-gray-700 rounded" />
137142
</div>
138-
<p className="text-default">{item.description}</p>
139-
</Link>
140-
))}
141-
</div>
143+
))}
144+
</div>
145+
) : (
146+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
147+
{dashboardItems.map((item, index) => (
148+
<Link
149+
key={index}
150+
href={item.link}
151+
prefetch={false}
152+
className={`border border-gray-200 dark:border-gray-700 border-l-4 ${item.accent} rounded-lg p-6 hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 hover:-translate-y-1`}
153+
>
154+
<div className="flex items-center mb-3">
155+
<div className="p-2 text-gray-600 dark:text-gray-400 rounded-lg">
156+
{item.icon}
157+
</div>
158+
<h2 className="text-lg font-semibold ml-2 dark:text-gray-100">{item.title}</h2>
159+
</div>
160+
<p className="text-sm text-gray-500 dark:text-gray-400">{item.description}</p>
161+
</Link>
162+
))}
163+
</div>
164+
)}
142165

143-
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6 text-black">
166+
<div className="mt-8">
144167
<RecentActivity />
145-
<QuickStats />
146168
</div>
147169
</div>
148170
);

app/api/admin/stats/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
33
import dbConnect from '@/app/lib/dbConnect';
44
import Post from '@/app/models/Post';
55
import Series from '@/app/models/Series';
6+
import View from '@/app/models/View';
67

78
export const dynamic = 'force-dynamic';
89

@@ -18,12 +19,17 @@ export async function GET() {
1819

1920
await dbConnect();
2021

21-
const [totalPosts, totalSeries, publicPosts, privatePosts] =
22+
const todayStart = new Date();
23+
todayStart.setHours(0, 0, 0, 0);
24+
25+
const [totalPosts, totalSeries, publicPosts, privatePosts, totalViews, todayViews] =
2226
await Promise.all([
2327
Post.countDocuments({}),
2428
Series.countDocuments({}),
2529
Post.countDocuments({ isPrivate: false }),
2630
Post.countDocuments({ isPrivate: true }),
31+
View.countDocuments({}),
32+
View.countDocuments({ createdAt: { $gte: todayStart } }),
2733
]);
2834

2935
return Response.json(
@@ -34,6 +40,8 @@ export async function GET() {
3440
totalSeries,
3541
publicPosts,
3642
privatePosts,
43+
totalViews,
44+
todayViews,
3745
},
3846
},
3947
{

app/entities/admin/dashboard/QuickStats.tsx

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,56 @@
11
'use client';
22

3-
import { useEffect, useState } from 'react';
3+
import { useEffect, useRef, useState } from 'react';
4+
5+
function useCountUp(target: number, duration = 1200) {
6+
const [count, setCount] = useState(0);
7+
const rafRef = useRef<number | null>(null);
8+
9+
useEffect(() => {
10+
if (target === 0) return;
11+
const start = performance.now();
12+
13+
const tick = (now: number) => {
14+
const elapsed = now - start;
15+
const progress = Math.min(elapsed / duration, 1);
16+
// ease-out cubic
17+
const eased = 1 - Math.pow(1 - progress, 3);
18+
setCount(Math.round(eased * target));
19+
if (progress < 1) rafRef.current = requestAnimationFrame(tick);
20+
};
21+
22+
rafRef.current = requestAnimationFrame(tick);
23+
return () => {
24+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
25+
};
26+
}, [target, duration]);
27+
28+
return count;
29+
}
430

531
interface Stats {
632
totalPosts: number;
733
totalSeries: number;
834
publicPosts: number;
935
privatePosts: number;
1036
activeSubscribers: number;
37+
totalViews: number;
38+
todayViews: number;
1139
}
1240

1341
const QuickStats = () => {
1442
const [stats, setStats] = useState<Stats | null>(null);
1543
const [loading, setLoading] = useState(true);
1644
const [error, setError] = useState<string | null>(null);
1745

46+
const totalViewsCount = useCountUp(stats?.totalViews ?? 0);
47+
const todayViewsCount = useCountUp(stats?.todayViews ?? 0);
48+
1849
useEffect(() => {
1950
const fetchStats = async () => {
2051
try {
2152
setLoading(true);
2253

23-
// Fetch both stats in parallel
2454
const [blogStatsRes, subscriberStatsRes] = await Promise.all([
2555
fetch('/api/admin/stats'),
2656
fetch('/api/admin/subscribers'),
@@ -54,55 +84,74 @@ const QuickStats = () => {
5484

5585
if (loading) {
5686
return (
57-
<div className="bg-white p-6 rounded-lg shadow-md">
58-
<h3 className="text-xl font-semibold mb-4">빠른 통계</h3>
59-
<div className="text-gray-500">로딩 중...</div>
87+
<div className="py-4 animate-pulse">
88+
<div className="h-7 w-28 bg-gray-200 dark:bg-gray-700 rounded mb-6" />
89+
<div className="grid grid-cols-2 gap-4 mb-6">
90+
{[...Array(2)].map((_, i) => (
91+
<div key={i} className="border border-gray-200 dark:border-gray-700 rounded-lg p-5">
92+
<div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded mb-3" />
93+
<div className="h-12 w-32 bg-gray-200 dark:bg-gray-700 rounded" />
94+
</div>
95+
))}
96+
</div>
97+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
98+
{[...Array(5)].map((_, i) => (
99+
<div key={i} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
100+
<div className="h-3 w-14 bg-gray-200 dark:bg-gray-700 rounded mb-2" />
101+
<div className="h-8 w-10 bg-gray-200 dark:bg-gray-700 rounded" />
102+
</div>
103+
))}
104+
</div>
60105
</div>
61106
);
62107
}
63108

64109
if (error || !stats) {
65110
return (
66-
<div className="bg-white p-6 rounded-lg shadow-md">
67-
<h3 className="text-xl font-semibold mb-4">빠른 통계</h3>
111+
<div className="py-4">
112+
<h3 className="text-xl font-semibold mb-4 dark:text-white">블로그 통계</h3>
68113
<div className="text-red-500">{error || '통계를 불러올 수 없습니다.'}</div>
69114
</div>
70115
);
71116
}
72117

118+
const secondaryStats = [
119+
{ label: '전체 게시글', value: stats.totalPosts },
120+
{ label: '전체 시리즈', value: stats.totalSeries },
121+
{ label: '공개 게시글', value: stats.publicPosts },
122+
{ label: '비공개 게시글', value: stats.privatePosts },
123+
{ label: '활성 구독자', value: stats.activeSubscribers },
124+
];
125+
73126
return (
74-
<div className="bg-white p-6 rounded-lg shadow-md">
75-
<h3 className="text-xl font-semibold mb-4">빠른 통계</h3>
76-
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
77-
<div className="bg-blue-50 p-4 rounded-lg">
78-
<p className="text-sm text-gray-600 mb-1">전체 게시글</p>
79-
<p className="text-2xl font-bold text-blue-600">{stats.totalPosts}</p>
80-
</div>
81-
<div className="bg-green-50 p-4 rounded-lg">
82-
<p className="text-sm text-gray-600 mb-1">전체 시리즈</p>
83-
<p className="text-2xl font-bold text-green-600">
84-
{stats.totalSeries}
85-
</p>
86-
</div>
87-
<div className="bg-purple-50 p-4 rounded-lg">
88-
<p className="text-sm text-gray-600 mb-1">공개 게시글</p>
89-
<p className="text-2xl font-bold text-purple-600">
90-
{stats.publicPosts}
91-
</p>
92-
</div>
93-
<div className="bg-orange-50 p-4 rounded-lg">
94-
<p className="text-sm text-gray-600 mb-1">비공개 게시글</p>
95-
<p className="text-2xl font-bold text-orange-600">
96-
{stats.privatePosts}
127+
<div className="py-4">
128+
<h3 className="text-xl font-semibold mb-6 dark:text-white">블로그 통계</h3>
129+
130+
{/* 조회수 강조 섹션 */}
131+
<div className="grid grid-cols-2 gap-4 mb-6">
132+
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5">
133+
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">전체 조회수</p>
134+
<p className="text-5xl font-bold tracking-tight dark:text-white">
135+
{totalViewsCount.toLocaleString()}
97136
</p>
98137
</div>
99-
<div className="bg-teal-50 p-4 rounded-lg">
100-
<p className="text-sm text-gray-600 mb-1">활성 구독자</p>
101-
<p className="text-2xl font-bold text-teal-600">
102-
{stats.activeSubscribers}
138+
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-5">
139+
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">오늘 조회수</p>
140+
<p className="text-5xl font-bold tracking-tight dark:text-white">
141+
{todayViewsCount.toLocaleString()}
103142
</p>
104143
</div>
105144
</div>
145+
146+
{/* 기타 통계 */}
147+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
148+
{secondaryStats.map(({ label, value }) => (
149+
<div key={label} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
150+
<p className="text-xs text-gray-400 dark:text-gray-500 mb-1">{label}</p>
151+
<p className="text-2xl font-semibold dark:text-white">{value.toLocaleString()}</p>
152+
</div>
153+
))}
154+
</div>
106155
</div>
107156
);
108157
};

0 commit comments

Comments
 (0)