'use client'; import Link from 'next/link'; import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { ArrowRight, CalendarClock, FileText, Loader2, PenLine, Settings, } from 'lucide-react'; import { getAdminDashboard } from '@/api/dashboard'; import { getCategories } from '@/api/category'; import { getAdminComments } from '@/api/comments'; import { getPosts } from '@/api/posts'; import AdminDashboardActionCenter from '@/components/admin/dashboard/AdminDashboardActionCenter'; import AdminDashboardCategoryHealth from '@/components/admin/dashboard/AdminDashboardCategoryHealth'; import AdminDashboardOverview from '@/components/admin/dashboard/AdminDashboardOverview'; import AdminDashboardPostPerformance from '@/components/admin/dashboard/AdminDashboardPostPerformance'; import AdminDashboardTrafficChart from '@/components/admin/dashboard/AdminDashboardTrafficChart'; import EmptyState from '@/components/ui/EmptyState'; import StatusBadge from '@/components/ui/StatusBadge'; import Surface from '@/components/ui/Surface'; import { AdminComment, AdminCommentListResponse, Category, DashboardPostStat, DashboardRange, PageMeta, Post, } from '@/types'; const countCategories = (categories: Category[] = []): number => { return categories.reduce((count, category) => { return count + 1 + countCategories(category.children || []); }, 0); }; const emptyPageMeta: PageMeta = { totalPages: 0, totalElements: 0, number: 0, last: true, }; const formatDate = (value?: string | Date, withTime = false) => { if (!value) return '-'; const date = value instanceof Date ? value : new Date(value); if (Number.isNaN(date.getTime())) return '-'; return new Intl.DateTimeFormat('ko-KR', { year: 'numeric', month: 'short', day: 'numeric', ...(withTime ? { hour: '2-digit', minute: '2-digit' } : {}), }).format(date); }; const getCommentListMeta = (data?: AdminCommentListResponse): PageMeta => { if (!data) return emptyPageMeta; return { totalElements: data.page?.totalElements ?? data.totalElements ?? 0, totalPages: data.page?.totalPages ?? data.totalPages ?? 0, number: data.page?.number ?? data.number ?? 0, last: data.page?.last ?? data.last, }; }; const getPostTotal = (data?: { page?: PageMeta; totalElements?: number }) => { return data?.page?.totalElements ?? data?.totalElements ?? 0; }; const getAuthorName = (comment: AdminComment) => { return comment.author || comment.memberNickname || comment.guestNickname || '익명'; }; const toPostStat = (post: Post): DashboardPostStat => ({ id: post.id, title: post.title, slug: post.slug, categoryName: post.categoryName, viewCount: post.viewCount, rangeViewCount: post.viewCount, createdAt: post.createdAt, }); function RecentPostRow({ post }: { post: Post }) { return (

{post.title}

{post.categoryName || '미분류'} · {formatDate(post.createdAt)}

); } function RecentCommentRow({ comment }: { comment: AdminComment }) { const content = ( <>

{comment.content}

{getAuthorName(comment)} · {comment.postTitle || '게시글 정보 없음'} · {formatDate(comment.createdAt, true)}

); if (comment.postSlug) { return ( {content} ); } return
{content}
; } function RecentActivity({ posts, comments, isLoading, }: { posts: Post[]; comments: AdminComment[]; isLoading: boolean; }) { return (

최근 글

발행 흐름을 빠르게 확인합니다.

관리
{isLoading ? (
) : posts.length > 0 ? (
{posts.slice(0, 5).map((post) => ( ))}
) : ( )}

최근 댓글

새 반응을 대시보드에서 바로 봅니다.

관리
{isLoading ? (
) : comments.length > 0 ? (
{comments.slice(0, 5).map((comment) => ( ))}
) : ( )}
); } export default function AdminPage() { const [range, setRange] = useState('30d'); const { data: dashboard, isLoading: isDashboardLoading, } = useQuery({ queryKey: ['admin-dashboard', range], queryFn: () => getAdminDashboard(range), retry: 0, }); const { data: latestData, isLoading: isLatestLoading } = useQuery({ queryKey: ['posts', 'admin', 'latest'], queryFn: () => getPosts({ size: 5, sort: 'createdAt,desc' }), retry: 0, }); const { data: popularData, isLoading: isPopularLoading } = useQuery({ queryKey: ['posts', 'admin', 'popular'], queryFn: () => getPosts({ size: 5, sort: 'viewCount,desc' }), retry: 0, }); const { data: categories, isLoading: isCategoriesLoading } = useQuery({ queryKey: ['categories'], queryFn: getCategories, retry: 0, }); const { data: commentsData, isLoading: isCommentsLoading } = useQuery({ queryKey: ['comments', 'admin', 'recent'], queryFn: () => getAdminComments(0, 5), retry: 0, }); const latestPosts = latestData?.content ?? []; const recentComments = dashboard?.recentComments ?? commentsData?.content ?? []; const recentPosts = dashboard?.recentPosts ?? latestPosts; const totalPosts = dashboard?.overview.totalPosts ?? getPostTotal(latestData); const totalCategories = dashboard?.overview.totalCategories ?? countCategories(categories); const totalComments = dashboard?.overview.totalComments ?? getCommentListMeta(commentsData).totalElements; const isFallback = !dashboard; const isFallbackLoading = isLatestLoading || isPopularLoading || isCategoriesLoading || isCommentsLoading; const topPosts = useMemo( () => dashboard?.topPosts ?? (popularData?.content ?? []).map(toPostStat), [dashboard?.topPosts, popularData?.content], ); return (
관리자

관리자 대시보드

공개 화면과 분리된 운영 공간입니다. 통계 API가 준비되기 전에도 기존 데이터로 운영 흐름을 확인합니다.

오늘 {formatDate(new Date())} {isDashboardLoading && ' · 통계 API 확인 중'} {isFallback && !isDashboardLoading && ' · fallback 데이터 사용 중'}

게시글 관리 새 글 작성
); }