"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { FilePlus2, Trash2, Eye, Pencil, ListChecks, ChevronLeft, ChevronRight, LayoutDashboard, FolderKanban, SunMoon, BarChart2, } from "lucide-react"; import { useAppLocale } from "@/features/i18n/LocaleProvider"; import { getDashboardMessages } from "@/features/i18n/messages/dashboard"; import { getProjectsMessages } from "@/features/i18n/messages/projects"; interface ProjectListItem { id: string; title: string; slug: string; status: string; createdAt: string; updatedAt: string; } export default function ProjectsPage() { const router = useRouter(); const locale = useAppLocale(); const t = getDashboardMessages(locale); const p = getProjectsMessages(locale); const [projects, setProjects] = useState([]); const [status, setStatus] = useState<"idle" | "loading" | "error">("idle"); const [errorMessage, setErrorMessage] = useState(null); const [selectedSlugs, setSelectedSlugs] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [isMenuOpen, setIsMenuOpen] = useState(false); const [emailVerified, setEmailVerified] = useState(null); const pageSize = 10; useEffect(() => { let cancelled = false; const load = async () => { try { setStatus("loading"); setErrorMessage(null); const res = await fetch("/api/projects"); if (!res.ok) { if (!cancelled) { setStatus("error"); if (res.status === 401) { setErrorMessage(p.errorFetchUnauthorized); router.push("/login"); } else { setErrorMessage(p.errorFetchGeneric); } } return; } const data = (await res.json()) as ProjectListItem[]; if (!cancelled) { setProjects(Array.isArray(data) ? data : []); setStatus("idle"); setErrorMessage(null); } } catch { if (!cancelled) { setStatus("error"); setErrorMessage(p.errorFetchGeneric); } } }; void load(); return () => { cancelled = true; }; }, []); // 이메일 인증 여부를 확인해 퍼블릭 페이지 링크 노출 여부를 결정한다. 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)); }, [projects.length, pageSize]); const handleDelete = async (slug: string) => { if (typeof window !== "undefined") { const confirmed = window.confirm(p.confirmDeleteSingle); if (!confirmed) return; } try { const res = await fetch(`/api/projects/${encodeURIComponent(slug)}`, { method: "DELETE", }); if (!res.ok) { setStatus("error"); setErrorMessage(p.errorDeleteSingle); return; } setProjects((prev) => prev.filter((p) => p.slug !== slug)); setSelectedSlugs((prev) => prev.filter((s) => s !== slug)); if (typeof window !== "undefined") { window.localStorage.removeItem(`pb:project:${slug}`); window.localStorage.removeItem(`pb:autosave:${slug}`); } setStatus("idle"); setErrorMessage(null); } catch { setStatus("error"); setErrorMessage(p.errorDeleteSingle); } }; const handleLogout = async () => { try { const res = await fetch("/api/auth/logout", { method: "POST", }); if (!res.ok) { setStatus("error"); setErrorMessage("로그아웃 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."); return; } router.push("/login"); } catch { setStatus("error"); setErrorMessage("로그아웃 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."); } }; const totalCount = projects.length; const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const pageProjects = projects.slice(startIndex, endIndex); const handleBulkDelete = async () => { const slugs = selectedSlugs; if (slugs.length === 0) return; if (typeof window !== "undefined") { const confirmed = window.confirm(p.confirmDeleteBulk); if (!confirmed) return; } try { const results = await Promise.all( slugs.map(async (slug) => { const res = await fetch(`/api/projects/${encodeURIComponent(slug)}`, { method: "DELETE" }); return { slug, ok: res.ok }; }), ); const okSlugs = results.filter((r) => r.ok).map((r) => r.slug); if (okSlugs.length === 0) { setStatus("error"); setErrorMessage(p.errorDeleteBulkAllFailed); return; } setProjects((prev) => prev.filter((p) => !okSlugs.includes(p.slug))); setSelectedSlugs((prev) => prev.filter((s) => !okSlugs.includes(s))); if (typeof window !== "undefined") { for (const slug of okSlugs) { window.localStorage.removeItem(`pb:project:${slug}`); window.localStorage.removeItem(`pb:autosave:${slug}`); } } if (okSlugs.length !== slugs.length) { setStatus("error"); setErrorMessage(p.errorDeleteBulkSomeFailed); } else { setStatus("idle"); setErrorMessage(null); } } catch { setStatus("error"); setErrorMessage(p.errorDeleteBulkAllFailed); } }; const handleToggleTheme = () => { if (typeof document === "undefined") { return; } const root = document.documentElement; if (!root) { return; } root.classList.toggle("dark"); }; return (

{p.headerTitle}

{p.headerDescription}

{isMenuOpen && (
)}
{status === "error" && (

{errorMessage ?? p.errorFetchGeneric}

)} {projects.length === 0 && status === "idle" && (

아직 저장된 프로젝트가 없습니다. 에디터에서 프로젝트를 저장해 보세요.

)} {projects.length > 0 && ( <>
{pageProjects.map((project) => { const checked = selectedSlugs.includes(project.slug); return ( ); })}
0 && pageProjects.every((p) => selectedSlugs.includes(p.slug)) } onChange={(e) => { if (e.target.checked) { const slugsOnPage = pageProjects.map((p) => p.slug); setSelectedSlugs((prev) => Array.from(new Set([...prev, ...slugsOnPage]))); } else { const slugsOnPage = pageProjects.map((p) => p.slug); setSelectedSlugs((prev) => prev.filter((s) => !slugsOnPage.includes(s))); } }} /> {p.tableHeaderTitle} {p.tableHeaderSlug} {p.tableHeaderStatus} {p.tableHeaderCreatedAt} {p.tableHeaderUpdatedAt} {p.tableHeaderActions}
{ if (e.target.checked) { setSelectedSlugs((prev) => prev.includes(project.slug) ? prev : [...prev, project.slug], ); } else { setSelectedSlugs((prev) => prev.filter((s) => s !== project.slug)); } }} /> {project.title} {project.slug} {project.status} {new Date(project.createdAt).toLocaleString()} {new Date(project.updatedAt).toLocaleString()}
{p.pagerTotalPrefix} {totalCount} {p.pagerTotalCountSuffix} {totalCount === 0 ? 0 : startIndex + 1}–{Math.min(endIndex, totalCount)}
{Array.from({ length: totalPages }).map((_, idx) => { const page = idx + 1; const isActive = page === currentPage; return ( ); })}
)}
); }