+
{children}
diff --git a/service/app/src/app/game/[gameId]/play/__tests__/game-play-e2e-scenarios.md b/service/app/src/app/game/[gameId]/play/__tests__/game-play-e2e-scenarios.md
index 848bdc76..038aa85a 100644
--- a/service/app/src/app/game/[gameId]/play/__tests__/game-play-e2e-scenarios.md
+++ b/service/app/src/app/game/[gameId]/play/__tests__/game-play-e2e-scenarios.md
@@ -4,6 +4,7 @@
- **URL**: `/game/[gameId]/play`
- **핵심 기능**: 게임 진행, 문제 네비게이션, 점수 관리, 정답 확인
+- **조작 방식**: 버튼 클릭 또는 키보드 (Enter, →, ←)
## 테스트 시나리오
@@ -11,12 +12,11 @@
- **홈 로고**: 클릭 시 나가기 다이얼로그 표시 → "네" 클릭 시 홈 이동
- **프로그레스바**: 현재 라운드/전체 라운드 비율로 진행률 표시
-- **이전 문제 버튼**: 첫 번째 문제에서는 숨김 처리
-- **다음 문제 버튼**: 항상 표시
- **게임 종료 버튼**: Cross 아이콘, 클릭 시 나가기 다이얼로그
- **문제 텍스트**: 현재 라운드의 질문 표시
- **문제 이미지**: 이미지가 있는 경우 표시, 없는 경우 숨김
-- **정답 보기 버튼**: 기본 상태에서 표시
+- **"정답은?" 버튼**: 정답 숨김 상태에서 표시
+- **"다음" 버튼**: 정답 공개 상태에서 표시
### 2. 점수판 기능
@@ -33,33 +33,37 @@
- **점수 범위 제한**: -100점 ~ +100점 범위 내에서만 조정 가능
- **점수 표시**: "X점" 형태로 표시
-### 3. 문제 네비게이션
+### 3. 정답 공개 및 문제 이동
-#### 3.1 이전 문제
+#### 3.1 버튼 클릭 조작
-- **첫 번째 문제**: "이전 문제" 버튼 숨김
-- **두 번째 문제 이상**: "이전 문제" 버튼 표시
-- **이전 문제 이동**: 클릭 시 이전 라운드로 이동, 프로그레스바 감소
-- **정답 상태 초기화**: 이전 문제 이동 시 정답 숨김 상태로 리셋
+- **"정답은?" 버튼 클릭**: 정답 공개, URL에 `answer=true` 추가, "다음" 버튼 표시
+- **"다음" 버튼 클릭**: 다음 문제로 이동, 정답 숨김 상태로 시작
-#### 3.2 다음 문제
+#### 3.2 키보드 조작
-- **중간 문제**: "다음 문제" 버튼 클릭 시 다음 라운드로 이동
-- **마지막 문제**: "다음 문제" 버튼 클릭 시 결과 페이지(`/game/[gameId]/result`)로 이동
-- **정답 상태 초기화**: 다음 문제 이동 시 정답 숨김 상태로 리셋
+| 키 | 정답 숨김 상태 | 정답 공개 상태 |
+|---|---|---|
+| Enter | 정답 공개 | 다음 문제 (정답 숨김) |
+| → (ArrowRight) | 정답 공개 | 다음 문제 (정답 숨김) |
+| ← (ArrowLeft) | 이전 문제 (정답 숨김) | 이전 문제 (정답 숨김) |
-### 4. 정답 확인 기능
+- **1번 문제에서 ← 키**: 무반응 (이전 문제 없음)
+- **마지막 문제 정답 공개 후 Enter/→**: 결과 페이지(`/game/[gameId]/result`)로 이동
-#### 4.1 정답 보기
+### 4. 문제 네비게이션
-- **정답 보기 버튼**: 클릭 시 정답 텍스트 표시
-- **정답 숨기기 버튼**: 정답이 표시된 상태에서 클릭 시 정답 숨김
-- **정답 텍스트**: `currentQuestion.questionAnswer` 표시
+#### 4.1 이전 문제
-#### 4.2 정답 상태 유지
+- **1번 문제**: ← 키 입력 시 무반응
+- **2번 문제 이상**: ← 키 입력 시 이전 라운드로 이동
+- **정답 상태 초기화**: 이전 문제 이동 시 정답 숨김 상태로 시작
-- **라운드 변경 시**: 이전/다음 문제 이동 시 정답 상태 초기화
-- **페이지 새로고침**: 정답 상태 유지 (Zustand persist)
+#### 4.2 다음 문제
+
+- **중간 문제**: Enter/→ 키 또는 "다음" 버튼으로 다음 라운드 이동
+- **마지막 문제**: 결과 페이지로 이동
+- **정답 상태 초기화**: 다음 문제 이동 시 정답 숨김 상태로 시작
### 5. 나가기 기능
@@ -77,15 +81,15 @@
### 6. 프로그레스바 동작
- **진행률 계산**: `(currentRound / totalRounds) * 100`
-- **첫 번째 문제**: 0% 또는 최소값
-- **마지막 문제**: 100% 또는 최대값
-- **중간 문제**: 해당 라운드에 맞는 진행률 표시
+- **다음 문제 이동**: 진행률 증가
+- **이전 문제 이동**: 진행률 감소
### 7. URL 파라미터 동작
-- **라운드 변경 시**: `?q=${newRound}` 형태로 URL 업데이트
-- **이전 문제**: `router.replace()` 사용하여 히스토리 스택에 추가하지 않음
-- **다음 문제**: `router.replace()` 사용하여 히스토리 스택에 추가하지 않음
+- **라운드 변경**: `?q=${round}` 형태로 URL 업데이트
+- **정답 공개**: `?q=${round}&answer=true` 형태로 URL 업데이트
+- **정답 숨김**: `answer` 파라미터 제거
+- **초기 상태**: query parameter 없음 (1번 문제, 정답 숨김)
### 8. 게임 상태 관리
@@ -94,15 +98,41 @@
- **게임 상세 정보**: 문제 목록과 질문/답변 데이터 관리
- **상태 지속성**: 페이지 새로고침 시에도 상태 유지 (팀 구성은 reload 후에도 유지, 점수는 이탈/종료 시 초기화됨)
+## 구현된 테스트 목록
+
+### 게임 진행 - 기본 UI
+- [x] 1번 문제: '정답은?' 버튼이 표시되어야 한다
+- [x] 홈 로고 클릭 시 나가기 다이얼로그가 뜨고, '아니요' 클릭 시 페이지에 머문다
+- [x] 게임 종료 아이콘 클릭 시 나가기 다이얼로그가 뜬다
+- [x] 점수판을 접고 다시 펼칠 수 있어야 한다
+- [x] 팀 점수를 클릭 당 +1, -1씩 조정할 수 있다
+
+### 게임 진행 - 버튼 클릭 조작
+- [x] '정답은?' 버튼 클릭 시 정답이 공개되고 '다음' 버튼이 표시된다
+- [x] 정답 공개 후 '다음' 버튼 클릭 시 다음 문제로 이동한다
+
+### 게임 진행 - 키보드 조작
+- [x] Enter 키로 정답을 공개할 수 있다
+- [x] → 키로 정답을 공개할 수 있다
+- [x] 정답 공개 후 Enter 키로 다음 문제로 이동할 수 있다
+- [x] 정답 공개 후 → 키로 다음 문제로 이동할 수 있다
+- [x] ← 키로 이전 문제로 이동할 수 있다 (2번 문제에서)
+- [x] 1번 문제에서 ← 키는 무반응이다
+- [x] 이전 문제로 돌아가면 정답 숨김 상태로 시작한다
+
+### 게임 진행 - 문제 이동 및 진행률
+- [x] 다음 문제로 이동하면 진행률이 증가한다
+- [x] 이전 문제로 이동하면 진행률이 감소한다
+
## 우선순위
-- **Critical**: 문제 네비게이션, 정답 확인, 나가기 기능
+- **Critical**: 정답 공개/숨김, 문제 네비게이션, 키보드 조작, 나가기 기능
- **High**: 점수 관리, 프로그레스바, 기본 UI 렌더링
- **Medium**: 점수판 펼침/접음, URL 파라미터
## 테스트 데이터 요구사항
-- **최소 2개 이상의 문제가 있는 게임**
+- **최소 3개 이상의 문제가 있는 게임** (진행률 테스트를 위해)
- **이미지가 있는 문제와 없는 문제 혼재**
- **기본 A팀, B팀 설정**
- **다양한 점수 범위 테스트 (-100 ~ +100)**
diff --git a/service/app/src/app/game/[gameId]/play/__tests__/game-play.spec.ts b/service/app/src/app/game/[gameId]/play/__tests__/game-play.spec.ts
index 9ce75d04..eb22f1be 100644
--- a/service/app/src/app/game/[gameId]/play/__tests__/game-play.spec.ts
+++ b/service/app/src/app/game/[gameId]/play/__tests__/game-play.spec.ts
@@ -3,40 +3,28 @@ import { expect, test } from "@playwright/test"
import { GameSetupPOM } from "../../setup/__tests__/gameSetupPOM"
import { GamePlayPOM } from "./gamePlayPOM"
-// E2E: 이미지 맞추기 게임 - 두 번째 문제 플로우 (503/504/505)
+// E2E: 게임 플레이 페이지 테스트
// guides/playwright-convention.mdc 및 test-convention.mdc 준수
-test.describe("게임 진행 - 두 번째 문제 플로우", () => {
+test.describe("게임 진행 - 기본 UI", () => {
let pageObj: GamePlayPOM
let setupPage: GameSetupPOM
test.beforeEach(async ({ page }) => {
- // Given: /setup에서 게임 시작을 통해 playing 상태로 전환
setupPage = new GameSetupPOM(page)
await setupPage.goto("1")
await setupPage.startGame()
-
- // When: /play로 이동 (초기 1번 문제)
pageObj = new GamePlayPOM(page)
})
- test("1번 문제: 이전 버튼은 비활성화되고 다음 버튼은 활성화되어야 한다", async () => {
- await expect(pageObj.prevQuestionButton).toBeDisabled()
- await expect(pageObj.nextQuestionButton).toBeEnabled()
+ test("1번 문제: '정답은?' 버튼이 표시되어야 한다", async () => {
+ await expect(pageObj.showAnswerButton).toBeVisible()
+ await expect(pageObj.nextQuestionButton).not.toBeVisible()
const progress = await pageObj.getProgressValue()
expect(progress).not.toBeNull()
})
- test("2번 문제: 이전/다음 버튼이 모두 활성화되어야 한다", async ({
- page,
- }) => {
- await pageObj.goToNextQuestion()
- await expect(page).toHaveURL(/\?q=2/)
- await expect(pageObj.prevQuestionButton).toBeEnabled()
- await expect(pageObj.nextQuestionButton).toBeEnabled()
- })
-
test("홈 로고 클릭 시 나가기 다이얼로그가 뜨고, '아니요' 클릭 시 페이지에 머문다", async ({
page,
}) => {
@@ -52,8 +40,8 @@ test.describe("게임 진행 - 두 번째 문제 플로우", () => {
})
test("점수판을 접고 다시 펼칠 수 있어야 한다", async () => {
- await pageObj.toggleScoreboard() // 접기
- await pageObj.toggleScoreboard() // 펼치기
+ await pageObj.toggleScoreboard()
+ await pageObj.toggleScoreboard()
})
test("팀 점수를 클릭 당 +1, -1씩 조정할 수 있다", async () => {
@@ -64,37 +52,146 @@ test.describe("게임 진행 - 두 번째 문제 플로우", () => {
await pageObj.decreaseScore(teamName, 1)
await pageObj.expectTeamScore(teamName, "0점")
})
+})
+
+test.describe("게임 진행 - 버튼 클릭 조작", () => {
+ let pageObj: GamePlayPOM
+ let setupPage: GameSetupPOM
+
+ test.beforeEach(async ({ page }) => {
+ setupPage = new GameSetupPOM(page)
+ await setupPage.goto("1")
+ await setupPage.startGame()
+ pageObj = new GamePlayPOM(page)
+ })
- test("'이전 문제' 클릭 시 이전 라운드로 이동하고 진행률이 감소한다", async ({
+ test("'정답은?' 버튼 클릭 시 정답이 공개되고 '다음' 버튼이 표시된다", async ({
page,
}) => {
- // 1번 → 2번 문제로 이동하고 URL 변경 대기
- await pageObj.goToNextQuestion()
- await page.waitForURL(/\?q=2/, { timeout: 1000 })
+ await pageObj.clickShowAnswer()
+ await expect(page).toHaveURL(/answer=true/)
+ await expect(pageObj.nextQuestionButton).toBeVisible()
+ await expect(pageObj.showAnswerButton).not.toBeVisible()
+ })
- // 2번 → 3번 문제로 이동하고 URL 변경 대기
- await pageObj.goToNextQuestion()
- await page.waitForURL(/\?q=3/, { timeout: 1000 })
+ test("정답 공개 후 '다음' 버튼 클릭 시 다음 문제로 이동한다", async ({
+ page,
+ }) => {
+ await pageObj.clickShowAnswer()
+ await pageObj.clickNext()
+ await expect(page).toHaveURL(/\?q=2/)
+ await expect(pageObj.showAnswerButton).toBeVisible()
+ })
+})
- // 진행률 측정 후 이전 문제로 이동
- const before = await pageObj.getProgressValue()
- await pageObj.goToPrevQuestion()
- await page.waitForURL(/\?q=2/, { timeout: 1000 })
+test.describe("게임 진행 - 키보드 조작", () => {
+ let pageObj: GamePlayPOM
+ let setupPage: GameSetupPOM
- const after = await pageObj.getProgressValue()
- expect(after).toBeLessThanOrEqual(before!)
+ test.beforeEach(async ({ page }) => {
+ setupPage = new GameSetupPOM(page)
+ await setupPage.goto("1")
+ await setupPage.startGame()
+ pageObj = new GamePlayPOM(page)
+ })
+
+ test("Enter 키로 정답을 공개할 수 있다", async ({ page }) => {
+ await pageObj.pressEnter()
+ await expect(page).toHaveURL(/answer=true/)
+ await expect(pageObj.nextQuestionButton).toBeVisible()
+ })
+
+ test("→ 키로 정답을 공개할 수 있다", async ({ page }) => {
+ await pageObj.pressArrowRight()
+ await expect(page).toHaveURL(/answer=true/)
+ await expect(pageObj.nextQuestionButton).toBeVisible()
})
- //실제 문제 개수를 알지 못하기 때문에, 정확한 value 측정보다는 비교 연산으로 테스트 진행
- test("'다음 문제' 클릭 시 다음 라운드로 이동하고 진행률이 증가한다 (1→2)", async ({
+ test("정답 공개 후 Enter 키로 다음 문제로 이동할 수 있다", async ({
page,
}) => {
+ await pageObj.pressEnter()
+ await page.waitForURL(/answer=true/)
+ await pageObj.pressEnter()
+ await expect(page).toHaveURL(/\?q=2/)
+ await expect(pageObj.showAnswerButton).toBeVisible()
+ })
+
+ test("정답 공개 후 → 키로 다음 문제로 이동할 수 있다", async ({ page }) => {
+ await pageObj.pressEnter()
+ await page.waitForURL(/answer=true/)
+ await pageObj.pressArrowRight()
+ await expect(page).toHaveURL(/\?q=2/)
+ await expect(pageObj.showAnswerButton).toBeVisible()
+ })
+
+ test("← 키로 이전 문제로 이동할 수 있다 (2번 문제에서)", async ({ page }) => {
+ await pageObj.pressEnter()
+ await page.waitForURL(/answer=true/)
+ await pageObj.pressEnter()
+ await page.waitForURL(/\?q=2/)
+
+ await pageObj.pressArrowLeft()
+ await expect(page).toHaveURL(/\?q=1/)
+ })
+
+ test("1번 문제에서 ← 키는 무반응이다", async ({ page }) => {
+ const urlBefore = page.url()
+ await pageObj.pressArrowLeft()
+ await expect(page).toHaveURL(urlBefore)
+ await expect(pageObj.showAnswerButton).toBeVisible()
+ })
+
+ test("이전 문제로 돌아가면 정답 숨김 상태로 시작한다", async ({ page }) => {
+ await pageObj.pressEnter()
+ await page.waitForURL(/answer=true/)
+ await pageObj.pressEnter()
+ await page.waitForURL(/\?q=2/)
+
+ await pageObj.pressArrowLeft()
+ await expect(page).toHaveURL(/\?q=1/)
+ await expect(page).not.toHaveURL(/answer=true/)
+ await expect(pageObj.showAnswerButton).toBeVisible()
+ })
+})
+
+test.describe("게임 진행 - 문제 이동 및 진행률", () => {
+ let pageObj: GamePlayPOM
+ let setupPage: GameSetupPOM
+
+ test.beforeEach(async ({ page }) => {
+ setupPage = new GameSetupPOM(page)
+ await setupPage.goto("1")
+ await setupPage.startGame()
+ pageObj = new GamePlayPOM(page)
+ })
+
+ test("다음 문제로 이동하면 진행률이 증가한다", async ({ page }) => {
const before = await pageObj.getProgressValue()
- await pageObj.goToNextQuestion()
+ await pageObj.pressEnter()
+ await page.waitForURL(/answer=true/)
+ await pageObj.pressEnter()
await expect(page).toHaveURL(/\?q=2/)
const after = await pageObj.getProgressValue()
+ expect(after).toBeGreaterThan(before!)
+ })
- expect(after).toBeGreaterThanOrEqual(before!)
+ test("이전 문제로 이동하면 진행률이 감소한다", async ({ page }) => {
+ await pageObj.pressEnter()
+ await page.waitForURL(/answer=true/)
+ await pageObj.pressEnter()
+ await page.waitForURL(/\?q=2/)
+ await pageObj.pressEnter()
+ await page.waitForURL(/answer=true/)
+ await pageObj.pressEnter()
+ await page.waitForURL(/\?q=3/)
+
+ const before = await pageObj.getProgressValue()
+ await pageObj.pressArrowLeft()
+ await page.waitForURL(/\?q=2/)
+
+ const after = await pageObj.getProgressValue()
+ expect(after).toBeLessThan(before!)
})
})
diff --git a/service/app/src/app/game/[gameId]/play/__tests__/gamePlayPOM.ts b/service/app/src/app/game/[gameId]/play/__tests__/gamePlayPOM.ts
index d250cf40..8272979f 100644
--- a/service/app/src/app/game/[gameId]/play/__tests__/gamePlayPOM.ts
+++ b/service/app/src/app/game/[gameId]/play/__tests__/gamePlayPOM.ts
@@ -5,14 +5,16 @@ import { expect, type Locator, type Page } from "@playwright/test"
* - 테스트 컨벤션: guides/test-convention.mdc를 따름
* - 가능한 한 접근성 셀렉터(getByRole, getByAltText 등)를 우선 사용
* - 접근성 속성이 없는 요소는 구조 기반 셀렉터로 보완
+ *
+ * 키보드 조작:
+ * - Enter / ArrowRight: 정답 숨김 → 정답 공개, 정답 공개 → 다음 문제
+ * - ArrowLeft: 이전 문제로 이동 (1번 문제에서는 무반응)
*/
export class GamePlayPOM {
readonly page: Page
// 헤더 영역
readonly homeLogoImage: Locator
- readonly prevQuestionButton: Locator
- readonly nextQuestionButton: Locator
readonly exitIconButton: Locator
readonly progressbar: Locator
@@ -22,7 +24,11 @@ export class GamePlayPOM {
// 메인 콘텐츠
readonly questionHeading: Locator
readonly questionImage: Locator
+ readonly answerArea: Locator
+
+ // 액션 버튼 ("정답은?" 또는 "다음")
readonly showAnswerButton: Locator
+ readonly nextQuestionButton: Locator
// 나가기 다이얼로그
readonly exitDialogConfirmButton: Locator
@@ -33,8 +39,6 @@ export class GamePlayPOM {
// 헤더
this.homeLogoImage = page.getByAltText("홈 로고")
- this.prevQuestionButton = page.getByRole("button", { name: "이전 문제" })
- this.nextQuestionButton = page.getByRole("button", { name: "다음 문제" })
this.exitIconButton = page.getByRole("button", { name: "게임 종료" })
// Progress
@@ -48,15 +52,20 @@ export class GamePlayPOM {
// 메인 콘텐츠
this.questionHeading = page.getByRole("heading", { level: 1 })
this.questionImage = page.getByAltText("문제 이미지")
- this.showAnswerButton = page.getByRole("button", { name: "정답 보기" })
+ this.answerArea = page.locator("[data-testid='answer-area']")
+
+ // 액션 버튼
+ this.showAnswerButton = page.getByRole("button", { name: "정답은?" })
+ this.nextQuestionButton = page.getByRole("button", { name: "다음" })
// 나가기 다이얼로그 버튼
this.exitDialogConfirmButton = page.getByRole("button", { name: "네" })
this.exitDialogCancelButton = page.getByRole("button", { name: "아니요" })
}
- async goto(gameId: string = "1", round?: number) {
- const search = round ? `?q=${round}` : ""
+ async goto(gameId: string = "1", round?: number, showAnswer?: boolean) {
+ let search = round ? `?q=${round}` : ""
+ if (showAnswer) search += "&answer=true"
await this.page.goto(`game/${gameId}/play${search}`)
}
@@ -64,14 +73,6 @@ export class GamePlayPOM {
await this.homeLogoImage.click()
}
- async goToPrevQuestion() {
- await this.prevQuestionButton.click()
- }
-
- async goToNextQuestion() {
- await this.nextQuestionButton.click()
- }
-
async clickExitIcon() {
await this.exitIconButton.click()
}
@@ -85,15 +86,57 @@ export class GamePlayPOM {
await this.scoreboardToggleButton.click()
}
+ // 정답 공개 버튼 클릭
+ async clickShowAnswer() {
+ await this.showAnswerButton.click()
+ }
+
+ // 다음 버튼 클릭 (정답 공개 상태에서)
+ async clickNext() {
+ await this.nextQuestionButton.click()
+ }
+
+ // 키보드 조작: Enter 키
+ async pressEnter() {
+ await this.page.locator("body").press("Enter")
+ }
+
+ // 키보드 조작: → 키
+ async pressArrowRight() {
+ await this.page.locator("body").press("ArrowRight")
+ }
+
+ // 키보드 조작: ← 키
+ async pressArrowLeft() {
+ await this.page.locator("body").press("ArrowLeft")
+ }
+
+ // 정답 공개 (버튼 또는 키보드)
+ async showAnswer() {
+ await this.pressEnter()
+ }
+
+ // 다음 문제로 이동 (정답 공개 상태에서)
+ async goToNextQuestion() {
+ await expect(this.nextQuestionButton).toBeVisible()
+ await this.pressEnter()
+ }
+
+ // 이전 문제로 이동 (키보드)
+ async goToPrevQuestion() {
+ await this.pressArrowLeft()
+ }
+
+ // 현재 정답 공개 상태인지 확인
+ async isAnswerVisible() {
+ return await this.nextQuestionButton.isVisible()
+ }
+
// 팀 관련 접근성 셀렉터 기반
teamCard(teamName: string) {
return this.page.getByRole("group", { name: `${teamName} 점수 카드` })
}
- teamScoreLabel(teamName: string) {
- return this.page.getByLabel(`${teamName} 현재 점수`)
- }
-
teamDecreaseButton(teamName: string) {
return this.page.getByRole("button", { name: `${teamName} 점수 감소` })
}
@@ -117,7 +160,8 @@ export class GamePlayPOM {
}
async expectTeamScore(teamName: string, expectedText: string) {
- await expect(this.teamScoreLabel(teamName)).toHaveText(expectedText)
+ const scoreButton = this.teamIncreaseButton(teamName)
+ await expect(scoreButton).toContainText(expectedText)
}
async getQuestionText() {
@@ -128,26 +172,6 @@ export class GamePlayPOM {
return await this.questionImage.isVisible()
}
- async showAnswerAndGetText() {
- await this.showAnswerButton.click()
- const answerButton = this.page
- .getByRole("button")
- .filter({ hasNotText: "정답 보기" })
- .first()
- await expect(this.showAnswerButton).not.toBeVisible()
- const answerText = (await answerButton.textContent())?.trim() ?? ""
- return answerText
- }
-
- async hideAnswer() {
- const answerButton = this.page
- .getByRole("button")
- .filter({ hasNotText: "정답 보기" })
- .first()
- await answerButton.click()
- await expect(this.showAnswerButton).toBeVisible()
- }
-
async confirmExit() {
await this.exitDialogConfirmButton.click()
}
diff --git a/service/app/src/app/game/[gameId]/play/components/gamePlayContent.tsx b/service/app/src/app/game/[gameId]/play/components/gamePlayContent.tsx
index 656a15ca..6b3d324c 100644
--- a/service/app/src/app/game/[gameId]/play/components/gamePlayContent.tsx
+++ b/service/app/src/app/game/[gameId]/play/components/gamePlayContent.tsx
@@ -1,15 +1,13 @@
+import { PrimaryBoxButton } from "@ject-5-fe/design/components/button"
import Image from "next/image"
-import Link from "next/link"
-import { useEffect, useState } from "react"
+import { useCallback, useEffect, useState } from "react"
import type { GameQuestion } from "@/entities/game/model"
-
-import { QuestionNavigationButton } from "./questionNavigationButton"
-
interface GamePlayContentProps {
currentQuestion: GameQuestion
currentRound: number
- totalRounds: number
+ showAnswer: boolean
+ onShowAnswer: () => void
onPrevQuestion: () => void
onNextQuestion: () => void
}
@@ -17,73 +15,98 @@ interface GamePlayContentProps {
export const GamePlayContent = ({
currentQuestion,
currentRound,
- totalRounds,
+ showAnswer,
+ onShowAnswer,
onPrevQuestion,
onNextQuestion,
}: GamePlayContentProps) => {
- const [showAnswer, setShowAnswer] = useState(false)
+ const [imageWidths, setImageWidths] = useState
>({})
+ const currentImageWidth = imageWidths[currentRound] ?? null
- useEffect(() => {
- setShowAnswer(false)
- }, [currentRound])
+ const handleKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ const isDialogOpen = document.querySelector("[role='dialog']") !== null
+ if (isDialogOpen) return
- return (
-
-
- {currentQuestion.questionText}
-
+ if (e.key === "Enter" || e.key === "ArrowRight") {
+ if (showAnswer) {
+ onNextQuestion()
+ } else {
+ onShowAnswer()
+ }
+ }
- {currentQuestion?.imageUrl && (
-
-
-
- )}
+ if (e.key === "ArrowLeft" && currentRound > 1) {
+ onPrevQuestion()
+ }
+ },
+ [showAnswer, currentRound, onNextQuestion, onPrevQuestion, onShowAnswer],
+ )
-
-
+ useEffect(() => {
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [handleKeyDown])
-
- {showAnswer ? (
-
setShowAnswer(false)}
- className="typography-heading-3xl-semibold inline-flex w-[572px] cursor-pointer flex-row items-center justify-center gap-12 rounded-12 border-[3px] border-border-interactive-secondary bg-background-interactive-inverse px-32 py-12 text-text-interactive-secondary hover:bg-background-interactive-secondary-hovered active:border-none active:bg-background-interactive-secondary-pressed"
- >
- {currentQuestion?.questionAnswer}
-
- ) : (
-
setShowAnswer(true)}
- className="typography-heading-3xl-semibold inline-flex w-[572px] cursor-pointer flex-row items-center justify-center gap-8 rounded-12 border-[3px] border-transparent bg-background-interactive-primary px-32 py-12 text-text-interactive-inverse hover:bg-background-interactive-primary-hovered active:border-transparent active:bg-background-interactive-primary-pressed"
- >
- 정답 보기
-
- )}
+ const hasImage = !!currentQuestion?.imageUrl
+
+ return (
+
+
+ {hasImage && (
+
+ {currentQuestion.questionText}
+
+ )}
- {currentRound === totalRounds && (
-
- 결과 화면으로 이동하기
-
+
+ {hasImage ? (
+ showAnswer ? (
+
+ {currentQuestion?.questionAnswer}
+
+ ) : (
+
{
+ const img = e.currentTarget
+ setImageWidths((prev) => ({
+ ...prev,
+ [currentRound]: img.offsetWidth,
+ }))
+ }}
+ />
+ )
+ ) : (
+
+ {showAnswer
+ ? currentQuestion?.questionAnswer
+ : currentQuestion.questionText}
+
)}
+
-
= totalRounds}
- />
+
+
(showAnswer ? onNextQuestion() : onShowAnswer())}
+ >
+ {showAnswer ? "다음" : "정답은?"}
+
)
diff --git a/service/app/src/app/game/[gameId]/play/components/gamePlayNavigation.tsx b/service/app/src/app/game/[gameId]/play/components/gamePlayNavigation.tsx
index 35ed7d9f..2a14c9b7 100644
--- a/service/app/src/app/game/[gameId]/play/components/gamePlayNavigation.tsx
+++ b/service/app/src/app/game/[gameId]/play/components/gamePlayNavigation.tsx
@@ -1,9 +1,9 @@
import { SecondaryPlainIconButton } from "@ject-5-fe/design/components/button"
import { Progress } from "@ject-5-fe/design/components/progress"
import { Cross } from "@ject-5-fe/design/icons"
-import Image from "next/image"
import { ThemeToggle } from "@/shared/themeToggleButton"
+import { HomeButton } from "@/widgets/components/homeButton"
interface GamePlayHeaderProps {
currentRound: number
@@ -17,20 +17,9 @@ export const GamePlayHeader = ({
onExit,
}: GamePlayHeaderProps) => {
return (
-
+
-
+
diff --git a/service/app/src/app/game/[gameId]/play/components/scoreboardSidebar.tsx b/service/app/src/app/game/[gameId]/play/components/scoreboardSidebar.tsx
index 008138a4..031dd8ab 100644
--- a/service/app/src/app/game/[gameId]/play/components/scoreboardSidebar.tsx
+++ b/service/app/src/app/game/[gameId]/play/components/scoreboardSidebar.tsx
@@ -1,6 +1,7 @@
import { PrimarySolidIconButton } from "@ject-5-fe/design/components/button"
import { PlayerStatus } from "@ject-5-fe/design/components/playerStatus"
import { Hidden, Show } from "@ject-5-fe/design/icons"
+import { cn } from "@ject-5-fe/design/utils/cn"
import { useState } from "react"
import type { Team } from "../../store/useGameStore"
@@ -21,21 +22,24 @@ export const ScoreboardSidebar = ({
const clampScore = (value: number) => Math.max(-100, Math.min(100, value))
return (
-
-
-
- 점수판
-
+
)
}
diff --git a/service/app/src/app/game/[gameId]/play/page.tsx b/service/app/src/app/game/[gameId]/play/page.tsx
index c3b5d314..07c4825d 100644
--- a/service/app/src/app/game/[gameId]/play/page.tsx
+++ b/service/app/src/app/game/[gameId]/play/page.tsx
@@ -15,7 +15,7 @@ import { gamePlaySchema } from "./schemas"
const ScoreboardGame = () => {
const router = useRouter()
- const [{ q: currentRound }, setSearchParams] =
+ const [{ q: currentRound, answer: showAnswer }, setSearchParams] =
useTypedSearchParams(gamePlaySchema)
const { gameDetail, teams, totalRounds, updateTeamScore, scores } =
@@ -42,7 +42,7 @@ const ScoreboardGame = () => {
router.push("./result")
return
}
- setSearchParams({ q: (currentRound + 1).toString() })
+ setSearchParams({ q: (currentRound + 1).toString(), answer: "" })
}
const { goBackToEntry } = useGameEntryNavigation()
@@ -60,21 +60,22 @@ const ScoreboardGame = () => {
})
}
/>
-
+
-
-
{
+ setSearchParams({ q: currentRound.toString(), answer: "true" })
+ }}
onPrevQuestion={() => {
if (currentRound <= 1) return
- setSearchParams({ q: (currentRound - 1).toString() })
+ setSearchParams({ q: (currentRound - 1).toString(), answer: "" })
}}
onNextQuestion={handleNextQuestion}
/>
diff --git a/service/app/src/app/game/[gameId]/play/schemas/index.ts b/service/app/src/app/game/[gameId]/play/schemas/index.ts
index 1ff12a2e..1f1fe010 100644
--- a/service/app/src/app/game/[gameId]/play/schemas/index.ts
+++ b/service/app/src/app/game/[gameId]/play/schemas/index.ts
@@ -2,4 +2,8 @@ import { z } from "zod"
export const gamePlaySchema = z.object({
q: z.string().default("1").transform(Number),
+ answer: z
+ .string()
+ .optional()
+ .transform((v) => v === "true"),
})
diff --git a/service/app/src/app/game/[gameId]/result/components/gameResultContent.tsx b/service/app/src/app/game/[gameId]/result/components/gameResultContent.tsx
index c6bb3a2c..63e9a515 100644
--- a/service/app/src/app/game/[gameId]/result/components/gameResultContent.tsx
+++ b/service/app/src/app/game/[gameId]/result/components/gameResultContent.tsx
@@ -17,9 +17,9 @@ export const GameResultContent = ({
}, [teams, scores])
return (
-
+
{/* Winner Title */}
-
+
🎉 이번 게임의 Winner는?
@@ -30,7 +30,7 @@ export const GameResultContent = ({
{topTeams.map((team) => (
{team.name}
diff --git a/service/app/src/app/game/[gameId]/result/components/gameResultNavigation.tsx b/service/app/src/app/game/[gameId]/result/components/gameResultNavigation.tsx
index 4d78cac0..cfb0451c 100644
--- a/service/app/src/app/game/[gameId]/result/components/gameResultNavigation.tsx
+++ b/service/app/src/app/game/[gameId]/result/components/gameResultNavigation.tsx
@@ -2,6 +2,7 @@ import {
PrimaryBoxButton,
SecondaryPlainIconButton,
} from "@ject-5-fe/design/components/button"
+import { Navigation } from "@ject-5-fe/design/components/navigation"
import { Cross } from "@ject-5-fe/design/icons"
import Image from "next/image"
@@ -17,30 +18,20 @@ export const GameResultNavigation = ({
onPreviousQuestion,
}: GameResultNavigationProps) => {
return (
-
-
-
-
-
-
- 게임 종료
-
-
-
-
+ }
+ centerContent={
+
+ 게임 종료
+
+ }
+ rightContent={
+ <>
-
이전 문제
-
-
-
-
+ >
+ }
+ />
)
}
diff --git a/service/app/src/app/game/[gameId]/setup/components/gameNavigation.tsx b/service/app/src/app/game/[gameId]/setup/components/gameNavigation.tsx
index 8a7484a6..28117dd2 100644
--- a/service/app/src/app/game/[gameId]/setup/components/gameNavigation.tsx
+++ b/service/app/src/app/game/[gameId]/setup/components/gameNavigation.tsx
@@ -1,28 +1,19 @@
"use client"
+
import { SecondaryPlainIconButton } from "@ject-5-fe/design/components/button"
import { Navigation } from "@ject-5-fe/design/components/navigation"
import { Cross } from "@ject-5-fe/design/icons"
-import Image from "next/image"
import { useGameEntryNavigation } from "@/entities/game/hooks/useGameEntryNavigation"
import { ThemeToggle } from "@/shared/themeToggleButton"
+import { HomeButton } from "@/widgets/components/homeButton"
export function GameNavigation() {
const { goBackToEntry } = useGameEntryNavigation()
return (
-
-
- }
+ leftContent={}
centerContent={
참가자 설정
diff --git a/service/app/src/app/game/[gameId]/setup/components/teamInputForm.tsx b/service/app/src/app/game/[gameId]/setup/components/teamInputForm.tsx
index 70aadee2..163a4be4 100644
--- a/service/app/src/app/game/[gameId]/setup/components/teamInputForm.tsx
+++ b/service/app/src/app/game/[gameId]/setup/components/teamInputForm.tsx
@@ -54,7 +54,7 @@ export function TeamInputForm() {
return (
)
diff --git a/service/app/src/app/game/[gameId]/setup/components/teamSidebar.tsx b/service/app/src/app/game/[gameId]/setup/components/teamSidebar.tsx
index da316a17..ed33feaf 100644
--- a/service/app/src/app/game/[gameId]/setup/components/teamSidebar.tsx
+++ b/service/app/src/app/game/[gameId]/setup/components/teamSidebar.tsx
@@ -1,11 +1,11 @@
"use client"
-import { PrimaryBoxButton } from "@ject-5-fe/design/components/button"
import { PlayerStatus } from "@ject-5-fe/design/components/playerStatus"
-import { StickyActionBar } from "@ject-5-fe/design/components/stickyActionBar"
import { v4 as uuidv4 } from "uuid"
import { useShallow } from "zustand/react/shallow"
+import { FloatingAddButton } from "@/shared/ui/floatingAddButton"
+
import { useGameStore } from "../../store/useGameStore"
import { generateTeamName } from "../../utils/generateTeamName"
import { MAX_TEAMS } from "../../utils/teamValidation"
@@ -29,30 +29,23 @@ export function TeamSidebar() {
}
return (
-