대시보드, GNB 추가
CI / test (push) Successful in 4m58s
CI / e2e (push) Has been skipped
CI / pr_and_merge (push) Successful in 1m45s

This commit is contained in:
2025-12-08 06:59:52 +09:00
parent b40be693ee
commit 9d8c4538c7
18 changed files with 1595 additions and 86 deletions
+103 -40
View File
@@ -11,6 +11,9 @@ import {
ListChecks,
ChevronLeft,
ChevronRight,
LayoutDashboard,
FolderKanban,
SunMoon,
} from "lucide-react";
interface ProjectListItem {
@@ -29,6 +32,7 @@ export default function ProjectsPage() {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [selectedSlugs, setSelectedSlugs] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const pageSize = 10;
useEffect(() => {
@@ -179,7 +183,7 @@ export default function ProjectsPage() {
window.localStorage.removeItem(`pb:autosave:${slug}`);
}
}
if (okSlugs.length !== slugs.length) {
setStatus("error");
setErrorMessage(
@@ -195,51 +199,101 @@ export default function ProjectsPage() {
}
};
const handleToggleTheme = () => {
if (typeof document === "undefined") {
return;
}
const root = document.documentElement;
if (!root) {
return;
}
root.classList.toggle("dark");
};
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">
<main className="min-h-screen flex flex-col bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-50">
<header className="border-b border-slate-200 px-6 py-4 flex items-center justify-between bg-white/80 backdrop-blur dark:border-slate-800 dark:bg-slate-950/80">
<div>
<h1 className="text-xl font-semibold"> </h1>
<p className="text-xs text-slate-400"> </p>
<h1 className="text-2xl font-bold tracking-tight"> </h1>
<p className="text-sm text-slate-500 dark:text-slate-400"> </p>
</div>
<div className="flex items-center gap-2 text-xs">
<div className="flex items-center gap-3 text-sm">
<nav className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white/80 px-1.5 py-1 shadow-sm dark:border-slate-700 dark:bg-slate-900/80">
<Link
href="/dashboard"
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"
>
<LayoutDashboard className="w-4 h-4" aria-hidden="true" />
<span></span>
</Link>
<Link
href="/projects"
aria-current="page"
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-semibold bg-sky-600 text-white shadow-sm hover:bg-sky-700"
>
<FolderKanban className="w-4 h-4" aria-hidden="true" />
<span> </span>
</Link>
<Link
href="/projects/submissions"
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium text-emerald-700 hover:bg-emerald-50 hover:text-emerald-800 dark:text-emerald-200 dark:hover:bg-emerald-900/60 dark:hover:text-emerald-100"
>
<ListChecks className="w-4 h-4" aria-hidden="true" />
<span> </span>
</Link>
</nav>
<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();
}}
className="inline-flex items-center gap-1 rounded-full border border-slate-300 bg-white/80 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
onClick={handleToggleTheme}
>
<SunMoon className="w-4 h-4" aria-hidden="true" />
<span> </span>
</button>
<Link
href="/projects/submissions"
className="inline-flex items-center gap-1 rounded border border-emerald-700 bg-emerald-950 px-3 py-1 text-emerald-100 hover:bg-emerald-900 shadow-sm"
>
<ListChecks className="w-4 h-4" aria-hidden="true" />
<span> </span>
</Link>
<Link
href="/editor?new=1"
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"
>
<FilePlus2 className="w-4 h-4" aria-hidden="true" />
<span> </span>
</Link>
<div className="relative">
<button
type="button"
className="inline-flex items-center gap-1 rounded-full border border-slate-300 bg-white/80 px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
onClick={() => {
setIsMenuOpen((prev) => !prev);
}}
>
</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);
void handleLogout();
}}
>
</button>
</div>
)}
</div>
</div>
</header>
<section className="flex-1 px-6 py-4 overflow-auto">
<section className="flex-1 px-6 py-4 overflow-auto bg-slate-50/60 dark:bg-transparent">
{status === "error" && (
<p className="text-xs text-red-300 mb-3">
<p className="text-xs text-red-500 mb-3 dark:text-red-300">
{errorMessage ?? "프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."}
</p>
)}
{projects.length === 0 && status === "idle" && (
<p className="text-xs text-slate-400"> . .</p>
<p className="text-xs text-slate-500 dark:text-slate-400"> . .</p>
)}
{projects.length > 0 && (
<>
<div className="flex items-center justify-between mb-2 text-[11px] text-slate-300">
<div
data-testid="projects-toolbar"
className="flex items-center justify-between mb-2 text-[11px] text-slate-300"
>
<div className="inline-flex items-center gap-2">
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-slate-900/70 border border-slate-700">
<ListChecks className="w-3 h-3 text-slate-400" aria-hidden="true" />
@@ -248,17 +302,26 @@ export default function ProjectsPage() {
</span>
</span>
</div>
<button
type="button"
className="inline-flex items-center gap-1 rounded border border-red-700 px-2 py-1 text-red-200 hover:bg-red-900/40 disabled:opacity-40 disabled:cursor-not-allowed"
disabled={selectedSlugs.length === 0}
onClick={() => {
void handleBulkDelete();
}}
>
<Trash2 className="w-3 h-3" aria-hidden="true" />
<span> </span>
</button>
<div className="flex items-center gap-2">
<button
type="button"
className="inline-flex items-center gap-1 rounded border border-red-700 px-2 py-1 text-red-200 hover:bg-red-900/40 disabled:opacity-40 disabled:cursor-not-allowed"
disabled={selectedSlugs.length === 0}
onClick={() => {
void handleBulkDelete();
}}
>
<Trash2 className="w-3 h-3" aria-hidden="true" />
<span> </span>
</button>
<Link
href="/editor?new=1"
className="inline-flex items-center gap-1 rounded border border-sky-700 bg-sky-950 px-2 py-1 text-sky-100 hover:bg-sky-900 shadow-sm"
>
<FilePlus2 className="w-3 h-3" aria-hidden="true" />
<span> </span>
</Link>
</div>
</div>
<table className="w-full text-xs text-left border-collapse mt-2">
<thead>