게시판장기로 이해하는 규칙 엔진과 평가 함수 탐색예제 소스

예제 소스

장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 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>
  );
}