221 lines
6.3 KiB
TypeScript
221 lines
6.3 KiB
TypeScript
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<AuthUser | null> {
|
|
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<string, number>();
|
|
|
|
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 },
|
|
);
|
|
}
|
|
}
|