"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, } from "lucide-react"; interface ProjectListItem { id: string; title: string; slug: string; status: string; createdAt: string; updatedAt: string; } export default function ProjectsPage() { const router = useRouter(); 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 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("프로젝트 목록을 불러오는 중 인증 오류가 발생했습니다. 다시 로그인해 주세요."); router.push("/login"); } else { setErrorMessage( "프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.", ); } } 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("프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."); } } }; void load(); 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( "이 프로젝트를 삭제할까요? 삭제 후에는 되돌릴 수 없습니다. (로컬 저장 및 자동저장 데이터도 함께 삭제됩니다.)", ); if (!confirmed) return; } try { const res = await fetch(`/api/projects/${encodeURIComponent(slug)}`, { method: "DELETE", }); if (!res.ok) { setStatus("error"); setErrorMessage("프로젝트 삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."); 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("프로젝트 삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."); } }; 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( "선택한 프로젝트를 모두 삭제할까요? 삭제 후에는 되돌릴 수 없습니다. (로컬 저장 및 자동저장 데이터도 함께 삭제됩니다.)", ); 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("선택한 프로젝트를 삭제하는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."); 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( "일부 프로젝트 삭제에 실패했습니다. 페이지를 새로고침한 뒤 목록을 확인해 주세요.", ); } else { setStatus("idle"); setErrorMessage(null); } } catch { setStatus("error"); setErrorMessage("선택한 프로젝트를 삭제하는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."); } }; return (

프로젝트 목록

저장된 프로젝트들을 한 눈에 볼 수 있는 목록

{status === "error" && (

{errorMessage ?? "프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."}

)} {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))); } }} /> 제목 주소(slug) 상태 생성일 수정일 액션
{ 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()}
전체 {totalCount}개 중{" "} {totalCount === 0 ? 0 : startIndex + 1}–{Math.min(endIndex, totalCount)}
{Array.from({ length: totalPages }).map((_, idx) => { const page = idx + 1; const isActive = page === currentPage; return ( ); })}
)}
); }