숫자 괄호 학습 준비의 play 폴더 위치 변경
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
|
||||
import AnswerForm from "@/components/game/AnswerForm";
|
||||
import type {
|
||||
ActiveAnswerInput,
|
||||
AnswerFormHandle,
|
||||
} from "@/components/game/AnswerForm";
|
||||
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 { 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;
|
||||
}
|
||||
|
||||
export default function NumberPlayClient({
|
||||
problemCount,
|
||||
}: NumberPlayClientProps) {
|
||||
const session = useGameStore((state) => state.session);
|
||||
const selectedProblemCount = useGameStore(
|
||||
(state) => state.selectedProblemCount,
|
||||
);
|
||||
const setProblemCount = useGameStore((state) => state.setProblemCount);
|
||||
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<HTMLDivElement>(null);
|
||||
const answerFormRef = useRef<AnswerFormHandle>(null);
|
||||
|
||||
const [answerA, setAnswerA] = useState("");
|
||||
const [answerB, setAnswerB] = useState("");
|
||||
const [activeInput, setActiveInput] = useState<ActiveAnswerInput>(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;
|
||||
const correctCount =
|
||||
session?.results.filter((result) => result?.correct).length ?? 0;
|
||||
const totalCount = session?.problems.length ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProblemCount !== problemCount) {
|
||||
setProblemCount(problemCount);
|
||||
}
|
||||
|
||||
if (sessionNeedsInit) {
|
||||
initSession(problemCount);
|
||||
}
|
||||
}, [
|
||||
initSession,
|
||||
problemCount,
|
||||
selectedProblemCount,
|
||||
sessionProblemCount,
|
||||
sessionNeedsInit,
|
||||
setProblemCount,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setAnswerA("");
|
||||
setAnswerB("");
|
||||
setActiveInput(0);
|
||||
setInputError("");
|
||||
}, [currentProblem?.key]);
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
if (currentResult || !currentProblem) return;
|
||||
|
||||
const parsedA = parseAnswer(answerA);
|
||||
const parsedB = parseAnswer(answerB);
|
||||
|
||||
if (parsedA === null || parsedB === null) {
|
||||
setInputError("두 칸 모두 실수로 입력하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setInputError("");
|
||||
submitAnswer({ a: parsedA, b: parsedB });
|
||||
}
|
||||
|
||||
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<HTMLElement>("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 (
|
||||
<main className="flex min-h-screen items-center bg-slate-50 px-6 py-12 text-slate-950">
|
||||
<section className="mx-auto w-full max-w-3xl">
|
||||
<p className="text-lg font-semibold text-slate-700">
|
||||
문제를 준비하고 있습니다.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
return (
|
||||
<ScoreSummary
|
||||
correctCount={correctCount}
|
||||
totalCount={totalCount}
|
||||
problems={session.problems}
|
||||
results={session.results}
|
||||
onRestart={resetSession}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentProblem) {
|
||||
return (
|
||||
<main className="flex min-h-screen items-center bg-slate-50 px-6 py-12 text-slate-950">
|
||||
<section className="mx-auto w-full max-w-3xl">
|
||||
<p className="text-lg font-semibold text-slate-700">
|
||||
현재 문제를 불러올 수 없습니다.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const submitted = currentResult !== null;
|
||||
const isLastProblem = session.currentIndex === session.problems.length - 1;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-50 px-6 py-10 text-slate-950">
|
||||
<section className="mx-auto flex w-full max-w-3xl flex-col gap-8">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold text-emerald-700">
|
||||
괄호 음수 분배
|
||||
</p>
|
||||
<h1 className="text-3xl font-bold tracking-normal sm:text-4xl">
|
||||
문제 풀이
|
||||
</h1>
|
||||
</div>
|
||||
<StageIndicator
|
||||
currentIndex={session.currentIndex}
|
||||
results={session.results}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={problemCardRef}
|
||||
className="rounded-md bg-white px-6 pt-6 pb-2 shadow-sm ring-1 ring-slate-200 sm:p-8"
|
||||
>
|
||||
<p className="mb-4 text-sm font-semibold text-slate-500">
|
||||
괄호를 풀어 두 항을 순서대로 입력하세요.
|
||||
</p>
|
||||
<div className="text-3xl font-semibold text-slate-950 sm:text-4xl">
|
||||
<KatexRenderer latex={currentProblem.latexBefore} displayMode />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!submitted ? (
|
||||
<AnswerForm
|
||||
ref={answerFormRef}
|
||||
answerA={answerA}
|
||||
answerB={answerB}
|
||||
activeInput={activeInput}
|
||||
error={inputError}
|
||||
onActiveInputChange={setActiveInput}
|
||||
onAnswerAChange={setAnswerA}
|
||||
onAnswerBChange={setAnswerB}
|
||||
onInputFocus={scrollKatexToViewportTop}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{currentResult ? (
|
||||
<FeedbackPanel
|
||||
result={currentResult}
|
||||
problem={currentProblem}
|
||||
isLastProblem={isLastProblem}
|
||||
onNext={handleNextProblem}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user