Compare commits

...

2 Commits

6 changed files with 131 additions and 29 deletions
+27 -5
View File
@@ -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">
+13 -2
View File
@@ -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)} />;
}
+66
View File
@@ -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
View File
@@ -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>
+7 -7
View File
@@ -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
View File
@@ -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) });
},
}));