From 33e065301fe1436206080a74628c5f2558498b80 Mon Sep 17 00:00:00 2001 From: jisangs Date: Sat, 30 May 2026 18:39:22 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=92=80=EC=9D=B4=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=98=20=EC=9E=85=EB=A0=A5=20=ED=95=AD=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EC=83=81=ED=83=9C=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=A9=EB=B2=95=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../play/NumberPlayClient.tsx | 15 ++- .../play/VariablePlayClient.tsx | 36 +++-- .../{AnswerForm.tsx => NumberAnswerForm.tsx} | 126 ++++++++++-------- components/game/VariableChoiceAnswerForm.tsx | 78 ++++++----- 4 files changed, 151 insertions(+), 104 deletions(-) rename components/game/{AnswerForm.tsx => NumberAnswerForm.tsx} (73%) diff --git a/app/(game)/number-parentheses/play/NumberPlayClient.tsx b/app/(game)/number-parentheses/play/NumberPlayClient.tsx index 523f082..2a4ba9c 100644 --- a/app/(game)/number-parentheses/play/NumberPlayClient.tsx +++ b/app/(game)/number-parentheses/play/NumberPlayClient.tsx @@ -5,11 +5,11 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { FormEvent } from "react"; import { flushSync } from "react-dom"; -import AnswerForm from "@/components/game/AnswerForm"; +import NumberAnswerForm from "@/components/game/NumberAnswerForm"; import type { ActiveAnswerInput, - AnswerFormHandle, -} from "@/components/game/AnswerForm"; + NumberAnswerFormHandle, +} from "@/components/game/NumberAnswerForm"; import FeedbackPanel from "@/components/game/FeedbackPanel"; import ScoreSummary from "@/components/game/ScoreSummary"; import StageIndicator from "@/components/game/StageIndicator"; @@ -46,7 +46,7 @@ export default function NumberPlayClient({ const resetSession = useGameStore((state) => state.resetSession); const problemCardRef = useRef(null); - const answerFormRef = useRef(null); + const answerFormRef = useRef(null); const [answerA, setAnswerA] = useState(""); const [answerB, setAnswerB] = useState(""); @@ -184,6 +184,10 @@ 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), + ]; return (
@@ -225,12 +229,13 @@ export default function NumberPlayClient({ {!submitted ? ( - { - const next: [string | null, string | null] = [...current]; - next[termIndex] = choiceId; - return next; - }); + const nextSelectedChoiceIds: [string | null, string | null] = [ + ...selectedChoiceIds, + ]; + nextSelectedChoiceIds[termIndex] = choiceId; + + setSelectedChoiceIds(nextSelectedChoiceIds); if (termIndex === 0) { setActiveTermIndex(1); + return; } - } - function handleSubmit() { if (currentResult || !currentProblem) return; - if (selectedChoiceIds[0] === null || selectedChoiceIds[1] === null) return; + if ( + nextSelectedChoiceIds[0] === null || + nextSelectedChoiceIds[1] === null + ) { + return; + } - submitAnswer({ selectedChoiceIds }); + submitAnswer({ selectedChoiceIds: nextSelectedChoiceIds }); } function handleNextProblem() { @@ -134,6 +140,16 @@ export default function VariablePlayClient({ const submitted = currentResult !== null; const isLastProblem = session.currentIndex === session.problems.length - 1; + const pendingChoiceLabels: [string, string] = [ + formatVariableTermLatex(currentProblem.firstTerm), + formatVariableTermLatex({ + ...currentProblem.secondTerm, + coefficient: + currentProblem.operator === "-" + ? -currentProblem.secondTerm.coefficient + : currentProblem.secondTerm.coefficient, + }), + ]; return (
@@ -176,8 +192,8 @@ export default function VariablePlayClient({ choices={currentProblem.choices} selectedChoiceIds={selectedChoiceIds} activeTermIndex={activeTermIndex} + pendingChoiceLabels={pendingChoiceLabels} onSelectChoice={handleSelectChoice} - onSubmit={handleSubmit} /> ) : null} diff --git a/components/game/AnswerForm.tsx b/components/game/NumberAnswerForm.tsx similarity index 73% rename from components/game/AnswerForm.tsx rename to components/game/NumberAnswerForm.tsx index f4dbaae..2dfcf06 100644 --- a/components/game/AnswerForm.tsx +++ b/components/game/NumberAnswerForm.tsx @@ -12,6 +12,7 @@ import { import type { FocusEvent, FormEvent } from "react"; import { DESKTOP_INPUT_DEVICE_QUERY } from "@/lib/input-device"; +import KatexRenderer from "@/components/math/KatexRenderer"; export type ActiveAnswerInput = 0 | 1; @@ -30,15 +31,16 @@ const KEYPAD_KEYS = [ "0", ]; -export interface AnswerFormHandle { +export interface NumberAnswerFormHandle { focusInput: () => void; } -interface AnswerFormProps { +interface NumberAnswerFormProps { answerA: string; answerB: string; activeInput: ActiveAnswerInput; error: string; + pendingAnswerLabels: [string, string]; onAnswerAChange: (value: string) => void; onAnswerBChange: (value: string) => void; onActiveInputChange: (input: ActiveAnswerInput) => void; @@ -47,12 +49,16 @@ interface AnswerFormProps { onSubmit: (event: FormEvent) => void; } -const AnswerForm = forwardRef(function AnswerForm( +const NumberAnswerForm = forwardRef< + NumberAnswerFormHandle, + NumberAnswerFormProps +>(function NumberAnswerForm( { answerA, answerB, activeInput, error, + pendingAnswerLabels, onAnswerAChange, onAnswerBChange, onActiveInputChange, @@ -71,6 +77,7 @@ const AnswerForm = forwardRef(function Answer const activeAnswer = activeInput === 0 ? answerA : answerB; const activeLabel = activeInput === 0 ? "첫째 항" : "둘째 항"; + const activeDescription = `${activeLabel}의 답을 입력하세요.`; const activeEnterKeyHint = activeInput === 0 ? "next" : "done"; const activeAnswerChange = activeInput === 0 ? onAnswerAChange : onAnswerBChange; @@ -210,62 +217,73 @@ const AnswerForm = forwardRef(function Answer className="rounded-md bg-white p-6 shadow-sm ring-1 ring-slate-200 sm:p-8" >
-
+
- updateAnswer(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - handleEnter(); - } - }} - autoComplete="off" - enterKeyHint={activeEnterKeyHint} - inputMode="none" - pattern="-?[0-9]*(\.[0-9]*)?" - className="h-12 w-full min-w-0 rounded-md border border-slate-300 bg-white px-3 text-lg font-semibold text-slate-950 outline-none transition focus:border-emerald-600 focus:ring-2 focus:ring-emerald-200 sm:px-4" - /> +
    + {answerButtons.map((button) => { + const selected = activeInput === button.index; + const completed = button.value !== ""; + const displayValue = + button.value || pendingAnswerLabels[button.index]; + + return ( +
  1. + + + + +
  2. + ); + })} +
-
- {answerButtons.map((button) => { - const selected = activeInput === button.index; - - return ( - - ); - })} -
+ updateAnswer(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + handleEnter(); + } + }} + autoComplete="off" + enterKeyHint={activeEnterKeyHint} + inputMode="none" + pattern="-?[0-9]*(\.[0-9]*)?" + className="h-12 w-full min-w-0 rounded-md border border-slate-300 bg-white px-3 text-lg font-semibold text-slate-950 outline-none transition focus:border-emerald-600 focus:ring-2 focus:ring-emerald-200 sm:px-4" + /> {keypadOpen && !desktopInputDevice ? (
(function Answer ); }); -export default AnswerForm; +export default NumberAnswerForm; diff --git a/components/game/VariableChoiceAnswerForm.tsx b/components/game/VariableChoiceAnswerForm.tsx index 2a9c832..aee6313 100644 --- a/components/game/VariableChoiceAnswerForm.tsx +++ b/components/game/VariableChoiceAnswerForm.tsx @@ -9,31 +9,33 @@ interface VariableChoiceAnswerFormProps { choices: [VariableTermChoice[], VariableTermChoice[]]; selectedChoiceIds: [string | null, string | null]; activeTermIndex: 0 | 1; + pendingChoiceLabels: [string, string]; onSelectChoice: (termIndex: 0 | 1, choiceId: string) => void; - onSubmit: () => void; } export default function VariableChoiceAnswerForm({ choices, selectedChoiceIds, activeTermIndex, + pendingChoiceLabels, onSelectChoice, - onSubmit, }: VariableChoiceAnswerFormProps) { const firstChoiceRef = useRef(null); - const submitButtonRef = useRef(null); - const canSubmit = selectedChoiceIds[0] !== null && selectedChoiceIds[1] !== null; - const activeChoices = choices[activeTermIndex]; + const activeChoices = [...choices[activeTermIndex]].sort( + (a, b) => a.coefficient - b.coefficient, + ); useEffect(() => { firstChoiceRef.current?.focus(); }, [activeTermIndex]); - useEffect(() => { - if (canSubmit) { - submitButtonRef.current?.focus(); - } - }, [canSubmit]); + function getSelectedChoice(index: 0 | 1) { + const selectedChoiceId = selectedChoiceIds[index]; + + if (!selectedChoiceId) return null; + + return choices[index].find((choice) => choice.id === selectedChoiceId) ?? null; + } return (
@@ -42,32 +44,48 @@ export default function VariableChoiceAnswerForm({

{activeTermIndex === 0 ? "첫째 항" : "둘째 항"}을 고르세요.

-

- 괄호를 풀었을 때의 부호를 선택합니다. -

-
{selectedChoiceIds.map((choiceId, index) => { const selected = activeTermIndex === index; + const selectedChoice = getSelectedChoice(index as 0 | 1); + const completed = choiceId !== null && selectedChoice !== null; return ( - - {choiceId ? `${index + 1}항 완료` : `${index + 1}항`} - + + {completed ? ( + + ) : ( + + )} + + + ); })} -
+
- -
); }