마이페이지, 리포트, 메일인증
CI / test (push) Failing after 13m23s
CI / e2e (push) Has been cancelled
CI / pr_and_merge (push) Has been cancelled

This commit is contained in:
2025-12-15 21:59:43 +09:00
parent a8e1a4a960
commit 87cdc1868b
30 changed files with 5488 additions and 16 deletions
+53
View File
@@ -14,6 +14,7 @@ import {
LayoutDashboard,
FolderKanban,
SunMoon,
BarChart2,
} from "lucide-react";
import { useAppLocale } from "@/features/i18n/LocaleProvider";
import { getDashboardMessages } from "@/features/i18n/messages/dashboard";
@@ -39,6 +40,7 @@ export default function ProjectsPage() {
const [selectedSlugs, setSelectedSlugs] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [emailVerified, setEmailVerified] = useState<boolean | null>(null);
const pageSize = 10;
useEffect(() => {
@@ -85,6 +87,31 @@ export default function ProjectsPage() {
};
}, []);
// 이메일 인증 여부를 확인해 퍼블릭 페이지 링크 노출 여부를 결정한다.
useEffect(() => {
let cancelled = false;
const loadMe = async () => {
try {
const res = await fetch("/api/auth/me");
if (cancelled || !res.ok) return;
const data = (await res.json().catch(() => null)) as any;
if (!cancelled && data) {
setEmailVerified(Boolean(data.emailVerified));
}
} catch {
// ignore – 인증 여부를 알 수 없으면 기존 동작(링크 노출)을 유지한다.
}
};
void loadMe();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
const totalPages = Math.max(1, Math.ceil(projects.length / pageSize));
setCurrentPage((prev) => Math.min(prev, totalPages));
@@ -241,6 +268,13 @@ export default function ProjectsPage() {
<ListChecks className="w-4 h-4" aria-hidden="true" />
<span>{t.navSubmissions}</span>
</Link>
<Link
href="/reports"
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-sky-700 dark:text-slate-200 dark:hover:bg-slate-800 dark:hover:text-sky-200"
>
<BarChart2 className="w-4 h-4" aria-hidden="true" />
<span>{t.navReports ?? "Reports"}</span>
</Link>
</nav>
<button
type="button"
@@ -262,6 +296,16 @@ export default function ProjectsPage() {
</button>
{isMenuOpen && (
<div className="absolute right-0 mt-1 w-32 rounded border border-slate-200 bg-white shadow-lg z-20 dark:border-slate-700 dark:bg-slate-900/95">
<button
type="button"
className="w-full px-3 py-2 text-left text-xs text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800"
onClick={() => {
setIsMenuOpen(false);
router.push("/mypage");
}}
>
{t.myPageLabel}
</button>
<button
type="button"
className="w-full px-3 py-2 text-left text-xs text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800"
@@ -403,6 +447,15 @@ export default function ProjectsPage() {
<Eye className="w-3 h-3" aria-hidden="true" />
<span>{p.actionPreview}</span>
</Link>
{emailVerified !== false && (
<Link
href={`/p/${encodeURIComponent(project.slug)}`}
className="inline-flex items-center gap-1 text-sky-700 hover:text-sky-900 underline-offset-2 hover:underline dark:text-sky-300 dark:hover:text-sky-200"
>
<Eye className="w-3 h-3" aria-hidden="true" />
<span>{t.projectOpenPublicPageLink}</span>
</Link>
)}
<Link
href={`/projects/${encodeURIComponent(project.slug)}/submissions`}
className="inline-flex items-center gap-1 text-emerald-600 hover:text-emerald-700 underline-offset-2 hover:underline dark:text-emerald-300 dark:hover:text-emerald-100"