Files
page-builder/src/app/api/reports/leads/route.ts
T
jaybe 87cdc1868b
CI / test (push) Failing after 13m23s
CI / e2e (push) Has been cancelled
CI / pr_and_merge (push) Has been cancelled
마이페이지, 리포트, 메일인증
2025-12-15 21:59:43 +09:00

170 lines
4.9 KiB
TypeScript

import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import { verifyAccessToken } from "@/features/auth/authCrypto";
// /api/reports/leads
// - 로그인한 사용자의 FormSubmission(리드) 목록을 기간/프로젝트 필터에 맞게 조회해,
// 최신순으로 정렬된 리드 리스트와 총 개수를 반환한다.
const prisma = new PrismaClient();
type RangeParam = "7d" | "30d" | "all";
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;
}
export async function GET(request: Request) {
try {
const url = new URL(request.url);
const range = (url.searchParams.get("range") as RangeParam | null) ?? "all";
const projectIdFilter = url.searchParams.get("projectId");
const fromParam = url.searchParams.get("from");
const toParam = url.searchParams.get("to");
if (range !== "7d" && range !== "30d" && range !== "all") {
return NextResponse.json({ message: "Invalid range parameter" }, { status: 400 });
}
const cookieHeader = request.headers.get("cookie");
const token = extractTokenFromCookieHeader(cookieHeader);
if (!token) {
return NextResponse.json({ message: "인증 정보가 없습니다." }, { status: 401 });
}
const payload = await verifyAccessToken(token);
if (!payload) {
return NextResponse.json({ message: "유효하지 않은 토큰입니다." }, { status: 401 });
}
const userId = payload.sub;
const [projects, submissions] = await Promise.all([
prisma.project.findMany({
where: { userId },
select: {
id: true,
title: true,
slug: true,
},
}),
prisma.formSubmission.findMany({
where: { userId },
select: {
id: true,
projectId: true,
projectSlug: true,
createdAt: true,
payloadJson: true,
},
}),
]);
let fromDate: Date | null = null;
let toDate: Date | null = null;
if (fromParam || toParam) {
if (fromParam) {
const parsedFrom = new Date(`${fromParam}T00:00:00.000Z`);
if (Number.isNaN(parsedFrom.getTime())) {
return NextResponse.json({ message: "Invalid from parameter" }, { status: 400 });
}
fromDate = parsedFrom;
}
if (toParam) {
const parsedTo = new Date(`${toParam}T23:59:59.999Z`);
if (Number.isNaN(parsedTo.getTime())) {
return NextResponse.json({ message: "Invalid to parameter" }, { status: 400 });
}
toDate = parsedTo;
}
} else {
const now = new Date();
if (range === "7d") {
fromDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
} else if (range === "30d") {
fromDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
}
}
// 1단계: 기간/프로젝트 필터 적용
const filtered = submissions.filter((s) => {
const createdAt = s.createdAt instanceof Date ? s.createdAt : new Date(s.createdAt as any);
if (fromDate && createdAt < fromDate) {
return false;
}
if (toDate && createdAt > toDate) {
return false;
}
if (projectIdFilter && s.projectId !== projectIdFilter) {
return false;
}
return true;
});
// 2단계: 최신순 정렬 (createdAt desc)
filtered.sort((a, b) => {
const aDate = a.createdAt instanceof Date ? a.createdAt : new Date(a.createdAt as any);
const bDate = b.createdAt instanceof Date ? b.createdAt : new Date(b.createdAt as any);
return bDate.getTime() - aDate.getTime();
});
const projectById = new Map<
string,
{
title: string;
slug: string;
}
>();
for (const p of projects) {
projectById.set(p.id, { title: p.title, slug: p.slug });
}
const items = filtered.map((s) => {
const createdAt = s.createdAt instanceof Date ? s.createdAt : new Date(s.createdAt as any);
const projectMeta = s.projectId ? projectById.get(s.projectId) : null;
return {
id: s.id,
projectId: s.projectId ?? null,
projectSlug: s.projectSlug,
projectTitle: projectMeta?.title ?? null,
createdAt: createdAt.toISOString(),
payload: (s as any).payloadJson ?? {},
};
});
const body = {
total: items.length,
items,
};
return NextResponse.json(body, { status: 200 });
} catch (error: any) {
return NextResponse.json(
{
message: "리드 리포트를 불러오는 중 오류가 발생했습니다.",
error: error?.message,
},
{ status: 500 },
);
}
}