Skip to content

Conversation

@kimnamheeee
Copy link
Contributor

@kimnamheeee kimnamheeee commented Dec 27, 2025

📝 설명

question, questionList 컴포넌트 관련 수정 요청사항을 처리하면서 일부 리팩토링, 접근성 개선을 진행했습니다.

🛠️ 주요 변경 사항

수정 요청사항 반영

  • error일 때는 ellipsis가 적용되지 않도록 수정
    • 이를 위해 state variant를 없앴습니다
  • 가장 위, 아래 question의 경우 위로가기 (또는 아래로 가기) 버튼 비활성화

기타 수정

  • question을 공통 컴포넌트에서 삭제
    • 도메인 로직이 강결합 되어 있다고 판단했고, 레이아웃 다양성이 높지 않아서 compound component 패턴도 필요 없다고 느낌
    • 레이아웃 유연성 고려 flex 도입
  • questionList, question을 listbox - option 기반으로 처리

리뷰 시 고려해야 할 사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 질문 목록에 키보드 네비게이션 추가 (화살표 키, Enter, Escape 지원)
    • 선택 가능한 질문 카드 UI 개선 및 ARIA 접근성 속성 추가
  • 버그 수정

    • 질문 이동 기능의 범위 검증 추가
  • 리팩토링

    • 내부 컴포넌트 구조 최적화

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 27, 2025

Walkthrough

문제 목록 UI를 새로운 Question 컴포넌트와 listbox 접근성 패턴으로 재구성하고, GameCreate 컴포넌트를 shared 디자인 라이브러리에서 로컬 엔티티로 이동했습니다. 테스트 셀렉터를 role 기반으로 업데이트하고 keyboard 네비게이션 훅을 추가했습니다.

Changes

코호트 / 파일 변경 요약
Question 컴포넌트 리팩토링
service/app/src/app/create/components/question.tsx, service/app/src/app/create/components/questionList.tsx
새로운 Question 컴포넌트 생성으로 선택 가능한 카드 렌더링 (role="option", 키보드 이벤트 처리, 삭제/이동 액션 버튼). questionList를 listbox 패턴으로 마이그레이션 (focusable container, useListboxNavigation 훅 통합)
접근성 및 테스트 업데이트
service/app/src/app/create/__tests__/createPOM.ts
셀렉터를 getByTestId에서 role 기반 쿼리로 변경: listbox ("문제 목록"), option (문제 카드)
Keyboard 네비게이션 훅
service/app/src/shared/lib/useListBoxNavigation.ts
ArrowUp/Down, Enter/Space, Escape를 처리하는 useListboxNavigation 훅 추가 (포커스 관리 포함)
GameCreate 컴포넌트 이동
service/app/src/entities/game/ui/gameCreate.tsx, shared/design/src/components/gameCreate/index.tsx
GameCreate를 shared 디자인에서 로컬 엔티티로 마이그레이션; onClick prop 제거, Link로 래핑
GameCreate import 경로 업데이트
service/app/src/app/dashboard/components/dashboardGameSection.tsx, service/app/src/entities/game/ui/gameLibraryGrid.tsx
GameCreate 컴포넌트 import 경로를 @ject-5-fe/design/components/gameCreate에서 @/entities/game/ui/gameCreate로 변경
SSRSafeSuspense 제거
service/app/src/shared/SSRSafeSuspense.tsx
파일 삭제
Storybook 스토리 제거
shared/design/src/stories/gameCreate.stories.tsx
GameCreate 스토리 파일 삭제
경계 체크 추가
service/app/src/app/create/store/useCreateGameStore.tsx
moveQuestion에서 존재하지 않는 질문 ID와 out-of-bounds 인덱스에 대한 early return 추가

Sequence Diagram

