미지수 괄호 문제 풀이 추가
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import {
|
||||
useVariableGameStore,
|
||||
type VariableProblemCount,
|
||||
} from "@/store/variableGameStore";
|
||||
|
||||
const problemCounts: VariableProblemCount[] = [5, 10];
|
||||
|
||||
const bracketKinds = [
|
||||
{
|
||||
label: "()",
|
||||
name: "소괄호",
|
||||
selected: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: "{}",
|
||||
name: "중괄호",
|
||||
selected: false,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: "[]",
|
||||
name: "대괄호",
|
||||
selected: false,
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function VariableParenthesesClient() {
|
||||
const startLinkRef = useRef<HTMLAnchorElement>(null);
|
||||
const selectedProblemCount = useVariableGameStore(
|
||||
(state) => state.selectedProblemCount,
|
||||
);
|
||||
const setProblemCount = useVariableGameStore(
|
||||
(state) => state.setProblemCount,
|
||||
);
|
||||
const initSession = useVariableGameStore((state) => state.initSession);
|
||||
|
||||
useEffect(() => {
|
||||
startLinkRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen items-center bg-slate-50 px-6 py-12 text-slate-950">
|
||||
<section className="mx-auto flex w-full max-w-3xl flex-col gap-8">
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-semibold text-emerald-700">
|
||||
미지수 괄호 학습 준비
|
||||
</p>
|
||||
<h1 className="text-4xl font-bold tracking-normal sm:text-5xl">
|
||||
미지수 괄호 풀기
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg leading-8 text-slate-700">
|
||||
괄호 안의 한 항에 미지수 x가 들어간 식을 풀며 부호 변화를
|
||||
연습합니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<fieldset className="space-y-3">
|
||||
<legend className="text-sm font-semibold text-slate-700">
|
||||
문제 수
|
||||
</legend>
|
||||
<div
|
||||
className="inline-flex gap-1 rounded-md border border-slate-300 bg-white p-1 shadow-sm"
|
||||
role="group"
|
||||
aria-label="문제 수 선택"
|
||||
>
|
||||
{problemCounts.map((count) => {
|
||||
const selected = selectedProblemCount === count;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={count}
|
||||
type="button"
|
||||
onClick={() => setProblemCount(count)}
|
||||
aria-pressed={selected}
|
||||
className={`h-10 min-w-16 rounded px-4 text-base font-semibold transition focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 ${
|
||||
selected
|
||||
? "bg-emerald-700 text-white"
|
||||
: "text-slate-700 hover:bg-slate-100"
|
||||
}`}
|
||||
>
|
||||
{count}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="space-y-3">
|
||||
<legend className="text-sm font-semibold text-slate-700">
|
||||
괄호 종류
|
||||
</legend>
|
||||
<div
|
||||
className="inline-flex gap-1 rounded-md border border-slate-300 bg-white p-1 shadow-sm"
|
||||
role="group"
|
||||
aria-label="괄호 종류 선택"
|
||||
>
|
||||
{bracketKinds.map((bracketKind) => (
|
||||
<button
|
||||
key={bracketKind.label}
|
||||
type="button"
|
||||
disabled={bracketKind.disabled}
|
||||
aria-pressed={bracketKind.selected}
|
||||
aria-label={`${bracketKind.name} ${bracketKind.disabled ? "준비 중" : "선택됨"}`}
|
||||
className={`h-10 min-w-16 rounded px-4 text-base font-semibold transition focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 ${
|
||||
bracketKind.selected
|
||||
? "bg-emerald-700 text-white"
|
||||
: "text-slate-700 hover:bg-slate-100"
|
||||
} ${
|
||||
bracketKind.disabled
|
||||
? "cursor-not-allowed bg-slate-100 text-slate-400 hover:bg-slate-100"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{bracketKind.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div>
|
||||
<Link
|
||||
ref={startLinkRef}
|
||||
href={`/variable-parentheses/play?count=${selectedProblemCount}`}
|
||||
onClick={() => initSession(selectedProblemCount)}
|
||||
className="inline-flex h-12 items-center justify-center rounded-md bg-emerald-700 px-6 text-base font-semibold text-white transition hover:bg-emerald-800 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2"
|
||||
>
|
||||
연습 시작
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-emerald-700 pl-4 text-sm leading-6 text-slate-600">
|
||||
<p>현재는 소괄호와 미지수 x 문제만 선택할 수 있습니다.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import VariableParenthesesClient from "./VariableParenthesesClient";
|
||||
|
||||
export default function VariableParenthesesPage() {
|
||||
return <VariableParenthesesClient />;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
|
||||
import StageIndicator from "@/components/game/StageIndicator";
|
||||
import VariableChoiceAnswerForm from "@/components/game/VariableChoiceAnswerForm";
|
||||
import VariableFeedbackPanel from "@/components/game/VariableFeedbackPanel";
|
||||
import VariableScoreSummary from "@/components/game/VariableScoreSummary";
|
||||
import KatexRenderer from "@/components/math/KatexRenderer";
|
||||
import {
|
||||
useVariableGameStore,
|
||||
type VariableProblemCount,
|
||||
} from "@/store/variableGameStore";
|
||||
|
||||
interface VariablePlayClientProps {
|
||||
problemCount: VariableProblemCount;
|
||||
}
|
||||
|
||||
export default function VariablePlayClient({
|
||||
problemCount,
|
||||
}: VariablePlayClientProps) {
|
||||
const session = useVariableGameStore((state) => state.session);
|
||||
const selectedProblemCount = useVariableGameStore(
|
||||
(state) => state.selectedProblemCount,
|
||||
);
|
||||
const setProblemCount = useVariableGameStore(
|
||||
(state) => state.setProblemCount,
|
||||
);
|
||||
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 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(() => {
|
||||
setSelectedChoiceIds([null, null]);
|
||||
setActiveTermIndex(0);
|
||||
}, [currentProblem?.key]);
|
||||
|
||||
function handleSelectChoice(termIndex: 0 | 1, choiceId: string) {
|
||||
setSelectedChoiceIds((current) => {
|
||||
const next: [string | null, string | null] = [...current];
|
||||
next[termIndex] = choiceId;
|
||||
return next;
|
||||
});
|
||||
|
||||
if (termIndex === 0) {
|
||||
setActiveTermIndex(1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (currentResult || !currentProblem) return;
|
||||
if (selectedChoiceIds[0] === null || selectedChoiceIds[1] === null) return;
|
||||
|
||||
submitAnswer({ selectedChoiceIds });
|
||||
}
|
||||
|
||||
function handleNextProblem() {
|
||||
flushSync(() => {
|
||||
nextProblem();
|
||||
});
|
||||
}
|
||||
|
||||
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 (
|
||||
<VariableScoreSummary
|
||||
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 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 ? (
|
||||
<VariableChoiceAnswerForm
|
||||
choices={currentProblem.choices}
|
||||
selectedChoiceIds={selectedChoiceIds}
|
||||
activeTermIndex={activeTermIndex}
|
||||
onSelectChoice={handleSelectChoice}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{currentResult ? (
|
||||
<VariableFeedbackPanel
|
||||
result={currentResult}
|
||||
problem={currentProblem}
|
||||
isLastProblem={isLastProblem}
|
||||
onNext={handleNextProblem}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import VariablePlayClient from "./VariablePlayClient";
|
||||
import type { VariableProblemCount } from "@/store/variableGameStore";
|
||||
|
||||
interface VariablePlayPageProps {
|
||||
searchParams?: {
|
||||
count?: string | string[];
|
||||
};
|
||||
}
|
||||
|
||||
function parseProblemCount(count?: string | string[]): VariableProblemCount {
|
||||
if (Array.isArray(count)) {
|
||||
return parseProblemCount(count[0]);
|
||||
}
|
||||
|
||||
return count === "5" ? 5 : 10;
|
||||
}
|
||||
|
||||
export default function VariablePlayPage({
|
||||
searchParams,
|
||||
}: VariablePlayPageProps) {
|
||||
return (
|
||||
<VariablePlayClient problemCount={parseProblemCount(searchParams?.count)} />
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user