"use client"; import { useEffect, useRef, useState } from "react"; import { catSprite } from "@/lib/game/data"; import { checkNickname, newSyncToken, registerNickname, recoverVillage, pushSave, } from "@/lib/game/sync"; import { engine } from "@/lib/game/engine"; type Mode = "new" | "recover"; type Status = | "idle" | "checking" | "available" | "taken" | "invalid" | "offline"; const NICKNAME_RE = /^[가-힣a-zA-Z0-9_-]{2,12}$/; export default function NicknameModal({ onDone }: { onDone: () => void }) { const [mode, setMode] = useState("new"); const [name, setName] = useState(""); const [status, setStatus] = useState("idle"); const [submitting, setSubmitting] = useState(false); const timer = useRef | null>(null); const latest = useRef(""); const trimmed = name.trim(); // 입력이 멈춘 1초 후 자동 중복 체크 useEffect(() => { latest.current = trimmed; if (timer.current) clearTimeout(timer.current); if (trimmed.length === 0) { setStatus("idle"); return; } if (!NICKNAME_RE.test(trimmed)) { setStatus("invalid"); return; } setStatus("checking"); timer.current = setTimeout(async () => { const result = await checkNickname(trimmed); if (latest.current !== trimmed) return; // 입력이 바뀌었으면 무시 if (result === null) setStatus("offline"); else setStatus(result.available ? "available" : "taken"); }, 1000); return () => { if (timer.current) clearTimeout(timer.current); }; }, [trimmed, mode]); // 모드별 결정 가능 조건: 새 마을=사용 가능, 불러오기=이미 존재 const canConfirm = !submitting && (mode === "new" ? status === "available" : status === "taken"); const confirm = async () => { if (!canConfirm) return; setSubmitting(true); if (mode === "new") { const token = newSyncToken(); const ok = await registerNickname(trimmed, token); if (ok === false) { setStatus("taken"); // 그 사이 누가 선점 setSubmitting(false); return; } // 서버 불가여도 로컬에 기록 — 다음 동기화 때 재등록 시도 engine.setNickname(trimmed, token); void pushSave(); } else { const found = await recoverVillage(trimmed); if (!found) { setStatus("available"); // 불러오기 모드에선 "그 이름의 마을은 없는걸?" setSubmitting(false); return; } engine.replaceState(found.data); engine.setNickname(trimmed, found.token); } setSubmitting(false); onDone(); }; const lines: Record> = { new: { idle: { text: "마을 문패에 걸 이름이야. 한글·영문·숫자 2~12자!", cls: "text-[#8a7551]" }, invalid: { text: "한글·영문·숫자·-·_ 로 2~12자만 가능해!", cls: "text-[#b0472a]" }, checking: { text: "다른 마을과 겹치는지 확인 중… 🐾", cls: "text-[#8a7551]" }, available: { text: "좋아, 이 이름은 비어 있어! ✨", cls: "text-[#5c8a2a]" }, taken: { text: "앗, 이미 다른 마을이 쓰고 있는 이름이야 🙀", cls: "text-[#b0472a]" }, offline: { text: "서버에 닿지 않아… 잠시 후 다시 시도해줘", cls: "text-[#b0472a]" }, }, recover: { idle: { text: "예전에 쓰던 마을 이름을 알려줘!", cls: "text-[#8a7551]" }, invalid: { text: "한글·영문·숫자·-·_ 로 2~12자만 가능해!", cls: "text-[#b0472a]" }, checking: { text: "마을 명부를 뒤적이는 중… 🐾", cls: "text-[#8a7551]" }, available: { text: "그 이름의 마을은 없는걸? 🙀", cls: "text-[#b0472a]" }, taken: { text: "마을을 찾았어! 데려올게 ✨", cls: "text-[#5c8a2a]" }, offline: { text: "서버에 닿지 않아… 잠시 후 다시 시도해줘", cls: "text-[#b0472a]" }, }, }; return (
{/* eslint-disable-next-line @next/next/no-img-element */} 촌장 냥이

{mode === "new" ? "그런데… 네 이름이 뭐야?" : "마을을 찾아줄게!"}

{mode === "new" ? "촌장 냥이가 마을 문패에 걸어둘 이름을 물어봅니다" : "예전 마을 이름을 입력하면 그대로 이어서 할 수 있어요"}

setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.nativeEvent.isComposing) void confirm(); }} maxLength={12} autoFocus placeholder={mode === "new" ? "닉네임 입력" : "기존 마을 이름"} className="font-jua mt-4 w-full rounded-xl border-2 border-[rgba(126,95,48,.35)] bg-white/80 px-4 py-3 text-center text-lg text-[#3f2c14] outline-none focus:border-[#7fb539]" />

{lines[mode][status].text}

); }