sequenceDiagram
    participant User
    participant QuestionList
    participant Question
    participant useListboxNavigation
    participant Store as useCreateGameStore

    User->>QuestionList: 포커스 (onFocus)
    activate QuestionList
    QuestionList->>useListboxNavigation: onListboxFocus 호출
    activate useListboxNavigation
    useListboxNavigation->>Question: 선택된 옵션 또는 첫 옵션으로 포커스
    deactivate useListboxNavigation
    deactivate QuestionList

    User->>Question: ArrowDown/Up 키 입력
    activate Question
    Question->>useListboxNavigation: onOptionKeyDown 호출
    activate useListboxNavigation
    useListboxNavigation->>Question: 다음/이전 옵션으로 포커스 이동
    deactivate useListboxNavigation
    deactivate Question

    User->>Question: Enter/Space 또는 클릭
    activate Question
    Question->>Store: 질문 선택 상태 업데이트
    activate Store
    Store-->>Question: 상태 반영
    deactivate Store
    deactivate Question

    User->>Question: 삭제/이동 액션 버튼 클릭
    activate Question
    Question->>Store: deleteQuestion / moveQuestion 호출
    activate Store
    Store->>Store: 경계 체크 수행
    Store-->>QuestionList: 업데이트된 질문 목록
    deactivate Store
    deactivate Question
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

👩🏻‍💻frontend, 🪄refactor, 🐛bug

Suggested reviewers

  • whdgur5717

Poem

🐰 문제 리스트, 이제 상자처럼
화살표로 쉽게 건너다니고
GameCreate는 집으로 돌아왔네
버튼들 정렬되고, 테스트 반짝반짝
접근성 좋고, 코드도 깔끔해! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 주요 변경사항인 question, questionList 컴포넌트 수정을 명확하게 반영하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션(설명, 주요 변경사항)을 포함하고 있으며, 변경사항이 구체적이고 명확합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-components

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee65678 and 257a641.

📒 Files selected for processing (1)
  • service/app/src/app/create/components/question.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • service/app/src/app/create/components/question.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Dec 27, 2025

Deploy preview for re-creation ready!

✅ Preview
https://re-creation-bhldk9g9l-whdgur5717s-projects.vercel.app

Built with commit 257a641.
This pull request is being automatically deployed with vercel-action

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
service/app/src/entities/game/ui/gameLibraryGrid.tsx (1)

66-68: 중첩된 Link 컴포넌트 제거 필요

GameCreate는 이미 /create로 이동하는 Link 컴포넌트입니다. 이를 다시 Link로 감싸면 중첩된 <a> 태그가 생성되어 HTML 유효성 검증 실패 및 접근성 문제가 발생합니다.

🔎 수정 제안
-      <Link href="/create" prefetch={true}>
-        <GameCreate />
-      </Link>
+      <GameCreate />

또는 GameCreate가 단순 div로 변경되어야 한다면, 외부 Link만 유지:

       <Link href="/create" prefetch={true}>
-        <GameCreate />
+        {/* GameCreate를 링크가 아닌 콘텐츠 컴포넌트로 변경 필요 */}
       </Link>
service/app/src/app/dashboard/components/dashboardGameSection.tsx (1)

47-49: 중복된 클릭 핸들러 제거 필수

GameCreate 컴포넌트는 이미 /create로 이동하는 Link 요소이고, handleCreateGamerouter.push("/create")만 수행합니다. 외부 divonClick={handleCreateGame}은 동일한 네비게이션을 중복으로 처리하고 있어 불필요합니다.

divLink 요소를 중첩하면 접근성 위반이 발생합니다:

  • 스크린 리더가 두 개의 인터랙티브 요소로 인식
  • 의미론적으로 올바르지 않은 HTML 구조
  • 사용자 혼란 유발 가능

해결: 래퍼 div를 제거하고 GameCreate 컴포넌트만 사용하거나, 추가 로직이 필요하면 GameCreate 내부에 통합하세요.

🧹 Nitpick comments (4)
service/app/src/entities/game/ui/gameCreate.tsx (2)

11-11: cn 유틸리티 사용 권장

코드베이스의 다른 컴포넌트들(예: gameCard.tsx)은 클래스명 결합에 cn 유틸리티를 사용하고 있습니다. 일관성을 위해 템플릿 리터럴 대신 cn을 사용하는 것을 권장합니다.

