예제 소스
SGF 기보로 학습하는 바둑 LLM 인공지능 · 파일 169개
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 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);
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}수</span>
</div>
{g.summary ? <p className={styles.summary}>{g.summary}</p> : null}
</button>
</li>
))}
</ul>
</div>
<nav className={styles.nav}>
<Link href="/">로비</Link>
<span className={styles.active}>기보</span>
<Link href="/play">대국</Link>
</nav>
</main>
);
}
export default function KiboPage() {
return (
<Suspense
fallback={
<main className={styles.page}>
<p className={styles.hint}>기보 불러오는 중…</p>
</main>
}
>
<KiboInner />
</Suspense>
);
}