diff --git a/app/admin/members/page.tsx b/app/admin/members/page.tsx index 028d3f3..98751f9 100644 --- a/app/admin/members/page.tsx +++ b/app/admin/members/page.tsx @@ -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"; @@ -28,74 +35,78 @@ export default async function AdminMembersPage() { }); return ( -
-
-

회원 관리

-

- 관리자: {admin.email} -

-
+ <> + + + -
-

- 승인 대기 ({pending.length}) -

- {pending.length === 0 ? ( -

대기 중인 가입 요청이 없습니다.

- ) : ( -
    - {pending.map((u) => ( -
  • -
    - {u.name} - - {u.email} - - - 가입 요청 {formatDate(u.joinedAt)} - -
    - -
  • - ))} -
- )} -
+
+ + 승인 대기 ({pending.length}) + {pending.length === 0 ? ( +

+ 대기 중인 가입 요청이 없습니다. +

+ ) : ( +
    + {pending.map((u) => ( +
  • +
    + + {u.name} + + + {u.email} + + + 가입 요청 {formatDate(u.joinedAt)} + +
    + +
  • + ))} +
+ )} +
-
-

- 활동 회원 ({active.length}) -

-
    - {active.map((u) => ( -
  • -
    - - {u.name} - {u.role === "ADMIN" && ( - - ADMIN + + 활동 회원 ({active.length}) +
      + {active.map((u) => ( +
    • +
      + + {u.name} + {u.role === "ADMIN" && ( + + ADMIN + + )} - )} - - - {u.email} - -
      - - 승인 {u.approvedAt ? formatDate(u.approvedAt) : "—"} - -
    • - ))} -
    -
-
+ + {u.email} + + + + 승인 {u.approvedAt ? formatDate(u.approvedAt) : "—"} + + + ))} + + + + + ); } diff --git a/app/globals.css b/app/globals.css index 6c329d1..1e42228 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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; } } @@ -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; + } } diff --git a/app/layout.tsx b/app/layout.tsx index ed34b41..ab88c02 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 기준이 됨. @@ -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 ( - {children} + + + {children} + ); } diff --git a/app/login/page.tsx b/app/login/page.tsx index 3fbf7e3..ed86e86 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -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 }>; @@ -39,34 +44,39 @@ export default async function LoginPage({ searchParams }: LoginPageProps) { } return ( -
-
-

뮤런

-

- 애니뮤 러닝 소모임 내부 아카이브. -
- SNU 구글 계정(@snu.ac.kr)으로 로그인하세요. -

-
+ + + 애니뮤 러닝 소모임 내부 아카이브. +
SNU 구글 계정으로 로그인하세요. + + } + /> - {errorMessage ? ( -
- {errorMessage} -
- ) : null} + + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} -
- -
+
+ +
+
-

+

첫 로그인 후 관리자 승인이 필요합니다.