🔎 제안된 리팩터링
+import { cn } from "@ject-5-fe/design/utils/cn"
 import Image from "next/image"
 import Link from "next/link"
 
 interface GameCreateProps {
   className?: string
 }
 
 export const GameCreate = ({ className = "" }: GameCreateProps) => {
   return (
     <Link
-      className={`flex w-full flex-col items-center gap-12 ${className}`}
+      className={cn("flex w-full flex-col items-center gap-12", className)}
       href="/create"
       aria-label="게임 만들기"
     >

15-21: Image 크기 속성 일관성 확인 필요

width={178}height={178} 속성을 지정하면서 동시에 className="size-full"을 사용하고 있습니다. size-fullwidth: 100%; height: 100%를 의미하므로 고정 크기와 충돌할 수 있습니다. Next.js Image의 fill 속성 사용 또는 고정 크기만 사용하는 것이 명확합니다.

🔎 제안된 수정 방안

방안 1: 고정 크기 유지 (현재 의도가 178px 고정인 경우)

       <Image
         src="/create-game-icon.svg"
         alt="게임 만들기 아이콘"
         width={178}
         height={178}
-        className="size-full object-contain"
+        className="object-contain"
       />

방안 2: 부모 컨테이너 크기에 맞추기 (반응형이 필요한 경우)

       <Image
         src="/create-game-icon.svg"
         alt="게임 만들기 아이콘"
-        width={178}
-        height={178}
-        className="size-full object-contain"
+        fill
+        className="object-contain"
       />
service/app/src/shared/lib/useListBoxNavigation.ts (1)

21-50: 키보드 네비게이션 구현 우수

ARIA listbox 패턴을 잘 따르는 키보드 핸들러입니다:

  • ArrowDown/Up으로 옵션 간 이동
  • Enter/Space로 선택
  • Escape로 포커스 복원
  • 자식 요소 이벤트 필터링

단, onSelect 매개변수가 UseListboxNavigationParams 인터페이스에 포함되지 않아 문서화가 부족합니다. 옵션별 콜백 지원이 의도된 것이라면 JSDoc 주석 추가를 고려하세요.

🔎 문서화 개선 제안
+/**
+ * Keyboard navigation handler for listbox options.
+ * @param e - Keyboard event
+ * @param onSelect - Optional callback invoked when Enter or Space is pressed
+ */
 const onOptionKeyDown = (
   e: React.KeyboardEvent<HTMLElement>,
   onSelect?: () => void,
 ) => {
service/app/src/app/create/components/question.tsx (1)

59-66: 제목 렌더링 및 에러 표시 우수

에러 시 전체 텍스트 표시와 ❗ 이모지 추가, 정상 시 line-clamp-1 적용이 잘 구현되었습니다. break-keep은 한국어 텍스트에 적합합니다.

단, Line 62의 클래스명에 약간의 중복이 있습니다:

🔎 클래스명 정리 제안

line-clamp-1은 Tailwind에서 이미 overflow-hiddentext-ellipsis를 포함하므로 중복 제거 가능:

           className={cn(
             "typography-heading-sm-medium w-full min-w-0 break-keep text-text-primary",
-            shouldClampTitle && "line-clamp-1 overflow-hidden text-ellipsis",
+            shouldClampTitle && "line-clamp-1",
           )}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8015193 and 5dd60dd.

📒 Files selected for processing (10)
  • service/app/src/app/create/__tests__/createPOM.ts
  • service/app/src/app/create/components/question.tsx
  • service/app/src/app/create/components/questionList.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/shared/SSRSafeSuspense.tsx
  • service/app/src/shared/lib/useListBoxNavigation.ts
  • shared/design/src/components/gameCreate/index.tsx
  • shared/design/src/stories/gameCreate.stories.tsx
💤 Files with no reviewable changes (3)
  • shared/design/src/stories/gameCreate.stories.tsx
  • service/app/src/shared/SSRSafeSuspense.tsx
  • shared/design/src/components/gameCreate/index.tsx
🧰 Additional context used
📓 Path-based instructions (9)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

All file names must be camelCase (not PascalCase or kebab-case)

Files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/app/create/components/question.tsx
  • service/app/src/shared/lib/useListBoxNavigation.ts
  • service/app/src/app/create/components/questionList.tsx
  • service/app/src/app/create/__tests__/createPOM.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Component names must use PascalCase in the code (while file names use camelCase)
Use Zustand for client-side state management
Use TanStack Query for server state and API caching
Use async/await for promises instead of .then() chains
Use custom fetch client with interceptors from shared/lib/fetchClient.ts for API requests

**/*.{ts,tsx}: Use TypeScript with ES modules; keep files lower-case kebab or camel per existing folder conventions (e.g., gameLibraryGrid.tsx)
Favor explicit domain names (e.g., GameCardOptions, useInfiniteMyGames) and colocate UI, API, and hooks within each entities/<domain> module
Run ESLint using lint scripts; linting is enforced via lint-staged

Files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/app/create/components/question.tsx
  • service/app/src/shared/lib/useListBoxNavigation.ts
  • service/app/src/app/create/components/questionList.tsx
  • service/app/src/app/create/__tests__/createPOM.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use shared ESLint configuration across all workspaces

Files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/app/create/components/question.tsx
  • service/app/src/shared/lib/useListBoxNavigation.ts
  • service/app/src/app/create/components/questionList.tsx
  • service/app/src/app/create/__tests__/createPOM.ts
**/*.{js,ts,jsx,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier with Tailwind CSS plugin for formatting

Files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/app/create/components/question.tsx
  • service/app/src/shared/lib/useListBoxNavigation.ts
  • service/app/src/app/create/components/questionList.tsx
  • service/app/src/app/create/__tests__/createPOM.ts
**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the codebase

Files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/app/create/components/question.tsx
  • service/app/src/shared/lib/useListBoxNavigation.ts
  • service/app/src/app/create/components/questionList.tsx
  • service/app/src/app/create/__tests__/createPOM.ts
**/*.{ts,tsx,css,json,md}

📄 CodeRabbit inference engine (AGENTS.md)

Apply Prettier formatting using yarn workspace <pkg> format; this is enforced via lint-staged

Files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/app/create/components/question.tsx
  • service/app/src/shared/lib/useListBoxNavigation.ts
  • service/app/src/app/create/components/questionList.tsx
  • service/app/src/app/create/__tests__/createPOM.ts
**/src/entities/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

In the Next.js app workspace, place feature logic under src/entities and colocate Vitest specs in feature folders

Files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Tailwind CSS utilities should be ordered via the Prettier Tailwind plugin; avoid inline style objects unless necessary

Files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/app/create/components/question.tsx
  • service/app/src/app/create/components/questionList.tsx
**/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

In the Next.js app workspace, UI routes live in src/app

Files:

  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
  • service/app/src/app/create/components/question.tsx
  • service/app/src/app/create/components/questionList.tsx
🧠 Learnings (4)
📚 Learning: 2025-11-27T13:46:12.950Z
Learnt from: CR
Repo: JECT-Study/JECT-5-FE PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T13:46:12.950Z
Learning: Applies to **/*.{ts,tsx} : Favor explicit domain names (e.g., `GameCardOptions`, `useInfiniteMyGames`) and colocate UI, API, and hooks within each `entities/<domain>` module

Applied to files:

  • service/app/src/entities/game/ui/gameCreate.tsx
  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
📚 Learning: 2025-11-27T13:46:12.950Z
Learnt from: CR
Repo: JECT-Study/JECT-5-FE PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T13:46:12.950Z
Learning: Applies to **/src/entities/**/*.tsx : In the Next.js app workspace, place feature logic under `src/entities` and colocate Vitest specs in feature folders

Applied to files:

  • service/app/src/entities/game/ui/gameCreate.tsx
📚 Learning: 2025-11-27T13:46:12.950Z
Learnt from: CR
Repo: JECT-Study/JECT-5-FE PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T13:46:12.950Z
Learning: Applies to **/src/components/**/*.{ts,tsx} : In `shared/design` package, define reusable UI components in `src/components` and use them in the app workspace instead of redefining widgets

Applied to files:

  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
📚 Learning: 2025-11-27T13:46:12.950Z
Learnt from: CR
Repo: JECT-Study/JECT-5-FE PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T13:46:12.950Z
Learning: Applies to **/src/icons/**/*.{ts,tsx} : In `shared/design` package, define reusable icons in `src/icons` and import from this shared package instead of duplicating icon definitions

Applied to files:

  • service/app/src/entities/game/ui/gameLibraryGrid.tsx
  • service/app/src/app/dashboard/components/dashboardGameSection.tsx
🧬 Code graph analysis (2)
service/app/src/entities/game/ui/gameCreate.tsx (1)
service/app/src/entities/game/ui/GameCard/gameCard.tsx (1)
  • Image (45-74)
service/app/src/app/create/components/questionList.tsx (2)
service/app/src/shared/lib/useListBoxNavigation.ts (1)
  • useListboxNavigation (7-56)
service/app/src/app/create/components/question.tsx (1)
  • Question (26-116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci
🔇 Additional comments (15)
service/app/src/app/dashboard/components/dashboardGameSection.tsx (1)

9-9: 컴포넌트 임포트 경로 변경 확인 완료

GameCreate 컴포넌트를 외부 디자인 라이브러리에서 내부 엔티티 경로로 이동한 것이 PR 목표와 일치합니다.

service/app/src/entities/game/ui/gameLibraryGrid.tsx (1)

10-10: 컴포넌트 임포트 경로 변경 확인 완료

내부 엔티티 경로로의 임포트 변경이 올바르게 적용되었습니다.

service/app/src/app/create/__tests__/createPOM.ts (2)

52-52: 접근성 기반 셀렉터로 개선

data-testid에서 role="listbox" 기반 셀렉터로 변경하여 실제 사용자 경험과 접근성을 반영하는 테스트가 되었습니다.


75-77: ARIA 역할 기반 셀렉터 개선

role="group"에서 role="option"으로 변경하여 ARIA listbox 패턴을 올바르게 반영합니다. 새로운 Question 컴포넌트의 구조와 일치합니다.

service/app/src/app/create/components/questionList.tsx (4)

5-17: listbox 네비게이션 훅 통합 우수

useListboxNavigation 훅을 올바르게 통합하여 키보드 네비게이션과 포커스 관리를 구현했습니다. 접근성 개선을 위한 좋은 아키텍처 선택입니다.


37-45: ARIA listbox 구현 우수

role="listbox", aria-label, tabIndex={0}, onFocus 핸들러를 사용하여 완전한 키보드 접근 가능한 listbox를 구현했습니다. ARIA 모범 사례를 잘 따르고 있습니다.


50-50: 이미지 소스 폴백 체인 우수

imageUrl || previewImageUrl || null 폴백 체인은 다양한 이미지 상태를 안전하게 처리합니다.


53-68: Question 컴포넌트 props 구조 우수

각 Question에 필요한 모든 속성(index, isSelected, hasError, 이미지, 액션 등)을 명확하게 전달하고 있습니다. 잘 구조화된 컴포넌트 인터페이스입니다.

service/app/src/shared/lib/useListBoxNavigation.ts (2)

10-19: 포커스 관리 로직 우수

선택된 옵션(aria-selected="true")이나 첫 번째 옵션으로 포커스를 이동하는 로직이 올바르게 구현되었습니다. currentTarget 체크로 이벤트 버블링을 적절히 처리합니다.


29-37: DOM 탐색 방식 검증 - 현재 구조에서 안전함

nextElementSiblingpreviousElementSibling을 사용한 옵션 간 이동은 현재 QuestionList 구조에서 정상 작동합니다. questions.map()으로 렌더링된 각 Question 컴포넌트가 listbox div의 직접 자식 요소로 형제 관계를 이루고 있기 때문입니다. 다만 향후 옵션 요소들을 추가 컨테이너로 감싸는 구조로 리팩터링할 경우, 이 방식은 실패할 수 있으니 주의가 필요합니다.

service/app/src/app/create/components/question.tsx (5)

9-24: Props 인터페이스 설계 우수

선택적 props와 액션 객체를 사용한 유연한 인터페이스 설계가 잘 되어 있습니다. 타입 안정성과 사용성을 모두 고려한 구조입니다.


37-38: 파생 값 로직 명확함

canDelete 기본값과 shouldClampTitle 로직이 명확합니다. 에러가 있을 때 전체 텍스트를 표시하는 것은 좋은 UX 결정입니다.


41-57: ARIA 옵션 구현 우수

role="option", aria-label, aria-selected, tabIndex={-1}을 사용한 완벽한 ARIA listbox option 구현입니다. 조건부 스타일링(에러/선택 상태)도 올바르게 우선순위가 지정되어 있습니다.


68-78: 삭제 버튼 구현 우수

stopPropagation으로 이벤트 전파를 막고, disabled 상태와 접근성 레이블을 올바르게 구현했습니다.


82-90: 선택적 이미지 렌더링 우수

imageSrc 존재 여부에 따른 조건부 렌더링과 일관된 스타일링이 잘 구현되어 있습니다.

Comment on lines 48 to 49
const questionError =
question.text.length > 50 || question.text.length < 1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for question text length constraints and validation patterns
rg -nP --type=ts --type=tsx -C3 '(question.*text|text.*length).*(50|1|length|validation)' -g '!node_modules' | head -100

Repository: JECT-Study/JECT-5-FE

Length of output: 91


🏁 Script executed:

# Also search for any constant definitions or type constraints
rg -nP --type=ts --type=tsx 'const.*50|MAX.*QUESTION|MIN.*QUESTION|length.*50' -g '!node_modules'

Repository: JECT-Study/JECT-5-FE

Length of output: 91


🏁 Script executed:

# Search for server-side validation in API routes or validation files
rg -nP --type=ts 'question.*text.*50|length.*50|text.*validate' service/api | head -50

Repository: JECT-Study/JECT-5-FE

Length of output: 115


🏁 Script executed:

# Look for the specific file and examine its context
fd -e tsx -e ts 'questionList' service/app/src/app/create/components/

Repository: JECT-Study/JECT-5-FE

Length of output: 118


🏁 Script executed:

# Search for any constants or validation schemas
rg -nP 'QUESTION.*TEXT|QUESTION.*LENGTH|validation.*question' --type=ts --type=tsx -g '!node_modules' | head -50

Repository: JECT-Study/JECT-5-FE

Length of output: 91


🏁 Script executed:

# Explore the repository structure
git ls-files | head -50

Repository: JECT-Study/JECT-5-FE

Length of output: 1721


🏁 Script executed:

# Read the questionList.tsx file
cat -n service/app/src/app/create/components/questionList.tsx

Repository: JECT-Study/JECT-5-FE

Length of output: 3373


🏁 Script executed:

# Search for validation patterns without file type restriction
rg 'length.*50|50.*length' -C3 | head -100

Repository: JECT-Study/JECT-5-FE

Length of output: 2457


🏁 Script executed:

# Search for question validation in the entire codebase
rg 'question.*text.*50|text.*50' -C2 | head -100

Repository: JECT-Study/JECT-5-FE

Length of output: 4303


🏁 Script executed:

# Look for server-side validation files
fd 'validator\|validation\|schema' service/

Repository: JECT-Study/JECT-5-FE

Length of output: 46


클라이언트 검증의 중앙화 및 서버 검증 필요

질문 텍스트 길이 제약(1-50자)이 questionList.tsx, createGameNavigation.tsx, questionInputForm.tsx에 걸쳐 하드코딩되어 있습니다. 또한 일부는 trim() 사용 여부가 다르며, 답변 필드도 동일 제약을 공유합니다. 이 비즈니스 규칙을 다음과 같이 개선하세요:

  • 길이 제약을 공유 상수로 정의 (예: shared/lib/constants.ts)
  • 모든 클라이언트 검증에서 이 상수 사용
  • 서버 측 검증 추가 필요 (현재 서버 측 유효성 검사 미발견)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
service/app/src/app/create/components/questionList.tsx (1)

48-49: 검증 상수 중앙화 필요 (이전 리뷰 지적사항)

질문 텍스트 길이 제약(1-50자)이 여전히 하드코딩되어 있습니다. 이전 리뷰에서 이미 지적된 사항으로, 공유 상수로 정의하고 서버 측 검증도 추가해야 합니다.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5dd60dd and ee65678.

📒 Files selected for processing (3)
  • service/app/src/app/create/components/question.tsx
  • service/app/src/app/create/components/questionList.tsx
  • service/app/src/app/create/store/useCreateGameStore.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • service/app/src/app/create/components/question.tsx
🧰 Additional context used
📓 Path-based instructions (8)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

All file names must be camelCase (not PascalCase or kebab-case)

Files:

  • service/app/src/app/create/store/useCreateGameStore.tsx
  • service/app/src/app/create/components/questionList.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Component names must use PascalCase in the code (while file names use camelCase)
Use Zustand for client-side state management
Use TanStack Query for server state and API caching
Use async/await for promises instead of .then() chains
Use custom fetch client with interceptors from shared/lib/fetchClient.ts for API requests

**/*.{ts,tsx}: Use TypeScript with ES modules; keep files lower-case kebab or camel per existing folder conventions (e.g., gameLibraryGrid.tsx)
Favor explicit domain names (e.g., GameCardOptions, useInfiniteMyGames) and colocate UI, API, and hooks within each entities/<domain> module
Run ESLint using lint scripts; linting is enforced via lint-staged

Files:

  • service/app/src/app/create/store/useCreateGameStore.tsx
  • service/app/src/app/create/components/questionList.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use shared ESLint configuration across all workspaces

Files:

  • service/app/src/app/create/store/useCreateGameStore.tsx
  • service/app/src/app/create/components/questionList.tsx
**/*.{js,ts,jsx,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier with Tailwind CSS plugin for formatting

Files:

  • service/app/src/app/create/store/useCreateGameStore.tsx
  • service/app/src/app/create/components/questionList.tsx
**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation throughout the codebase

Files:

  • service/app/src/app/create/store/useCreateGameStore.tsx
  • service/app/src/app/create/components/questionList.tsx
**/*.{ts,tsx,css,json,md}

📄 CodeRabbit inference engine (AGENTS.md)

Apply Prettier formatting using yarn workspace <pkg> format; this is enforced via lint-staged

Files:

  • service/app/src/app/create/store/useCreateGameStore.tsx
  • service/app/src/app/create/components/questionList.tsx
**/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

In the Next.js app workspace, UI routes live in src/app

Files:

  • service/app/src/app/create/store/useCreateGameStore.tsx
  • service/app/src/app/create/components/questionList.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Tailwind CSS utilities should be ordered via the Prettier Tailwind plugin; avoid inline style objects unless necessary

Files:

  • service/app/src/app/create/store/useCreateGameStore.tsx
  • service/app/src/app/create/components/questionList.tsx
🧬 Code graph analysis (1)
service/app/src/app/create/components/questionList.tsx (2)
service/app/src/shared/lib/useListBoxNavigation.ts (1)
  • useListboxNavigation (7-56)
service/app/src/app/create/components/question.tsx (1)
  • Question (28-122)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci
🔇 Additional comments (4)
service/app/src/app/create/store/useCreateGameStore.tsx (1)

78-85: 경계 검사 로직이 올바르게 구현되었습니다!

moveQuestion 함수에 추가된 방어 코드가 적절합니다:

  • 유효하지 않은 인덱스에 대한 조기 반환
  • 배열 경계를 벗어나는 이동 방지

이는 UI에서 첫 번째/마지막 질문의 버튼을 비활성화하는 변경사항과 잘 연계됩니다.

service/app/src/app/create/components/questionList.tsx (3)

5-17: 접근성 개선을 위한 리팩토링이 잘 적용되었습니다.

useListboxNavigation 훅과 새로운 Question 컴포넌트를 사용하여 키보드 탐색이 가능한 리스트박스 패턴을 적절히 도입했습니다.


37-45: 리스트박스 ARIA 속성이 올바르게 구현되었습니다.

role="listbox", aria-label, tabIndex={0} 및 포커스 핸들러가 적절히 설정되어 스크린 리더 및 키보드 사용자에게 접근 가능한 인터페이스를 제공합니다.


63-70: 액션 버튼 비활성화 로직이 정확합니다.

첫 번째/마지막 질문에서 이동 버튼을 비활성화하는 로직(canMoveUp, canMoveDown)이 useCreateGameStore의 경계 검사와 올바르게 연동됩니다. PR 목표와 일치합니다.

onMoveUp: () => moveQuestion(question.id, "up"),
onMoveDown: () => moveQuestion(question.id, "down"),
}}
onKeyDown={onOptionKeyDown}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

키보드로 질문 선택이 작동하지 않습니다.

onOptionKeyDownonSelect 콜백이 전달되지 않아, 키보드 사용자가 Enter 또는 Space 키로 질문을 선택할 수 없습니다. useListboxNavigation 훅의 구현을 보면 Enter/Space 입력 시 onSelect?.() 를 호출하는데, 현재는 undefined입니다.

🔎 제안하는 수정 방법

onKeyDown 핸들러를 래핑하여 onSelect 콜백을 전달하세요:

              onKeyDown={onOptionKeyDown}
+             onKeyDown={(e) => 
+               onOptionKeyDown(e, () => setSelectedQuestionId(question.id))
+             }

또는 더 명확하게:

+             onKeyDown={(e) => {
+               onOptionKeyDown(e, () => {
+                 setSelectedQuestionId(question.id)
+               })
+             }}
-             onKeyDown={onOptionKeyDown}
🤖 Prompt for AI Agents
In service/app/src/app/create/components/questionList.tsx around line 71, the
onKeyDown handler is set to onOptionKeyDown without passing the onSelect
callback so keyboard Enter/Space cannot trigger selection; fix by wrapping the
handler to call onOptionKeyDown with the onSelect prop (or modify
onOptionKeyDown to accept and forward onSelect) — e.g. provide an inline
function that calls onOptionKeyDown(event, { onSelect }) or bind the onSelect
into the hook call so that when Enter/Space is detected it invokes the provided
onSelect callback.

Comment on lines +52 to +60
className={cn(
"flex min-h-[134px] w-full min-w-0 justify-between gap-16 rounded-8 bg-background-primary px-20 py-24",
onClick ? "cursor-pointer" : "cursor-default",
hasError && "ring-2 ring-inset ring-border-interactive-input-error",
!hasError &&
isSelected &&
"ring-2 ring-inset ring-border-interactive-primary",
className,
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cva 이용해서 정의해도 좋을것 같습니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 service/app에 cva가 따로 설치가 안되어 있어서 우선 cn만 썼는데 참고하겠습니다

@@ -0,0 +1,56 @@
import { RefObject } from "react"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 저 리스트를 키보드로 focus 이동하기위한 hook인가요? 그렇다면 roving-tabindex, roving-focus 같은 키워드로 한번 찾아보시면 좋을것 같습니다. 훨씬 깔끔하게 처리할 수 있을것같아요
https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/src/tabs.tsx

Comment on lines +38 to +45
<div
ref={listboxRef}
className="flex flex-col gap-24 overflow-y-auto"
role="listbox"
aria-label="문제 목록"
tabIndex={0}
onFocus={onListboxFocus}
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

목록이라면 div가 아니라 ui/li 태그로 처리해도 되지 않나요?
https://techblog.woowahan.com/24605/ 한번 참고해서 보이스오버로 의도한대로 읽히는지도 테스트한번 해보면 좋을것 같습니다

  • ul 태그를 쓰더라도 css reset 처리를 해놨으면 role=list는 유지해야 한다고 하네요

Copy link
Contributor Author

@kimnamheeee kimnamheeee Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 ul/li는 좀 더 단순 텍스트 위주 요소에 붙는 게 맞다고 봅니다
지금은 카드형 ui + 그 자체로 인터랙션 컨테이너 역할을 해서 div가 적합하다고 생각해요

보이스오버 테스트는 곧 따로 진행할 예정입니다

@kimnamheeee kimnamheeee merged commit b19a1b1 into dev Dec 27, 2025
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 4, 2026
@kimnamheeee kimnamheeee self-assigned this Jan 6, 2026
@kimnamheeee kimnamheeee deleted the fix-components branch January 7, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants