import { NextResponse } from "next/server"; import { PrismaClient } from "@prisma/client"; import { verifyAccessToken } from "@/features/auth/authCrypto"; // 대시보드 개요 API (/api/dashboard/overview) // - 로그인한 사용자의 프로젝트/폼 제출 통계를 집계해 요약/프로젝트별 지표를 반환한다. // - DB 스키마: Project, FormSubmission // - 인증: pb_access 쿠키(JWT)를 검증해 현재 사용자 식별 const prisma = new PrismaClient(); interface AuthUser { id: string; email: string; tokenVersion: number; } // 공통: pb_access 쿠키에서 JWT 토큰을 추출한다. function extractTokenFromCookieHeader(cookieHeader: string | null): string | null { if (!cookieHeader) return null; const parts = cookieHeader.split(";"); for (const part of parts) { const trimmed = part.trim(); if (trimmed.startsWith("pb_access=")) { const value = trimmed.substring("pb_access=".length); return decodeURIComponent(value); } } return null; } // 공통: Request 헤더의 쿠키에서 인증된 유저 정보를 복원한다. async function getAuthUserFromRequest(request: Request): Promise { const cookieHeader = request.headers.get("cookie"); const token = extractTokenFromCookieHeader(cookieHeader); if (!token) return null; const payload = await verifyAccessToken(token); if (!payload) return null; return { id: payload.sub, email: payload.email, tokenVersion: payload.tokenVersion, }; } interface SummaryStats { totalProjects: number; totalSubmissions: number; todaySubmissions: number; last7DaysSubmissions: number; } interface DailySubmissionItem { // 일별 제출 수 그래프를 위한 집계 데이터 date: string; // YYYY-MM-DD (UTC 기준) count: number; } interface ProjectStatsItem { projectId: string; title: string; slug: string; status: string; totalSubmissions: number; lastSubmissionAt: string | null; } // GET /api/dashboard/overview // - 현재 로그인 사용자의 프로젝트 목록/폼 제출 내역을 조회해 통계를 계산한다. export async function GET(request: Request) { const authUser = await getAuthUserFromRequest(request); if (!authUser) { return NextResponse.json( { message: "대시보드를 조회하려면 로그인이 필요합니다." }, { status: 401 }, ); } try { // 현재 사용자 소유 프로젝트와, 해당 사용자가 소유한 제출 내역을 한 번에 로드한다. const [projects, submissions] = await Promise.all([ prisma.project.findMany({ where: { userId: authUser.id }, select: { id: true, title: true, slug: true, status: true, }, }), prisma.formSubmission.findMany({ where: { userId: authUser.id }, select: { id: true, projectId: true, projectSlug: true, createdAt: true, }, }), ]); const totalProjects = projects.length; const totalSubmissions = submissions.length; // 오늘/최근 7일 기준 시각 계산 const now = new Date(); const startOfToday = new Date(now); startOfToday.setHours(0, 0, 0, 0); const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); let todaySubmissions = 0; let last7DaysSubmissions = 0; for (const submission of submissions) { const createdAt = submission.createdAt instanceof Date ? submission.createdAt : new Date(submission.createdAt as unknown as string); if (createdAt >= startOfToday) { todaySubmissions += 1; } if (createdAt >= sevenDaysAgo) { last7DaysSubmissions += 1; } } // 프로젝트별 통계 맵을 구성한다. const statsByProjectId = new Map< string, { projectId: string; title: string; slug: string; status: string; totalSubmissions: number; lastSubmissionAt: Date | null; } >(); for (const project of projects) { statsByProjectId.set(project.id, { projectId: project.id, title: project.title, slug: project.slug, status: project.status, totalSubmissions: 0, lastSubmissionAt: null, }); } for (const submission of submissions) { const stat = submission.projectId ? statsByProjectId.get(submission.projectId) : null; if (!stat) continue; stat.totalSubmissions += 1; const createdAt = submission.createdAt instanceof Date ? submission.createdAt : new Date(submission.createdAt as unknown as string); if (!stat.lastSubmissionAt || createdAt > stat.lastSubmissionAt) { stat.lastSubmissionAt = createdAt; } } const projectStats: ProjectStatsItem[] = Array.from(statsByProjectId.values()).map((item) => ({ projectId: item.projectId, title: item.title, slug: item.slug, status: item.status, totalSubmissions: item.totalSubmissions, lastSubmissionAt: item.lastSubmissionAt ? item.lastSubmissionAt.toISOString() : null, })); const summaryStats: SummaryStats = { totalProjects, totalSubmissions, todaySubmissions, last7DaysSubmissions, }; // 일별 전체 제출 수를 집계해 그래프용 데이터로 변환한다. const countsByDate = new Map(); for (const submission of submissions) { const createdAt = submission.createdAt instanceof Date ? submission.createdAt : new Date(submission.createdAt as unknown as string); const dateKey = createdAt.toISOString().slice(0, 10); // YYYY-MM-DD const prev = countsByDate.get(dateKey) ?? 0; countsByDate.set(dateKey, prev + 1); } const dailySubmissions: DailySubmissionItem[] = Array.from(countsByDate.entries()) .map(([date, count]) => ({ date, count })) .sort((a, b) => a.date.localeCompare(b.date)); return NextResponse.json( { summaryStats, projectStats, dailySubmissions, }, { status: 200 }, ); } catch (error: any) { return NextResponse.json( { message: "대시보드 데이터를 계산하는 중 오류가 발생했습니다.", error: error?.message, }, { status: 500 }, ); } }