괄호 안의 항을 2, 3, 4 항으로 설정하는 기능 추가
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,12 @@ export default function VariableParenthesesClient() {
|
||||
const setProblemCount = useVariableGameStore(
|
||||
(state) => state.setProblemCount,
|
||||
);
|
||||
const selectedMaxTermCount = useVariableGameStore(
|
||||
(state) => state.selectedMaxTermCount,
|
||||
);
|
||||
const setMaxTermCount = useVariableGameStore(
|
||||
(state) => state.setMaxTermCount,
|
||||
);
|
||||
const initSession = useVariableGameStore((state) => state.initSession);
|
||||
|
||||
return (
|
||||
@@ -18,10 +24,12 @@ export default function VariableParenthesesClient() {
|
||||
title="미지수 괄호 풀기"
|
||||
description="괄호 안의 한 항에 미지수 x가 들어간 식을 풀며 부호 변화를 연습합니다."
|
||||
notice="현재는 소괄호와 미지수 x 문제만 선택할 수 있습니다."
|
||||
playHref={`/variable-parentheses/play?count=${selectedProblemCount}`}
|
||||
playHref={`/variable-parentheses/play?count=${selectedProblemCount}&terms=${selectedMaxTermCount}`}
|
||||
selectedProblemCount={selectedProblemCount}
|
||||
selectedMaxTermCount={selectedMaxTermCount}
|
||||
onProblemCountChange={setProblemCount}
|
||||
onStart={() => initSession(selectedProblemCount)}
|
||||
onMaxTermCountChange={setMaxTermCount}
|
||||
onStart={() => initSession(selectedProblemCount, selectedMaxTermCount)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,15 +12,18 @@ import KatexRenderer from "@/components/math/KatexRenderer";
|
||||
import { formatVariableTermLatex } from "@/lib/engine/variable-parenthesis";
|
||||
import {
|
||||
useVariableGameStore,
|
||||
type VariableMaxTermCount,
|
||||
type VariableProblemCount,
|
||||
} from "@/store/variableGameStore";
|
||||
|
||||
interface VariablePlayClientProps {
|
||||
problemCount: VariableProblemCount;
|
||||
maxTermCount: VariableMaxTermCount;
|
||||
}
|
||||
|
||||
export default function VariablePlayClient({
|
||||
problemCount,
|
||||
maxTermCount,
|
||||
}: VariablePlayClientProps) {
|
||||
const session = useVariableGameStore((state) => state.session);
|
||||
const selectedProblemCount = useVariableGameStore(
|
||||
@@ -29,15 +32,21 @@ export default function VariablePlayClient({
|
||||
const setProblemCount = useVariableGameStore(
|
||||
(state) => state.setProblemCount,
|
||||
);
|
||||
const selectedMaxTermCount = useVariableGameStore(
|
||||
(state) => state.selectedMaxTermCount,
|
||||
);
|
||||
const setMaxTermCount = useVariableGameStore(
|
||||
(state) => state.setMaxTermCount,
|
||||
);
|
||||
const initSession = useVariableGameStore((state) => state.initSession);
|
||||
const submitAnswer = useVariableGameStore((state) => state.submitAnswer);
|
||||
const nextProblem = useVariableGameStore((state) => state.nextProblem);
|
||||
const resetSession = useVariableGameStore((state) => state.resetSession);
|
||||
|
||||
const [selectedChoiceIds, setSelectedChoiceIds] = useState<
|
||||
[string | null, string | null]
|
||||
>([null, null]);
|
||||
const [activeTermIndex, setActiveTermIndex] = useState<0 | 1>(0);
|
||||
const [selectedChoiceIds, setSelectedChoiceIds] = useState<(string | null)[]>(
|
||||
() => Array.from({ length: maxTermCount }, () => null),
|
||||
);
|
||||
const [activeTermIndex, setActiveTermIndex] = useState(0);
|
||||
|
||||
const sessionProblemCount = session?.problems.length ?? null;
|
||||
const currentProblem = session?.problems[session.currentIndex] ?? null;
|
||||
@@ -45,7 +54,8 @@ export default function VariablePlayClient({
|
||||
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;
|
||||
@@ -55,40 +65,46 @@ export default function VariablePlayClient({
|
||||
setProblemCount(problemCount);
|
||||
}
|
||||
|
||||
if (selectedMaxTermCount !== maxTermCount) {
|
||||
setMaxTermCount(maxTermCount);
|
||||
}
|
||||
|
||||
if (sessionNeedsInit) {
|
||||
initSession(problemCount);
|
||||
initSession(problemCount, maxTermCount);
|
||||
}
|
||||
}, [
|
||||
initSession,
|
||||
maxTermCount,
|
||||
problemCount,
|
||||
selectedMaxTermCount,
|
||||
selectedProblemCount,
|
||||
sessionProblemCount,
|
||||
sessionNeedsInit,
|
||||
setMaxTermCount,
|
||||
setProblemCount,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedChoiceIds([null, null]);
|
||||
setSelectedChoiceIds(
|
||||
Array.from({ length: currentProblem?.terms.length ?? maxTermCount }, () => null),
|
||||
);
|
||||
setActiveTermIndex(0);
|
||||
}, [currentProblem?.key]);
|
||||
}, [currentProblem?.key, currentProblem?.terms.length, maxTermCount]);
|
||||
|
||||
function handleSelectChoice(termIndex: 0 | 1, choiceId: string) {
|
||||
const nextSelectedChoiceIds: [string | null, string | null] = [
|
||||
...selectedChoiceIds,
|
||||
];
|
||||
function handleSelectChoice(termIndex: number, choiceId: string) {
|
||||
const nextSelectedChoiceIds = [...selectedChoiceIds];
|
||||
nextSelectedChoiceIds[termIndex] = choiceId;
|
||||
|
||||
setSelectedChoiceIds(nextSelectedChoiceIds);
|
||||
|
||||
if (termIndex === 0) {
|
||||
setActiveTermIndex(1);
|
||||
if (termIndex < nextSelectedChoiceIds.length - 1) {
|
||||
setActiveTermIndex(termIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentResult || !currentProblem) return;
|
||||
if (
|
||||
nextSelectedChoiceIds[0] === null ||
|
||||
nextSelectedChoiceIds[1] === null
|
||||
nextSelectedChoiceIds.some((selectedChoiceId) => selectedChoiceId === null)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -140,16 +156,15 @@ export default function VariablePlayClient({
|
||||
|
||||
const submitted = currentResult !== null;
|
||||
const isLastProblem = session.currentIndex === session.problems.length - 1;
|
||||
const pendingChoiceLabels: [string, string] = [
|
||||
formatVariableTermLatex(currentProblem.firstTerm),
|
||||
const pendingChoiceLabels = currentProblem.terms.map((term, index) =>
|
||||
formatVariableTermLatex({
|
||||
...currentProblem.secondTerm,
|
||||
...term,
|
||||
coefficient:
|
||||
currentProblem.operator === "-"
|
||||
? -currentProblem.secondTerm.coefficient
|
||||
: currentProblem.secondTerm.coefficient,
|
||||
index > 0 && currentProblem.operators[index - 1] === "-"
|
||||
? -term.coefficient
|
||||
: term.coefficient,
|
||||
}),
|
||||
];
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-slate-50 px-6 py-10 text-slate-950">
|
||||
@@ -173,9 +188,6 @@ export default function VariablePlayClient({
|
||||
</div>
|
||||
|
||||
<div 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>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import VariablePlayClient from "./VariablePlayClient";
|
||||
import type { VariableProblemCount } from "@/store/variableGameStore";
|
||||
import type {
|
||||
VariableMaxTermCount,
|
||||
VariableProblemCount,
|
||||
} from "@/store/variableGameStore";
|
||||
|
||||
interface VariablePlayPageProps {
|
||||
searchParams?: {
|
||||
count?: string | string[];
|
||||
terms?: string | string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,10 +19,24 @@ function parseProblemCount(count?: string | string[]): VariableProblemCount {
|
||||
return count === "5" ? 5 : 10;
|
||||
}
|
||||
|
||||
function parseMaxTermCount(terms?: string | string[]): VariableMaxTermCount {
|
||||
if (Array.isArray(terms)) {
|
||||
return parseMaxTermCount(terms[0]);
|
||||
}
|
||||
|
||||
if (terms === "3") return 3;
|
||||
if (terms === "4") return 4;
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
export default function VariablePlayPage({
|
||||
searchParams,
|
||||
}: VariablePlayPageProps) {
|
||||
return (
|
||||
<VariablePlayClient problemCount={parseProblemCount(searchParams?.count)} />
|
||||
<VariablePlayClient
|
||||
problemCount={parseProblemCount(searchParams?.count)}
|
||||
maxTermCount={parseMaxTermCount(searchParams?.terms)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user