Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 76 additions & 65 deletions app/admin/members/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { requireAdmin } from "@/lib/guard";
import { db } from "@/lib/db";
import {
PageHeader,
PageShell,
SectionLabel,
SubNav,
UtilityCard,
} from "@/components/layout/AppChrome";

import { PendingMemberActions } from "./_components/PendingMemberActions";

Expand Down Expand Up @@ -28,74 +35,78 @@ export default async function AdminMembersPage() {
});

return (
<main className="container mx-auto max-w-2xl p-6">
<header className="mb-6 flex flex-col gap-1">
<h1 className="text-2xl font-bold tracking-tight">회원 관리</h1>
<p className="text-xs text-muted-foreground">
관리자: {admin.email}
</p>
</header>
<>
<SubNav title="회원 관리" />
<PageShell width="content" surface="parchment">
<PageHeader
eyebrow="Admin"
title="회원 관리"
description={`관리자: ${admin.email}`}
/>

<section className="mb-10">
<h2 className="mb-3 text-sm font-semibold uppercase text-muted-foreground">
승인 대기 ({pending.length})
</h2>
{pending.length === 0 ? (
<p className="text-sm text-muted-foreground">대기 중인 가입 요청이 없습니다.</p>
) : (
<ul className="flex flex-col gap-2">
{pending.map((u) => (
<li
key={u.id}
className="flex flex-col gap-2 rounded-md border p-3 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex flex-col">
<span className="font-medium">{u.name}</span>
<span className="font-mono text-xs text-muted-foreground">
{u.email}
</span>
<span className="text-xs text-muted-foreground">
가입 요청 {formatDate(u.joinedAt)}
</span>
</div>
<PendingMemberActions userId={u.id} />
</li>
))}
</ul>
)}
</section>
<div className="grid gap-8 lg:grid-cols-[1fr_1fr]">
<UtilityCard>
<SectionLabel>승인 대기 ({pending.length})</SectionLabel>
{pending.length === 0 ? (
<p className="font-text text-sm leading-[1.43] tracking-[-0.224px] text-apple-muted-48">
대기 중인 가입 요청이 없습니다.
</p>
) : (
<ul className="flex flex-col gap-3">
{pending.map((u) => (
<li
key={u.id}
className="flex flex-col gap-4 rounded-[18px] border border-apple-hairline bg-apple-parchment p-4"
>
<div className="flex flex-col gap-1">
<span className="font-text text-[17px] font-semibold leading-[1.24] tracking-[-0.374px]">
{u.name}
</span>
<span className="font-mono text-xs text-apple-muted-48">
{u.email}
</span>
<span className="font-text text-xs leading-none tracking-[-0.12px] text-apple-muted-48">
가입 요청 {formatDate(u.joinedAt)}
</span>
</div>
<PendingMemberActions userId={u.id} />
</li>
))}
</ul>
)}
</UtilityCard>

<section>
<h2 className="mb-3 text-sm font-semibold uppercase text-muted-foreground">
활동 회원 ({active.length})
</h2>
<ul className="flex flex-col gap-2">
{active.map((u) => (
<li
key={u.id}
className="flex flex-col gap-1 rounded-md border p-3 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex flex-col">
<span className="font-medium">
{u.name}
{u.role === "ADMIN" && (
<span className="ml-2 rounded bg-secondary px-1.5 py-0.5 text-xs">
ADMIN
<UtilityCard>
<SectionLabel>활동 회원 ({active.length})</SectionLabel>
<ul className="flex flex-col gap-3">
{active.map((u) => (
<li
key={u.id}
className="flex flex-col gap-3 rounded-[18px] border border-apple-hairline bg-apple-parchment p-4 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex flex-col gap-1">
<span className="font-text text-[17px] font-semibold leading-[1.24] tracking-[-0.374px]">
{u.name}
{u.role === "ADMIN" && (
<span className="ml-2 rounded-full bg-apple-canvas px-2 py-1 font-text text-xs font-normal leading-none tracking-[-0.12px] text-apple-primary">
ADMIN
</span>
)}
</span>
)}
</span>
<span className="font-mono text-xs text-muted-foreground">
{u.email}
</span>
</div>
<span className="text-xs text-muted-foreground">
승인 {u.approvedAt ? formatDate(u.approvedAt) : "—"}
</span>
</li>
))}
</ul>
</section>
</main>
<span className="font-mono text-xs text-apple-muted-48">
{u.email}
</span>
</div>
<span className="font-text text-xs leading-none tracking-[-0.12px] text-apple-muted-48">
승인 {u.approvedAt ? formatDate(u.approvedAt) : "—"}
</span>
</li>
))}
</ul>
</UtilityCard>
</div>
</PageShell>
</>
);
}

