Compare commits
2 Commits
909535c14e
...
2779bf1b0e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2779bf1b0e | |||
| 44e4d46c6c |
@@ -9,6 +9,7 @@ import ScoreSummary from "@/components/game/ScoreSummary";
|
||||
import StageIndicator from "@/components/game/StageIndicator";
|
||||
import KatexRenderer from "@/components/math/KatexRenderer";
|
||||
import { useGameStore } from "@/store/gameStore";
|
||||
import type { ProblemCount } from "@/store/gameStore";
|
||||
|
||||
function parseAnswer(value: string) {
|
||||
const trimmed = value.trim();
|
||||
@@ -18,8 +19,16 @@ function parseAnswer(value: string) {
|
||||
return Number(trimmed);
|
||||
}
|
||||
|
||||
export default function PlayClient() {
|
||||
interface PlayClientProps {
|
||||
problemCount: ProblemCount;
|
||||
}
|
||||
|
||||
export default function PlayClient({ problemCount }: PlayClientProps) {
|
||||
const session = useGameStore((state) => state.session);
|
||||
const selectedProblemCount = useGameStore(
|
||||
(state) => state.selectedProblemCount,
|
||||
);
|
||||
const setProblemCount = useGameStore((state) => state.setProblemCount);
|
||||
const initSession = useGameStore((state) => state.initSession);
|
||||
const submitAnswer = useGameStore((state) => state.submitAnswer);
|
||||
const nextProblem = useGameStore((state) => state.nextProblem);
|
||||
@@ -34,15 +43,28 @@ export default function PlayClient() {
|
||||
const isComplete = Boolean(
|
||||
session && session.currentIndex >= session.problems.length,
|
||||
);
|
||||
const sessionNeedsInit =
|
||||
session === null || session.problems.length !== problemCount;
|
||||
const correctCount =
|
||||
session?.results.filter((result) => result?.correct).length ?? 0;
|
||||
const totalCount = session?.problems.length ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (session === null) {
|
||||
initSession();
|
||||
if (selectedProblemCount !== problemCount) {
|
||||
setProblemCount(problemCount);
|
||||
}
|
||||
}, [initSession, session]);
|
||||
|
||||
if (sessionNeedsInit) {
|
||||
initSession(problemCount);
|
||||
}
|
||||
}, [
|
||||
initSession,
|
||||
problemCount,
|
||||
selectedProblemCount,
|
||||
session,
|
||||
sessionNeedsInit,
|
||||
setProblemCount,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setAnswerA("");
|
||||
@@ -67,7 +89,7 @@ export default function PlayClient() {
|
||||
submitAnswer({ a: parsedA, b: parsedB });
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
if (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">
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import PlayClient from "./PlayClient";
|
||||
import type { ProblemCount } from "@/store/gameStore";
|
||||
|
||||
export default function PlayPage() {
|
||||
return <PlayClient />;
|
||||
interface PlayPageProps {
|
||||
searchParams?: {
|
||||
count?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function parseProblemCount(count?: string): ProblemCount {
|
||||
return count === "5" ? 5 : 10;
|
||||
}
|
||||
|
||||
export default function PlayPage({ searchParams }: PlayPageProps) {
|
||||
return <PlayClient problemCount={parseProblemCount(searchParams?.count)} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"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<HTMLAnchorElement>(null);
|
||||
const selectedProblemCount = useGameStore(
|
||||
(state) => state.selectedProblemCount,
|
||||
);
|
||||
const setProblemCount = useGameStore((state) => state.setProblemCount);
|
||||
|
||||
useEffect(() => {
|
||||
startLinkRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div>
|
||||
<Link
|
||||
ref={startLinkRef}
|
||||
href={`/play?count=${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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2
-9
@@ -1,4 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import StartControls from "./StartControls";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
@@ -17,14 +17,7 @@ export default function Home() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link
|
||||
href="/play"
|
||||
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>
|
||||
<StartControls />
|
||||
|
||||
<div className="border-l-4 border-emerald-700 pl-4 text-sm leading-6 text-slate-600">
|
||||
<p>이번 단계에서는 화면 이동만 확인합니다.</p>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
NAS SSH에서 한 번만 실행합니다.
|
||||
|
||||
```bash
|
||||
docker network create npm-proxy
|
||||
docker network create proxy-network
|
||||
```
|
||||
|
||||
이미 같은 이름의 네트워크가 있으면 에러가 나도 괜찮습니다.
|
||||
@@ -22,13 +22,13 @@ docker network create npm-proxy
|
||||
NPM 컨테이너가 이미 실행 중이라면 같은 네트워크에 붙입니다.
|
||||
|
||||
```bash
|
||||
docker network connect npm-proxy <npm-container-name>
|
||||
docker network connect proxy-network <npm-container-name>
|
||||
```
|
||||
|
||||
예를 들어 NPM 컨테이너 이름이 `nginx-proxy-manager`라면:
|
||||
|
||||
```bash
|
||||
docker network connect npm-proxy nginx-proxy-manager
|
||||
docker network connect proxy-network nginx-proxy-manager
|
||||
```
|
||||
|
||||
## 2. 프로젝트 파일 업로드
|
||||
@@ -117,7 +117,7 @@ NPM 관리자 화면에서 Proxy Host를 추가합니다.
|
||||
- SSL: Let's Encrypt 인증서 발급
|
||||
- Force SSL: 활성화
|
||||
|
||||
NPM 컨테이너와 `math-quiz` 컨테이너가 `npm-proxy` 네트워크를 공유해야 `Forward Hostname`에 `math-quiz`를 사용할 수 있습니다.
|
||||
NPM 컨테이너와 `math-quiz` 컨테이너가 `proxy-network` 네트워크를 공유해야 `Forward Hostname`에 `math-quiz`를 사용할 수 있습니다.
|
||||
|
||||
## 6. 업데이트 반영
|
||||
|
||||
@@ -137,12 +137,12 @@ docker logs -f math-quiz
|
||||
|
||||
## 참고: 네트워크 이름이 다를 때
|
||||
|
||||
NPM에서 이미 쓰는 Docker 네트워크 이름이 있다면 `docker-compose.yml`의 `npm-proxy`를 해당 네트워크 이름으로 바꿉니다.
|
||||
NPM에서 이미 쓰는 Docker 네트워크 이름이 있다면 `docker-compose.yml`의 `proxy-network`를 해당 네트워크 이름으로 바꿉니다.
|
||||
|
||||
```yaml
|
||||
networks:
|
||||
npm-proxy:
|
||||
proxy-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
위 블록의 `npm-proxy` 이름과 서비스의 `networks` 값을 같은 이름으로 맞추면 됩니다.
|
||||
위 블록의 `proxy-network` 이름과 서비스의 `networks` 값을 같은 이름으로 맞추면 됩니다.
|
||||
|
||||
+16
-6
@@ -16,17 +16,21 @@ export interface GameSession {
|
||||
results: (GradeResult | null)[];
|
||||
}
|
||||
|
||||
export type ProblemCount = 5 | 10;
|
||||
|
||||
interface GameState {
|
||||
session: GameSession | null;
|
||||
initSession: (count?: number) => void;
|
||||
selectedProblemCount: ProblemCount;
|
||||
setProblemCount: (count: ProblemCount) => void;
|
||||
initSession: (count?: ProblemCount) => void;
|
||||
submitAnswer: (answer: UserAnswer) => void;
|
||||
nextProblem: () => void;
|
||||
resetSession: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_SESSION_COUNT = 10;
|
||||
const DEFAULT_SESSION_COUNT: ProblemCount = 10;
|
||||
|
||||
function createSession(count = DEFAULT_SESSION_COUNT): GameSession {
|
||||
function createSession(count: ProblemCount = DEFAULT_SESSION_COUNT): GameSession {
|
||||
const problems = generateSession(count);
|
||||
|
||||
return {
|
||||
@@ -38,8 +42,14 @@ function createSession(count = DEFAULT_SESSION_COUNT): GameSession {
|
||||
|
||||
export const useGameStore = create<GameState>((set, get) => ({
|
||||
session: null,
|
||||
initSession: (count = DEFAULT_SESSION_COUNT) => {
|
||||
set({ session: createSession(count) });
|
||||
selectedProblemCount: DEFAULT_SESSION_COUNT,
|
||||
setProblemCount: (count) => {
|
||||
set({ selectedProblemCount: count });
|
||||
},
|
||||
initSession: (count) => {
|
||||
const selectedProblemCount = count ?? get().selectedProblemCount;
|
||||
|
||||
set({ session: createSession(selectedProblemCount) });
|
||||
},
|
||||
submitAnswer: (answer) => {
|
||||
const { session } = get();
|
||||
@@ -73,6 +83,6 @@ export const useGameStore = create<GameState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
resetSession: () => {
|
||||
set({ session: createSession() });
|
||||
set({ session: createSession(get().selectedProblemCount) });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user