게임 플레이 관련 페이지 레이아웃 / 기능 업데이트#273
Conversation
add new button / keyboard control features
Walkthrough게임 플레이와 설정 UI를 리팩토링하여 FloatingAddButton을 도입하고, 정답 표시를 URL 검색매개변수(answer)로 제어하도록 변경했으며 키보드 네비게이션(Enter/Arrow)과 헤더/사이드바 레이아웃을 조정했습니다. Changes
Sequence Diagram(s)mermaid mermaid Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Deploy preview for re-creation ready! ✅ Preview Built with commit 54c2591. |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (11)
service/app/src/app/game/[gameId]/play/components/scoreboardSidebar.tsx (1)
27-27:top-[10px]— 디자인 토큰 대신 임의값 사용동일 파일 내 모든 다른 간격(
p-24,left-20,bottom-32등)은 프로젝트 디자인 토큰을 사용하는데,top-[10px]만 임의값입니다. 해당 값에 대응하는 토큰이 있다면 토큰으로 교체하는 것이 일관성 유지에 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/app/game/`[gameId]/play/components/scoreboardSidebar.tsx at line 27, In the ScoreboardSidebar component replace the inline utility "top-[10px]" in the className string with the project spacing design token used elsewhere (matching the visual spacing of 10px) so it matches tokens like "left-20", "p-24", "bottom-32"; locate the className in scoreboardSidebar.tsx and swap the arbitrary top value for the correct token (e.g., the project's top-?? token) to keep spacing consistent.shared/design/src/components/playerStatus/index.tsx (2)
5-12:scoreprop —scoreView=false시 미사용, 선택적(optional)으로 변경 고려현재
score: string은 항상 필수 prop이지만,scoreView=false일 때는 렌더링에 사용되지 않습니다. 스토리에서도score=""를 전달하고 있어 API 사용이 어색합니다.score?: string으로 변경하면 호출 측의 사용 의도가 더 명확해집니다.♻️ 제안 수정
interface PlayerStatusProps { name: string - score: string + score?: string scoreView?: boolean onScoreIncrease?: () => void onScoreDecrease?: () => void className?: string }- <span className="typography-heading-lg-semibold text-text-primary"> + <span className="typography-heading-lg-semibold text-text-primary"> {score}참고:
scoreView=true일 때score가 undefined이면 빈 span이 렌더링되므로, 필요 시{score ?? ""}또는 별도 가드를 추가해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shared/design/src/components/playerStatus/index.tsx` around lines 5 - 12, Change the PlayerStatusProps interface so the score prop is optional (make score?: string) because when scoreView is false the value is not used; update any call sites and stories that pass an empty string to instead omit score where appropriate. In addition, update the PlayerStatus component rendering to guard for undefined when scoreView is true (e.g., render {score ?? ""} or another explicit guard) to avoid rendering "undefined" if score is missing. Finally run typechecks to fix any downstream places relying on score being mandatory.
50-50: 하드코딩된 hex 색상 값 — 디자인 토큰으로 대체 권장
#FFC7C8,#BDDCFF는 토큰 시스템(variables.json)에 존재하지 않는 커스텀 값입니다 (가장 가까운 기존 토큰인red/200=#FFC9C9,blue/200=#BEDBFF와도 불일치). 향후 디자인 토큰이 업데이트될 경우 이 컴포넌트의 색상은 자동으로 반영되지 않아 일관성이 깨질 수 있습니다.이 색상들을 새로운 시맨틱 토큰(예:
color/background/score/decrease-hover,color/background/score/increase-hover등)으로 추가하거나, 기존 원시 토큰과 정확히 일치하도록 조정한 후 Tailwind 설정에 연결하는 방식을 권장합니다.Also applies to: 62-62
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shared/design/src/components/playerStatus/index.tsx` at line 50, The component in shared/design/src/components/playerStatus/index.tsx uses hardcoded hex values (`#FFC7C8` and `#BDDCFF`) inside the className (the hover/active classes on the element with className="... hover:bg-[`#FFC7C8`] active:bg-[`#FF6467`]" and similarly at line ~62) — replace these literal hex colors with semantic design tokens instead: add new tokens (suggested names like color/background/score/decrease-hover and color/background/score/increase-hover) or map the exact existing primitive tokens (e.g., red/200 or blue/200) so they match, then reference those tokens via Tailwind utility classes (configured in tailwind/variables.json and tailwind.config) rather than inline hex values; update the className in the PlayerStatus component to use the token-based Tailwind classes.service/app/src/app/game/[gameId]/result/components/gameResultNavigation.tsx (1)
22-26: 로고 버튼의 접근성 레이블 명확성 개선 권장
<button onClick={onExit}>안의 이미지 alt="홈 로고"가 버튼의 접근 가능 이름이 됩니다. 그러나 클릭 시 "게임 종료"가 실행되므로 스크린 리더 사용자에게는 동작이 레이블과 불일치합니다. 우측의 Cross 버튼(aria-label="게임 종료")과 일관성을 위해aria-label추가를 고려해 보세요.♿ 개선 제안
- <button onClick={onExit}> + <button onClick={onExit} aria-label="게임 종료 및 홈으로 이동">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/app/game/`[gameId]/result/components/gameResultNavigation.tsx around lines 22 - 26, 왼쪽 로고 버튼의 시멘틱 레이블이 이미지의 alt("홈 로고")에만 의존해 실제 클릭 동작(onExit)이 스크린리더에 올바르게 전달되지 않습니다; gameResultNavigation 컴포넌트에서 해당 <button onClick={onExit}> 요소에 aria-label="게임 종료" (혹은 우측 Cross 버튼과 동일한 레이블) 속성을 추가해 접근성 이름을 명확히 하세요 — 참조 심볼: onExit, leftContent, 해당 Image 요소.service/app/src/shared/ui/floatingAddButton/index.tsx (1)
8-12:aria-label을 기본 타입에서도 Omit해야 함
Omit<..., "children">만으로는aria-label이ComponentPropsWithoutRef<typeof PrimaryBoxButton>에 여전히 포함됩니다. 컴포넌트 내부에서는aria-label={ariaLabel}뒤에{...props}를 스프레드하므로, 호출 측이 실수로aria-label="..."를 직접 전달하면 의도한ariaLabel값이 덮어써집니다.🛡️ 개선 제안
interface FloatingAddButtonProps - extends Omit<ComponentPropsWithoutRef<typeof PrimaryBoxButton>, "children"> { + extends Omit<ComponentPropsWithoutRef<typeof PrimaryBoxButton>, "children" | "aria-label"> { ariaLabel: string containerClassName?: string }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/shared/ui/floatingAddButton/index.tsx` around lines 8 - 12, The prop type currently extends Omit<ComponentPropsWithoutRef<typeof PrimaryBoxButton>, "children"> but still allows callers to pass an "aria-label" that would be spread over the intended ariaLabel prop; update FloatingAddButtonProps to also Omit the aria-label key (i.e. Omit<..., "children" | "aria-label">) so the component’s ariaLabel prop cannot be overwritten by {...props}, and keep using ariaLabel internally when rendering the PrimaryBoxButton.service/app/src/app/game/[gameId]/play/__tests__/gamePlayPOM.ts (1)
120-123: POM 액션 메서드 내expect어서션 분리 권장
goToNextQuestion내부에서expect(this.nextQuestionButton).toBeVisible()를 호출하면, 실패 시 테스트 오류 메시지가 POM 내부에서 발생하여 어느 테스트 의도가 깨졌는지 파악하기 어렵습니다. 어서션은 테스트 코드 내에서 명시적으로 작성하는 것이 POM 패턴에 부합합니다.♻️ 리팩토링 제안
async goToNextQuestion() { - await expect(this.nextQuestionButton).toBeVisible() - await this.pressEnter() + await this.pressEnter() }테스트 코드에서 필요 시 별도로
await expect(pageObj.nextQuestionButton).toBeVisible()어서션을 추가하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/app/game/`[gameId]/play/__tests__/gamePlayPOM.ts around lines 120 - 123, 요약: POM 메서드 goToNextQuestion 내부에서 expect(this.nextQuestionButton).toBeVisible() 어서션을 제거하고 POM은 동작만 수행하도록 수정하세요; 구체적으로 goToNextQuestion에서는 오직 await this.pressEnter()만 남기고, 테스트 코드에서 필요 시 await expect(pageObj.nextQuestionButton).toBeVisible()를 호출해 명시적인 어서션을 수행하도록 변경합니다 (대상 식별자: goToNextQuestion, nextQuestionButton, pressEnter).service/app/src/app/game/[gameId]/play/__tests__/game-play.spec.ts (1)
57-196: 4개 describe 블록에서 동일한beforeEach중복"버튼 클릭 조작", "키보드 조작", "문제 이동 및 진행률" 블록 모두 "기본 UI"와 동일한 셋업 코드를 반복합니다. 헬퍼 함수로 추출하면 유지보수성이 향상됩니다.
♻️ 리팩토링 제안
+async function setupGame(page: Page) { + const setupPage = new GameSetupPOM(page) + await setupPage.goto("1") + await setupPage.startGame() + return new GamePlayPOM(page) +} test.describe("게임 진행 - 기본 UI", () => { 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) + pageObj = await setupGame(page) }) // ... }) 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) + pageObj = await setupGame(page) }) // ... })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/app/game/`[gameId]/play/__tests__/game-play.spec.ts around lines 57 - 196, Three describe blocks duplicate the same beforeEach setup (creating GameSetupPOM, goto("1"), startGame(), and new GamePlayPOM), so extract that repeated setup into a single reusable helper or shared beforeEach: create a setup function (e.g., setupGame) that accepts the Playwright page, instantiates GameSetupPOM, calls goto("1") and startGame(), and returns a new GamePlayPOM (or move that beforeEach into an outer describe that encloses the three inner describes); then replace the per-block beforeEach with a call to this helper or rely on the outer beforeEach so tests use the returned GamePlayPOM (reference symbols: GameSetupPOM, GamePlayPOM, setupPage, pageObj, goto, startGame).service/app/src/app/game/[gameId]/play/schemas/index.ts (1)
5-8: Zod 4의z.stringbool()대안으로 고려 가능현재
.optional().transform((v) => v === "true")패턴은 URL 파라미터 파싱에 최적화되어 있습니다. 파라미터가 없으면undefined → false,"true"면true, 다른 값들은 모두false로 조용히 처리됩니다.Zod 4는
z.stringbool()을 제공하며,"true","false","yes","no"등 다양한 불리언 문자열을 인식합니다. 다만 인식하지 못하는 값은 검증 오류를 발생시키므로, URL 파라미터 같이 자유로운 입력을 받는 경우라면 현재의 관대한 처리 방식이 더 적합합니다. 의도하는 동작이 명확하다면 현재 코드는 유지해도 무방합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/app/game/`[gameId]/play/schemas/index.ts around lines 5 - 8, The current answer schema uses .string().optional().transform((v) => v === "true") which silently maps undefined/any non-"true" to false; if you want stricter parsing use Zod 4's z.stringbool() instead (replace the answer schema with z.stringbool().optional() or z.stringbool() and handle validation errors), otherwise keep the existing permissive transform but add a clarifying comment near the answer definition to document the intended behavior; update the symbol named answer in the schema accordingly.service/app/src/widgets/components/homeButton.tsx (1)
25-25:className조합에cn()유틸리티를 사용하는 것이 좋겠습니다.코드베이스 전반에서
cn()을 사용하여 Tailwind 클래스를 조합하고 있는데, 이 컴포넌트만 템플릿 리터럴로 문자열을 직접 연결하고 있습니다.cn()을 사용하면 Prettier Tailwind 플러그인의 클래스 정렬이 적용되고, 조건부 클래스나 중복 클래스 처리도 올바르게 동작합니다.♻️ cn() 유틸리티 사용 수정안
+import { cn } from "@ject-5-fe/design/lib" import Image from "next/image" import Link from "next/link"- const buttonClassName = `flex shrink-0 cursor-pointer flex-col items-center justify-center gap-2.5 p-3.5 focus:outline-none ${className}` + const buttonClassName = cn( + "flex shrink-0 cursor-pointer flex-col items-center justify-center gap-2.5 p-3.5 focus:outline-none", + className, + )As per coding guidelines, "Tailwind CSS utilities should be ordered via the Prettier Tailwind plugin; avoid inline style objects unless necessary."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/widgets/components/homeButton.tsx` at line 25, Replace the template-literal class concatenation used to build buttonClassName with the cn() utility so Tailwind classes are ordered and conditional/duplicate classes are handled; specifically, change the const buttonClassName = `flex ... ${className}` usage to const buttonClassName = cn("flex shrink-0 cursor-pointer flex-col items-center justify-center gap-2.5 p-3.5 focus:outline-none", className) and ensure cn is imported (from your project's utils/helpers) if not already; remove the string interpolation and rely on cn() for combining base classes and the className prop so Prettier Tailwind plugin can sort classes correctly.service/app/src/app/game/[gameId]/play/components/gamePlayNavigation.tsx (1)
20-46: 좌우 영역의 고정 너비w-[420px]가 소형 화면에서 레이아웃 문제를 일으킬 수 있습니다.헤더의 좌우 섹션이 각각
w-[420px]로 고정되어 있어, 합산 840px + Progress 바 영역이 됩니다. 사이드바가sm:w-[334px],md:w-[400px]으로 반응형 처리되어 있는데, 헤더는 고정 너비라 작은 뷰포트에서 오버플로우가 발생할 수 있습니다. 반응형 너비 적용을 검토해 보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/app/game/`[gameId]/play/components/gamePlayNavigation.tsx around lines 20 - 46, The left and right header containers use a fixed class w-[420px] which causes overflow on small viewports; update the two divs with className "flex w-[420px] ..." to use responsive sizing and allow shrinking—replace the fixed width with responsive classes (e.g., sm:w-[334px], md:w-[400px]) or use max-width and flex-shrink (e.g., max-w-[420px] plus flex-shrink-0/allow-shrink) so they collapse on small screens while keeping the center Progress (the Progress component inside the div with className "flex flex-1 items-center justify-center") flexible; ensure HomeButton, ThemeToggle and SecondaryPlainIconButton remain aligned after the change.service/app/src/app/game/[gameId]/play/__tests__/game-play-e2e-scenarios.md (1)
36-66: 섹션 3과 섹션 4의 내용이 상당 부분 중복됩니다.섹션 3 (정답 공개 및 문제 이동)과 섹션 4 (문제 네비게이션)가 이전/다음 문제 이동, 정답 상태 초기화, 마지막 문제 처리 등 동일한 동작을 반복 설명하고 있습니다. 하나의 섹션으로 통합하면 유지보수가 쉬워집니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/app/game/`[gameId]/play/__tests__/game-play-e2e-scenarios.md around lines 36 - 66, Merge sections titled "3. 정답 공개 및 문제 이동" and "4. 문제 네비게이션" into a single consolidated section to remove duplication: create one section (e.g., "정답 공개 및 문제 이동/네비게이션") that includes the button behavior ("정답은?" sets answer=true in URL and shows "다음"), keyboard mappings (Enter and → to reveal answer or advance, ← to go back), edge cases (no-op on ← at question 1, Enter/→ on last revealed answer navigates to /game/[gameId]/result), and the invariant that moving to previous/next rounds resets answer-hidden state; keep the table of key behaviors and the short bullet rules for previous/next navigation but remove repeated sentences across the two original sections.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@service/app/src/app/game/`[gameId]/play/__tests__/gamePlayPOM.ts:
- Around line 66-70: The goto helper constructs query string incorrectly when
showAnswer is true but round is undefined; update the goto function (search
variable and URL assembly) to build query parameters safely (e.g., use a params
array or URLSearchParams) so the first param is prefixed with '?' and subsequent
params with '&'; locate goto in gamePlayPOM.ts and change the logic that appends
"&answer=true" to conditionally add "?" when search is empty or use
URLSearchParams to produce a correct query string for
page.goto(`game/${gameId}/play${search}`).
In `@service/app/src/app/game/`[gameId]/play/components/gamePlayContent.tsx:
- Around line 26-46: The keydown handler (handleKeyDown) currently runs
unconditionally and triggers navigation even when a confirmation dialog is open;
update handleKeyDown to early-return when a dialog/modal is open by adding a
prop or state guard (e.g., accept an isDialogOpen boolean prop passed from the
parent) and check that at the top of handleKeyDown before processing Enter/Arrow
keys; also include isDialogOpen in the useCallback dependency array and in the
useEffect dependency list so the listener logic updates correctly;
alternatively, if you prefer the other suggested approach, call
e.preventDefault() and return when a local dialog-open boolean is true.
- Around line 55-82: The current conditional wraps the entire question/answer
block with currentQuestion?.imageUrl, so when imageUrl is an empty string the
showAnswer branch never renders; update the JSX so the showAnswer answer display
(the div that shows currentQuestion?.questionAnswer) is rendered
outside/independent of the currentQuestion?.imageUrl check while keeping only
the <Image ... /> element inside the imageUrl conditional (use the existing
showAnswer, currentQuestion, currentQuestion?.questionAnswer,
currentQuestion.imageUrl, setImageWidths and currentRound identifiers to locate
the branches to refactor).
In `@service/app/src/app/game/`[gameId]/play/components/scoreboardSidebar.tsx:
- Around line 33-42: The toggle button (PrimarySolidIconButton) is missing
aria-expanded and aria-controls; update the onClick/button element to include
aria-expanded={isSidebarOpen} and aria-controls pointing to the scoreboard
panel's id, and add a matching id attribute to the panel div (the element
rendered when isSidebarOpen is true) so screen readers can associate the button
with the controlled region; use existing symbols isSidebarOpen,
setIsSidebarOpen, PrimarySolidIconButton and the panel div to implement this
linkage.
In `@shared/design/src/stories/playerStatus.stories.tsx`:
- Around line 228-268: AllStatesComparison story's inner decorator uses a fixed
400px width causing overflow inside the meta decorator's 350px container and the
decorator function also ignores Story/context so story args are unused; update
the decorator that currently returns <div className="flex w-[400px] ..."> to use
a responsive width like w-full (or match outer width w-[350px]) instead of
w-[400px], and either remove the unused args (e.g., args: { name, score,
scoreView }) from the story or change the decorator signature to accept (Story,
context) and forward context.args into the rendered PlayerStatus components so
Storybook controls actually affect PlayerStatus (refer to AllStatesComparison
decorator and PlayerStatus props like name/score/scoreView).
---
Nitpick comments:
In `@service/app/src/app/game/`[gameId]/play/__tests__/game-play-e2e-scenarios.md:
- Around line 36-66: Merge sections titled "3. 정답 공개 및 문제 이동" and "4. 문제 네비게이션"
into a single consolidated section to remove duplication: create one section
(e.g., "정답 공개 및 문제 이동/네비게이션") that includes the button behavior ("정답은?" sets
answer=true in URL and shows "다음"), keyboard mappings (Enter and → to reveal
answer or advance, ← to go back), edge cases (no-op on ← at question 1, Enter/→
on last revealed answer navigates to /game/[gameId]/result), and the invariant
that moving to previous/next rounds resets answer-hidden state; keep the table
of key behaviors and the short bullet rules for previous/next navigation but
remove repeated sentences across the two original sections.
In `@service/app/src/app/game/`[gameId]/play/__tests__/game-play.spec.ts:
- Around line 57-196: Three describe blocks duplicate the same beforeEach setup
(creating GameSetupPOM, goto("1"), startGame(), and new GamePlayPOM), so extract
that repeated setup into a single reusable helper or shared beforeEach: create a
setup function (e.g., setupGame) that accepts the Playwright page, instantiates
GameSetupPOM, calls goto("1") and startGame(), and returns a new GamePlayPOM (or
move that beforeEach into an outer describe that encloses the three inner
describes); then replace the per-block beforeEach with a call to this helper or
rely on the outer beforeEach so tests use the returned GamePlayPOM (reference
symbols: GameSetupPOM, GamePlayPOM, setupPage, pageObj, goto, startGame).
In `@service/app/src/app/game/`[gameId]/play/__tests__/gamePlayPOM.ts:
- Around line 120-123: 요약: POM 메서드 goToNextQuestion 내부에서
expect(this.nextQuestionButton).toBeVisible() 어서션을 제거하고 POM은 동작만 수행하도록 수정하세요;
구체적으로 goToNextQuestion에서는 오직 await this.pressEnter()만 남기고, 테스트 코드에서 필요 시 await
expect(pageObj.nextQuestionButton).toBeVisible()를 호출해 명시적인 어서션을 수행하도록 변경합니다 (대상
식별자: goToNextQuestion, nextQuestionButton, pressEnter).
In `@service/app/src/app/game/`[gameId]/play/components/gamePlayNavigation.tsx:
- Around line 20-46: The left and right header containers use a fixed class
w-[420px] which causes overflow on small viewports; update the two divs with
className "flex w-[420px] ..." to use responsive sizing and allow
shrinking—replace the fixed width with responsive classes (e.g., sm:w-[334px],
md:w-[400px]) or use max-width and flex-shrink (e.g., max-w-[420px] plus
flex-shrink-0/allow-shrink) so they collapse on small screens while keeping the
center Progress (the Progress component inside the div with className "flex
flex-1 items-center justify-center") flexible; ensure HomeButton, ThemeToggle
and SecondaryPlainIconButton remain aligned after the change.
In `@service/app/src/app/game/`[gameId]/play/components/scoreboardSidebar.tsx:
- Line 27: In the ScoreboardSidebar component replace the inline utility
"top-[10px]" in the className string with the project spacing design token used
elsewhere (matching the visual spacing of 10px) so it matches tokens like
"left-20", "p-24", "bottom-32"; locate the className in scoreboardSidebar.tsx
and swap the arbitrary top value for the correct token (e.g., the project's
top-?? token) to keep spacing consistent.
In `@service/app/src/app/game/`[gameId]/play/schemas/index.ts:
- Around line 5-8: The current answer schema uses
.string().optional().transform((v) => v === "true") which silently maps
undefined/any non-"true" to false; if you want stricter parsing use Zod 4's
z.stringbool() instead (replace the answer schema with z.stringbool().optional()
or z.stringbool() and handle validation errors), otherwise keep the existing
permissive transform but add a clarifying comment near the answer definition to
document the intended behavior; update the symbol named answer in the schema
accordingly.
In
`@service/app/src/app/game/`[gameId]/result/components/gameResultNavigation.tsx:
- Around line 22-26: 왼쪽 로고 버튼의 시멘틱 레이블이 이미지의 alt("홈 로고")에만 의존해 실제 클릭 동작(onExit)이
스크린리더에 올바르게 전달되지 않습니다; gameResultNavigation 컴포넌트에서 해당 <button onClick={onExit}>
요소에 aria-label="게임 종료" (혹은 우측 Cross 버튼과 동일한 레이블) 속성을 추가해 접근성 이름을 명확히 하세요 — 참조
심볼: onExit, leftContent, 해당 Image 요소.
In `@service/app/src/shared/ui/floatingAddButton/index.tsx`:
- Around line 8-12: The prop type currently extends
Omit<ComponentPropsWithoutRef<typeof PrimaryBoxButton>, "children"> but still
allows callers to pass an "aria-label" that would be spread over the intended
ariaLabel prop; update FloatingAddButtonProps to also Omit the aria-label key
(i.e. Omit<..., "children" | "aria-label">) so the component’s ariaLabel prop
cannot be overwritten by {...props}, and keep using ariaLabel internally when
rendering the PrimaryBoxButton.
In `@service/app/src/widgets/components/homeButton.tsx`:
- Line 25: Replace the template-literal class concatenation used to build
buttonClassName with the cn() utility so Tailwind classes are ordered and
conditional/duplicate classes are handled; specifically, change the const
buttonClassName = `flex ... ${className}` usage to const buttonClassName =
cn("flex shrink-0 cursor-pointer flex-col items-center justify-center gap-2.5
p-3.5 focus:outline-none", className) and ensure cn is imported (from your
project's utils/helpers) if not already; remove the string interpolation and
rely on cn() for combining base classes and the className prop so Prettier
Tailwind plugin can sort classes correctly.
In `@shared/design/src/components/playerStatus/index.tsx`:
- Around line 5-12: Change the PlayerStatusProps interface so the score prop is
optional (make score?: string) because when scoreView is false the value is not
used; update any call sites and stories that pass an empty string to instead
omit score where appropriate. In addition, update the PlayerStatus component
rendering to guard for undefined when scoreView is true (e.g., render {score ??
""} or another explicit guard) to avoid rendering "undefined" if score is
missing. Finally run typechecks to fix any downstream places relying on score
being mandatory.
- Line 50: The component in shared/design/src/components/playerStatus/index.tsx
uses hardcoded hex values (`#FFC7C8` and `#BDDCFF`) inside the className (the
hover/active classes on the element with className="... hover:bg-[`#FFC7C8`]
active:bg-[`#FF6467`]" and similarly at line ~62) — replace these literal hex
colors with semantic design tokens instead: add new tokens (suggested names like
color/background/score/decrease-hover and color/background/score/increase-hover)
or map the exact existing primitive tokens (e.g., red/200 or blue/200) so they
match, then reference those tokens via Tailwind utility classes (configured in
tailwind/variables.json and tailwind.config) rather than inline hex values;
update the className in the PlayerStatus component to use the token-based
Tailwind classes.
| 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}`) | ||
| } |
There was a problem hiding this comment.
goto에서 showAnswer=true이고 round가 없을 때 URL이 잘못 구성됨
round가 undefined일 때 search는 빈 문자열("")이고, 여기에 "&answer=true"를 붙이면 ? 없이 game/1/play&answer=true가 됩니다. 현재 테스트에서는 goto에 showAnswer=true만 전달하는 케이스가 없어 문제가 드러나지 않지만, API 자체는 잘못된 동작을 합니다.
🐛 버그 수정 제안
- 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}`)
- }
+ async goto(gameId: string = "1", round?: number, showAnswer?: boolean) {
+ const params: string[] = []
+ if (round) params.push(`q=${round}`)
+ if (showAnswer) params.push("answer=true")
+ const search = params.length ? `?${params.join("&")}` : ""
+ await this.page.goto(`game/${gameId}/play${search}`)
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@service/app/src/app/game/`[gameId]/play/__tests__/gamePlayPOM.ts around lines
66 - 70, The goto helper constructs query string incorrectly when showAnswer is
true but round is undefined; update the goto function (search variable and URL
assembly) to build query parameters safely (e.g., use a params array or
URLSearchParams) so the first param is prefixed with '?' and subsequent params
with '&'; locate goto in gamePlayPOM.ts and change the logic that appends
"&answer=true" to conditionally add "?" when search is empty or use
URLSearchParams to produce a correct query string for
page.goto(`game/${gameId}/play${search}`).
| <PrimarySolidIconButton | ||
| className="right-0 top-16" | ||
| onClick={() => setIsSidebarOpen(!isSidebarOpen)} | ||
| aria-label="점수판 토글" | ||
| className="absolute right-24" | ||
| > | ||
| {isSidebarOpen ? <Show /> : <Hidden />} | ||
| </PrimarySolidIconButton> | ||
| </div> | ||
| {isSidebarOpen && ( | ||
| <div className="flex max-h-[850px] flex-1 flex-col items-center gap-24 overflow-y-scroll pb-48"> | ||
| <div className="flex min-h-0 flex-1 flex-col items-center gap-24 overflow-y-auto px-24 pb-24"> |
There was a problem hiding this comment.
토글 버튼에 aria-expanded 및 aria-controls 누락
현재 버튼은 aria-label만 있고 aria-expanded가 없어, 스크린 리더 사용자가 점수판 패널의 현재 펼침/접힘 상태를 알 수 없습니다. 토글 버튼은 제어하는 위젯의 id를 aria-controls에 설정하고, aria-expanded를 현재 상태로 설정해야 합니다.
♿ 제안된 수정 사항
+ {isSidebarOpen && (
+ <div
+ id="scoreboard-content"
+ className="flex min-h-0 flex-1 flex-col items-center gap-24 overflow-y-auto px-24 pb-24"
+ >
- {isSidebarOpen && (
- <div className="flex min-h-0 flex-1 flex-col items-center gap-24 overflow-y-auto px-24 pb-24"> <PrimarySolidIconButton
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
aria-label="점수판 토글"
+ aria-expanded={isSidebarOpen}
+ aria-controls="scoreboard-content"
className="absolute right-24"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <PrimarySolidIconButton | |
| className="right-0 top-16" | |
| onClick={() => setIsSidebarOpen(!isSidebarOpen)} | |
| aria-label="점수판 토글" | |
| className="absolute right-24" | |
| > | |
| {isSidebarOpen ? <Show /> : <Hidden />} | |
| </PrimarySolidIconButton> | |
| </div> | |
| {isSidebarOpen && ( | |
| <div className="flex max-h-[850px] flex-1 flex-col items-center gap-24 overflow-y-scroll pb-48"> | |
| <div className="flex min-h-0 flex-1 flex-col items-center gap-24 overflow-y-auto px-24 pb-24"> | |
| <PrimarySolidIconButton | |
| onClick={() => setIsSidebarOpen(!isSidebarOpen)} | |
| aria-label="점수판 토글" | |
| aria-expanded={isSidebarOpen} | |
| aria-controls="scoreboard-content" | |
| className="absolute right-24" | |
| > | |
| {isSidebarOpen ? <Show /> : <Hidden />} | |
| </PrimarySolidIconButton> | |
| </div> | |
| {isSidebarOpen && ( | |
| <div | |
| id="scoreboard-content" | |
| className="flex min-h-0 flex-1 flex-col items-center gap-24 overflow-y-auto px-24 pb-24" | |
| > |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@service/app/src/app/game/`[gameId]/play/components/scoreboardSidebar.tsx
around lines 33 - 42, The toggle button (PrimarySolidIconButton) is missing
aria-expanded and aria-controls; update the onClick/button element to include
aria-expanded={isSidebarOpen} and aria-controls pointing to the scoreboard
panel's id, and add a matching id attribute to the panel div (the element
rendered when isSidebarOpen is true) so screen readers can associate the button
with the controlled region; use existing symbols isSidebarOpen,
setIsSidebarOpen, PrimarySolidIconButton and the panel div to implement this
linkage.
| decorators: [ | ||
| () => ( | ||
| <div className="flex w-[400px] flex-col gap-16"> | ||
| <div className="text-sm font-medium text-gray-600"> | ||
| 점수 표시 상태 (scoreView=true) | ||
| </div> | ||
| <div className="flex flex-col gap-8"> | ||
| <PlayerStatus | ||
| name="A팀" | ||
| score="0점" | ||
| scoreView={true} | ||
| onScoreIncrease={() => console.log("점수 증가")} | ||
| onScoreDecrease={() => console.log("점수 감소")} | ||
| /> | ||
| <PlayerStatus | ||
| name="우승팀" | ||
| score="15점" | ||
| scoreView={true} | ||
| onScoreIncrease={() => console.log("점수 증가")} | ||
| onScoreDecrease={() => console.log("점수 감소")} | ||
| /> | ||
| <PlayerStatus | ||
| name="매우긴팀이름테스트" | ||
| score="20점" | ||
| scoreView={true} | ||
| onScoreIncrease={() => console.log("점수 증가")} | ||
| onScoreDecrease={() => console.log("점수 감소")} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="mt-16 text-sm font-medium text-gray-600"> | ||
| 팀명만 표시 (scoreView=false) | ||
| </div> | ||
| <div className="flex flex-col gap-8"> | ||
| <PlayerStatus name="A팀" score="" scoreView={false} /> | ||
| <PlayerStatus name="매우긴팀이름테스트" score="" scoreView={false} /> | ||
| <PlayerStatus name="Champions" score="" scoreView={false} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ), | ||
| ), | ||
| ], |
There was a problem hiding this comment.
AllStatesComparison 스토리 — 데코레이터 너비 충돌로 인한 오버플로 및 사용되지 않는 args
이 스토리에는 두 가지 문제가 있습니다.
1. 레이아웃 오버플로 (minor)
Storybook 데코레이터는 안쪽에서 바깥쪽으로 합성됩니다.
- 메타 데코레이터(바깥쪽):
<div className="w-[350px]"><Story /></div> - 스토리 데코레이터(안쪽):
<div className="flex w-[400px] flex-col gap-16">...</div>
결과적으로 400px 컨테이너가 350px 컨테이너 안에 렌더링되어 오버플로가 발생합니다. 스토리 데코레이터의 고정 너비를 w-full 또는 w-[350px]로 변경하거나 제거해 주세요.
2. 사용되지 않는 args (optional)
스토리 데코레이터 함수가 () => (...) 형태로 Story와 context 인자를 모두 무시하므로, args: { name: "비교용", score: "0점", scoreView: true }는 실제로 렌더링에 영향을 주지 않는 데드 코드입니다. Storybook Controls 패널에서 값을 변경해도 UI에 반영되지 않아 혼란을 줄 수 있습니다. args를 제거하거나, 의도적으로 무시한다는 주석을 추가해 주세요.
🛠️ 제안 수정
decorators: [
() => (
- <div className="flex w-[400px] flex-col gap-16">
+ <div className="flex w-full flex-col gap-16"> export const AllStatesComparison: Story = {
- args: {
- name: "비교용",
- score: "0점",
- scoreView: true,
- },
decorators: [🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shared/design/src/stories/playerStatus.stories.tsx` around lines 228 - 268,
AllStatesComparison story's inner decorator uses a fixed 400px width causing
overflow inside the meta decorator's 350px container and the decorator function
also ignores Story/context so story args are unused; update the decorator that
currently returns <div className="flex w-[400px] ..."> to use a responsive width
like w-full (or match outer width w-[350px]) instead of w-[400px], and either
remove the unused args (e.g., args: { name, score, scoreView }) from the story
or change the decorator signature to accept (Story, context) and forward
context.args into the rendered PlayerStatus components so Storybook controls
actually affect PlayerStatus (refer to AllStatesComparison decorator and
PlayerStatus props like name/score/scoreView).
prevent keyboard navigation when dialog is open / set layout on cases where no imageUrl exists
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
service/app/src/app/game/[gameId]/play/components/gamePlayContent.tsx (1)
51-51: 필수 propcurrentQuestion에 불필요한 옵셔널 체이닝 사용.
currentQuestion은 non-optional prop(currentQuestion: GameQuestion, line 7)이지만currentQuestion?.imageUrl(line 51),currentQuestion?.questionAnswer(lines 73, 95)에서 불필요하게 옵셔널 체이닝을 사용하고 있습니다. line 77과 96에서는 옵셔널 체이닝 없이 사용하는 것과 대비됩니다. 잘못된 타입 이해를 유발할 수 있으므로 일관성 있게?.을 제거하는 것이 좋습니다.♻️ 수정 제안
- const hasImage = !!currentQuestion?.imageUrl + const hasImage = !!currentQuestion.imageUrl- {currentQuestion?.questionAnswer} + {currentQuestion.questionAnswer}(lines 73 and 95 모두 동일하게 적용)
Also applies to: 73-73, 95-95
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@service/app/src/app/game/`[gameId]/play/components/gamePlayContent.tsx at line 51, The prop currentQuestion is non-optional so remove unnecessary optional chaining: replace occurrences of currentQuestion?.imageUrl (used in hasImage) and currentQuestion?.questionAnswer (used where questionAnswer is accessed around the render logic) with plain currentQuestion.imageUrl and currentQuestion.questionAnswer to be consistent with other uses; ensure hasImage and any other references use currentQuestion without ?. so types remain accurate and consistent (check usages near hasImage, and the two spots that access questionAnswer).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@service/app/src/app/game/`[gameId]/play/components/gamePlayContent.tsx:
- Around line 39-41: The keyboard handler for navigation is missing
e.preventDefault(), causing page scrolling to occur alongside navigation; update
the key handling branches in the component that checks e.key (the ArrowLeft,
ArrowRight and Enter cases) to call e.preventDefault() before invoking
onPrevQuestion(), onNextQuestion(), or the submit handler so navigation doesn't
trigger page scroll or default browser behavior (ensure ArrowLeft retains its
currentRound > 1 guard and ArrowRight/Enter branches mirror the preventDefault
addition).
- Around line 56-97: The UI hides questionText and uses a plain span for answers
when hasImage is false; move the <h1> that renders currentQuestion.questionText
out of the hasImage conditional so the question is always shown, and change the
no-image branch inside the image/container area to render the same highlighted
answer box used in the image case when showAnswer is true (use the same classes
as the div that displays currentQuestion?.questionAnswer in the hasImage true
path); update the conditional that currently returns the plain <span> to return
the emphasized answer container when !hasImage && showAnswer, otherwise keep
showing the question text.
- Around line 31-37: The global keydown handler is causing duplicate calls
because pressing Enter on a focused PrimaryBoxButton fires both keydown and the
button's onClick (calling onNextQuestion/onShowAnswer); in the window keydown
listener in gamePlayContent.tsx, detect if the event originates from a button
(e.g., check event.target or document.activeElement is an HTMLButtonElement or
has role="button") and early-return when true so the handler only acts for
non-button key events, preserving existing logic that calls onNextQuestion and
onShowAnswer otherwise.
---
Duplicate comments:
In `@service/app/src/app/game/`[gameId]/play/components/gamePlayContent.tsx:
- Around line 28-29: The current guard using
document.querySelector("[role='dialog']") in gamePlayContent.tsx (variable
isDialogOpen) is acceptable; no change required—keep the check as implemented
(isDialogOpen = document.querySelector("[role='dialog']") !== null) because
Radix UI Dialog sets role="dialog" and this runtime DOM query avoids stale
closure issues.
---
Nitpick comments:
In `@service/app/src/app/game/`[gameId]/play/components/gamePlayContent.tsx:
- Line 51: The prop currentQuestion is non-optional so remove unnecessary
optional chaining: replace occurrences of currentQuestion?.imageUrl (used in
hasImage) and currentQuestion?.questionAnswer (used where questionAnswer is
accessed around the render logic) with plain currentQuestion.imageUrl and
currentQuestion.questionAnswer to be consistent with other uses; ensure hasImage
and any other references use currentQuestion without ?. so types remain accurate
and consistent (check usages near hasImage, and the two spots that access
questionAnswer).
| if (e.key === "Enter" || e.key === "ArrowRight") { | ||
| if (showAnswer) { | ||
| onNextQuestion() | ||
| } else { | ||
| onShowAnswer() | ||
| } | ||
| } |
There was a problem hiding this comment.
포커스된 버튼에서 Enter 입력 시 onNextQuestion이 두 번 호출될 수 있습니다.
window keydown 핸들러와 PrimaryBoxButton의 onClick이 동일한 함수를 호출합니다. 사용자가 Tab으로 버튼에 포커스한 후 Enter를 누르면, 브라우저가 keydown → click 이벤트를 순차적으로 발생시켜 onNextQuestion()(또는 onShowAnswer())이 중복 호출됩니다. showAnswer === true일 때 두 번 호출되면 문제를 한 라운드 건너뛸 수 있습니다.
🐛 수정 제안 — 버튼 요소가 이벤트 발생원일 경우 early return 추가
(e: KeyboardEvent) => {
const isDialogOpen = document.querySelector("[role='dialog']") !== null
if (isDialogOpen) return
+ if (e.target instanceof HTMLButtonElement) return
+
if (e.key === "Enter" || e.key === "ArrowRight") {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@service/app/src/app/game/`[gameId]/play/components/gamePlayContent.tsx around
lines 31 - 37, The global keydown handler is causing duplicate calls because
pressing Enter on a focused PrimaryBoxButton fires both keydown and the button's
onClick (calling onNextQuestion/onShowAnswer); in the window keydown listener in
gamePlayContent.tsx, detect if the event originates from a button (e.g., check
event.target or document.activeElement is an HTMLButtonElement or has
role="button") and early-return when true so the handler only acts for
non-button key events, preserving existing logic that calls onNextQuestion and
onShowAnswer otherwise.
| if (e.key === "ArrowLeft" && currentRound > 1) { | ||
| onPrevQuestion() | ||
| } |
There was a problem hiding this comment.
Arrow 키에 e.preventDefault() 누락으로 스크롤과 네비게이션이 동시에 발생할 수 있습니다.
ArrowLeft/ArrowRight 처리 시 e.preventDefault()를 호출하지 않아, 페이지에 스크롤 가능한 영역이 있을 경우 네비게이션과 스크롤이 함께 발생합니다. Enter에도 같은 이슈가 있으나, ArrowRight 분기도 동일하게 적용하는 것이 일관성 있습니다.
🐛 수정 제안
if (e.key === "Enter" || e.key === "ArrowRight") {
+ e.preventDefault()
if (showAnswer) {
onNextQuestion()
} else {
onShowAnswer()
}
}
if (e.key === "ArrowLeft" && currentRound > 1) {
+ e.preventDefault()
onPrevQuestion()
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@service/app/src/app/game/`[gameId]/play/components/gamePlayContent.tsx around
lines 39 - 41, The keyboard handler for navigation is missing
e.preventDefault(), causing page scrolling to occur alongside navigation; update
the key handling branches in the component that checks e.key (the ArrowLeft,
ArrowRight and Enter cases) to call e.preventDefault() before invoking
onPrevQuestion(), onNextQuestion(), or the submit handler so navigation doesn't
trigger page scroll or default browser behavior (ensure ArrowLeft retains its
currentRound > 1 guard and ArrowRight/Enter branches mirror the preventDefault
addition).
| {hasImage && ( | ||
| <h1 className="text-center text-text-primary sm:typography-heading-3xl-extrabold md:typography-heading-4xl-extrabold"> | ||
| {currentQuestion.questionText} | ||
| </h1> | ||
| )} | ||
|
|
||
| {currentRound === totalRounds && ( | ||
| <Link | ||
| href="./result" | ||
| 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-secondary px-32 py-12 text-text-interactive-inverse hover:bg-background-interactive-primary-hovered active:border-transparent active:bg-background-interactive-primary-pressed" | ||
| > | ||
| 결과 화면으로 이동하기 | ||
| </Link> | ||
| <div | ||
| className="flex h-[246px] items-center justify-center overflow-hidden rounded-lg sm:h-[406px] md:h-[499px]" | ||
| style={ | ||
| hasImage && currentImageWidth | ||
| ? { width: currentImageWidth } | ||
| : undefined | ||
| } | ||
| > | ||
| {hasImage ? ( | ||
| showAnswer ? ( | ||
| <div className="flex size-full items-center justify-center rounded-12 border-2 border-border-interactive-secondary bg-background-interactive-inverse px-32 py-12 text-text-interactive-secondary sm:typography-heading-3xl-semibold md:typography-heading-4xl-semibold"> | ||
| {currentQuestion?.questionAnswer} | ||
| </div> | ||
| ) : ( | ||
| <Image | ||
| src={currentQuestion.imageUrl} | ||
| alt="문제 이미지" | ||
| width={0} | ||
| height={0} | ||
| sizes="100vw" | ||
| className="h-full w-auto" | ||
| onLoad={(e) => { | ||
| const img = e.currentTarget | ||
| setImageWidths((prev) => ({ | ||
| ...prev, | ||
| [currentRound]: img.offsetWidth, | ||
| })) | ||
| }} | ||
| /> | ||
| ) | ||
| ) : ( | ||
| <span className="text-center text-text-primary sm:typography-heading-3xl-extrabold md:typography-heading-4xl-extrabold"> | ||
| {showAnswer | ||
| ? currentQuestion?.questionAnswer | ||
| : currentQuestion.questionText} | ||
| </span> |
There was a problem hiding this comment.
이미지 없는 문제에서 정답 공개 시 questionText가 사라지며, 정답 스타일도 불일치합니다.
두 가지 문제가 함께 존재합니다:
-
questionText미노출:hasImage가false이면<h1>레더링이 생략되고, 정답 공개 시<span>이questionAnswer로 교체되면서 질문 텍스트가 완전히 사라집니다. 이미지 있는 문제는 정답 공개 후에도<h1>을 통해 질문이 항상 보이는 것과 비교하면 비대칭적인 UX 회귀입니다. -
정답 스타일 불일치: 이미지 있는 문제의 정답은
border,bg-background-interactive-inverse등이 적용된 강조 박스로 표시되지만, 이미지 없는 문제의 정답은 질문 텍스트와 동일한 일반<span>으로 표시되어 사용자가 현재 정답을 보고 있는지 구분하기 어렵습니다.
🐛 수정 제안
- {hasImage && (
- <h1 className="text-center text-text-primary sm:typography-heading-3xl-extrabold md:typography-heading-4xl-extrabold">
- {currentQuestion.questionText}
- </h1>
- )}
+ {(hasImage || showAnswer) && (
+ <h1 className="text-center text-text-primary sm:typography-heading-3xl-extrabold md:typography-heading-4xl-extrabold">
+ {currentQuestion.questionText}
+ </h1>
+ )}이미지 없는 문제에서 정답 공개 시 컨테이너 안의 <span> 내용도 동일한 강조 스타일을 사용하도록 변경:
) : (
- <span className="text-center text-text-primary sm:typography-heading-3xl-extrabold md:typography-heading-4xl-extrabold">
- {showAnswer
- ? currentQuestion?.questionAnswer
- : currentQuestion.questionText}
- </span>
+ showAnswer ? (
+ <div className="flex size-full items-center justify-center rounded-12 border-2 border-border-interactive-secondary bg-background-interactive-inverse px-32 py-12 text-text-interactive-secondary sm:typography-heading-3xl-semibold md:typography-heading-4xl-semibold">
+ {currentQuestion?.questionAnswer}
+ </div>
+ ) : (
+ <span className="text-center text-text-primary sm:typography-heading-3xl-extrabold md:typography-heading-4xl-extrabold">
+ {currentQuestion.questionText}
+ </span>
+ )
)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@service/app/src/app/game/`[gameId]/play/components/gamePlayContent.tsx around
lines 56 - 97, The UI hides questionText and uses a plain span for answers when
hasImage is false; move the <h1> that renders currentQuestion.questionText out
of the hasImage conditional so the question is always shown, and change the
no-image branch inside the image/container area to render the same highlighted
answer box used in the image case when showAnswer is true (use the same classes
as the div that displays currentQuestion?.questionAnswer in the hasImage true
path); update the conditional that currently returns the plain <span> to return
the emphasized answer container when !hasImage && showAnswer, otherwise keep
showing the question text.
📝 설명
🛠️ 주요 변경 사항
리뷰 시 고려해야 할 사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
스타일
테스트