-
+ ); } diff --git a/app/page.tsx b/app/page.tsx index 57d1c4e..c69c5b5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,6 +3,11 @@ import { redirect } from "next/navigation"; import { auth, signOut } from "@/lib/auth"; import { Button } from "@/components/ui/button"; +import { + PageHeader, + PageShell, + UtilityCard, +} from "@/components/layout/AppChrome"; export const dynamic = "force-dynamic"; @@ -17,38 +22,29 @@ export default async function HomePage() { } return ( -
-
-

뮤런

-

- 안녕하세요, {session.user.name}. -

-
+ + 안녕하세요, {session.user.name}. 오늘의 러닝 기록을 남겨요.} + /> -
- - - - - - - - - -
+ + + + + -
+
{session.user.role === "ADMIN" && ( - + 회원 관리 )} @@ -58,6 +54,6 @@ export default async function HomePage() {
-
+ ); } diff --git a/app/pending/page.tsx b/app/pending/page.tsx index e72ce08..6ee9d83 100644 --- a/app/pending/page.tsx +++ b/app/pending/page.tsx @@ -3,6 +3,11 @@ import { redirect } from "next/navigation"; import { requireUser } from "@/lib/guard"; import { signOut } from "@/lib/auth"; import { Button } from "@/components/ui/button"; +import { + PageHeader, + PageShell, + UtilityCard, +} from "@/components/layout/AppChrome"; export default async function PendingPage() { const user = await requireUser(); @@ -14,24 +19,29 @@ export default async function PendingPage() { } return ( -
-
-

승인 대기 중

-

- {user.email} 으로 가입 요청을 보냈습니다. -
- 애니뮤 운영진의 승인 후 이용할 수 있어요. -

-

+ + + {user.email} 으로 가입 요청을 보냈습니다. +
운영진 승인 후 이용할 수 있어요. + + } + /> + + +

단톡방에 운영진에게 가입 요청을 보냈다고 알려주세요.

-
- -
- -
-
+
+ +
+ + ); } diff --git a/app/runners/_components/MemberView.tsx b/app/runners/_components/MemberView.tsx index cdf6f26..aef4e06 100644 --- a/app/runners/_components/MemberView.tsx +++ b/app/runners/_components/MemberView.tsx @@ -6,6 +6,14 @@ import { formatPace, } from "@/lib/pace"; import type { MemberStats } from "@/lib/members"; +import { + BackLink, + PageHeader, + PageShell, + SectionLabel, + SubNav, + UtilityCard, +} from "@/components/layout/AppChrome"; import { PaceChart } from "./PaceChart"; @@ -17,113 +25,95 @@ type Props = { export function MemberView({ stats, isSelf }: Props) { return ( -
- - -
-

- {stats.name} - {stats.role === "ADMIN" && ( - - ADMIN - - )} -

-

- {isSelf ? "내 기록" : "러너 기록"} · 가입{" "} - {new Intl.DateTimeFormat("ko-KR", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(stats.joinedAt)} -

-
- -
- 0 - ? `${stats.totalDistanceKm.toFixed(1)} km` - : "—" + <> + + 아카이브 + + + + {isSelf ? "My running" : "Runner"} · 가입 {formatDate(stats.joinedAt)} + } + title={ + <> + {stats.name} + {stats.role === "ADMIN" && ( + + ADMIN + + )} + + } + description="거리, 페이스, 최근 참여 기록을 한 화면에서 확인합니다." /> - - -
-
-
-

- 최근 페이스 추이 -

- {stats.paceHistory.length > 0 && ( - - 최근 {stats.paceHistory.length} 개 - - )} -
- -
+
+ 0 + ? `${stats.totalDistanceKm.toFixed(1)} km` + : "—" + } + /> + + +
+ + +
+ 최근 페이스 추이 + {stats.paceHistory.length > 0 && ( + + 최근 {stats.paceHistory.length} 개 + + )} +
+ +
-
-

- 최근 참여 ({stats.recent.length}) -

- {stats.recent.length === 0 ? ( -

- 아직 참여한 세션이 없어요. -

- ) : ( -
    - {stats.recent.map((r) => ( -
  • - + 최근 참여 ({stats.recent.length}) + {stats.recent.length === 0 ? ( +

    + 아직 참여한 세션이 없어요. +

    + ) : ( +
      + {stats.recent.map((r) => ( +
    • - - {new Intl.DateTimeFormat("ko-KR", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(r.date)} - - - {r.location} - - -
      - {formatDistanceKm(r.distanceKm)} - {formatDurationSec(r.durationSec)} - - {formatPace(r.paceSecPerKm)} - -
      -
    • - ))} -
    - )} -
-
+ + + {formatDate(r.date)} + + + {r.location} + + +
+ {formatDistanceKm(r.distanceKm)} + {formatDurationSec(r.durationSec)} + {formatPace(r.paceSecPerKm)} +
+ + ))} + + )} + + + ); } @@ -137,11 +127,23 @@ function Kpi({ mono?: boolean; }) { return ( -
- {label} - + + + {label} + + {value} -
+ ); } + +function formatDate(d: Date): string { + return new Intl.DateTimeFormat("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }).format(d); +} diff --git a/app/runners/_components/PaceChart.tsx b/app/runners/_components/PaceChart.tsx index 584ba67..1c95c27 100644 --- a/app/runners/_components/PaceChart.tsx +++ b/app/runners/_components/PaceChart.tsx @@ -21,7 +21,7 @@ const PAD_LEFT = 64; export function PaceChart({ points }: Props) { if (points.length === 0) { return ( -
+
페이스 데이터가 아직 없어요.
); @@ -61,7 +61,7 @@ export function PaceChart({ points }: Props) { viewBox={`0 0 ${VB_WIDTH} ${VB_HEIGHT}`} role="img" aria-label="최근 페이스 추이" - className="w-full text-foreground" + className="w-full text-apple-primary" > {/* grid */} diff --git a/app/sessions/[id]/_components/MyParticipationForm.tsx b/app/sessions/[id]/_components/MyParticipationForm.tsx index dd5a1c4..5061838 100644 --- a/app/sessions/[id]/_components/MyParticipationForm.tsx +++ b/app/sessions/[id]/_components/MyParticipationForm.tsx @@ -4,6 +4,7 @@ import { useActionState } from "react"; import { ErrorAlert } from "@/components/form/ErrorAlert"; import { SubmitButton } from "@/components/form/SubmitButton"; +import { UtilityCard } from "@/components/layout/AppChrome"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; @@ -39,15 +40,19 @@ export function MyParticipationForm({ sessionId, existing }: Props) { upsertState && !upsertState.ok ? upsertState.error : null; return ( -
-
-

내 기록

+ +
+

+ 내 기록 +

{has && ( - 현재 입력됨 + + 현재 입력됨 + )}
-
+ @@ -69,7 +74,7 @@ export function MyParticipationForm({ sessionId, existing }: Props) {
-
+
- + - +
@@ -127,6 +130,6 @@ export function MyParticipationForm({ sessionId, existing }: Props) { /> )} -
+ ); } diff --git a/app/sessions/[id]/_components/PhotoReplaceButton.tsx b/app/sessions/[id]/_components/PhotoReplaceButton.tsx index 081bc71..eac9e17 100644 --- a/app/sessions/[id]/_components/PhotoReplaceButton.tsx +++ b/app/sessions/[id]/_components/PhotoReplaceButton.tsx @@ -37,7 +37,7 @@ export function PhotoReplaceButton({ return (
-