사용자 로그인, 가입 처리
CI / test (push) Successful in 3m40s
CI / pr_and_merge (push) Successful in 1m8s

This commit is contained in:
2025-11-30 23:08:38 +09:00
parent 25162e4c12
commit 64e4a59244
82 changed files with 2817 additions and 28 deletions
+59 -1
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
FilePlus2,
Trash2,
@@ -22,8 +23,10 @@ interface ProjectListItem {
}
export default function ProjectsPage() {
const router = useRouter();
const [projects, setProjects] = useState<ProjectListItem[]>([]);
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [selectedSlugs, setSelectedSlugs] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
@@ -34,11 +37,21 @@ export default function ProjectsPage() {
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;
}
@@ -47,10 +60,12 @@ export default function ProjectsPage() {
if (!cancelled) {
setProjects(Array.isArray(data) ? data : []);
setStatus("idle");
setErrorMessage(null);
}
} catch {
if (!cancelled) {
setStatus("error");
setErrorMessage("프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
}
}
};
@@ -83,6 +98,7 @@ export default function ProjectsPage() {
if (!res.ok) {
setStatus("error");
setErrorMessage("프로젝트 삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
return;
}
@@ -93,8 +109,31 @@ export default function ProjectsPage() {
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("로그아웃 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
}
};
@@ -127,6 +166,7 @@ export default function ProjectsPage() {
if (okSlugs.length === 0) {
setStatus("error");
setErrorMessage("선택한 프로젝트를 삭제하는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
return;
}
@@ -142,9 +182,16 @@ export default function ProjectsPage() {
if (okSlugs.length !== slugs.length) {
setStatus("error");
setErrorMessage(
"일부 프로젝트 삭제에 실패했습니다. 페이지를 새로고침한 뒤 목록을 확인해 주세요.",
);
} else {
setStatus("idle");
setErrorMessage(null);
}
} catch {
setStatus("error");
setErrorMessage("선택한 프로젝트를 삭제하는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
}
};
@@ -156,6 +203,15 @@ export default function ProjectsPage() {
<p className="text-xs text-slate-400"> </p>
</div>
<div className="flex items-center gap-2 text-xs">
<button
type="button"
className="inline-flex items-center gap-1 rounded border border-slate-700 px-3 py-1 text-slate-200 hover:bg-slate-900"
onClick={() => {
void handleLogout();
}}
>
</button>
<Link
href="/editor"
className="inline-flex items-center gap-1 rounded border border-sky-700 bg-sky-950 px-3 py-1 text-sky-100 hover:bg-sky-900 shadow-sm"
@@ -167,7 +223,9 @@ export default function ProjectsPage() {
</header>
<section className="flex-1 px-6 py-4 overflow-auto">
{status === "error" && (
<p className="text-xs text-red-300 mb-3"> . .</p>
<p className="text-xs text-red-300 mb-3">
{errorMessage ?? "프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."}
</p>
)}
{projects.length === 0 && status === "idle" && (
<p className="text-xs text-slate-400"> . .</p>