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

예제 소스

장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 59개

KiboListModal.tsx

"use client";

import { useCallback, useEffect, useState } from "react";
import type { GameListItem } from "@/lib/gameTypes";
import { getOrCreateGuestId } from "@/lib/guest";
import { modeLabel } from "@/lib/kibo";
import styles from "./KiboListModal.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())}`;
}

type Props = {
  open: boolean;
  onClose: () => void;
  onSelect: (item: GameListItem) => void;
};

export function KiboListModal({ open, onClose, onSelect }: Props) {
  const [games, setGames] = useState<GameListItem[]>([]);
  const [dbFile, setDbFile] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const load = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const gid = getOrCreateGuestId();
      const res = await fetch(`/api/game/list?guestId=${encodeURIComponent(gid)}`);
      if (!res.ok) {
        setError("기보 목록을 불러오지 못했습니다.");
        return;
      }
      const data = (await res.json()) as { games: GameListItem[]; dbFile?: string };
      setGames(data.games || []);
      setDbFile(data.dbFile || "");
    } catch {
      setError("기보 서버 연결 실패");
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    if (!open) return;
    void load();
  }, [open, load]);

  if (!open) return null;

  return (
    <div className={styles.backdrop} role="dialog" aria-modal="true" aria-label="기보 목록">
      <div className={styles.modal}>
        <h2>기보</h2>
        <p>저장된 대국을 고르면 그 시점부터 이어집니다.{dbFile ? ` · ${dbFile}` : ""}</p>
        {loading ? <p className={styles.muted}>불러오는 중…</p> : null}
        {error ? <p className={styles.err}>{error}</p> : null}
        {!loading && !error && games.length === 0 ? (
          <p className={styles.muted}>저장된 기보가 없습니다.</p>
        ) : null}
        <ul className={styles.list}>
          {games.map((g) => (
            <li key={g.code}>
              <button
                type="button"
                className={styles.card}
                onClick={() => {
                  onSelect(g);
                  onClose();
                }}
              >
                <div className={styles.top}>
                  <span className={styles.code}>{g.code}</span>
                  <span className={g.status === "ended" ? styles.badgeEnded : styles.badgePlay}>
                    {g.status === "ended" ? "종료" : "진행중"}
                  </span>
                </div>
                <div className={styles.meta}>
                  <span>{formatWhen(g.updated_at)}</span>
                  <span>{g.move_no}수</span>
                  <span>{modeLabel(g.mode)}</span>
                </div>
                {g.summary ? <p className={styles.summary}>{g.summary}</p> : null}
              </button>
            </li>
          ))}
        </ul>
        <button type="button" className={styles.close} onClick={onClose}>
          닫기
        </button>
      </div>
    </div>
  );
}