/** 설정(계정) 페이지 (docs/05 §7). 헤더 계정 메뉴 → 설정으로 들어와요. */ import { useEffect, useState } from 'react'; import { Link, Navigate, useNavigate } from 'react-router'; import { catalogById, nicknameError, normalizeNickname, type MyGameSummary } from '@bg/shared/lite'; import { Avatar, ConfirmSheet, TopBar } from '../components/ui'; import { gameName } from '../components/GameNotice'; import { api, discordLoginUrl } from '../net/api'; import { useSession } from '../store/session'; import { toast } from '../store/toast'; function Seg({ value, options, onChange, label }: { value: T; options: [T, string][]; onChange: (v: T) => void; label: string }) { return (
{label}
{options.map(([k, l]) => ( ))}
); } export default function MePage() { const nav = useNavigate(); const { me, settings, discordEnabled, useAvatar, hasDiscordAvatar, updateSettings, role } = useSession(); const [editing, setEditing] = useState(false); const [nick, setNick] = useState(me?.nickname ?? ''); const [error, setError] = useState(null); const [stats, setStats] = useState<{ gameId: string; played: number; wins: number; draws: number }[]>([]); const [confirm, setConfirm] = useState(null); useEffect(() => { if (me) api.stats().then((r) => setStats(r.stats)).catch(() => {}); }, [me]); const saveNick = async () => { const n = normalizeNickname(nick); const e = nicknameError(n); if (e) return setError(e); try { const r = await api.updateMe({ nickname: n }); useSession.getState().setMe(r.me); setEditing(false); toast('닉네임을 바꿨어요.'); } catch (ex) { setError((ex as Error).message); } }; const display = (

화면 설정

updateSettings({ fontScale })} /> updateSettings({ theme })} /> updateSettings({ sound: v === 'on' })} /> updateSettings({ vibrate: v === 'on' })} /> updateSettings({ tapConfirm: v === 'on' })} />
); // 로그인 안 했으면 로그인 페이지로 (로그인 페이지에는 로그인만 있어요). if (!me) return ; return ( <> nav('/')} />
{editing ? (
(setNick(e.target.value), setError(null))} maxLength={20} autoFocus aria-label="새 닉네임" /> {error &&

{error}

}

게임 중에 바꾸면 그 게임이 끝난 뒤에 보여요.

) : ( <>

{me.nickname}

)}

{me.kind === 'member' ? '디스코드로 로그인함' : '게스트(이 기기에만 저장돼요)'}

{me.kind === 'guest' && discordEnabled && ( 디스코드 연결하기 (기록을 안전하게 보관) )} {me.kind === 'member' && hasDiscordAvatar && ( )}
{role !== 'user' && ( )} {display}

내 기록

{stats.length === 0 ? (

아직 끝까지 한 게임이 없어요.

) : ( stats.map((s) => (
{catalogById(s.gameId)?.nameKo ?? s.gameId} {s.played}판 {s.wins}승{s.draws ? ` ${s.draws}무` : ''}
)) )}
{me.kind === 'member' && ( )}
{confirm === 'logout' && ( { await api.logout(); nav('/'); await useSession.getState().refresh(); }} onClose={() => setConfirm(null)} /> )} {confirm === 'logoutAll' && ( { await api.logoutAll(); nav('/'); await useSession.getState().refresh(); }} onClose={() => setConfirm(null)} /> )} {confirm === 'delete' && ( { await api.deleteMe(); nav('/'); await useSession.getState().refresh(); }} onClose={() => setConfirm(null)} /> )}
); } function resultLabel(g: MyGameSummary): string { if (g.endReason === 'void') return '무효'; if (g.myRank === null) return '끝남'; if (g.players.length === 1) return '혼자 했어요'; if (g.players.every((p) => p.rank === 1)) return '무승부'; if (g.players.length === 2) return g.myRank === 1 ? '이겼어요' : '졌어요'; return `${g.myRank}등`; } /** 내 게임 기록: finished games, newest first, with a 다시 보기 button (docs/09 §8). */ function MyGames() { const [games, setGames] = useState(null); const [next, setNext] = useState(null); const [loading, setLoading] = useState(false); const load = (before: string | null) => { setLoading(true); api .myGames(before) .then((r) => { setGames((prev) => [...(before ? (prev ?? []) : []), ...r.games]); setNext(r.next); }) .catch((e) => toast((e as Error).message, 'error')) .finally(() => setLoading(false)); }; useEffect(() => load(null), []); return (

내 게임 기록

{games === null ? (

불러오는 중…

) : games.length === 0 ? (

아직 다시 볼 게임이 없어요.

) : (
    {games.map((g) => { const label = resultLabel(g); return (
  • {gameName(g.gameId)} {label}
    {new Date(g.startedAt).toLocaleString('ko-KR', { month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit' })} {g.players.map((p, i) => ( {i > 0 && ' · '} {p.me ? 나 : p.nickname} ))}
    {g.replayable ? ( ▶ 다시 보기 ) : ( 다시 볼 수 없어요 )}
  • ); })}
)} {next && ( )}
); }