Expand Down
38 changes: 21 additions & 17 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--foreground: 240 3% 12%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--card-foreground: 240 3% 12%;
--primary: 210 100% 40%;
--primary-foreground: 0 0% 100%;
--secondary: 240 11% 96%;
--secondary-foreground: 240 3% 12%;
--muted: 240 11% 96%;
--muted-foreground: 0 0% 48%;
--accent: 210 100% 40%;
--accent-foreground: 0 0% 100%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 88%;
--input: 0 0% 88%;
--ring: 209 100% 45%;
--radius: 18px;
}
}

Expand All @@ -30,7 +30,11 @@
@apply border-border;
}
body {
@apply bg-background text-foreground;
@apply bg-apple-canvas font-text text-[17px] font-normal leading-[1.47] tracking-[-0.374px] text-apple-ink antialiased;
font-feature-settings: "rlig" 1, "calt" 1;
}

::selection {
@apply bg-apple-primary text-white;
}
}
16 changes: 13 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Metadata, Viewport } from "next";
import "./globals.css";
import { GlobalNav } from "@/components/layout/AppChrome";
import { auth } from "@/lib/auth";

// NEXT_PUBLIC_APP_URL 이 비어 있으면 Next 기본(요청 host)을 쓴다.
// local/prod 의 .env 에 외부 URL 을 넣어두면 OG 이미지/링크가 그 URL 기준이 됨.
Expand Down Expand Up @@ -30,17 +32,25 @@ export const metadata: Metadata = {
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: "#ffffff",
themeColor: "#0066cc",
};

export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();

return (
<html lang="ko">
<body className="min-h-screen antialiased">{children}</body>
<body>
<GlobalNav
approved={Boolean(session?.user.approved)}
isAdmin={session?.user.role === "ADMIN"}
/>
{children}
</body>
</html>
);
}
58 changes: 34 additions & 24 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { redirect } from "next/navigation";

import { auth, signIn } from "@/lib/auth";
import { Button } from "@/components/ui/button";
import {
PageHeader,
PageShell,
UtilityCard,
} from "@/components/layout/AppChrome";

type LoginPageProps = {
searchParams: Promise<{ callbackUrl?: string; error?: string }>;
Expand Down Expand Up @@ -39,34 +44,39 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
}

return (
<main className="container mx-auto flex min-h-screen max-w-md flex-col items-center justify-center gap-8 p-6">
<div className="flex flex-col items-center gap-3 text-center">
<h1 className="text-3xl font-bold tracking-tight">뮤런</h1>
<p className="text-sm text-muted-foreground">
애니뮤 러닝 소모임 내부 아카이브.
<br />
SNU 구글 계정(<span className="font-mono">@snu.ac.kr</span>)으로 로그인하세요.
</p>
</div>
<PageShell width="narrow" surface="parchment">
<PageHeader
align="center"
eyebrow="SNU Google only"
title="뮤런"
description={
<>
애니뮤 러닝 소모임 내부 아카이브.
<br />SNU 구글 계정으로 로그인하세요.
</>
}
/>

{errorMessage ? (
<div
role="alert"
className="w-full rounded-md border border-destructive/40 bg-destructive/10 px-4 py-3 text-sm text-destructive"
>
{errorMessage}
</div>
) : null}
<UtilityCard className="flex flex-col gap-5">
{errorMessage ? (
<div
role="alert"
className="rounded-[18px] border border-destructive/40 bg-white px-5 py-4 font-text text-sm leading-[1.43] tracking-[-0.224px] text-destructive"
>
{errorMessage}
</div>
) : null}

<form action={signInWithGoogle} className="w-full">
<Button type="submit" className="w-full" size="lg">
SNU 구글로 계속하기
</Button>
</form>
<form action={signInWithGoogle}>
<Button type="submit" className="w-full" size="lg">
SNU 구글로 계속하기
</Button>
</form>
</UtilityCard>

<p className="text-center text-xs text-muted-foreground">
<p className="mt-6 text-center font-text text-xs leading-none tracking-[-0.12px] text-apple-muted-48">
첫 로그인 후 관리자 승인이 필요합니다.
</p>
</main>
</PageShell>
);
}
Loading
Loading