괄호 앞 음수 분배 스테이지 추가
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
# 02. 괄호 앞 음수 분배 — 간단한 풀이 화면
|
||||
|
||||
## 계획 평가
|
||||
|
||||
Claude Code가 작성한 기존 계획은 방향이 맞다. `01-project-start.md`에서 만든 `/` -> `/play` 흐름 위에 문제 생성 엔진, KaTeX 표시, 답 입력, 채점, 10문제 세션을 붙이는 순서가 자연스럽다.
|
||||
|
||||
다만 실제 구현 단계로 쓰기에는 몇 가지를 조정하는 편이 좋다.
|
||||
|
||||
- 엔진 단위 테스트만 하는 단계에서는 `@vitejs/plugin-react`, `jsdom`, `@testing-library/react`가 아직 필요하지 않다.
|
||||
- `generateSession()`의 중복 방지는 랜덤 `id`가 아니라 `multiplier/a/operator/b` 조합 기준이어야 한다.
|
||||
- Zustand는 한 문제 풀이가 동작한 뒤 도입한다. 이 순서를 지키면 버그가 엔진, UI, 상태 관리 중 어디에서 생겼는지 분리하기 쉽다.
|
||||
- `PlayClient`에서 세션을 초기화할 때 렌더링 중 `initSession()`을 직접 호출하지 않고 `useEffect`에서 호출해야 한다.
|
||||
- 이번 문서는 “간단한 풀이 화면”이 목표이므로 Framer Motion, 정교한 진행 바, 배너 디자인, E2E는 다음 단계로 넘긴다.
|
||||
|
||||
---
|
||||
|
||||
## 목표
|
||||
|
||||
`/play` 화면에서 **괄호 앞 음수 분배** 문제를 10개 풀고 점수를 확인할 수 있게 만든다.
|
||||
|
||||
이번 단계의 최종 상태는 다음과 같다.
|
||||
|
||||
- 자체 알고리즘으로 괄호 앞 음수 분배 문제를 생성한다.
|
||||
- 문제와 정답 수식을 KaTeX로 렌더링한다.
|
||||
- 사용자가 두 항의 답을 입력하고 채점 결과를 확인한다.
|
||||
- 10문제 풀이가 끝나면 점수 요약을 보여준다.
|
||||
|
||||
---
|
||||
|
||||
## 이번 단계에서 할 것
|
||||
|
||||
- `lib/engine/`에 문제 생성·채점 순수 함수와 Vitest 단위 테스트 추가
|
||||
- `components/math/`에 KaTeX 렌더링 컴포넌트 추가
|
||||
- `/play`에 한 문제 풀이 UI 구현
|
||||
- 한 문제 풀이 확인 후 `store/gameStore.ts`로 10문제 세션 구현
|
||||
- 세션 완료 후 간단한 점수 요약 표시
|
||||
|
||||
## 이번 단계에서 하지 않을 것
|
||||
|
||||
- Framer Motion 애니메이션
|
||||
- 정교한 ProgressBar, FeedbackBanner, ScoreSummary 디자인 컴포넌트화
|
||||
- DB, Prisma, Redis, NextAuth
|
||||
- 유형 선택 화면, 모드 선택 화면
|
||||
- 스피드 퀴즈 모드
|
||||
- 미지수 `x`, `y`, `z`가 포함된 문제
|
||||
- Playwright E2E 테스트
|
||||
|
||||
---
|
||||
|
||||
## 의존성
|
||||
|
||||
필요한 시점에 나누어 설치한다.
|
||||
|
||||
### 엔진 테스트
|
||||
|
||||
```bash
|
||||
pnpm add -D vitest
|
||||
```
|
||||
|
||||
### 수식 렌더링과 세션 상태
|
||||
|
||||
```bash
|
||||
pnpm add katex react-katex zustand
|
||||
pnpm add -D @types/katex
|
||||
```
|
||||
|
||||
React 컴포넌트 테스트를 추가하지 않는 한 `@testing-library/react`, `jsdom`, `@vitejs/plugin-react`는 이번 단계에 넣지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 생성 또는 수정 파일
|
||||
|
||||
### 신규 생성
|
||||
|
||||
```text
|
||||
lib/engine/types.ts
|
||||
lib/engine/parenthesis-sign.ts
|
||||
lib/engine/parenthesis-sign.test.ts
|
||||
vitest.config.ts
|
||||
components/math/KatexRenderer.tsx
|
||||
store/gameStore.ts
|
||||
app/(game)/play/PlayClient.tsx
|
||||
```
|
||||
|
||||
### 수정
|
||||
|
||||
```text
|
||||
package.json # test script 추가
|
||||
app/layout.tsx # KaTeX CSS import 추가
|
||||
app/(game)/play/page.tsx # PlayClient 마운트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 작업 순서
|
||||
|
||||
### 2-A. 문제 생성 엔진
|
||||
|
||||
완료 기준: `pnpm test` 통과
|
||||
|
||||
`lib/engine/types.ts`
|
||||
|
||||
```ts
|
||||
export interface ParenthesisSignProblem {
|
||||
id: string
|
||||
key: string
|
||||
multiplier: number
|
||||
a: number
|
||||
operator: '+' | '-'
|
||||
b: number
|
||||
distributedA: number
|
||||
distributedB: number
|
||||
latexBefore: string
|
||||
latexAfter: string
|
||||
}
|
||||
|
||||
export interface UserAnswer {
|
||||
a: number
|
||||
b: number
|
||||
}
|
||||
|
||||
export interface GradeResult {
|
||||
correct: boolean
|
||||
correctAnswer: [number, number]
|
||||
}
|
||||
```
|
||||
|
||||
`lib/engine/parenthesis-sign.ts`에 구현할 함수:
|
||||
|
||||
- `generateProblem(): ParenthesisSignProblem`
|
||||
- `multiplier`: `-9`부터 `-1` 사이 정수
|
||||
- `a`, `b`: `1`부터 `9` 사이 정수
|
||||
- `a !== b`
|
||||
- `operator`: `'+' | '-'`
|
||||
- `distributedA = multiplier * a`
|
||||
- `distributedB = operator === '-' ? multiplier * -b : multiplier * b`
|
||||
- `key`는 `multiplier:a:operator:b` 형식으로 만든다.
|
||||
- `latexBefore`: 예시 `-2(3 - 6)`
|
||||
- `latexAfter`: 예시 `-6 + 12`
|
||||
|
||||
- `generateSession(count = 10): ParenthesisSignProblem[]`
|
||||
- `count`가 `1` 이상인지 확인한다.
|
||||
- 가능한 문제 수는 `9 * 8 * 2 = 144`개이므로 `count > 144`이면 에러를 던진다.
|
||||
- 중복 방지는 `id`가 아니라 `key` 기준으로 한다.
|
||||
|
||||
- `gradeAnswer(problem, answer): GradeResult`
|
||||
- 두 항이 모두 일치할 때만 정답이다.
|
||||
- 입력 순서를 바꾸어 쓴 답은 오답으로 처리한다.
|
||||
|
||||
`lib/engine/parenthesis-sign.test.ts` 검증 항목:
|
||||
|
||||
- `multiplier`는 항상 음수 정수다.
|
||||
- `a`, `b`는 범위 안에 있고 서로 다르다.
|
||||
- `distributedA`, `distributedB` 계산이 `operator`별로 맞다.
|
||||
- `gradeAnswer()`는 정답과 오답을 정확히 구분한다.
|
||||
- `generateSession(10)`은 10개 문제를 만들고 `key` 중복이 없다.
|
||||
- `generateSession(145)`는 에러를 던진다.
|
||||
|
||||
`vitest.config.ts`
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`package.json`
|
||||
|
||||
```json
|
||||
"test": "vitest run"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2-B. KaTeX 렌더링
|
||||
|
||||
완료 기준: 브라우저에서 문제 수식이 KaTeX로 표시됨
|
||||
|
||||
`app/layout.tsx`에 전역 CSS를 추가한다.
|
||||
|
||||
```ts
|
||||
import 'katex/dist/katex.min.css'
|
||||
```
|
||||
|
||||
`components/math/KatexRenderer.tsx`
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { BlockMath, InlineMath } from 'react-katex'
|
||||
|
||||
interface KatexRendererProps {
|
||||
latex: string
|
||||
displayMode?: boolean
|
||||
}
|
||||
|
||||
export default function KatexRenderer({
|
||||
latex,
|
||||
displayMode = false,
|
||||
}: KatexRendererProps) {
|
||||
return displayMode ? <BlockMath math={latex} /> : <InlineMath math={latex} />
|
||||
}
|
||||
```
|
||||
|
||||
이 단계에서는 `/play`에 임시 문제 하나를 표시해서 KaTeX 렌더링만 확인한다.
|
||||
|
||||
---
|
||||
|
||||
### 2-C. 한 문제 풀이 UI
|
||||
|
||||
완료 기준: 한 문제를 풀고 정답·오답 피드백이 표시됨
|
||||
|
||||
`app/(game)/play/PlayClient.tsx`를 클라이언트 컴포넌트로 만든다.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
```
|
||||
|
||||
구성:
|
||||
|
||||
- 문제 수식: `KatexRenderer`로 `problem.latexBefore` 표시
|
||||
- 답 입력: 숫자 입력 두 칸
|
||||
- 제출 버튼
|
||||
- Enter 제출
|
||||
- 정답이면 간단한 성공 문구
|
||||
- 오답이면 정답 수식과 정답 두 항 표시
|
||||
- 새 문제 버튼
|
||||
|
||||
로컬 상태:
|
||||
|
||||
```ts
|
||||
const [problem, setProblem] = useState<ParenthesisSignProblem>(() => generateProblem())
|
||||
const [answerA, setAnswerA] = useState('')
|
||||
const [answerB, setAnswerB] = useState('')
|
||||
const [result, setResult] = useState<GradeResult | null>(null)
|
||||
```
|
||||
|
||||
입력 처리:
|
||||
|
||||
- 빈 문자열은 제출하지 않는다.
|
||||
- `Number.parseInt(value, 10)` 결과가 `NaN`이면 제출하지 않는다.
|
||||
- 채점 후에는 같은 문제에서 중복 제출하지 않도록 제출 버튼을 비활성화한다.
|
||||
|
||||
이 단계에서는 Zustand를 사용하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
### 2-D. 10문제 세션
|
||||
|
||||
완료 기준: 10문제를 모두 풀면 점수 요약 화면이 표시됨
|
||||
|
||||
`store/gameStore.ts`
|
||||
|
||||
```ts
|
||||
interface GameSession {
|
||||
problems: ParenthesisSignProblem[]
|
||||
currentIndex: number
|
||||
results: (GradeResult | null)[]
|
||||
}
|
||||
|
||||
interface GameState {
|
||||
session: GameSession | null
|
||||
initSession: (count?: number) => void
|
||||
submitAnswer: (answer: UserAnswer) => void
|
||||
nextProblem: () => void
|
||||
resetSession: () => void
|
||||
currentProblem: () => ParenthesisSignProblem | null
|
||||
currentResult: () => GradeResult | null
|
||||
isComplete: () => boolean
|
||||
score: () => { correct: number; total: number }
|
||||
}
|
||||
```
|
||||
|
||||
상태 규칙:
|
||||
|
||||
- `initSession(10)`은 10개 문제와 `null` 결과 배열을 만든다.
|
||||
- `submitAnswer()`는 현재 문제의 결과가 `null`일 때만 채점한다.
|
||||
- `nextProblem()`은 채점 후에만 다음 문제로 이동한다.
|
||||
- 마지막 문제에서 `nextProblem()`을 호출하면 완료 상태가 된다.
|
||||
- `resetSession()`은 기존 세션을 버리고 새 세션을 시작한다.
|
||||
|
||||
`PlayClient.tsx` 전환:
|
||||
|
||||
- `useEffect`에서 `session === null`이면 `initSession()` 호출
|
||||
- `currentProblem() === null`이면 로딩 또는 완료 화면 표시
|
||||
- 풀이 중에는 현재 문제 번호를 텍스트로 표시한다. 예: `3 / 10`
|
||||
- 세션 완료 후 맞은 개수와 전체 개수를 표시한다.
|
||||
- 다시 시작 버튼은 `resetSession()`을 호출한다.
|
||||
|
||||
`app/(game)/play/page.tsx`
|
||||
|
||||
```tsx
|
||||
import PlayClient from './PlayClient'
|
||||
|
||||
export default function PlayPage() {
|
||||
return <PlayClient />
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 화면 흐름
|
||||
|
||||
```text
|
||||
/ 시작 화면
|
||||
- "연습 시작" 클릭
|
||||
|
||||
↓
|
||||
|
||||
/play 풀이 화면
|
||||
├─ 현재 문제 번호
|
||||
├─ 문제 수식 (KaTeX)
|
||||
├─ 답 입력 두 칸
|
||||
├─ 제출 버튼
|
||||
├─ 정답/오답 피드백
|
||||
├─ 다음 문제 버튼
|
||||
└─ 10문제 완료 후 점수 요약
|
||||
- n / 10 정답
|
||||
- 다시 시작
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- `pnpm test` 통과
|
||||
- `pnpm lint` 통과
|
||||
- 브라우저에서 `/play` 수식이 KaTeX로 렌더링된다.
|
||||
- 정답 입력 시 정답 피드백이 표시된다.
|
||||
- 오답 입력 시 오답 피드백과 정답이 표시된다.
|
||||
- 10문제를 모두 풀면 점수 요약 화면이 표시된다.
|
||||
- 다시 시작을 누르면 새 10문제 세션이 시작된다.
|
||||
|
||||
## 확인 명령
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
pnpm lint
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
브라우저 확인은 `pnpm dev` 실행 후 `http://localhost:3000/play`에서 한다.
|
||||
|
||||
---
|
||||
|
||||
## 다음 단계
|
||||
|
||||
`03-ux-polish.md`에서 UI 품질을 높인다.
|
||||
|
||||
- 진행률 바
|
||||
- 정답·오답 배너 컴포넌트
|
||||
- 점수 요약 컴포넌트
|
||||
- Framer Motion 기반 전환 애니메이션
|
||||
- 키보드 탐색과 포커스 처리 개선
|
||||
Reference in New Issue
Block a user