로그인 페이지: 디스코드 로그인 크게, 아래 게스트로 시작하기 → /login/guest에서 계정 이름 정하기
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,18 @@ import { nicknameError, normalizeNickname, suggestNickname } from '@bg/shared/li
|
|||||||
import { api, discordLoginUrl } from '../net/api';
|
import { api, discordLoginUrl } from '../net/api';
|
||||||
import { useSession } from '../store/session';
|
import { useSession } from '../store/session';
|
||||||
|
|
||||||
export function NicknameForm({ next, submitLabel = '이 이름으로 시작하기', onDone }: { next: string; submitLabel?: string; onDone?: () => void }) {
|
export function NicknameForm({
|
||||||
|
next,
|
||||||
|
submitLabel = '이 이름으로 시작하기',
|
||||||
|
onDone,
|
||||||
|
showDiscord = true,
|
||||||
|
}: {
|
||||||
|
next: string;
|
||||||
|
submitLabel?: string;
|
||||||
|
onDone?: () => void;
|
||||||
|
/** The login page shows its own big Discord button, so it hides this one. */
|
||||||
|
showDiscord?: boolean;
|
||||||
|
}) {
|
||||||
const [nick, setNick] = useState(() => suggestNickname());
|
const [nick, setNick] = useState(() => suggestNickname());
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -49,7 +60,7 @@ export function NicknameForm({ next, submitLabel = '이 이름으로 시작하
|
|||||||
<button className="btn btn-primary btn-block" disabled={busy}>
|
<button className="btn btn-primary btn-block" disabled={busy}>
|
||||||
{busy ? '잠시만요…' : submitLabel}
|
{busy ? '잠시만요…' : submitLabel}
|
||||||
</button>
|
</button>
|
||||||
{discord && (
|
{discord && showDiscord && (
|
||||||
<a className="btn btn-secondary btn-block" href={discordLoginUrl(next)}>
|
<a className="btn btn-secondary btn-block" href={discordLoginUrl(next)}>
|
||||||
디스코드로 로그인
|
디스코드로 로그인
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ function Shell() {
|
|||||||
<Route path="/g/:gameId" element={<GamePage />} />
|
<Route path="/g/:gameId" element={<GamePage />} />
|
||||||
<Route path="/rules/:gameId" element={<RulesPage />} />
|
<Route path="/rules/:gameId" element={<RulesPage />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/login/guest" element={<LoginPage />} />
|
||||||
<Route path="/me" element={<MePage />} />
|
<Route path="/me" element={<MePage />} />
|
||||||
<Route path="/replay/:gamePk" element={<ReplayPage />} />
|
<Route path="/replay/:gamePk" element={<ReplayPage />} />
|
||||||
<Route path="/admin/*" element={<AdminApp />} />
|
<Route path="/admin/*" element={<AdminApp />} />
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
/** 로그인만 있는 페이지: 닉네임으로 시작 또는 디스코드 로그인. 설정은 로그인 뒤 헤더 계정 메뉴 → 설정(/me). */
|
/**
|
||||||
import { Navigate, useNavigate, useSearchParams } from 'react-router';
|
* 로그인 페이지: 큰 [디스코드로 로그인] + 아래 [게스트로 시작하기].
|
||||||
|
* 게스트를 고르면 /login/guest에서 계정 이름(닉네임)을 정한다. 설정은 로그인 뒤 헤더 계정 메뉴 → 설정(/me).
|
||||||
|
*/
|
||||||
|
import { Link, Navigate, useMatch, useNavigate, useSearchParams } from 'react-router';
|
||||||
import { TopBar } from '../components/ui';
|
import { TopBar } from '../components/ui';
|
||||||
import { NicknameForm } from '../components/NicknameForm';
|
import { NicknameForm } from '../components/NicknameForm';
|
||||||
|
import { discordLoginUrl } from '../net/api';
|
||||||
import { useSession } from '../store/session';
|
import { useSession } from '../store/session';
|
||||||
|
|
||||||
/** Only same-site paths, so ?next= can't send people to another site. */
|
/** Only same-site paths, so ?next= can't send people to another site. */
|
||||||
@@ -10,15 +14,51 @@ const safeNext = (v: string | null) => (v && v.startsWith('/') && !v.startsWith(
|
|||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const nav = useNavigate();
|
const nav = useNavigate();
|
||||||
const me = useSession((s) => s.me);
|
const me = useSession((s) => s.me);
|
||||||
const next = safeNext(useSearchParams()[0].get('next'));
|
const discord = useSession((s) => s.discordEnabled);
|
||||||
|
const guest = useMatch('/login/guest') !== null;
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
const next = safeNext(params.get('next'));
|
||||||
|
const query = params.toString() ? `?${params.toString()}` : '';
|
||||||
if (me) return <Navigate to={next} replace />;
|
if (me) return <Navigate to={next} replace />;
|
||||||
|
|
||||||
|
if (guest) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="로그인" onBack={() => nav('/')} />
|
<TopBar title="게스트로 시작하기" onBack={() => nav(`/login${query}`)} />
|
||||||
<main className="page" style={{ maxWidth: 480 }}>
|
<main className="page" style={{ maxWidth: 480 }}>
|
||||||
<section className="card stack" style={{ padding: 24 }}>
|
<section className="card stack" style={{ padding: 24 }}>
|
||||||
<h1>로그인</h1>
|
<h1>계정 이름 정하기</h1>
|
||||||
<NicknameForm next={next} onDone={() => nav(next, { replace: true })} />
|
<p className="muted">게임에서 다른 사람에게 보일 이름이에요.</p>
|
||||||
|
<NicknameForm next={next} showDiscord={false} onDone={() => nav(next, { replace: true })} />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TopBar title="로그인" onBack={() => nav('/')} />
|
||||||
|
<main className="page" style={{ maxWidth: 480 }}>
|
||||||
|
<section className="card stack" style={{ padding: 24, gap: 16 }}>
|
||||||
|
<h1>로그인</h1>
|
||||||
|
{discord && (
|
||||||
|
<>
|
||||||
|
<a className="btn btn-block btn-discord" href={discordLoginUrl(next)}>
|
||||||
|
디스코드로 로그인
|
||||||
|
</a>
|
||||||
|
<p className="muted small" style={{ marginTop: -6 }}>
|
||||||
|
기록이 계정에 저장돼서 다른 기기에서도 이어서 할 수 있어요.
|
||||||
|
</p>
|
||||||
|
<div className="login-divider" aria-hidden>
|
||||||
|
또는
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Link className={`btn btn-block ${discord ? 'btn-secondary' : 'btn-primary'}`} to={`/login/guest${query}`}>
|
||||||
|
게스트로 시작하기
|
||||||
|
</Link>
|
||||||
|
<p className="muted small">가입 없이 이름만 정하면 돼요. 기록은 이 기기에만 남아요.</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1185,3 +1185,28 @@ button.header-user {
|
|||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
border-bottom-color: var(--primary);
|
border-bottom-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Login page (docs/07 §3.2): big Discord button, guest below */
|
||||||
|
.btn-discord {
|
||||||
|
min-height: 68px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
background: #5865f2;
|
||||||
|
border-color: #5865f2;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-discord:hover {
|
||||||
|
background: #4752c4;
|
||||||
|
}
|
||||||
|
.login-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--text-sub);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.login-divider::before,
|
||||||
|
.login-divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
└──────────────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
- 어느 화면에서든 코드를 바로 넣어 참여할 수 있게 상단에 코드 입력칸을 둔다.
|
- 어느 화면에서든 코드를 바로 넣어 참여할 수 있게 상단에 코드 입력칸을 둔다.
|
||||||
- 오른쪽 끝: 로그인 전에는 [로그인](→ `/login`, 닉네임으로 시작·디스코드 로그인만 있음). 로그인 뒤에는 닉네임(디스코드로 가입했으면 디스코드 이름)을 보이고, 누르면 [⚙️ 설정](→ `/me`)·[🚪 로그아웃] 메뉴가 열린다. 화면·소리 설정은 `/me`에만 있다.
|
- 오른쪽 끝: 로그인 전에는 [로그인](→ `/login`: 큰 [디스코드로 로그인], 아래 [게스트로 시작하기] → `/login/guest`에서 계정 이름 정하기). 로그인 뒤에는 닉네임(디스코드로 가입했으면 디스코드 이름)을 보이고, 누르면 [⚙️ 설정](→ `/me`)·[🚪 로그아웃] 메뉴가 열린다. 화면·소리 설정은 `/me`에만 있다.
|
||||||
- 내용 폭은 최대 1280px, 가운데 정렬. 게임 화면만 화면 전체 폭을 쓴다.
|
- 내용 폭은 최대 1280px, 가운데 정렬. 게임 화면만 화면 전체 폭을 쓴다.
|
||||||
|
|
||||||
### 3.3 홈 (PC)
|
### 3.3 홈 (PC)
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ try {
|
|||||||
const c = await (await browser.newContext({ locale: 'ko-KR' })).newPage();
|
const c = await (await browser.newContext({ locale: 'ko-KR' })).newPage();
|
||||||
await c.goto(`${ORIGIN}/`);
|
await c.goto(`${ORIGIN}/`);
|
||||||
await c.goto(`${ORIGIN}/me`);
|
await c.goto(`${ORIGIN}/me`);
|
||||||
|
await c.getByRole('link', { name: '게스트로 시작하기' }).click();
|
||||||
await c.getByLabel('내 이름(닉네임)').fill('구경꾼');
|
await c.getByLabel('내 이름(닉네임)').fill('구경꾼');
|
||||||
await c.getByRole('button', { name: /시작하기/ }).click();
|
await c.getByRole('button', { name: /시작하기/ }).click();
|
||||||
await c.getByText('아직 다시 볼 게임이 없어요.').waitFor();
|
await c.getByText('아직 다시 볼 게임이 없어요.').waitFor();
|
||||||
|
|||||||
Reference in New Issue
Block a user