From 335898b721a05b1006dfdc2c29d49ab433dc5df8 Mon Sep 17 00:00:00 2001 From: jisangs Date: Fri, 29 May 2026 22:48:23 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=99=EC=8A=B5=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=ED=99=94=EB=A9=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 12 +- README.md | 16 +- .../NumberParenthesesClient.tsx | 142 ++++++++++ app/(game)/number-parentheses/page.tsx | 5 + app/(game)/play/PlayClient.tsx | 4 +- app/StartControls.tsx | 68 ----- app/page.tsx | 84 +++++- docs/vibe/03-select-subject.md | 246 ++++++++++++++++++ 8 files changed, 495 insertions(+), 82 deletions(-) create mode 100644 app/(game)/number-parentheses/NumberParenthesesClient.tsx create mode 100644 app/(game)/number-parentheses/page.tsx delete mode 100644 app/StartControls.tsx create mode 100644 docs/vibe/03-select-subject.md diff --git a/AGENTS.md b/AGENTS.md index d83f0e8..317c8e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,7 @@ AI 에이전트(Claude Code 등)가 이 프로젝트에서 작업할 때 따라 ``` app/ # Next.js App Router 루트 (game)/ # 게임 화면 라우트 그룹 + number-parentheses/ # 숫자 괄호 준비 화면 select-type/ # 문제 유형 선택 select-mode/ # 게임 모드 선택 play/ # 문제 풀이 @@ -87,7 +88,7 @@ prisma/ - 게임 화면에서 재사용되는 입력, 피드백, 점수 요약, 진행 단계 UI는 `components/game/` 하위에 둔다. - 정답·오답 판정 라인처럼 피드백 화면과 결과 화면이 공유하는 표시는 `components/game/` 하위 공통 컴포넌트로 분리한다. - 수식 렌더링 전용 컴포넌트는 `components/math/` 하위에 둔다. -- 라우트별 조립 컴포넌트(`PlayClient` 등)는 해당 `app/` 라우트 폴더에 둘 수 있다. +- 라우트별 조립 컴포넌트(`PlayClient`, `NumberParenthesesClient` 등)는 해당 `app/` 라우트 폴더에 둘 수 있다. ### 접근성 / 키보드 조작 @@ -161,10 +162,15 @@ NEXTAUTH_SECRET=your-secret ### 화면 흐름 ``` -시작 화면 → 문제 유형 선택 → 게임 모드 선택 → 문제 풀이 → 결과 및 기록 +학습 내용 선택 → 숫자 괄호 준비 → 문제 풀이 → 결과 및 기록 ``` -- 시작 화면은 문제 수 `[5] [10]` 선택 버튼 그룹을 제공하고 기본값은 `10`이다. +- 시작 화면(`/`)은 학습 내용 선택 화면이다. +- 현재 활성 학습 내용은 `숫자 괄호 풀기`이며 `/number-parentheses`로 이동한다. +- `분수 괄호 풀기`, `실수 괄호 풀기`, `미지수 괄호 풀기`는 준비 중 항목으로 disabled 상태로 표시한다. +- 숫자 괄호 준비 화면(`/number-parentheses`)은 문제 수 `[5] [10]` 선택 버튼 그룹을 제공하고 기본값은 `10`이다. +- 숫자 괄호 준비 화면은 괄호 종류 `()`, `{}`, `[]` 버튼 그룹을 제공한다. 현재는 `()`만 선택 가능하고 `{}`, `[]`는 disabled 상태로 표시한다. +- 준비 화면의 `연습 시작`은 Zustand 세션을 초기화하고 `/play?count=N`으로 이동한다. - 문제 풀이 화면은 URL의 `count` 쿼리와 Zustand의 선택 문제 수를 동기화한다. 허용 문제 수는 `5`, `10`이며 유효하지 않은 값은 `10`으로 처리한다. - 정답·오답 피드백은 `계산식 = 내가 입력한 답` 형식으로 표시한다. 정답은 `○`, 오답은 붉은색 입력값과 `×`, 굵은 정답값을 함께 보여준다. - 최종 결과 화면은 지금까지 푼 문제를 `문제 = 내가 입력한 답` 형식으로 나열하고, 피드백 화면과 같은 정답·오답 색상 및 기호 규칙을 사용한다. diff --git a/README.md b/README.md index 6f99a00..5a8d862 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,19 @@ ## 화면 흐름 ``` -시작 화면 → 문제 유형 선택 → 게임 모드 선택 → 문제 풀이 → 결과 및 기록 +학습 내용 선택 → 숫자 괄호 준비 → 문제 풀이 → 결과 및 기록 ``` --- ## 현재 구현 상태 -- 시작 화면에서 문제 수를 5개 또는 10개로 선택한 뒤 `/play` 풀이 화면으로 이동할 수 있습니다. +- 시작 화면(`/`)에서 학습 내용을 선택합니다. + - `숫자 괄호 풀기`는 활성 상태이며 `/number-parentheses` 준비 화면으로 이동합니다. + - `분수 괄호 풀기`, `실수 괄호 풀기`, `미지수 괄호 풀기`는 준비 중 항목으로 비활성화되어 있습니다. +- 숫자 괄호 준비 화면(`/number-parentheses`)에서 문제 수를 5개 또는 10개로 선택한 뒤 `/play` 풀이 화면으로 이동할 수 있습니다. - 문제 수 기본값은 10개이며, `/play?count=5` 또는 `/play?count=10`으로 선택값을 전달합니다. +- 숫자 괄호 준비 화면의 괄호 종류는 현재 `()`만 선택 가능하고 `{}`, `[]`는 비활성화되어 있습니다. - 문제와 정답 수식은 KaTeX로 렌더링합니다. - 진행 단계는 현재 문제, 완료된 정답, 완료된 오답, 남은 문제를 구분해서 표시합니다. - 정답·오답 피드백은 각 항의 `계산식 = 내가 입력한 답` 형식으로 표시합니다. @@ -124,6 +128,7 @@ pnpm start ``` app/ # Next.js App Router 루트 (game)/ # 게임 화면 라우트 그룹 + number-parentheses/ # 숫자 괄호 준비 화면 select-type/ # 문제 유형 선택 select-mode/ # 게임 모드 선택 play/ # 문제 풀이 @@ -149,6 +154,13 @@ prisma/ --- +## 다음 확장 방향 + +- 분수 괄호, 실수 괄호, 미지수 괄호는 각 문제 생성 엔진이 준비되면 시작 화면에서 활성화합니다. +- `{}`, `[]` 괄호 종류는 생성 엔진과 상태/URL 파라미터 연동이 준비된 뒤 숫자 괄호 준비 화면에서 활성화합니다. + +--- + ## 배포 - **플랫폼**: Synology NAS + Docker Compose + Nginx Proxy Manager diff --git a/app/(game)/number-parentheses/NumberParenthesesClient.tsx b/app/(game)/number-parentheses/NumberParenthesesClient.tsx new file mode 100644 index 0000000..a0daf01 --- /dev/null +++ b/app/(game)/number-parentheses/NumberParenthesesClient.tsx @@ -0,0 +1,142 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useRef } from "react"; + +import { useGameStore } from "@/store/gameStore"; +import type { ProblemCount } from "@/store/gameStore"; + +const problemCounts: ProblemCount[] = [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 NumberParenthesesClient() { + const startLinkRef = useRef(null); + const selectedProblemCount = useGameStore( + (state) => state.selectedProblemCount, + ); + const setProblemCount = useGameStore((state) => state.setProblemCount); + const initSession = useGameStore((state) => state.initSession); + + useEffect(() => { + startLinkRef.current?.focus(); + }, []); + + return ( +
+
+
+

