170 lines
4.9 KiB
TypeScript
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 },
|
|
);
|
|
}
|
|
}
|