From 01dc03e2792575073c8e9ecdc7c130610fefb9b4 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 3 Mar 2026 10:06:00 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=88=98=EC=99=80=20=EC=98=A4=EB=8A=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=88=98=EB=A5=BC=20=EB=B3=BC=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 7 ++- app/api/admin/stats/route.ts | 10 +++- app/entities/admin/dashboard/QuickStats.tsx | 63 ++++++++++++--------- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index d4a54e8..57030e3 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -121,6 +121,10 @@ const AdminDashboard = () => { +
+ +
+
{dashboardItems.map((item, index) => ( { ))}
-
+
-
); 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..9eb1e5a 100644 --- a/app/entities/admin/dashboard/QuickStats.tsx +++ b/app/entities/admin/dashboard/QuickStats.tsx @@ -8,6 +8,8 @@ interface Stats { publicPosts: number; privatePosts: number; activeSubscribers: number; + totalViews: number; + todayViews: number; } const QuickStats = () => { @@ -20,7 +22,6 @@ const QuickStats = () => { try { setLoading(true); - // Fetch both stats in parallel const [blogStatsRes, subscriberStatsRes] = await Promise.all([ fetch('/api/admin/stats'), fetch('/api/admin/subscribers'), @@ -55,7 +56,7 @@ const QuickStats = () => { if (loading) { return (
-

빠른 통계

+

블로그 통계

로딩 중...
); @@ -64,7 +65,7 @@ const QuickStats = () => { if (error || !stats) { return (
-

빠른 통계

+

블로그 통계

{error || '통계를 불러올 수 없습니다.'}
); @@ -72,35 +73,41 @@ const QuickStats = () => { return (
-

빠른 통계

-
-
-

전체 게시글

-

{stats.totalPosts}

+

블로그 통계

+ + {/* 조회수 강조 섹션 */} +
+
+

전체 조회수

+

{stats.totalViews.toLocaleString()}

+
+
+

오늘 조회수

+

{stats.todayViews.toLocaleString()}

+
+
+ + {/* 기타 통계 */} +
+
+

전체 게시글

+

{stats.totalPosts}

-
-

전체 시리즈

-

- {stats.totalSeries} -

+
+

전체 시리즈

+

{stats.totalSeries}

-
-

공개 게시글

-

- {stats.publicPosts} -

+
+

공개 게시글

+

{stats.publicPosts}

-
-

비공개 게시글

-

- {stats.privatePosts} -

+
+

비공개 게시글

+

{stats.privatePosts}

-
-

활성 구독자

-

- {stats.activeSubscribers} -

+
+

활성 구독자

+

{stats.activeSubscribers}

From 58ecc7e412987b464aa2807bb4fc0374e427b112 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 3 Mar 2026 10:11:01 +0900 Subject: [PATCH 2/6] =?UTF-8?q?style:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=AC=B4=EC=B1=84=EC=83=89=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/admin/dashboard/QuickStats.tsx | 60 ++++++++++----------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/app/entities/admin/dashboard/QuickStats.tsx b/app/entities/admin/dashboard/QuickStats.tsx index 9eb1e5a..ea3b675 100644 --- a/app/entities/admin/dashboard/QuickStats.tsx +++ b/app/entities/admin/dashboard/QuickStats.tsx @@ -55,7 +55,7 @@ const QuickStats = () => { if (loading) { return ( -
+

블로그 통계

로딩 중...
@@ -64,51 +64,49 @@ const QuickStats = () => { 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.totalViews.toLocaleString()}

+
+

전체 조회수

+

+ {stats.totalViews.toLocaleString()} +

-
-

오늘 조회수

-

{stats.todayViews.toLocaleString()}

+
+

오늘 조회수

+

+ {stats.todayViews.toLocaleString()} +

{/* 기타 통계 */} -
-
-

전체 게시글

-

{stats.totalPosts}

-
-
-

전체 시리즈

-

{stats.totalSeries}

-
-
-

공개 게시글

-

{stats.publicPosts}

-
-
-

비공개 게시글

-

{stats.privatePosts}

-
-
-

활성 구독자

-

{stats.activeSubscribers}

-
+
+ {secondaryStats.map(({ label, value }) => ( +
+

{label}

+

{value.toLocaleString()}

+
+ ))}
); From 1e529a990ca311aeee6c85a6bf696d950135740c Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 3 Mar 2026 10:17:47 +0900 Subject: [PATCH 3/6] =?UTF-8?q?style:=20=EB=B8=94=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=96=B4=EB=93=9C=EB=AF=BC=20=EB=8F=84=EA=B5=AC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 24 +++++++------- app/entities/admin/dashboard/QuickStats.tsx | 35 +++++++++++++++++++-- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 57030e3..a07d989 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -45,49 +45,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', }, ]; @@ -131,15 +131,15 @@ const AdminDashboard = () => { key={index} href={item.link} prefetch={false} - className={`${item.bgColor} p-6 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-1`} + className={`border border-gray-200 border-l-4 ${item.accent} rounded-lg p-6 hover:bg-gray-50 transition-all duration-200 hover:-translate-y-1`} > -
-
+
+
{item.icon}
-

{item.title}

+

{item.title}

-

{item.description}

+

{item.description}

))}
diff --git a/app/entities/admin/dashboard/QuickStats.tsx b/app/entities/admin/dashboard/QuickStats.tsx index ea3b675..3d8620b 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; @@ -17,6 +43,9 @@ 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 { @@ -88,13 +117,13 @@ const QuickStats = () => {

전체 조회수

- {stats.totalViews.toLocaleString()} + {totalViewsCount.toLocaleString()}

오늘 조회수

- {stats.todayViews.toLocaleString()} + {todayViewsCount.toLocaleString()}

From 1d5309a014b835901f10c206e13e1a3e8ee254e9 Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 3 Mar 2026 10:20:24 +0900 Subject: [PATCH 4/6] =?UTF-8?q?style:=20=EC=B5=9C=EA=B7=BC=20=ED=99=9C?= =?UTF-8?q?=EB=8F=99=20=EC=84=B9=EC=85=98=EB=8F=84=20=EC=9D=BC=EA=B4=80?= =?UTF-8?q?=EB=90=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/admin/dashboard/QuickStats.tsx | 6 +++--- .../admin/dashboard/RecentActivity.tsx | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/entities/admin/dashboard/QuickStats.tsx b/app/entities/admin/dashboard/QuickStats.tsx index 3d8620b..ecd2ccd 100644 --- a/app/entities/admin/dashboard/QuickStats.tsx +++ b/app/entities/admin/dashboard/QuickStats.tsx @@ -84,7 +84,7 @@ const QuickStats = () => { if (loading) { return ( -
+

블로그 통계

로딩 중...
@@ -93,7 +93,7 @@ const QuickStats = () => { if (error || !stats) { return ( -
+

블로그 통계

{error || '통계를 불러올 수 없습니다.'}
@@ -109,7 +109,7 @@ const QuickStats = () => { ]; return ( -
+

블로그 통계

{/* 조회수 강조 섹션 */} diff --git a/app/entities/admin/dashboard/RecentActivity.tsx b/app/entities/admin/dashboard/RecentActivity.tsx index f800fa5..54b15f5 100644 --- a/app/entities/admin/dashboard/RecentActivity.tsx +++ b/app/entities/admin/dashboard/RecentActivity.tsx @@ -42,7 +42,7 @@ const RecentActivity = () => { if (loading) { return ( -
+

최근 활동

로딩 중...
@@ -51,7 +51,7 @@ const RecentActivity = () => { if (error) { return ( -
+

최근 활동

{error}
@@ -59,24 +59,24 @@ const RecentActivity = () => { } return ( -
-

최근 활동

+
+

최근 활동

{posts.length === 0 ? ( -

최근 게시글이 없습니다.

+

최근 게시글이 없습니다.

) : ( -
    +
      {posts.map((post) => (
    • {post.title} - + {formatDate(post.date)}
    • From 4dc538388765c8c69626d618524cc3ba9367815b Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 3 Mar 2026 10:22:14 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=EC=96=B4=EB=93=9C=EB=AF=BC?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=8B=A4=ED=81=AC=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 12 ++++----- app/entities/admin/dashboard/QuickStats.tsx | 26 +++++++++---------- .../admin/dashboard/RecentActivity.tsx | 16 ++++++------ 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index a07d989..795bcba 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -121,7 +121,7 @@ const AdminDashboard = () => { -
      +
      @@ -131,20 +131,20 @@ const AdminDashboard = () => { key={index} href={item.link} prefetch={false} - className={`border border-gray-200 border-l-4 ${item.accent} rounded-lg p-6 hover:bg-gray-50 transition-all duration-200 hover:-translate-y-1`} + 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`} >
      -
      +
      {item.icon}
      -

      {item.title}

      +

      {item.title}

      -

      {item.description}

      +

      {item.description}

      ))}
      -
      +
      diff --git a/app/entities/admin/dashboard/QuickStats.tsx b/app/entities/admin/dashboard/QuickStats.tsx index ecd2ccd..f8e6bc1 100644 --- a/app/entities/admin/dashboard/QuickStats.tsx +++ b/app/entities/admin/dashboard/QuickStats.tsx @@ -85,8 +85,8 @@ const QuickStats = () => { if (loading) { return (
      -

      블로그 통계

      -
      로딩 중...
      +

      블로그 통계

      +
      로딩 중...
      ); } @@ -94,7 +94,7 @@ const QuickStats = () => { if (error || !stats) { return (
      -

      블로그 통계

      +

      블로그 통계

      {error || '통계를 불러올 수 없습니다.'}
      ); @@ -110,19 +110,19 @@ const QuickStats = () => { return (
      -

      블로그 통계

      +

      블로그 통계

      {/* 조회수 강조 섹션 */}
      -
      -

      전체 조회수

      -

      +

      +

      전체 조회수

      +

      {totalViewsCount.toLocaleString()}

      -
      -

      오늘 조회수

      -

      +

      +

      오늘 조회수

      +

      {todayViewsCount.toLocaleString()}

      @@ -131,9 +131,9 @@ const QuickStats = () => { {/* 기타 통계 */}
      {secondaryStats.map(({ label, value }) => ( -
      -

      {label}

      -

      {value.toLocaleString()}

      +
      +

      {label}

      +

      {value.toLocaleString()}

      ))}
      diff --git a/app/entities/admin/dashboard/RecentActivity.tsx b/app/entities/admin/dashboard/RecentActivity.tsx index 54b15f5..6652610 100644 --- a/app/entities/admin/dashboard/RecentActivity.tsx +++ b/app/entities/admin/dashboard/RecentActivity.tsx @@ -43,8 +43,8 @@ const RecentActivity = () => { if (loading) { return (
      -

      최근 활동

      -
      로딩 중...
      +

      최근 활동

      +
      로딩 중...
      ); } @@ -52,7 +52,7 @@ const RecentActivity = () => { if (error) { return (
      -

      최근 활동

      +

      최근 활동

      {error}
      ); @@ -60,23 +60,23 @@ const RecentActivity = () => { return (
      -

      최근 활동

      +

      최근 활동

      {posts.length === 0 ? ( -

      최근 게시글이 없습니다.

      +

      최근 게시글이 없습니다.

      ) : (
        {posts.map((post) => (
      • {post.title} - + {formatDate(post.date)}
      • From 44f24c34f905cb837529b917be8439e75fa862cb Mon Sep 17 00:00:00 2001 From: JeongwooSeo Date: Tue, 3 Mar 2026 10:26:15 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=84=B9=EC=85=98=20=EB=B3=84=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/page.tsx | 53 +++++++++++++------ app/entities/admin/dashboard/QuickStats.tsx | 21 ++++++-- .../admin/dashboard/RecentActivity.tsx | 16 ++++-- 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 795bcba..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('관리자 페이지에 오신 것을 환영합니다.'); } @@ -125,24 +127,41 @@ const AdminDashboard = () => {
      -
      - {dashboardItems.map((item, index) => ( - -
      -
      - {item.icon} + {!mounted ? ( +
      + {[...Array(7)].map((_, i) => ( +
      +
      +
      +
      -

      {item.title}

      +
      -

      {item.description}

      - - ))} -
      + ))} +
      + ) : ( +
      + {dashboardItems.map((item, index) => ( + +
      +
      + {item.icon} +
      +

      {item.title}

      +
      +

      {item.description}

      + + ))} +
      + )}
      diff --git a/app/entities/admin/dashboard/QuickStats.tsx b/app/entities/admin/dashboard/QuickStats.tsx index f8e6bc1..34d42bd 100644 --- a/app/entities/admin/dashboard/QuickStats.tsx +++ b/app/entities/admin/dashboard/QuickStats.tsx @@ -84,9 +84,24 @@ const QuickStats = () => { if (loading) { return ( -
      -

      블로그 통계

      -
      로딩 중...
      +
      +
      +
      + {[...Array(2)].map((_, i) => ( +
      +
      +
      +
      + ))} +
      +
      + {[...Array(5)].map((_, i) => ( +
      +
      +
      +
      + ))} +
      ); } diff --git a/app/entities/admin/dashboard/RecentActivity.tsx b/app/entities/admin/dashboard/RecentActivity.tsx index 6652610..add70df 100644 --- a/app/entities/admin/dashboard/RecentActivity.tsx +++ b/app/entities/admin/dashboard/RecentActivity.tsx @@ -42,9 +42,19 @@ const RecentActivity = () => { if (loading) { return ( -
      -

      최근 활동

      -
      로딩 중...
      +
      +
      +
        + {[...Array(5)].map((_, i) => ( +
      • +
        +
        +
      • + ))} +
      ); }