+ 숫자 괄호 학습 준비 +

+

+ 숫자 괄호 풀기 +

+

+ 정수로 이루어진 괄호식을 풀며 음수 분배와 부호 변화를 + 연습합니다. +

+
+ +
+
+ + 문제 수 + +
+ {problemCounts.map((count) => { + const selected = selectedProblemCount === count; + + return ( + + ); + })} +
+
+ +
+ + 괄호 종류 + +
+ {bracketKinds.map((bracketKind) => ( + + ))} +
+
+ +
+ 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" + > + 연습 시작 + +
+
+ +
+

현재는 소괄호 숫자 문제만 선택할 수 있습니다.

+
+
+
+ ); +} diff --git a/app/(game)/number-parentheses/page.tsx b/app/(game)/number-parentheses/page.tsx new file mode 100644 index 0000000..30a2587 --- /dev/null +++ b/app/(game)/number-parentheses/page.tsx @@ -0,0 +1,5 @@ +import NumberParenthesesClient from "./NumberParenthesesClient"; + +export default function NumberParenthesesPage() { + return ; +} diff --git a/app/(game)/play/PlayClient.tsx b/app/(game)/play/PlayClient.tsx index e0c2cca..e3c790a 100644 --- a/app/(game)/play/PlayClient.tsx +++ b/app/(game)/play/PlayClient.tsx @@ -121,7 +121,7 @@ export default function PlayClient({ problemCount }: PlayClientProps) { const rect = katexElement.getBoundingClientRect(); const scrollElement = document.scrollingElement ?? document.documentElement; - const elementDocumentTop = window.pageYOffset + rect.top; + const elementDocumentTop = window.scrollY + rect.top; const maxScrollTop = Math.max( 0, scrollElement.scrollHeight - window.innerHeight, @@ -131,7 +131,7 @@ export default function PlayClient({ problemCount }: PlayClientProps) { Math.max(0, elementDocumentTop), ); - if (Math.abs(window.pageYOffset - scrollTop) < 4) return; + if (Math.abs(window.scrollY - scrollTop) < 4) return; scrollElement.scrollTop = scrollTop; document.body.scrollTop = scrollTop; diff --git a/app/StartControls.tsx b/app/StartControls.tsx deleted file mode 100644 index d84058b..0000000 --- a/app/StartControls.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useEffect, useRef } from "react"; - -import { useGameStore } from "@/store/gameStore"; -import type { ProblemCount } from "@/store/gameStore"; - -const problemCounts: ProblemCount[] = [5, 10]; - -export default function StartControls() { - const startLinkRef = useRef(null); - const selectedProblemCount = useGameStore( - (state) => state.selectedProblemCount, - ); - const setProblemCount = useGameStore((state) => state.setProblemCount); - const initSession = useGameStore((state) => state.initSession); - - useEffect(() => { - startLinkRef.current?.focus(); - }, []); - - return ( -
-
- 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" - > - 연습 시작 - -
- -
- - 문제 수 - -
- {problemCounts.map((count) => { - const selected = selectedProblemCount === count; - - return ( - - ); - })} -
-
-
- ); -} diff --git a/app/page.tsx b/app/page.tsx index 4690264..ce8c793 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,4 +1,41 @@ -import StartControls from "./StartControls"; +import Link from "next/link"; + +type Subject = + | { + label: string; + description: string; + href: string; + disabled: false; + } + | { + label: string; + description: string; + disabled: true; + }; + +const subjects: Subject[] = [ + { + label: "숫자 괄호 풀기", + description: "정수로 이루어진 괄호식을 풀며 부호 변화를 연습합니다.", + href: "/number-parentheses", + disabled: false, + }, + { + label: "분수 괄호 풀기", + description: "분수가 들어간 괄호식은 이후 단계에서 추가됩니다.", + disabled: true, + }, + { + label: "실수 괄호 풀기", + description: "실수가 들어간 괄호식은 이후 단계에서 추가됩니다.", + disabled: true, + }, + { + label: "미지수 괄호 풀기", + description: "미지수가 들어간 괄호식은 이후 단계에서 추가됩니다.", + disabled: true, + }, +]; export default function Home() { return ( @@ -6,21 +43,54 @@ export default function Home() {

- 중학교 2학년 수학 - 괄호 풀기 연습 + 중학교 2학년 수학 - 학습 내용 선택

- 괄호 음수 분배 연습 + 오늘 연습할 괄호 유형을 고르세요

- 괄호 앞의 음수를 분배할 때 부호가 어떻게 바뀌는지 차근차근 - 연습합니다. + 괄호 앞의 음수를 분배할 때 부호가 어떻게 바뀌는지 학습 + 내용별로 차근차근 연습합니다.

- +
+ {subjects.map((subject) => { + if (!subject.disabled) { + return ( + + + {subject.label} + + + {subject.description} + + + ); + } + + return ( + + ); + })} +
-

반복할 문제 수를 선택한 뒤, [연습 시작] 버튼을 클릭하세요.

+

지금은 숫자 괄호 학습만 사용할 수 있습니다.

diff --git a/docs/vibe/03-select-subject.md b/docs/vibe/03-select-subject.md new file mode 100644 index 0000000..7150a2b --- /dev/null +++ b/docs/vibe/03-select-subject.md @@ -0,0 +1,246 @@ +# 03. 학습 내용 선택과 숫자 괄호 준비 화면 + +## 계획 평가 + +Codex가 작성한 기존 계획은 방향이 맞다. `StartControls.tsx` 이동, `/number-parentheses` 라우트 신설, 첫 화면을 학습 내용 선택으로 전환하는 흐름이 현재 코드 구조와 잘 맞는다. + +다만 실제 코드 상태와 맞추어 조정할 부분이 있다. + +- `PlayClient`는 URL `?count=N`으로 문제 수를 받고, `gameStore`에 `selectedProblemCount`와 `setProblemCount`가 이미 구현되어 있다. 준비 화면에서 이 흐름을 그대로 이어받으면 된다. +- 괄호 종류 선택은 이번 단계에서 UI만 추가한다. `()`만 선택 가능하고 `{}`, `[]`는 disabled로 표시한다. 괄호 종류를 Zustand나 URL에 저장하는 것은 실제 문제 생성이 지원할 때로 미룬다. +- `app/StartControls.tsx`는 준비 화면으로 내용을 옮긴 뒤 삭제한다. +- 문제 풀이 화면(`PlayClient.tsx`, `play/page.tsx`)은 이번 단계에서 수정하지 않는다. + +--- + +## 목표 + +앞으로 학습 내용을 계속 추가할 수 있도록 첫 화면을 학습 내용 선택 화면으로 바꾸고, 현재 숫자 괄호 풀이 시작 기능을 별도 준비 화면으로 분리한다. + +이번 단계의 최종 상태는 다음과 같다. + +- `/`에서 학습 내용을 선택한다. +- `숫자 괄호`를 선택하면 문제 수와 괄호 종류를 고르는 준비 화면으로 이동한다. +- 분수 괄호, 실수 괄호, 미지수 괄호는 버튼만 표시하고 비활성화한다. +- `/play` 문제 풀이 화면과 풀이 로직은 현재 동작 그대로 유지한다. + +--- + +## 이번 단계에서 할 것 + +- `app/page.tsx`를 학습 내용 선택 화면으로 교체 +- `app/(game)/number-parentheses/page.tsx`, `NumberParenthesesClient.tsx` 신규 생성 +- `StartControls.tsx`의 문제 수 선택과 연습 시작 흐름을 준비 화면으로 이동 +- 준비 화면에 괄호 종류 버튼 그룹 추가 (`()` 선택 가능, `{}` / `[]` disabled) +- `app/StartControls.tsx` 삭제 + +## 이번 단계에서 하지 않을 것 + +- 분수 괄호, 실수 괄호, 미지수 괄호 문제 생성 +- `{}`, `[]` 괄호 문제 생성 +- 괄호 종류를 Zustand나 URL에 저장 +- 문제 풀이 화면 UI 수정 +- 새로운 게임 모드 추가 +- DB, Redis, NextAuth + +--- + +## 생성 또는 수정 파일 + +### 신규 생성 + +```text +app/(game)/number-parentheses/page.tsx +app/(game)/number-parentheses/NumberParenthesesClient.tsx +``` + +### 수정 + +```text +app/page.tsx +``` + +### 삭제 + +```text +app/StartControls.tsx +``` + +### 변경 없음 + +```text +app/(game)/play/page.tsx +app/(game)/play/PlayClient.tsx +store/gameStore.ts +lib/engine/ +components/ +``` + +--- + +## 작업 순서 + +### 3-A. 숫자 괄호 준비 화면 신설 + +완료 기준: `/number-parentheses`에서 문제 수를 고르고 연습을 시작할 수 있다. + +`app/(game)/number-parentheses/page.tsx`는 Server Component 래퍼로 만든다. + +```tsx +import NumberParenthesesClient from './NumberParenthesesClient' + +export default function NumberParenthesesPage() { + return +} +``` + +`app/(game)/number-parentheses/NumberParenthesesClient.tsx`에는 `'use client'`를 붙이고, 현재 `StartControls.tsx`의 내용을 기반으로 구성한다. + +**문제 수 버튼 그룹** + +현재 `StartControls.tsx`의 문제 수 선택 로직을 그대로 가져온다. + +- `useGameStore`에서 `selectedProblemCount`, `setProblemCount`, `initSession`을 가져온다. +- `5`, `10` 버튼 그룹 (기본값 `10`) +- 선택된 문제 수는 `selectedProblemCount`로 표시한다. + +**괄호 종류 버튼 그룹** + +```tsx +type BracketKind = 'round' | 'curly' | 'square' + +const brackets: { kind: BracketKind; label: string; disabled: boolean }[] = [ + { kind: 'round', label: '( )', disabled: false }, + { kind: 'curly', label: '{ }', disabled: true }, + { kind: 'square', label: '[ ]', disabled: true }, +] +``` + +- `round`는 항상 selected 상태로 고정 표시한다. +- `curly`, `square`는 disabled 스타일을 적용하고 클릭을 막는다. +- 괄호 종류 상태를 useState나 Zustand에 저장하지 않는다. 이번 단계에서는 `()` 하나만 유효하다. + +**연습 시작 버튼** + +현재 `StartControls.tsx`의 Link를 그대로 가져온다. + +```tsx + initSession(selectedProblemCount)} +> + 연습 시작 + +``` + +**뒤로 가기 링크** + +준비 화면에서 첫 화면으로 돌아갈 수 있도록 "처음으로" 링크를 추가한다. + +```tsx +처음으로 +``` + +--- + +### 3-B. 첫 화면을 학습 내용 선택으로 교체 + +완료 기준: `/`에서 네 가지 학습 내용 버튼이 표시된다. + +`app/page.tsx`를 수정한다. + +- 제목과 안내 문구를 여러 학습 내용을 선택하는 맥락으로 바꾼다. +- `StartControls` 호출을 제거한다. +- 학습 내용 목록을 렌더링한다. + +버튼 목록: + +| 학습 내용 | 상태 | 이동 | +|---|---|---| +| 숫자 괄호 | 활성 | `/number-parentheses` | +| 분수 괄호 | disabled | — | +| 실수 괄호 | disabled | — | +| 미지수 괄호 | disabled | — | + +활성 버튼은 `Link`로, disabled 버튼은 `