172 lines
6.2 KiB
TypeScript
172 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
interface SubmissionItem {
|
|
id: string;
|
|
createdAt: string;
|
|
projectSlug: string;
|
|
projectTitle: string | null;
|
|
payload: Record<string, unknown>;
|
|
}
|
|
|
|
type PageStatus = "idle" | "loading" | "error";
|
|
|
|
export default function AllProjectSubmissionsPage() {
|
|
const router = useRouter();
|
|
const [submissions, setSubmissions] = useState<SubmissionItem[]>([]);
|
|
const [status, setStatus] = useState<PageStatus>("idle");
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const load = async () => {
|
|
try {
|
|
setStatus("loading");
|
|
setErrorMessage(null);
|
|
|
|
const res = await fetch("/api/projects/submissions");
|
|
|
|
if (!res.ok) {
|
|
if (cancelled) return;
|
|
|
|
if (res.status === 401) {
|
|
setStatus("error");
|
|
setErrorMessage("폼 제출 내역을 조회하려면 로그인이 필요합니다. 다시 로그인해 주세요.");
|
|
router.push("/login");
|
|
return;
|
|
}
|
|
|
|
setStatus("error");
|
|
setErrorMessage("폼 제출 내역을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
|
|
return;
|
|
}
|
|
|
|
const data = (await res.json()) as SubmissionItem[];
|
|
|
|
if (!cancelled) {
|
|
setSubmissions(Array.isArray(data) ? data : []);
|
|
setStatus("idle");
|
|
setErrorMessage(null);
|
|
}
|
|
} catch {
|
|
if (!cancelled) {
|
|
setStatus("error");
|
|
setErrorMessage("폼 제출 내역을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
|
|
}
|
|
}
|
|
};
|
|
|
|
void load();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const renderOtherFields = (payload: Record<string, unknown>): string => {
|
|
const knownKeys = new Set(["name", "email", "phone", "birthdate"]);
|
|
const entries = Object.entries(payload).filter(([key]) => !knownKeys.has(key));
|
|
|
|
if (entries.length === 0) {
|
|
return "-";
|
|
}
|
|
|
|
return entries
|
|
.map(([key, value]) => {
|
|
const stringValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
return `${key}: ${stringValue}`;
|
|
})
|
|
.join("\n");
|
|
};
|
|
|
|
return (
|
|
<main className="min-h-screen flex flex-col bg-slate-950 text-slate-50">
|
|
<header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between bg-slate-950/80 backdrop-blur">
|
|
<div>
|
|
<h1 className="text-xl font-semibold">전체 폼 제출 내역</h1>
|
|
<p className="text-xs text-slate-400">로그인한 계정의 모든 프로젝트에 대한 폼 제출 데이터를 확인할 수 있습니다.</p>
|
|
</div>
|
|
<div className="text-xs text-slate-300 flex flex-col items-end gap-1">
|
|
<a
|
|
href="/projects"
|
|
role="link"
|
|
className="text-[11px] text-sky-400 hover:text-sky-300 underline-offset-2 hover:underline"
|
|
onClick={(event) => {
|
|
event.preventDefault();
|
|
router.push("/projects");
|
|
}}
|
|
>
|
|
프로젝트 목록
|
|
</a>
|
|
</div>
|
|
</header>
|
|
|
|
<section className="flex-1 px-6 py-4 overflow-auto">
|
|
{status === "error" && errorMessage && (
|
|
<p className="text-xs text-red-300 mb-3">{errorMessage}</p>
|
|
)}
|
|
|
|
{status === "loading" && (
|
|
<p className="text-xs text-slate-400 mb-3">폼 제출 내역을 불러오는 중입니다...</p>
|
|
)}
|
|
|
|
{submissions.length === 0 && status === "idle" && !errorMessage && (
|
|
<p className="text-xs text-slate-400">아직 수집된 폼 제출 내역이 없습니다.</p>
|
|
)}
|
|
|
|
{submissions.length > 0 && (
|
|
<table className="w-full text-xs text-left border-collapse mt-2">
|
|
<thead>
|
|
<tr className="border-b border-slate-800 text-slate-400">
|
|
<th className="py-2 pr-4">제출 시각</th>
|
|
<th className="py-2 pr-4">프로젝트</th>
|
|
<th className="py-2 pr-4">Slug</th>
|
|
<th className="py-2 pr-4">이름</th>
|
|
<th className="py-2 pr-4">이메일</th>
|
|
<th className="py-2 pr-4">전화번호</th>
|
|
<th className="py-2 pr-4">생년월일</th>
|
|
<th className="py-2 pr-4">기타 필드</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{submissions.map((item) => {
|
|
const payload = (item.payload ?? {}) as Record<string, unknown>;
|
|
|
|
const nameValue = payload["name"];
|
|
const emailValue = payload["email"];
|
|
const phoneValue = payload["phone"];
|
|
const birthdateValue = payload["birthdate"];
|
|
|
|
const name = typeof nameValue === "string" ? nameValue : "";
|
|
const email = typeof emailValue === "string" ? emailValue : "";
|
|
const phone = typeof phoneValue === "string" ? phoneValue : "";
|
|
const birthdate = typeof birthdateValue === "string" ? birthdateValue : "";
|
|
|
|
return (
|
|
<tr key={item.id} className="border-b border-slate-900 hover:bg-slate-900/60">
|
|
<td className="py-2 pr-4 text-slate-300 text-[11px]">
|
|
{new Date(item.createdAt).toLocaleString()}
|
|
</td>
|
|
<td className="py-2 pr-4 text-slate-100">{item.projectTitle ?? "-"}</td>
|
|
<td className="py-2 pr-4 text-slate-300">{item.projectSlug}</td>
|
|
<td className="py-2 pr-4 text-slate-100">{name}</td>
|
|
<td className="py-2 pr-4 text-slate-300">{email}</td>
|
|
<td className="py-2 pr-4 text-slate-300">{phone}</td>
|
|
<td className="py-2 pr-4 text-slate-300">{birthdate}</td>
|
|
<td className="py-2 pr-4 text-slate-300 whitespace-pre-wrap">
|
|
{renderOtherFields(payload)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|