괄호 안의 항을 2, 3, 4 항으로 설정하는 기능 추가

This commit is contained in:
2026-05-31 13:40:45 +09:00
parent e148a957ea
commit ac3c939051
21 changed files with 708 additions and 344 deletions
@@ -8,6 +8,10 @@ export default function NumberParenthesesClient() {
(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);
return (
@@ -16,10 +20,12 @@ export default function NumberParenthesesClient() {
title="숫자 괄호 풀기"
description="정수로 이루어진 괄호식을 풀며 음수 분배와 부호 변화를 연습합니다."
notice="현재는 소괄호 숫자 문제만 선택할 수 있습니다."
playHref={`/number-parentheses/play?count=${selectedProblemCount}`}
playHref={`/number-parentheses/play?count=${selectedProblemCount}&terms=${selectedMaxTermCount}`}
selectedProblemCount={selectedProblemCount}
selectedMaxTermCount={selectedMaxTermCount}
onProblemCountChange={setProblemCount}
onStart={() => initSession(selectedProblemCount)}
onMaxTermCountChange={setMaxTermCount}
onStart={() => initSession(selectedProblemCount, selectedMaxTermCount)}
/>
);
}
@@ -16,7 +16,7 @@ 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";
import type { MaxTermCount, ProblemCount } from "@/store/gameStore";
const REAL_NUMBER_PATTERN = /^-?\d+(?:\.\d+)?$/;
@@ -30,16 +30,22 @@ function parseAnswer(value: string) {
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);
@@ -48,8 +54,9 @@ export default function NumberPlayClient({
const problemCardRef = useRef<HTMLDivElement>(null);
const answerFormRef = useRef<NumberAnswerFormHandle>(null);
const [answerA, setAnswerA] = useState("");
const [answerB, setAnswerB] = useState("");
const [answers, setAnswers] = useState<string[]>(() =>
Array.from({ length: maxTermCount }, () => ""),
);
const [activeInput, setActiveInput] = useState<ActiveAnswerInput>(0);
const [inputError, setInputError] = useState("");
@@ -59,7 +66,8 @@ export default function NumberPlayClient({
const isComplete = Boolean(
session && session.currentIndex >= session.problems.length,
);
const sessionNeedsInit = sessionProblemCount !== problemCount;
const sessionNeedsInit =
sessionProblemCount !== problemCount || session?.termCount !== maxTermCount;
const correctCount =
session?.results.filter((result) => result?.correct).length ?? 0;
const totalCount = session?.problems.length ?? 0;
@@ -69,40 +77,53 @@ export default function NumberPlayClient({
setProblemCount(problemCount);
}
if (selectedMaxTermCount !== maxTermCount) {
setMaxTermCount(maxTermCount);
}
if (sessionNeedsInit) {
initSession(problemCount);
initSession(problemCount, maxTermCount);
}
}, [
initSession,
maxTermCount,
problemCount,
selectedMaxTermCount,
selectedProblemCount,
sessionProblemCount,
sessionNeedsInit,
setMaxTermCount,
setProblemCount,
]);
useEffect(() => {
setAnswerA("");
setAnswerB("");
setAnswers(Array.from({ length: currentProblem?.terms.length ?? maxTermCount }, () => ""));
setActiveInput(0);
setInputError("");
}, [currentProblem?.key]);
}, [currentProblem?.key, currentProblem?.terms.length, maxTermCount]);
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (currentResult || !currentProblem) return;
const parsedA = parseAnswer(answerA);
const parsedB = parseAnswer(answerB);
const parsedAnswers = answers.map(parseAnswer);
if (parsedA === null || parsedB === null) {
setInputError("두 칸 모두 실수로 입력하세요.");
if (parsedAnswers.some((answer) => answer === null)) {
setInputError("모든 항을 실수로 입력하세요.");
return;
}
setInputError("");
submitAnswer({ a: parsedA, b: parsedB });
submitAnswer({ terms: parsedAnswers as number[] });
}
function handleAnswerChange(index: ActiveAnswerInput, value: string) {
setAnswers((currentAnswers) =>
currentAnswers.map((answer, answerIndex) =>
answerIndex === index ? value : answer,
),
);
}
function handleNextProblem() {
@@ -184,10 +205,13 @@ export default function NumberPlayClient({
const submitted = currentResult !== null;
const isLastProblem = session.currentIndex === session.problems.length - 1;
const pendingAnswerLabels: [string, string] = [
String(currentProblem.a),
String(currentProblem.operator === "-" ? -currentProblem.b : currentProblem.b),
];
const pendingAnswerLabels = currentProblem.terms.map((term, index) => {
if (index === 0) return String(term);
return String(
currentProblem.operators[index - 1] === "-" ? -term : term,
);
});
return (
<main className="min-h-screen bg-slate-50 px-6 py-10 text-slate-950">
@@ -214,9 +238,6 @@ export default function NumberPlayClient({
ref={problemCardRef}
className="rounded-md bg-white px-6 pt-6 pb-4 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>
@@ -231,14 +252,12 @@ export default function NumberPlayClient({
{!submitted ? (
<NumberAnswerForm
ref={answerFormRef}
answerA={answerA}
answerB={answerB}
answers={answers}
activeInput={activeInput}
error={inputError}
pendingAnswerLabels={pendingAnswerLabels}
onActiveInputChange={setActiveInput}
onAnswerAChange={setAnswerA}
onAnswerBChange={setAnswerB}
onAnswerChange={handleAnswerChange}
onInputFocus={scrollKatexToViewportTop}
onSubmit={handleSubmit}
/>
+17 -2
View File
@@ -1,9 +1,10 @@
import NumberPlayClient from "./NumberPlayClient";
import type { ProblemCount } from "@/store/gameStore";
import type { MaxTermCount, ProblemCount } from "@/store/gameStore";
interface NumberPlayPageProps {
searchParams?: {
count?: string | string[];
terms?: string | string[];
};
}
@@ -15,8 +16,22 @@ function parseProblemCount(count?: string | string[]): ProblemCount {
return count === "5" ? 5 : 10;
}
function parseMaxTermCount(terms?: string | string[]): MaxTermCount {
if (Array.isArray(terms)) {
return parseMaxTermCount(terms[0]);
}
if (terms === "3") return 3;
if (terms === "4") return 4;
return 2;
}
export default function NumberPlayPage({ searchParams }: NumberPlayPageProps) {
return (
<NumberPlayClient problemCount={parseProblemCount(searchParams?.count)} />
<NumberPlayClient
problemCount={parseProblemCount(searchParams?.count)}
maxTermCount={parseMaxTermCount(searchParams?.terms)}
/>
);
}