"use client"; import Link from "next/link"; import { useCallback, useEffect, useRef, useState } from "react"; import type { FormEvent } from "react"; import { flushSync } from "react-dom"; import NumberAnswerForm from "@/components/game/NumberAnswerForm"; import type { ActiveAnswerInput, NumberAnswerFormHandle, } from "@/components/game/NumberAnswerForm"; import FeedbackPanel from "@/components/game/FeedbackPanel"; import ScoreSummary from "@/components/game/ScoreSummary"; import StageIndicator from "@/components/game/StageIndicator"; import { DESKTOP_INPUT_DEVICE_QUERY } from "@/lib/input-device"; import KatexRenderer from "@/components/math/KatexRenderer"; import { useGameStore } from "@/store/gameStore"; import type { MaxTermCount, ProblemCount } from "@/store/gameStore"; const REAL_NUMBER_PATTERN = /^-?\d+(?:\.\d+)?$/; function parseAnswer(value: string) { const trimmed = value.trim(); if (!REAL_NUMBER_PATTERN.test(trimmed)) return null; return Number(trimmed); } interface NumberPlayClientProps { problemCount: ProblemCount; maxTermCount: MaxTermCount; } export default function NumberPlayClient({ problemCount, maxTermCount, }: NumberPlayClientProps) { const session = useGameStore((state) => state.session); const selectedProblemCount = useGameStore( (state) => state.selectedProblemCount, ); const setProblemCount = useGameStore((state) => state.setProblemCount); const selectedMaxTermCount = useGameStore( (state) => state.selectedMaxTermCount, ); const setMaxTermCount = useGameStore((state) => state.setMaxTermCount); const initSession = useGameStore((state) => state.initSession); const submitAnswer = useGameStore((state) => state.submitAnswer); const nextProblem = useGameStore((state) => state.nextProblem); const resetSession = useGameStore((state) => state.resetSession); const problemCardRef = useRef(null); const answerFormRef = useRef(null); const [answers, setAnswers] = useState(() => Array.from({ length: maxTermCount }, () => ""), ); const [activeInput, setActiveInput] = useState(0); const [inputError, setInputError] = useState(""); const sessionProblemCount = session?.problems.length ?? null; const currentProblem = session?.problems[session.currentIndex] ?? null; const currentResult = session?.results[session.currentIndex] ?? null; const isComplete = Boolean( session && session.currentIndex >= session.problems.length, ); const sessionNeedsInit = sessionProblemCount !== problemCount || session?.termCount !== maxTermCount; const correctCount = session?.results.filter((result) => result?.correct).length ?? 0; const totalCount = session?.problems.length ?? 0; useEffect(() => { if (selectedProblemCount !== problemCount) { setProblemCount(problemCount); } if (selectedMaxTermCount !== maxTermCount) { setMaxTermCount(maxTermCount); } if (sessionNeedsInit) { initSession(problemCount, maxTermCount); } }, [ initSession, maxTermCount, problemCount, selectedMaxTermCount, selectedProblemCount, sessionProblemCount, sessionNeedsInit, setMaxTermCount, setProblemCount, ]); useEffect(() => { setAnswers(Array.from({ length: currentProblem?.terms.length ?? maxTermCount }, () => "")); setActiveInput(0); setInputError(""); }, [currentProblem?.key, currentProblem?.terms.length, maxTermCount]); function handleSubmit(event: FormEvent) { event.preventDefault(); if (currentResult || !currentProblem) return; const parsedAnswers = answers.map(parseAnswer); if (parsedAnswers.some((answer) => answer === null)) { setInputError("모든 항을 실수로 입력하세요."); return; } setInputError(""); submitAnswer({ terms: parsedAnswers as number[] }); } function handleAnswerChange(index: ActiveAnswerInput, value: string) { setAnswers((currentAnswers) => currentAnswers.map((answer, answerIndex) => answerIndex === index ? value : answer, ), ); } function handleNextProblem() { flushSync(() => { nextProblem(); }); answerFormRef.current?.focusInput(); } const scrollKatexToViewportTop = useCallback(() => { if (window.matchMedia(DESKTOP_INPUT_DEVICE_QUERY).matches) return; function alignKatex() { const katexElement = problemCardRef.current?.querySelector("span.katex"); if (!katexElement) return; const rect = katexElement.getBoundingClientRect(); const scrollElement = document.scrollingElement ?? document.documentElement; const elementDocumentTop = window.scrollY + rect.top; const maxScrollTop = Math.max( 0, scrollElement.scrollHeight - window.innerHeight, ); const scrollTop = Math.min( maxScrollTop, Math.max(0, elementDocumentTop), ); if (Math.abs(window.scrollY - scrollTop) < 4) return; scrollElement.scrollTop = scrollTop; document.body.scrollTop = scrollTop; window.scrollTo(0, scrollTop); } window.requestAnimationFrame(() => { window.requestAnimationFrame(alignKatex); }); }, []); if (!session || sessionNeedsInit) { return (

문제를 준비하고 있습니다.

); } if (isComplete) { return ( ); } if (!currentProblem) { return (

현재 문제를 불러올 수 없습니다.

); } const submitted = currentResult !== null; const isLastProblem = session.currentIndex === session.problems.length - 1; const pendingAnswerLabels = currentProblem.terms.map((term, index) => { if (index === 0) return String(term); return String( currentProblem.operators[index - 1] === "-" ? -term : term, ); }); return (

괄호 음수 분배

문제 풀이

포기
{!submitted ? ( ) : null} {currentResult ? ( ) : null}
); }