"use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { BarChart2, FolderKanban, LayoutDashboard, ListChecks, SunMoon } from "lucide-react"; import { useAppLocale } from "@/features/i18n/LocaleProvider"; import { getDashboardMessages } from "@/features/i18n/messages/dashboard"; // 대시보드 페이지 (/dashboard) // - /api/dashboard/overview 에서 요약/프로젝트별 통계를 가져와 카드 형태로 렌더링한다. // - 비로그인(401) 응답 시 로그인 페이지로 리다이렉트하고 에러 메시지를 표시한다. interface SummaryStats { totalProjects: number; totalSubmissions: number; todaySubmissions: number; last7DaysSubmissions: number; } interface DailySubmissionItem { // 일별 제출 수 그래프를 위한 집계 데이터 date: string; // YYYY-MM-DD count: number; } interface ProjectStatsItem { projectId: string; title: string; slug: string; status: string; totalSubmissions: number; lastSubmissionAt: string | null; } type PageStatus = "idle" | "loading" | "error"; export default function DashboardPage() { const router = useRouter(); const locale = useAppLocale(); const t = getDashboardMessages(locale); const [summary, setSummary] = useState(null); const [projects, setProjects] = useState([]); const [dailySubmissions, setDailySubmissions] = useState([]); const [status, setStatus] = useState("idle"); const [errorMessage, setErrorMessage] = useState(null); const [isMenuOpen, setIsMenuOpen] = useState(false); const [emailVerified, setEmailVerified] = useState(null); // 마운트 시 대시보드 개요 데이터를 불러온다. useEffect(() => { let cancelled = false; const load = async () => { try { setStatus("loading"); setErrorMessage(null); const res = await fetch("/api/dashboard/overview"); if (!res.ok) { if (cancelled) return; if (res.status === 401) { setStatus("error"); setErrorMessage(t.errorUnauthorized); router.push("/login"); return; } setStatus("error"); setErrorMessage(t.errorGeneric); return; } const data = (await res.json()) as { summaryStats: SummaryStats; projectStats: ProjectStatsItem[]; dailySubmissions?: DailySubmissionItem[]; }; if (cancelled) return; setSummary(data.summaryStats ?? null); setProjects(Array.isArray(data.projectStats) ? data.projectStats : []); setDailySubmissions( Array.isArray(data.dailySubmissions) ? data.dailySubmissions : [], ); setStatus("idle"); setErrorMessage(null); } catch { if (!cancelled) { setStatus("error"); setErrorMessage(t.errorGeneric); } } }; void load(); return () => { cancelled = true; }; }, [router]); // 마운트 시 현재 사용자 이메일 인증 여부를 확인한다. useEffect(() => { let cancelled = false; const loadMe = async () => { try { const res = await fetch("/api/auth/me"); if (cancelled || !res.ok) return; const data = (await res.json().catch(() => null)) as any; if (!cancelled && data) { setEmailVerified(Boolean(data.emailVerified)); } } catch { // ignore – 인증 여부를 알 수 없으면 배너만 표시하지 않는다. } }; void loadMe(); return () => { cancelled = true; }; }, []); const safeSummary: SummaryStats = summary ?? { totalProjects: 0, totalSubmissions: 0, todaySubmissions: 0, last7DaysSubmissions: 0, }; const handleLogout = async () => { try { const res = await fetch("/api/auth/logout", { method: "POST", }); if (!res.ok) { return; } router.push("/login"); } catch { } }; const handleToggleTheme = () => { if (typeof document === "undefined") { return; } const root = document.documentElement; if (!root) { return; } root.classList.toggle("dark"); }; return (

{t.title}

{t.description}

{isMenuOpen && (
)}
{status === "error" && errorMessage && (

{errorMessage}

)} {emailVerified === false && (

{t.emailNotVerifiedBanner}

)} {status === "loading" && (

{t.loadingText}

)} {/* 상단 요약 위젯 영역 */}
{t.summaryTotalProjectsLabel} {safeSummary.totalProjects}
{t.summaryTotalSubmissionsLabel} {safeSummary.totalSubmissions}
{t.summaryTodaySubmissionsLabel} {safeSummary.todaySubmissions}
{t.summaryLast7DaysSubmissionsLabel} {safeSummary.last7DaysSubmissions}
{/* 일별 제출 수 간단 그래프 영역 */}

{t.chartTitle}

{t.chartSubtitle}
{dailySubmissions.length === 0 ? (

{t.chartEmptyLabel}

) : (
{dailySubmissions.map((item) => (
{item.count} {item.date}
))}
)}
{/* 프로젝트 카드 리스트 영역 */}

{t.sectionProjectsTitle}

{projects.length === 0 && status === "idle" && !errorMessage && (

{t.sectionProjectsEmpty}

)} {projects.length > 0 && (
{projects.map((project) => { const lastSubmitted = project.lastSubmissionAt ? new Date(project.lastSubmissionAt).toLocaleString() : t.projectLatestSubmissionEmpty; return (

{project.title}

{project.slug}

{project.status}
{t.projectTotalSubmissionsPrefix} {project.totalSubmissions} {t.projectTotalSubmissionsSuffix} {t.projectLatestSubmissionPrefix} {lastSubmitted}
{t.projectViewSubmissionsLink} {t.projectOpenPublicPageLink}
); })}
)}
); }