Refine ranking placement, visibility, and consistency

- Move cumulative ranking above the gold ₩1M card (ranking → challenge flow)
- Drop redundant "전체 보기" link (inline folding shows full Top 10);
  /leaderboard route kept for direct access
- Make fold toggle purple + match its size to the tab buttons; enlarge
  title/tab/row text for readability
- Match email input & submit button radius to the ranking box (rounded-xl)
- Leaderboard full page: top button = "투표로 돌아가기" (history back, home fallback)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main
Haewon Kam 2026-06-18 16:47:57 +09:00
parent 965bd374c3
commit 996d5faaf4
6 changed files with 58 additions and 34 deletions

View File

@ -20,12 +20,12 @@ export default async function LeaderboardPage({
return ( return (
<main className="shell"> <main className="shell">
<Hero back /> <Hero back backHistory />
<p className="mt-6 text-center text-[13px] leading-relaxed text-[var(--ink-muted)]"> <p className="mt-6 text-center text-[13px] leading-relaxed text-[var(--ink-muted)]">
{t.goldDesc} {t.goldDesc}
</p> </p>
{/* 전체 페이지: 펼친 상태(폴딩 토글·전체보기 링크 숨김) */} {/* 전체 페이지: 펼친 상태(폴딩 토글 숨김) */}
<Leaderboard lang={lang} defaultOpen showAllLink={false} /> <Leaderboard lang={lang} defaultOpen />
<Footer /> <Footer />
</main> </main>
); );

View File

@ -340,7 +340,7 @@ export default function Arena({
value={email} value={email}
onChange={(e) => onEmailChange(e.target.value)} onChange={(e) => onEmailChange(e.target.value)}
placeholder={t.emailPh} placeholder={t.emailPh}
className="w-full rounded-2xl border border-[var(--share)] bg-[#0f1217] px-4 py-3 text-[16px] text-white shadow-[0_0_0_2px_rgba(166,94,255,0.12)] outline-none focus:shadow-[0_0_0_2px_rgba(166,94,255,0.3)]" className="w-full rounded-xl border border-[var(--share)] bg-[#0f1217] px-4 py-3 text-[16px] text-white shadow-[0_0_0_2px_rgba(166,94,255,0.12)] outline-none focus:shadow-[0_0_0_2px_rgba(166,94,255,0.3)]"
/> />
<label className="mt-2.5 flex items-start gap-2 text-[13px] leading-snug text-[var(--ink-muted)]"> <label className="mt-2.5 flex items-start gap-2 text-[13px] leading-snug text-[var(--ink-muted)]">
<input <input
@ -354,7 +354,7 @@ export default function Arena({
<button <button
onClick={confirmSubmit} onClick={confirmSubmit}
disabled={!emailValid} disabled={!emailValid}
className="btn-share mt-3.5 flex w-full items-center justify-center gap-1.5 rounded-2xl py-3 text-[15px] font-extrabold transition active:scale-[0.99] disabled:opacity-[0.72]" className="btn-share mt-3.5 flex w-full items-center justify-center gap-1.5 rounded-xl py-3 text-[15px] font-extrabold transition active:scale-[0.99] disabled:opacity-[0.72]"
> >
{t.submit} <span aria-hidden></span> {t.submit} <span aria-hidden></span>
</button> </button>
@ -459,6 +459,9 @@ export default function Arena({
</section> </section>
)} )}
{/* ===== 누적 랭킹 TOP 10 (골드 카드 위 · 폴딩) — 랭킹→상금 도전 흐름 ===== */}
<Leaderboard lang={lang} />
{/* ===== 골드 상금 (003) ===== */} {/* ===== 골드 상금 (003) ===== */}
<section className="gold-card mt-5 rounded-2xl p-5"> <section className="gold-card mt-5 rounded-2xl p-5">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
@ -477,9 +480,6 @@ export default function Arena({
</div> </div>
</section> </section>
{/* ===== 누적 랭킹 TOP 10 (골드 카드 직후 · 폴딩) ===== */}
<Leaderboard lang={lang} />
{/* ===== Crowd Pick (003) — 보팅 비율(≠ AI 승리 확률) ===== */} {/* ===== Crowd Pick (003) — 보팅 비율(≠ AI 승리 확률) ===== */}
<section className="mt-6"> <section className="mt-6">
<div className="mb-2.5 text-[20px] font-extrabold"> <div className="mb-2.5 text-[20px] font-extrabold">

27
components/BackButton.tsx Normal file
View File

@ -0,0 +1,27 @@
"use client";
import { useRouter } from "next/navigation";
// 뒤로가기 — 직전 페이지(예: 투표하던 매치 페이지)로 복귀.
// 히스토리가 없으면(직접 진입) 홈으로 폴백.
export default function BackButton({
label,
home,
}: {
label: string;
home: string;
}) {
const router = useRouter();
const goBack = () => {
if (typeof window !== "undefined" && window.history.length > 1) router.back();
else router.push(home);
};
return (
<button
onClick={goBack}
className="flex items-center gap-1.5 rounded-full border border-white/12 bg-white/8 px-[18px] py-[9px] text-[18px] font-bold text-white/85 active:scale-95"
>
{label}
</button>
);
}

View File

@ -1,6 +1,7 @@
import Link from "next/link"; import Link from "next/link";
import ShareButton from "./ShareButton"; import ShareButton from "./ShareButton";
import LangSwitch from "./LangSwitch"; import LangSwitch from "./LangSwitch";
import BackButton from "./BackButton";
import { type Lang, dict } from "@/lib/i18n"; import { type Lang, dict } from "@/lib/i18n";
type Share = { url: string; title: string; text: string }; type Share = { url: string; title: string; text: string };
@ -8,10 +9,12 @@ type Share = { url: string; title: string; text: string };
// 브랜드 헤더 (대시보드·상세 공용). 경기별 후킹 카피는 상세 페이지에서 별도 노출. // 브랜드 헤더 (대시보드·상세 공용). 경기별 후킹 카피는 상세 페이지에서 별도 노출.
export default function Hero({ export default function Hero({
back = false, back = false,
backHistory = false,
share, share,
lang = "ko", lang = "ko",
}: { }: {
back?: boolean; back?: boolean;
backHistory?: boolean; // true면 홈 대신 직전 페이지로 복귀(뒤로가기)
share?: Share; share?: Share;
lang?: Lang; lang?: Lang;
}) { }) {
@ -21,12 +24,16 @@ export default function Hero({
<header className="pt-5 text-center"> <header className="pt-5 text-center">
{back && ( {back && (
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<Link {backHistory ? (
href={home} <BackButton label={t.backPrev} home={home} />
className="flex items-center gap-1.5 rounded-full border border-white/12 bg-white/8 px-[18px] py-[9px] text-[18px] font-bold text-white/85 active:scale-95" ) : (
> <Link
{t.back} href={home}
</Link> className="flex items-center gap-1.5 rounded-full border border-white/12 bg-white/8 px-[18px] py-[9px] text-[18px] font-bold text-white/85 active:scale-95"
>
{t.back}
</Link>
)}
{share && ( {share && (
<ShareButton {...share} label={t.share} copiedLabel={t.shareCopied} /> <ShareButton {...share} label={t.share} copiedLabel={t.shareCopied} />
)} )}

View File

@ -1,7 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Link from "next/link";
import { type Lang, dict } from "@/lib/i18n"; import { type Lang, dict } from "@/lib/i18n";
import { import {
fetchLeaderboard, fetchLeaderboard,
@ -14,11 +13,9 @@ import {
export default function Leaderboard({ export default function Leaderboard({
lang = "ko", lang = "ko",
defaultOpen = false, defaultOpen = false,
showAllLink = true,
}: { }: {
lang?: Lang; lang?: Lang;
defaultOpen?: boolean; defaultOpen?: boolean;
showAllLink?: boolean;
}) { }) {
const t = dict(lang); const t = dict(lang);
const [open, setOpen] = useState(defaultOpen); const [open, setOpen] = useState(defaultOpen);
@ -37,17 +34,7 @@ export default function Leaderboard({
return ( return (
<section className="mt-5 rounded-2xl border border-[var(--line-d)] bg-[var(--bg2)] p-4"> <section className="mt-5 rounded-2xl border border-[var(--line-d)] bg-[var(--bg2)] p-4">
<div className="flex items-center justify-between gap-2"> <h3 className="text-[19px] font-extrabold">{t.lbTitle}</h3>
<h3 className="text-[16px] font-extrabold">{t.lbTitle}</h3>
{showAllLink && (
<Link
href={lang === "en" ? "/leaderboard?lang=en" : "/leaderboard"}
className="shrink-0 text-[12px] font-bold text-[var(--share)]"
>
{t.lbAll}
</Link>
)}
</div>
{open && ( {open && (
<> <>
@ -62,7 +49,7 @@ export default function Leaderboard({
<button <button
key={val} key={val}
onClick={() => setTab(val)} onClick={() => setTab(val)}
className={`rounded-xl py-2.5 text-[14px] font-extrabold transition ${ className={`rounded-xl py-2.5 text-[15px] font-extrabold transition ${
tab === val tab === val
? "bg-[var(--share)] text-white" ? "bg-[var(--share)] text-white"
: "border border-[var(--line-d)] text-[var(--ink-muted)]" : "border border-[var(--line-d)] text-[var(--ink-muted)]"
@ -74,7 +61,7 @@ export default function Leaderboard({
</div> </div>
{/* 헤더 라벨 */} {/* 헤더 라벨 */}
<div className="mt-3 flex items-center justify-between px-3 text-[11px] font-bold text-[var(--ink-muted)]"> <div className="mt-3 flex items-center justify-between px-3 text-[12px] font-bold text-[var(--ink-muted)]">
<span>{tab === "ai" ? t.lbTabAI : t.lbTabVoters}</span> <span>{tab === "ai" ? t.lbTabAI : t.lbTabVoters}</span>
<span>{t.lbPoints}</span> <span>{t.lbPoints}</span>
</div> </div>
@ -93,16 +80,16 @@ export default function Leaderboard({
}`} }`}
> >
<span <span
className={`w-6 shrink-0 text-center font-mono text-[15px] font-extrabold tabular-nums ${ className={`w-6 shrink-0 text-center font-mono text-[16px] font-extrabold tabular-nums ${
top3 ? "text-[var(--share)]" : "text-[var(--ink-muted)]" top3 ? "text-[var(--share)]" : "text-[var(--ink-muted)]"
}`} }`}
> >
{i + 1} {i + 1}
</span> </span>
<span className="min-w-0 flex-1 truncate text-[15px] font-bold text-white"> <span className="min-w-0 flex-1 truncate text-[16px] font-bold text-white">
{r.label} {r.label}
</span> </span>
<span className="shrink-0 font-mono text-[15px] font-extrabold tabular-nums text-white"> <span className="shrink-0 font-mono text-[16px] font-extrabold tabular-nums text-white">
{r.points.toLocaleString()} {r.points.toLocaleString()}
<span className="ml-0.5 text-[12px] text-[var(--ink-muted)]"> <span className="ml-0.5 text-[12px] text-[var(--ink-muted)]">
{t.lbUnit} {t.lbUnit}
@ -119,7 +106,7 @@ export default function Leaderboard({
{!defaultOpen && ( {!defaultOpen && (
<button <button
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
className="mt-3 flex w-full items-center justify-center gap-1 rounded-xl border border-[var(--line-d)] py-2.5 text-[13px] font-bold text-[var(--ink-muted)] transition active:scale-[0.99]" className="mt-3 flex w-full items-center justify-center gap-1.5 rounded-xl border border-[var(--share)] bg-[rgba(166,94,255,0.12)] py-2.5 text-[15px] font-extrabold text-[var(--share)] transition active:scale-[0.99]"
> >
{open ? t.lbClose : t.lbOpen} {open ? t.lbClose : t.lbOpen}
<span aria-hidden>{open ? "▲" : "▼"}</span> <span aria-hidden>{open ? "▲" : "▼"}</span>

View File

@ -40,6 +40,7 @@ export function roundLabel(label: string, lang: Lang): string {
type Dict = { type Dict = {
heroPill: string; heroPill: string;
back: string; back: string;
backPrev: string;
share: string; share: string;
shareCopied: string; shareCopied: string;
introLead: string; introLead: string;
@ -113,6 +114,7 @@ export const DICT: Record<Lang, Dict> = {
ko: { ko: {
heroPill: "AI와 함께하는 2026 글로벌 축구 축전 승부예측 챌린지", heroPill: "AI와 함께하는 2026 글로벌 축구 축전 승부예측 챌린지",
back: "← 전체 일정", back: "← 전체 일정",
backPrev: "← 투표로 돌아가기",
share: "투표 공유하기", share: "투표 공유하기",
shareCopied: "링크 복사됨 ✓", shareCopied: "링크 복사됨 ✓",
introLead: "GPT · Claude · Gemini가 매 경기를\n서로 다르게 예측합니다.", introLead: "GPT · Claude · Gemini가 매 경기를\n서로 다르게 예측합니다.",
@ -186,6 +188,7 @@ export const DICT: Record<Lang, Dict> = {
en: { en: {
heroPill: "AI-powered Global Football Festival 2026 match predictions", heroPill: "AI-powered Global Football Festival 2026 match predictions",
back: "← All matches", back: "← All matches",
backPrev: "← Back to voting",
share: "Share vote", share: "Share vote",
shareCopied: "Link copied ✓", shareCopied: "Link copied ✓",
introLead: "GPT · Claude · Gemini predict\nevery match differently.", introLead: "GPT · Claude · Gemini predict\nevery match differently.",