diff --git a/app/layout.tsx b/app/layout.tsx index ab88c02..2796071 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata, Viewport } from "next"; import "./globals.css"; -import { GlobalNav } from "@/components/layout/AppChrome"; +import { GlobalNav, SiteFooter } from "@/components/layout/AppChrome"; import { auth } from "@/lib/auth"; // NEXT_PUBLIC_APP_URL 이 비어 있으면 Next 기본(요청 host)을 쓴다. @@ -42,14 +42,15 @@ export default async function RootLayout({ }) { const session = await auth(); + const approved = Boolean(session?.user.approved); + const isAdmin = session?.user.role === "ADMIN"; + return ( - + {children} + ); diff --git a/app/login/page.tsx b/app/login/page.tsx index 23455ad..447a10b 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -46,6 +46,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) { return ( <> 안녕하세요, {session.user.name}. 오늘의 러닝 기록을 남겨요.} + description={<>안녕하세요, {session.user.name}. 달린 만큼 남습니다.} actions={ <> - } > -
- - - -
+
+
+ + + +
-
- - Create -

- 오늘의 세션을 만들고 사진과 기록을 이어 붙입니다. -

- -
- - Archive -

- 날짜, 장소, 멤버로 지난 러닝의 맥락을 다시 찾습니다. -

- -
- - Me -

- 누적 거리와 페이스 흐름을 한 화면에서 확인합니다. -

- -
-
- -
- {session.user.role === "ADMIN" && ( - - 회원 관리 - - )} -
- -
-
+
+ {session.user.role === "ADMIN" && ( + + 회원 관리 + + )} +
+ +
+
); } -function HeroStat({ label, value }: { label: string; value: string }) { +function HomeCard({ + eyebrow, + title, + body, + href, + cta, +}: { + eyebrow: string; + title: string; + body: string; + href: string; + cta: string; +}) { return ( -
- - {label} + + + {eyebrow} - - {value} + + {title} -
+

+ {body} +

+ + {cta} › + + ); } + +function formatKm(km: number): string { + return km >= 100 ? String(Math.round(km)) : km.toFixed(1); +} diff --git a/app/pending/page.tsx b/app/pending/page.tsx index 067c1c1..bc693f0 100644 --- a/app/pending/page.tsx +++ b/app/pending/page.tsx @@ -21,6 +21,8 @@ export default async function PendingPage() { return ( <> 아카이브 {isSelf ? "My running" : "Runner"} · 가입 {formatDate(stats.joinedAt)} diff --git a/app/sessions/[id]/page.tsx b/app/sessions/[id]/page.tsx index a9d2c8e..cc8ad86 100644 --- a/app/sessions/[id]/page.tsx +++ b/app/sessions/[id]/page.tsx @@ -101,6 +101,7 @@ export default async function SessionDetailPage({ params }: PageProps) { -
+
{photoSrc ? ( )} + + 참여 {props.participantCount}명 +
-
-
- - {formatDate(props.date)} - - - 참여 {props.participantCount}명 - -
-

+

+ + {formatDate(props.date)} + +

{props.location}

diff --git a/app/sessions/page.tsx b/app/sessions/page.tsx index 6376318..bdc9275 100644 --- a/app/sessions/page.tsx +++ b/app/sessions/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; +import { db } from "@/lib/db"; import { requireApproved } from "@/lib/guard"; import { listApprovedMembers, listSessions } from "@/lib/sessions"; import { @@ -33,9 +34,10 @@ export default async function SessionsArchivePage({ searchParams }: PageProps) { const sp = await searchParams; const { cursorId, filters, hasActiveFilters } = parseSessionArchiveParams(sp); - const [page, members] = await Promise.all([ + const [page, members, totalCount] = await Promise.all([ listSessions({ cursorId, filters }), listApprovedMembers(), + db.session.count(), ]); const nextHref = @@ -52,20 +54,18 @@ export default async function SessionsArchivePage({ searchParams }: PageProps) { - 새 세션 만들기 - + description={ + <> + 지금까지 {totalCount}번 달렸습니다. 날짜, 장소, 참여 멤버로 세션을 + 찾아보세요. + } - > - - + /> - ); } - -function ArchiveSummary() { - return ( -

- - - -
- ); -} - -function SummaryCell({ label, value }: { label: string; value: string }) { - return ( -
- - {label} - - - {value} - -
- ); -} diff --git a/components/layout/AppChrome.tsx b/components/layout/AppChrome.tsx index 65e8b67..0451852 100644 --- a/components/layout/AppChrome.tsx +++ b/components/layout/AppChrome.tsx @@ -18,19 +18,19 @@ export function GlobalNav({ {approved ? (
- + 아카이브 - + 새 세션 - + 내 기록 {isAdmin ? ( 회원 관리 @@ -39,7 +39,7 @@ export function GlobalNav({ ) : null} {approved ? "새 세션" : "로그인"} @@ -145,7 +145,7 @@ export function PageHeader({ >
{eyebrow ? ( -

+

{eyebrow}

) : null} @@ -170,6 +170,8 @@ export function HeroBand({ actions, children, tone = "dark", + align = "left", + size = "tall", }: { eyebrow?: ReactNode; title: ReactNode; @@ -177,25 +179,41 @@ export function HeroBand({ actions?: ReactNode; children?: ReactNode; tone?: "dark" | "light"; + align?: "left" | "center"; + size?: "tall" | "compact"; }) { const isDark = tone === "dark"; + const isCenter = align === "center"; + const isTall = size === "tall"; return (
-
-
+
+
{eyebrow ? (

{eyebrow} @@ -203,7 +221,10 @@ export function HeroBand({ ) : null}

@@ -212,7 +233,7 @@ export function HeroBand({ {description ? (

@@ -220,13 +241,50 @@ export function HeroBand({

) : null}

- {actions ?
{actions}
: null} - {children ?
{children}
: null} + {actions ? ( +
+ {actions} +
+ ) : null} + {children ?
{children}
: null}
); } +export function StatStrip({ + items, + className, +}: { + items: { label: string; value: string }[]; + className?: string; +}) { + return ( +
+ {items.map((item) => ( +
+
+ {item.label} +
+
+ {item.value} +
+
+ ))} +
+ ); +} + export function UtilityCard({ children, className, @@ -253,3 +311,50 @@ export function SectionLabel({ children }: { children: ReactNode }) { ); } + +export function SiteFooter({ + approved = false, + isAdmin = false, +}: { + approved?: boolean; + isAdmin?: boolean; +}) { + return ( + + ); +}