게시판바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기예제 소스

예제 소스

바둑 on-policy 학습: 대국 로그를 KataGo 라벨로 바꾸기 · 파일 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>
  );
}