예제 소스
장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 59개
page.tsx
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import type { GameListItem } from "@/lib/gameTypes";
import { getOrCreateGuestId, rememberGame } from "@/lib/guest";
import { modeLabel } from "@/lib/kibo";
import { MoreGames } from "@/components/MoreGames";
import styles from "./kibo.module.css";
function formatWhen(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
const pad = (n: number) => String(n).padStart(2, "0");
return (
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}`
);
}
function KiboInner() {
const router = useRouter();
const [games, setGames] = useState<GameListItem[]>([]);
const [dbFile, setDbFile] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showMore, setShowMore] = useState(false);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const gid = getOrCreateGuestId();
const res = await fetch(`/api/game/list?guestId=${encodeURIComponent(gid)}`);
if (!res.ok) {
if (!cancelled) setError("기보 목록을 불러오지 못했습니다.");
return;
}
const data = (await res.json()) as { games: GameListItem[]; dbFile?: string };
if (!cancelled) {
setGames(data.games || []);
setDbFile(data.dbFile || "");
}
} catch {
if (!cancelled) setError("기보 서버 연결 실패");
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, []);
function openGame(g: GameListItem) {
rememberGame(g.code, g.access_key);
router.push(`/play?g=${encodeURIComponent(g.code)}&k=${encodeURIComponent(g.access_key)}`);
}
return (
<main className={styles.page}>
<header className={styles.top}>
<h1 className={styles.brand}>기보</h1>
<p className={styles.sub}>선택하면 저장된 시점의 대국으로 바로 이어집니다</p>
{dbFile ? <p className={styles.dbMeta}>DB · {dbFile}</p> : null}
</header>
<div className={styles.body}>
{loading ? <p className={styles.hint}>불러오는 중…</p> : null}
{error ? <p className={styles.error}>{error}</p> : null}
{!loading && !error && games.length === 0 ? (
<p className={styles.hint}>저장된 기보가 없습니다. 대국을 시작해 보세요.</p>
) : null}
<ul className={styles.list}>
{games.map((g) => (
<li key={g.code}>
<button type="button" className={styles.card} onClick={() => openGame(g)}>
<div className={styles.cardTop}>
<span className={styles.code}>{g.code}</span>
<span className={g.status === "ended" ? styles.badgeEnded : styles.badgePlay}>
{g.status === "ended" ? "종료" : "진행중"}
</span>
</div>
<div className={styles.cardMeta}>
<span>{formatWhen(g.updated_at)}</span>
<span>
{g.move_no}수 · {modeLabel(g.mode)}
</span>
</div>
{g.summary ? <p className={styles.summary}>{g.summary}</p> : null}
</button>
</li>
))}
</ul>
</div>
<nav className={styles.nav}>
<Link href="/">홈</Link>
<Link href="/play">대국</Link>
<span className={styles.active}>기보</span>
<button type="button" className={styles.navBtn} onClick={() => setShowMore(true)}>
다른 게임
</button>
</nav>
<MoreGames open={showMore} onClose={() => setShowMore(false)} excludeId="janggy" />
</main>
);
}
export default function KiboPage() {
return (
<Suspense
fallback={
<main className={styles.page}>
<p className={styles.hint}>기보 불러오는 중…</p>
</main>
}
>
<KiboInner />
</Suspense>
);
}