Skip to content

Conversation

@kimnamheeee
Copy link
Contributor

@kimnamheeee kimnamheeee commented Jul 31, 2025

📝 설명

게임 생성 폼의 초기 구현을 완료했습니다. 질문 입력, 이미지 업로드, 상태 관리 기능을 포함한 게임 생성 플로우를 구현했습니다.

🛠️ 주요 변경 사항

상태 관리 및 데이터 구조

리뷰 시 고려해야 할 사항

Summary by CodeRabbit

  • 신규 기능

    • 게임 생성 페이지에 게임 생성 컨텍스트 및 상태 관리 기능이 도입되어, 문제 추가/삭제, 이동, 이미지 업로드, 게임명 및 문제 입력의 실시간 검증과 상태 관리를 지원합니다.
    • 문제 목록, 문제 입력, 파일 업로드, 게임명 입력 등 주요 UI가 컨텍스트 기반으로 동작하며, 입력 오류 및 상태에 따른 피드백을 제공합니다.
    • 저장 기능이 개선되어 저장 진행 상태와 오류 처리가 명확하게 반영됩니다.
    • 이미지 업로드 시 파일 타입, 용량(2MB 이하) 검증이 강화되고, 업로드 실패 시 안내 팝업이 표시됩니다.
    • 게임 저장 시 이미지 업로드를 위한 사전 서명 URL 요청 및 S3 업로드 과정을 포함한 전체 저장 프로세스가 자동화되었습니다.
  • 버그 수정

    • 파일 검증 오류 메시지가 명확한 코드와 한글 메시지로 제공됩니다.
    • 이미지 업로드 및 저장 과정에서 발생할 수 있는 오류 처리와 안내가 개선되었습니다.
  • 리팩터링/스타일

    • 주요 UI 컴포넌트가 컨텍스트 기반의 상태 관리로 구조가 개선되어 유지보수성과 일관성이 향상되었습니다.
    • 문제 카드에 이미지 미리보기, 삭제 가능 여부, 클릭 이벤트 지원이 추가되었습니다.
  • 테스트

    • 파일 검증 테스트가 오류 코드 및 메시지 검증으로 보강되었습니다.
  • 기타

@kimnamheeee kimnamheeee linked an issue Jul 31, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Jul 31, 2025

Walkthrough

게임 생성 기능의 상태 관리 구조가 대폭 도입되었습니다. 새로운 컨텍스트와 리듀서, 셀렉터, 유틸리티 함수, 액션 타입 등이 추가되어 게임 생성 페이지와 관련 컴포넌트들이 이 구조를 활용하도록 리팩터링되었습니다. 이미지 업로드 및 저장 로직, 파일 검증, API 연동, UI 상태 관리가 모두 통합적으로 관리됩니다.

Changes

Cohort / File(s) Change Summary
게임 생성 상태 관리 도입
service/app/src/entities/game/model/state/create/actions.ts, .../reducer.ts, .../selectors.ts, .../state.ts, .../useGameCreation.ts, .../utils.ts, .../gameCreationContext.tsx, .../index.ts, service/app/src/entities/game/model/index.ts, service/app/src/entities/game/model/state/index.ts
게임 생성 전용 컨텍스트, 리듀서, 액션, 셀렉터, 상태 타입, 유틸리티 함수 등 상태 관리 모듈 일체 신설 및 index에서 재수출. 기존 index에 관련 export 추가.
게임 생성 페이지 및 UI 컴포넌트 리팩터링
service/app/src/app/create/page.tsx, .../ui/components/createGameNavigation.tsx, .../ui/components/questionList.tsx, .../ui/components/fileUploadArea.tsx, .../ui/components/questionInputForm.tsx, .../ui/popupManager.tsx, shared/design/src/components/question/index.tsx
게임 생성 컨텍스트 기반으로 입력, 질문 목록, 파일 업로드, 네비게이션, 팝업 등 UI 컴포넌트 전면 리팩터링. 입력값, 상태, 유효성 검사, 이벤트 핸들러 등이 컨텍스트/셀렉터/액션을 통해 동작하도록 변경.
게임 저장 및 이미지 업로드 유틸리티
service/app/src/entities/game/utils/gameSave.ts, .../s3Upload.ts, .../index.ts
게임 저장 로직 분리 및 이미지 S3 업로드 유틸리티 개선. 업로드 결과 타입 명확화 및 오류 처리 방식 개선.
API 및 파일 검증 로직
service/app/src/entities/game/api/createGame.ts, .../api/index.ts, service/app/src/entities/game/utils/fileValidation.ts, .../utils/__tests__/fileValidation.test.ts
createGame API 단순화(이미지 업로드 제거), 파일 검증 로직의 에러 코드화, 테스트 코드 수정, API index에서 createGame 재수출.
기타 환경설정
shared/lib/fetchClient.ts
fetchClient의 기본 baseUrl을 "http://localhost:3000"으로 변경.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CreateGamePage
  participant GameCreationProvider
  participant UIComponents
  participant Reducer/Selectors
  participant API

  User->>CreateGamePage: 페이지 진입
  CreateGamePage->>GameCreationProvider: 컨텍스트 초기화
  GameCreationProvider->>UIComponents: 상태/액션/셀렉터 제공
  User->>UIComponents: 입력/질문/이미지 업로드/저장 등 상호작용
  UIComponents->>Reducer/Selectors: 액션 디스패치 및 상태 조회
  UIComponents->>PopupManager: 팝업 표시/숨김
  UIComponents->>API: 저장(이미지 업로드 및 게임 생성)
  API-->>UIComponents: 저장 결과 반환
  UIComponents-->>User: 결과/오류 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • whdgur5717

Poem

🐰
새로운 상태의 봄이 왔네
질문도 답도, 이미지도 한데
컨텍스트 품에 쏙 안기고
팝업과 저장도 똑똑하게
토끼는 기뻐 깡충깡충
코드를 보며 춤을 추네!
🥕✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4be8a3f and b23e8d9.

📒 Files selected for processing (4)
  • service/app/src/entities/game/model/state/create/selectors.ts (1 hunks)
  • service/app/src/entities/game/ui/components/questionInputForm.tsx (1 hunks)
  • service/app/src/entities/game/ui/components/questionList.tsx (1 hunks)
  • service/app/src/entities/game/ui/popupManager.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • service/app/src/entities/game/ui/components/questionList.tsx
  • service/app/src/entities/game/ui/components/questionInputForm.tsx
  • service/app/src/entities/game/model/state/create/selectors.ts
⏰ 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 (2)
service/app/src/entities/game/ui/popupManager.tsx (2)

5-8: 새로운 의존성 추가가 적절합니다.

게임 생성 상태 관리와 저장 유틸리티의 통합을 위한 import들이 올바르게 추가되었습니다.


20-20: 상태 관리 컨텍스트 통합이 잘 구현되었습니다.

게임 생성 컨텍스트에서 state와 actions를 올바르게 가져와 사용하고 있습니다.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 35-implement-initial-game-creation-form

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@kimnamheeee kimnamheeee self-assigned this Jul 31, 2025
@kimnamheeee kimnamheeee added ✨feature 구현, 개선 사항 관련 부분 👩🏻‍💻frontend 프론트엔드 작업 labels Jul 31, 2025
@kimnamheeee kimnamheeee changed the title 35 implement initial game creation form 게임 생성 폼 상태관리 추가 Jul 31, 2025
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

🧹 Nitpick comments (4)
service/app/src/entities/game/ui/components/questionInputForm.tsx (1)

28-37: 제어된 컴포넌트로의 리팩터링이 우수합니다.

하지만 검증 로직을 개선할 수 있습니다. 현재는 값이 비어있으면 "error" 상태로 설정하는데, 더 정교한 검증 로직을 고려해보세요.

다음과 같이 검증 로직을 개선할 수 있습니다:

-        <Field 
-          type="labelOn" 
-          state={selectedQuestion?.text ? "default" : "error"} 
-          name="question" 
-          className="w-full"
-        >
+        <Field 
+          type="labelOn" 
+          state={selectedQuestion?.text?.trim() ? "default" : "error"} 
+          name="question" 
+          className="w-full"
+        >

또한 selectors에서 검증 상태를 가져오는 것을 고려해보세요:

-  const selectedQuestion = selectors.selectedQuestion;
+  const selectedQuestion = selectors.selectedQuestion;
+  const validationErrors = selectors.validationErrors;

Also applies to: 44-53

service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1)

3-3: 임포트 문에서 공백 누락이 있습니다.

ReactNode,useContext 사이에 공백이 누락되었습니다.

-import { createContext, ReactNode,useContext } from 'react';
+import { createContext, ReactNode, useContext } from 'react';
shared/design/src/components/question/index.tsx (1)

43-60: 이미지 표시 로직 개선 제안

이미지 표시 구현이 기본적으로는 잘 되어 있지만, 몇 가지 개선점이 있습니다:

  1. 이미지 로딩 실패 처리가 없습니다
  2. /checker.svg 경로가 하드코딩되어 있습니다

다음과 같이 개선할 수 있습니다:

      {/* Image - 오른쪽 */}
      <div className="absolute right-14 top-5">
        {image ? (
          <img 
            src={image} 
            alt="질문 이미지" 
            className="size-[78px] rounded-[7px] object-cover"
+           onError={(e) => {
+             e.currentTarget.src = "/checker.svg";
+           }}
          />
        ) : (
          <div className="flex size-[78px] items-center justify-center rounded-[7px] bg-gray-200">
            <img 
-             src="/checker.svg" 
+             src="/images/default-question.svg" 
              alt="기본 이미지" 
              className="size-[78px] rounded-[7px]"
            />
          </div>
        )}
      </div>
service/app/src/entities/game/ui/components/questionList.tsx (1)

22-22: 질문 상태 결정 로직 개선 제안

현재 로직이 기본적으로는 좋지만, 더 명확하게 만들 수 있습니다:

-              state={isSelected ? "selected" : question.text ? "default" : "error"}
+              state={isSelected ? "selected" : (question.text?.trim() ? "default" : "error")}

빈 문자열이나 공백만 있는 경우도 에러 상태로 처리하도록 개선할 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf4738f and 580e590.

⛔ Files ignored due to path filters (1)
  • service/app/public/checker.svg is excluded by !**/*.svg
📒 Files selected for processing (19)
  • service/app/src/app/create/page.tsx (2 hunks)
  • service/app/src/entities/game/model/gameCreationReducer.ts (1 hunks)
  • service/app/src/entities/game/model/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/actions.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1 hunks)
  • service/app/src/entities/game/model/state/create/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/reducer.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/selectors.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/state.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/useGameCreation.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/utils.ts (1 hunks)
  • service/app/src/entities/game/model/state/index.ts (1 hunks)
  • service/app/src/entities/game/ui/components/createGameNavigation.tsx (2 hunks)
  • service/app/src/entities/game/ui/components/fileUploadArea.tsx (2 hunks)
  • service/app/src/entities/game/ui/components/questionInputForm.tsx (1 hunks)
  • service/app/src/entities/game/ui/components/questionList.tsx (1 hunks)
  • service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (2 hunks)
  • service/app/src/entities/game/utils/fileValidation.ts (2 hunks)
  • shared/design/src/components/question/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (1)
service/app/src/entities/game/utils/fileValidation.ts (2)
  • validateImageFile (18-40)
  • validateMultipleFiles (48-56)
shared/design/src/components/question/index.tsx (3)
shared/design/src/components/button/destructiveSolidIconButton.tsx (2)
  • DestructiveSolidIconButton (32-45)
  • DestructiveSolidIconButtonProps (26-30)
service/app/src/entities/game/ui/questionList.tsx (1)
  • QuestionList (5-30)
service/app/src/entities/game/model/gameRequest.ts (1)
  • GameUpdateQuestion (28-34)
service/app/src/entities/game/model/state/create/state.ts (2)
service/app/src/entities/game/model/gameRequest.ts (5)
  • GameCreateQuestion (21-26)
  • GameCreateRequest (36-42)
  • GameUpdateQuestion (28-34)
  • GameUpdateRequest (44-50)
  • PresignedUrlImage (52-55)
service/app/src/entities/game/model/game.ts (2)
  • GameQuestion (20-27)
  • GameDetailData (29-35)
🔇 Additional comments (75)
service/app/src/entities/game/utils/fileValidation.ts (5)

10-10: 타입 안전성 개선 승인

FileValidationError 타입 추가로 에러 처리의 타입 안전성이 크게 개선되었습니다. 문자열 리터럴 유니온 타입 사용이 적절합니다.


12-16: 구조화된 에러 리포팅 시스템 개선 승인

FileValidationResult 인터페이스의 개선이 우수합니다. error 코드와 message를 분리하여 프로그래매틱한 처리와 사용자 친화적인 메시지를 동시에 지원할 수 있게 되었습니다.


24-29: 한국어 에러 메시지 및 에러 코드 처리 승인

INVALID_FILE_TYPE 에러에 대한 한국어 메시지와 구조화된 에러 코드 반환이 적절합니다. 지원 형식을 동적으로 표시하는 것도 좋은 개선점입니다.


31-37: 파일 크기 에러 처리 개선 승인

FILE_SIZE_TOO_LARGE 에러에 대한 한국어 메시지와 동적 크기 표시가 우수합니다. 2MB 제한이 적절히 반영되어 있습니다.


8-8: 파일 크기 제한(10MB → 2MB) 변경 검증 완료

rg 검색 결과, “10MB” 관련 참조는 없으며, 서비스 엔티티와 테스트 외에 다른 모듈에서 MAX_FILE_SIZE를 사용하고 있지 않음을 확인했습니다. Storybook 내 로컬 MAX_SIZE도 이미 2MB로 정의되어 있어 영향 범위가 한정적입니다.

  • service/app/src/entities/game/utils/fileValidation.ts: MAX_FILE_SIZE가 2MB로 일관되게 적용됨
  • service/app/src/entities/game/utils/tests/fileValidation.test.ts: 3MB 파일 실패 테스트 통과
  • shared/design/src/stories/upload.stories.tsx: 로컬 MAX_SIZE 기본값이 2MB로 설정됨
service/app/src/entities/game/model/state/create/state.ts (5)

12-18: PopupState 인터페이스 승인

팝업 상태를 세분화하여 관리하는 접근법이 우수합니다. 각각의 팝업 상태(종료 확인, 저장 확인, 이미지 업로드 에러 등)를 독립적으로 관리할 수 있어 UI 제어가 정확해집니다.


20-23: LoadingState 인터페이스 승인

저장과 업로드 작업의 로딩 상태를 분리하여 관리하는 것이 적절합니다. 사용자에게 더 구체적인 피드백을 제공할 수 있습니다.


25-28: ErrorState 인터페이스 승인

게임명 에러와 전역 에러를 분리하여 관리하는 구조가 좋습니다. 에러 처리의 세분화로 더 나은 사용자 경험을 제공할 수 있습니다.


30-41: GameCreationState 메인 인터페이스 승인

전체적인 상태 구조가 잘 설계되었습니다. 게임 데이터, UI 상태, 에러 상태, 로딩 상태를 논리적으로 분리하여 관리하는 것이 우수합니다. 특히 isGameNameEditing과 isGameNameFocused 같은 세부적인 UI 상태까지 고려한 것이 좋습니다.


1-10: Question 인터페이스 필드 매핑 확인 완료

아래와 같이 기존 인터페이스와 일치함을 확인했습니다:

  • order ⇔ questionOrder
  • text ⇔ questionText
  • answer ⇔ questionAnswer

추가된 UI 상태 관리용 필드(imageFile, imageUploadError, isImageHovered)도 적절히 설계되었습니다.

service/app/src/entities/game/model/state/index.ts (1)

1-1: Barrel 파일 구조 승인

State 관리 모듈의 구조화를 위한 barrel 파일 추가가 적절합니다. 향후 확장성을 고려한 좋은 설계입니다.

service/app/src/entities/game/model/state/create/index.ts (1)

1-6: 게임 생성 모듈 Barrel 파일 승인

게임 생성 상태 관리와 관련된 모든 모듈을 중앙 집중식으로 재내보내는 구조가 우수합니다. 소비자 코드에서 간단한 import로 필요한 모든 기능에 접근할 수 있어 개발자 경험이 개선됩니다.

service/app/src/entities/game/model/index.ts (1)

22-22: 게임 생성 상태 관리 모듈 통합 승인

새로운 './state/create' 모듈 재내보내기 추가가 적절합니다. 기존 내보내기를 유지하면서 새로운 게임 생성 기능을 통합하여 하위 호환성과 확장성을 모두 고려한 좋은 설계입니다.

service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (4)

25-26: 테스트 어설션 개선이 잘 적용되었습니다.

구체적인 에러 코드 검증과 한국어 메시지 포함 여부 확인으로 테스트 정확도가 향상되었습니다.


30-30: 파일 크기 테스트가 새로운 제한에 맞게 조정되었습니다.

3MB 파일로 테스트하여 2MB 제한을 초과하는 시나리오를 올바르게 검증합니다.


33-34: 에러 처리 구조화가 일관되게 적용되었습니다.

파일 크기 초과 에러에 대해서도 동일한 패턴으로 에러 코드와 메시지를 검증합니다.


52-53: 다중 파일 검증 테스트도 동일한 패턴으로 업데이트되었습니다.

validateMultipleFiles 함수에 대한 테스트도 구조화된 에러 처리 방식에 맞게 일관되게 수정되었습니다.

Also applies to: 60-61

service/app/src/app/create/page.tsx (2)

1-1: 게임 생성 컨텍스트 통합이 올바르게 구현되었습니다.

GameCreationProvider 임포트를 통해 중앙 집중식 상태 관리 시스템을 페이지에 통합했습니다.


10-27: 컨텍스트 프로바이더 계층 구조가 적절합니다.

GameCreationProvider를 최상위에 배치하고 PopupManager를 내부에 두어, 모든 자식 컴포넌트가 게임 생성 상태에 접근할 수 있도록 했습니다.

service/app/src/entities/game/ui/components/questionInputForm.tsx (2)

5-9: 컨텍스트 통합이 올바르게 구현되었습니다.

useGameCreationContext 훅을 사용하여 중앙 집중식 상태 관리와 연결했습니다.


11-21: 이벤트 핸들러가 적절하게 구현되었습니다.

선택된 질문이 있을 때만 업데이트를 수행하는 안전한 로직입니다.

service/app/src/entities/game/model/state/create/gameCreationContext.tsx (2)

7-19: React 컨텍스트 패턴이 올바르게 구현되었습니다.

TypeScript 타입 추론과 컨텍스트 프로바이더 구현이 적절합니다.


21-27: 컨텍스트 사용 검증 로직이 우수합니다.

프로바이더 외부에서 사용 시 명확한 에러 메시지를 제공하여 개발자 경험을 향상시킵니다.

service/app/src/entities/game/ui/components/fileUploadArea.tsx (3)

4-7: 필요한 의존성이 올바르게 추가되었습니다.

Next.js Image 컴포넌트와 컨텍스트, 검증 유틸리티 임포트가 적절합니다.


15-34: 파일 업로드 핸들러가 우수하게 구현되었습니다.

선택된 질문 확인, 첫 번째 파일만 처리, 검증 결과에 따른 적절한 팝업 표시, 그리고 컨텍스트를 통한 업로드 액션 디스패치가 잘 구현되었습니다.


46-56: 이미지 미리보기 기능이 잘 구현되었습니다.

조건부 렌더링과 Next.js Image 컴포넌트를 사용한 최적화된 이미지 표시가 적절합니다.

service/app/src/entities/game/ui/components/createGameNavigation.tsx (4)

11-11: 컨텍스트 가져오기가 올바르게 구현됨

새로운 중앙 집중식 상태 관리 시스템과의 통합이 적절하게 이루어졌습니다.


16-28: 이벤트 핸들러 구현이 명확하고 일관됨

게임 이름 입력에 대한 이벤트 핸들러들이 컨텍스트 액션과 올바르게 연결되어 있습니다.


61-68: 버튼 활성화 조건 로직이 적절함

canAddQuestion 셀렉터를 통한 조건부 버튼 활성화가 올바르게 구현되어 있습니다.


36-52: 폼 검증 및 오류 표시 로직 확인 완료

selectors.gameNameError
service/app/src/entities/game/model/state/create/selectors.ts에 정의되어 있으며,
<Field> 컴포넌트에서 error 상태 전환과 <ErrorText> 렌더링이 의도한 대로 동작합니다.
해당 부분은 추가 수정이 필요하지 않습니다.

shared/design/src/components/question/index.tsx (3)

12-14: 새로운 Props 추가가 적절함

이미지 표시, 삭제 제어, 클릭 핸들링을 위한 새로운 props가 올바르게 정의되었습니다.


32-36: 클릭 가능한 스타일링과 이벤트 핸들러

cursor-pointer 클래스와 onClick 핸들러가 적절하게 추가되었습니다.


64-72: 조건부 삭제 버튼 로직이 올바름

canDelete prop을 통한 조건부 비활성화가 적절하게 구현되었습니다.

service/app/src/entities/game/model/gameCreationReducer.ts (6)

1-4: Import 구조가 명확함

필요한 유틸리티 함수들과 타입들이 올바르게 import되었습니다.


28-37: ADD_QUESTION 액션 처리 로직 확인

새 질문 추가 시 자동으로 선택되는 로직이 적절합니다.


81-106: 이미지 업로드 시작 시 검증 로직이 우수함

파일 검증을 먼저 수행하고, 유효하지 않은 경우 에러 상태로 즉시 처리하는 로직이 잘 구현되었습니다.


108-122: 업로드 성공 처리가 적절함

로딩 상태 해제와 이미지 URL 업데이트, 에러 상태 초기화가 올바르게 처리되었습니다.


39-53: DELETE_QUESTION에서 빈 질문 배열 처리 정상 확인됨

updatedQuestions.length > 0 ? updatedQuestions[0].id : null 로 질문이 모두 삭제된 경우 selectedQuestionIdnull로 설정하여 빈 배열 상황을 안전하게 처리하고 있습니다. 추가 수정이 필요하지 않습니다.


209-210: RESET_FORM 액션에 사용된 createInitialState 함수 정의 확인 완료

service/app/src/entities/game/model/state/create/utils.ts에서
export const createInitialState = (): GameCreationState => ({ … })
로 초기 상태를 올바르게 반환하도록 구현되어 있어,
case 'RESET_FORM': return createInitialState(); 을 그대로 사용해도 문제가 없습니다.

service/app/src/entities/game/ui/components/questionList.tsx (2)

5-8: 컨텍스트 통합이 올바르게 구현됨

새로운 게임 생성 컨텍스트와의 통합이 적절하게 이루어졌습니다.


13-30: 동적 질문 렌더링 로직이 우수함

질문 상태 결정 로직과 이벤트 핸들러 연결이 잘 구현되었습니다. 특히 canDelete 조건이 질문 개수에 따라 적절하게 설정되어 있습니다.

service/app/src/entities/game/model/state/create/actions.ts (3)

3-28: 액션 타입 정의가 포괄적이고 체계적임

게임 생성 과정의 모든 상태 변경을 다루는 잘 구조화된 discriminated union이 정의되었습니다.


30-36: 팝업 상태 인터페이스가 명확함

다양한 팝업 타입들이 명확하게 정의되어 있어 UI 상태 관리가 용이합니다.


38-129: 액션 크리에이터가 타입 안전하게 구현됨

모든 액션 크리에이터가 적절한 타입과 페이로드로 구현되었습니다. 코드의 일관성과 타입 안전성이 우수합니다.

service/app/src/entities/game/model/state/create/reducer.ts (17)

1-11: 임포트 구조가 잘 정리되어 있습니다.

필요한 의존성들이 적절히 임포트되어 있고, 유틸리티 함수들과 타입 정의가 명확하게 분리되어 있습니다.


17-21: 게임 이름 설정 로직이 올바릅니다.

불변성을 유지하며 상태를 적절히 업데이트하고 있습니다.


23-33: 게임 이름 포커스/편집 상태 관리가 적절합니다.

불변성을 유지하며 UI 상태를 올바르게 관리하고 있습니다.


35-44: 질문 추가 로직이 올바르게 구현되어 있습니다.

새로 추가된 질문을 자동으로 선택하는 UX가 좋으며, 유틸리티 함수를 적절히 활용하고 있습니다.


46-60: 질문 삭제 로직이 잘 구현되어 있습니다.

선택된 질문이 삭제될 때의 처리와 새로운 선택 대상 결정 로직이 적절합니다.


62-70: 질문 업데이트 로직이 간결하고 올바릅니다.

유틸리티 함수를 활용한 깔끔한 구현입니다.


72-76: 질문 선택 로직이 올바릅니다.

간단하고 명확한 상태 업데이트입니다.


78-86: 질문 이동 로직이 적절합니다.

유틸리티 함수를 활용한 깔끔한 구현으로, 방향 정보를 올바르게 전달하고 있습니다.


88-113: 이미지 업로드 시작 로직이 잘 구현되어 있습니다.

파일 검증을 먼저 수행하고, 실패 시 조기 반환하는 패턴이 효율적입니다. 로딩 상태와 에러 상태 관리도 적절합니다.


115-129: 이미지 업로드 성공 처리가 올바릅니다.

로딩 상태 해제와 에러 초기화, 이미지 URL 저장이 적절히 처리되고 있습니다.


131-144: 이미지 업로드 에러 처리가 적절합니다.

로딩 상태를 해제하고 에러 메시지를 올바르게 저장하고 있습니다.


146-152: 이미지 호버 상태 관리가 올바릅니다.

유틸리티 함수를 활용한 깔끔한 상태 업데이트입니다.


154-163: 팝업 표시 로직이 잘 구현되어 있습니다.

계산된 속성명을 사용하여 특정 팝업만 선택적으로 업데이트하는 방식이 효율적입니다.


165-174: 팝업 숨김 로직이 일관되게 구현되어 있습니다.

SHOW_POPUP과 동일한 패턴을 사용하여 일관성을 유지하고 있습니다.


176-205: 게임 저장 관련 상태 관리가 잘 구현되어 있습니다.

비동기 작업의 시작, 성공, 실패 상태를 일관되게 관리하고 있으며, 에러 처리도 적절합니다.


207-214: 전역 에러 설정 로직이 올바릅니다.

에러 상태를 적절히 관리하고 있습니다.


216-221: 폼 리셋과 기본 케이스가 올바르게 구현되어 있습니다.

초기 상태로 완전히 리셋하는 로직과 표준 리듀서 패턴을 잘 따르고 있습니다.

service/app/src/entities/game/model/state/create/useGameCreation.ts (4)

1-15: 훅 설정이 올바르게 구성되어 있습니다.

클라이언트 사이드 렌더링 지시자와 TypeScript 타이핑이 적절하며, useReducer 설정이 올바릅니다.


17-91: 액션 콜백 함수들이 올바르게 구현되어 있습니다.

useCallback을 사용한 성능 최적화와 빈 의존성 배열 사용이 적절하며, 각 액션에 대한 디스패치가 정확합니다.


93-106: 파생 상태 계산이 잘 구조화되어 있습니다.

셀렉터를 활용한 검증, 권한, 상태 정보 제공이 체계적이며, UI 컴포넌트에서 필요한 모든 정보를 포괄하고 있습니다.


108-144: 질문별 셀렉터와 반환 구조가 잘 설계되어 있습니다.

관심사의 적절한 분리와 UI 컴포넌트에서 사용하기 편리한 구조로 되어 있습니다.

service/app/src/entities/game/model/state/create/utils.ts (6)

3-35: 초기 상태 생성 함수들이 올바르게 구현되어 있습니다.

기본값 설정과 한국어 로컬라이제이션이 적절하며, 초기 상태 구조가 완전합니다.


37-57: 질문 이동 함수가 잘 구현되어 있습니다.

경계 검사와 유효하지 않은 ID 처리가 적절하며, 순서 업데이트 로직이 올바릅니다.


59-64: 질문 순서 업데이트 함수가 간결하고 올바릅니다.

인덱스 기반 순서 업데이트 로직이 명확합니다.


66-80: 이미지 URL 관리 함수들이 적절히 구현되어 있습니다.

FileReader 사용과 blob URL 검사 로직이 올바르며, 메모리 관리를 고려한 구현입니다.


82-93: 게임 이름 생성 함수가 올바르게 구현되어 있습니다.

중복 방지 로직이 적절하며, 한국어 기본 이름 사용이 좋습니다. 대량의 기존 게임이 있을 경우 성능을 고려할 수 있지만, 일반적인 사용 사례에서는 문제없습니다.


95-126: 질문 CRUD 함수들이 잘 구현되어 있습니다.

불변성 유지, 마지막 질문 삭제 방지, 고유 ID 생성 로직이 적절합니다. 타임스탬프와 랜덤값을 조합한 ID 생성은 실용적이며 충돌 가능성이 매우 낮습니다.

service/app/src/entities/game/model/state/create/selectors.ts (4)

3-26: 검증 함수들이 적절히 구현되어 있습니다.

문자 수 제한과 한국어 에러 메시지가 적절하며, 게임 콘텐츠에 맞는 합리적인 제한 사항들입니다.


28-43: 기본 셀렉터들이 올바르게 구현되어 있습니다.

비즈니스 로직이 적절하며, 최대 질문 수 제한과 마지막 질문 삭제 방지 로직이 좋은 UX를 제공합니다.


45-81: 이동 및 저장 관련 셀렉터들이 잘 구현되어 있습니다.

순서 기반 이동 제한 로직과 포괄적인 저장 검증이 적절하며, 상태 매핑을 통한 UI 피드백 지원이 좋습니다.


87-103: 질문별 셀렉터들이 적절히 구현되어 있습니다.

기존 셀렉터들을 재사용하여 일관성을 유지하고 있으며, 질문별 작업에 필요한 편의 메서드들을 잘 제공하고 있습니다.

Comment on lines 82 to 84
hasChanges: (state: GameCreationState): boolean => {
return state.gameName !== '게임1' || state.questions.length > 0;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

변경 사항 감지 로직에 문제가 있습니다.

hasChanges 함수가 state.questions.length > 0를 확인하는데, 초기 상태에도 항상 하나의 질문이 있으므로 변경되지 않은 폼에서도 항상 true를 반환합니다.

다음과 같이 수정하는 것을 제안합니다:

  hasChanges: (state: GameCreationState): boolean => {
-    return state.gameName !== '게임1' || state.questions.length > 0;
+    return state.gameName !== '게임1' || 
+           state.questions.length > 1 || 
+           state.questions.some(q => q.text.trim() !== '' || q.answer.trim() !== '');
  }
📝 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.

Suggested change
hasChanges: (state: GameCreationState): boolean => {
return state.gameName !== '게임1' || state.questions.length > 0;
}
hasChanges: (state: GameCreationState): boolean => {
return state.gameName !== '게임1' ||
state.questions.length > 1 ||
state.questions.some(q => q.text.trim() !== '' || q.answer.trim() !== '');
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/selectors.ts around lines 82
to 84, the hasChanges function incorrectly returns true when
state.questions.length > 0, because the initial state always contains one
question. To fix this, update the logic to compare the current questions array
with the initial questions array or check for actual modifications beyond just
length, ensuring hasChanges only returns true when real changes have been made.

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: 5

🧹 Nitpick comments (10)
service/app/src/entities/game/model/state/index.ts (1)

1-1: 빈 배럴 파일 검토 필요

현재 이 파일은 주석만 포함하고 있으며 실제 export가 없습니다. 실제 export가 필요할 때 추가하는 것을 고려해보세요.

service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1)

3-3: import 문에서 공백 누락이 있습니다.

useContext 앞에 공백이 누락되었습니다.

-import { createContext, ReactNode,useContext } from 'react';
+import { createContext, ReactNode, useContext } from 'react';
service/app/src/entities/game/model/state/create/actions.ts (1)

30-36: PopupState 인터페이스가 내부적으로만 사용되는데 export가 누락되었습니다.

Line 19-20에서 keyof PopupState를 사용하고 있지만, 인터페이스가 export되지 않아서 다른 파일에서 타입을 재사용할 수 없습니다.

필요하다면 다음과 같이 export를 추가하세요:

-interface PopupState {
+export interface PopupState {
service/app/src/entities/game/model/state/create/useGameCreation.ts (2)

17-91: useCallback 의존성 배열 검토가 필요합니다.

모든 useCallback 함수들이 빈 의존성 배열 []을 사용하고 있습니다. dispatch는 React에서 안정적이므로 문제없지만, 코드 일관성을 위해 명시적으로 [dispatch]를 사용하는 것을 고려해보세요.


108-113: getQuestionSelectors 성능 최적화가 필요합니다.

getQuestionSelectors 함수가 호출될 때마다 새로운 객체를 생성합니다. 이는 불필요한 리렌더링을 유발할 수 있습니다.

다음과 같이 useMemo로 최적화할 수 있습니다:

+import { useCallback, useMemo, useReducer } from 'react';
+
+  const getQuestionSelectors = useMemo(() => 
+    (questionId: string) => ({
+      canDelete: questionSelectors.canDelete(state),
+      canMoveUp: questionSelectors.canMoveUp(state, questionId),
+      canMoveDown: questionSelectors.canMoveDown(state, questionId),
+      isSelected: questionSelectors.isSelected(state, questionId)
+    }), [state]);
-  const getQuestionSelectors = (questionId: string) => ({
-    canDelete: questionSelectors.canDelete(state),
-    canMoveUp: questionSelectors.canMoveUp(state, questionId),
-    canMoveDown: questionSelectors.canMoveDown(state, questionId),
-    isSelected: questionSelectors.isSelected(state, questionId)
-  });
service/app/src/entities/game/model/state/create/utils.ts (3)

3-12: ID 생성 방식의 일관성을 개선해주세요.

createInitialQuestion에서는 단순한 문자열 템플릿을 사용하고, addQuestion에서는 타임스탬프 + 랜덤을 사용합니다. 일관된 ID 생성 전략을 사용하는 것이 좋겠습니다.

crypto.randomUUID() 또는 nanoid 같은 라이브러리 사용을 고려해보세요:

+import { nanoid } from 'nanoid';
+
 export const createInitialQuestion = (order: number): Question => ({
-  id: `question-${order}`,
+  id: nanoid(),
   text: '',
   answer: '',
   imageFile: null,
   imageUrl: null,
   imageUploadError: null,
   isImageHovered: false,
   order
 });

82-93: 게임 이름 생성 알고리즘의 성능을 개선해주세요.

현재 구현은 O(n*m) 시간 복잡도를 가집니다 (n: 기존 이름 개수, m: 찾는 번호). 기존 이름들을 Set으로 변환하여 O(m) 시간 복잡도로 개선할 수 있습니다.

 export const generateGameName = (existingNames: string[]): string => {
+  const nameSet = new Set(existingNames);
   const baseName = '게임';
   let counter = 1;
   let newName = `${baseName}${counter}`;
   
-  while (existingNames.includes(newName)) {
+  while (nameSet.has(newName)) {
     counter++;
     newName = `${baseName}${counter}`;
   }
   
   return newName;
 };

120-126: ID 충돌 가능성을 검토해주세요.

Date.now() + Math.random() 조합은 이론적으로 충돌 가능성이 있습니다. 특히 빠른 연속 호출 시 같은 타임스탬프를 가질 수 있습니다.

더 안전한 ID 생성을 위해 crypto.randomUUID() 사용을 고려해보세요:

 export const addQuestion = (questions: Question[]): Question[] => {
   const newQuestion = {
     ...createInitialQuestion(questions.length),
-    id: `question-${Date.now()}-${Math.random()}`
+    id: crypto.randomUUID()
   };
   return [...questions, newQuestion];
 };
service/app/src/entities/game/model/state/create/selectors.ts (2)

3-26: 매직 넘버를 상수로 정의해주세요.

유효성 검사에서 사용되는 숫자값들(30자, 50자)을 상수로 정의하면 유지보수성이 향상됩니다.

+const GAME_NAME_MAX_LENGTH = 30;
+const QUESTION_TEXT_MAX_LENGTH = 50;
+const ANSWER_TEXT_MAX_LENGTH = 50;
+
 export const validateGameName = (name: string): string | null => {
   if (name.length === 0) return "게임 이름을 입력해주세요.";
-  if (name.length > 30) return "게임 이름은 30자를 초과할 수 없습니다.";
+  if (name.length > GAME_NAME_MAX_LENGTH) return `게임 이름은 ${GAME_NAME_MAX_LENGTH}자를 초과할 수 없습니다.`;
   return null;
 };

87-89: 중복된 로직을 제거해주세요.

questionSelectors.canDeleteselectors.canDeleteQuestion과 동일한 로직을 중복으로 구현하고 있습니다.

 export const questionSelectors = {
   canDelete: (state: GameCreationState): boolean => {
-    return state.questions.length > 1;
+    return selectors.canDeleteQuestion(state);
   },
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf4738f and 580e590.

⛔ Files ignored due to path filters (1)
  • service/app/public/checker.svg is excluded by !**/*.svg
📒 Files selected for processing (19)
  • service/app/src/app/create/page.tsx (2 hunks)
  • service/app/src/entities/game/model/gameCreationReducer.ts (1 hunks)
  • service/app/src/entities/game/model/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/actions.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1 hunks)
  • service/app/src/entities/game/model/state/create/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/reducer.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/selectors.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/state.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/useGameCreation.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/utils.ts (1 hunks)
  • service/app/src/entities/game/model/state/index.ts (1 hunks)
  • service/app/src/entities/game/ui/components/createGameNavigation.tsx (2 hunks)
  • service/app/src/entities/game/ui/components/fileUploadArea.tsx (2 hunks)
  • service/app/src/entities/game/ui/components/questionInputForm.tsx (1 hunks)
  • service/app/src/entities/game/ui/components/questionList.tsx (1 hunks)
  • service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (2 hunks)
  • service/app/src/entities/game/utils/fileValidation.ts (2 hunks)
  • shared/design/src/components/question/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (1)
service/app/src/entities/game/utils/fileValidation.ts (2)
  • validateImageFile (18-40)
  • validateMultipleFiles (48-56)
service/app/src/entities/game/ui/components/questionInputForm.tsx (4)
service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1)
  • useGameCreationContext (21-27)
service/app/src/entities/game/model/state/create/selectors.ts (1)
  • selectors (28-85)
shared/design/src/components/input/index.tsx (4)
  • Root (85-85)
  • Field (95-111)
  • Label (114-123)
  • Control (127-164)
service/app/src/entities/game/ui/questionInputForm.tsx (1)
  • QuestionInputForm (5-23)
shared/design/src/components/question/index.tsx (2)
shared/design/src/components/button/destructiveSolidIconButton.tsx (2)
  • DestructiveSolidIconButton (32-45)
  • DestructiveSolidIconButtonProps (26-30)
service/app/src/entities/game/ui/questionList.tsx (1)
  • QuestionList (5-30)
service/app/src/entities/game/model/state/create/utils.ts (2)
service/app/src/entities/game/model/state/create/state.ts (2)
  • Question (1-10)
  • GameCreationState (30-41)
service/app/src/entities/game/model/gameRequest.ts (4)
  • GameCreateQuestion (21-26)
  • GameUpdateQuestion (28-34)
  • GameUpdateRequest (44-50)
  • GameCreateRequest (36-42)
🔇 Additional comments (39)
service/app/src/entities/game/utils/fileValidation.ts (4)

8-8: 파일 크기 제한 변경 승인

10MB에서 2MB로의 파일 크기 제한 감소는 웹 성능 최적화에 적합합니다. 2MB는 대부분의 최적화된 이미지에 충분한 크기입니다.


10-10: 타입 안전성 개선

FileValidationError 유니온 타입의 도입으로 에러 처리가 더욱 안전하고 예측 가능해졌습니다. 명확한 에러 코드로 타입 안전성이 크게 향상되었습니다.


14-15: 인터페이스 개선 우수

타입이 지정된 error 필드와 선택적 message 필드 도입으로 에러 처리가 크게 개선되었습니다. 타입 안전성과 사용자 경험 모두 향상되었습니다.


26-27: 구조화된 에러 반환 우수

타입이 지정된 에러 코드와 사용자 친화적인 한국어 메시지를 함께 반환하는 구조로 개선되어 개발자 경험과 사용자 경험이 모두 향상되었습니다.

Also applies to: 34-35

service/app/src/entities/game/model/state/create/index.ts (1)

1-6: 배럴 파일 구조 우수

게임 생성 상태 관리의 모든 기능을 중앙화하는 깔끔한 배럴 파일입니다. 액션, 리듀서, 셀렉터, 상태, 훅, 유틸리티를 포괄적으로 export하여 사용하기 편리한 API를 제공합니다.

service/app/src/entities/game/model/index.ts (1)

22-22: 상태 관리 모듈 통합 승인

게임 생성 상태 관리 기능을 메인 모델 모듈의 공개 API에 적절히 통합했습니다. 일관된 export 패턴을 따르고 있습니다.

service/app/src/app/create/page.tsx (2)

1-1: 컨텍스트 프로바이더 import 승인

GameCreationProvider의 import 경로가 올바르고 기존 import 패턴을 잘 따르고 있습니다.


10-27: 프로바이더 구조 설계 우수

GameCreationProvider를 최상위 래퍼로 배치하여 모든 하위 컴포넌트가 게임 생성 상태와 액션에 접근할 수 있도록 했습니다. PopupManager와의 중첩 구조도 적절합니다.

service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (3)

25-26: 에러 처리 방식 개선이 잘 되었습니다.

정확한 에러 코드 확인과 한국어 메시지 포함 여부를 확인하는 방식으로 개선되어 타입 안전성과 테스트 정확도가 향상되었습니다.


30-30: 파일 크기 테스트 값이 적절하게 조정되었습니다.

최대 파일 크기 제한이 2MB로 변경됨에 따라 테스트 파일 크기를 3MB로 설정하여 제한을 초과하도록 적절히 조정했습니다.


33-34: 일관된 에러 검증 방식 적용이 좋습니다.

모든 테스트 케이스에서 동일한 패턴으로 에러 코드와 메시지를 검증하여 테스트의 일관성과 신뢰성이 향상되었습니다.

Also applies to: 52-53, 60-61

service/app/src/entities/game/ui/components/fileUploadArea.tsx (4)

6-7: 컨텍스트 통합이 적절합니다.

게임 생성 컨텍스트와 파일 유효성 검사 유틸리티를 올바르게 가져와서 중앙화된 상태 관리를 구현했습니다.


11-13: 컨텍스트 사용 방식이 올바릅니다.

useGameCreationContext를 통해 액션과 셀렉터를 가져오고, 선택된 질문 정보를 올바르게 접근하고 있습니다.


15-34: 파일 업로드 로직이 잘 구현되었습니다.

  • 선택된 질문이 없을 때의 예외 처리가 적절합니다
  • 첫 번째 파일만 처리하는 로직이 명확합니다
  • 파일 유효성 검사 후 적절한 팝업을 표시합니다
  • 성공 시 컨텍스트 액션을 통해 상태를 업데이트합니다

46-56: 이미지 미리보기 기능이 잘 구현되었습니다.

조건부 렌더링을 통해 업로드된 이미지를 표시하고, 이미지가 없을 때는 기본 플레이스홀더를 보여주는 UX가 좋습니다. Next.js Image 컴포넌트 사용도 적절합니다.

service/app/src/entities/game/model/state/create/gameCreationContext.tsx (3)

7-9: 타입 정의가 적절합니다.

useGameCreation 훅의 반환 타입을 활용하여 타입 안전성을 보장하는 좋은 패턴입니다.


11-19: 프로바이더 구현이 표준적입니다.

React 컨텍스트 프로바이더의 표준 패턴을 잘 따르고 있으며, 훅을 통해 상태를 초기화하는 방식이 적절합니다.


21-27: 에러 처리가 포함된 커스텀 훅이 좋습니다.

컨텍스트를 사용할 때 프로바이더 외부에서 사용되는 경우를 감지하여 명확한 에러 메시지를 제공하는 것이 좋은 개발자 경험을 제공합니다.

shared/design/src/components/question/index.tsx (4)

12-14: 새로운 props 정의가 적절합니다.

이미지, 삭제 가능 여부, 클릭 핸들러 등 새로운 기능을 위한 선택적 props가 잘 정의되었습니다.


22-25: 기본값 설정이 적절합니다.

canDelete의 기본값을 true로 설정하여 기존 동작을 유지하면서 필요할 때만 삭제를 비활성화할 수 있게 했습니다.


32-36: 클릭 가능한 UI 구현이 좋습니다.

cursor-pointer 클래스와 onClick 핸들러를 추가하여 질문 선택 기능을 제공하는 것이 좋은 UX입니다.


66-66: 삭제 버튼 제어 로직이 적절합니다.

canDelete prop을 통해 삭제 버튼의 활성화/비활성화를 제어하는 것이 좋은 설계입니다.

service/app/src/entities/game/ui/components/questionInputForm.tsx (4)

5-5: 컨텍스트 import가 적절합니다.

게임 생성 컨텍스트를 올바르게 가져와서 중앙화된 상태 관리를 구현했습니다.


8-10: 컨텍스트 사용이 올바릅니다.

액션과 셀렉터를 구조 분해 할당으로 가져오고, 선택된 질문 정보를 올바르게 접근하고 있습니다.


11-21: 변경 핸들러가 잘 구현되었습니다.

선택된 질문이 있을 때만 업데이트를 수행하는 안전한 패턴이며, 각각 텍스트와 답안을 별도로 업데이트하는 로직이 명확합니다.


26-38: 제어된 입력 컴포넌트 구현이 우수합니다.

  • 동적 상태 관리 (text/answer 존재 여부에 따른 error/default 상태)
  • 제어된 value 속성과 onChange 핸들러
  • null 안전성을 위한 기본값 처리 (|| "")

실시간 유효성 검사 피드백을 제공하는 좋은 UX 구현입니다.

Also applies to: 42-54

service/app/src/entities/game/ui/components/createGameNavigation.tsx (3)

18-28: 이벤트 핸들러 구현이 잘 되어 있습니다.

Context 액션을 활용한 이벤트 핸들러들이 명확하고 간결하게 구현되어 있습니다. 게임명 입력, 포커스, 블러 상태를 적절히 관리하고 있습니다.


36-52: 상태 기반 UI 렌더링이 적절하게 구현되었습니다.

Context의 state와 selectors를 활용하여 입력 필드의 상태와 에러 메시지를 동적으로 렌더링하는 로직이 잘 구현되어 있습니다.


61-68: 버튼 상태 제어가 올바르게 구현되었습니다.

canAddQuestion selector를 사용하여 버튼의 활성화 상태를 제어하고, addQuestion 액션을 통해 질문 추가 기능을 연결한 구현이 적절합니다.

service/app/src/entities/game/model/gameCreationReducer.ts (4)

28-37: 새 질문 추가 시 selectedQuestionId 업데이트 로직이 적절합니다.

새로 추가된 질문을 자동으로 선택하는 로직이 사용자 경험 측면에서 좋습니다.


39-53: 질문 삭제 시 선택된 질문 처리 로직이 잘 구현되었습니다.

삭제된 질문이 현재 선택된 질문인 경우 첫 번째 질문으로 자동 전환하는 로직이 적절합니다.


81-106: 이미지 업로드 시작 시 유효성 검사 및 에러 처리가 잘 되어 있습니다.

파일 유효성 검사를 먼저 수행하고, 유효하지 않은 경우 즉시 에러를 설정하는 로직이 적절합니다. 유효한 경우에만 업로드 상태를 true로 설정하는 것도 좋습니다.


1-4: import 경로 적합성 확인됨

아래 import 경로는 모두 올바르게 참조되고 있습니다:

  • ../utils/fileValidationservice/app/src/entities/game/utils/fileValidation.ts
  • ./state/createservice/app/src/entities/game/model/state/create/index.ts

경로 불일치 문제가 없으므로 해당 코멘트는 무시하셔도 됩니다.

Likely an incorrect or invalid review comment.

service/app/src/entities/game/ui/components/questionList.tsx (2)

13-30: 동적 질문 렌더링 로직이 잘 구현되었습니다.

Context state를 기반으로 질문 목록을 동적으로 렌더링하고, 각 질문의 선택 상태와 유효성 검사 결과를 적절히 반영하는 로직이 잘 구현되어 있습니다. 특히 질문이 비어있을 때 에러 상태로 표시하는 것이 좋습니다.


23-23: 삭제 가능 여부 체크 로직이 적절합니다.

state.questions.length > 1 조건으로 최소 하나의 질문은 유지되도록 하는 로직이 좋습니다.

service/app/src/entities/game/model/state/create/reducer.ts (1)

1-11: import 경로가 이전 파일보다 일관성 있게 정리되었습니다.

상대적 경로를 사용하여 모듈들을 import하는 방식이 더 명확하고 일관성 있습니다.

service/app/src/entities/game/model/state/create/actions.ts (2)

38-129: 액션 생성자들이 타입 안전하고 일관성 있게 구현되었습니다.

모든 액션 타입에 대한 생성자 함수가 적절히 구현되어 있고, 페이로드 타입이 정확히 매칭됩니다. 코드 구조도 깔끔하고 이해하기 쉽습니다.


3-28: GameCreationAction 유니온 타입이 포괄적으로 정의되었습니다.

게임 생성 과정에서 필요한 모든 액션들이 적절히 정의되어 있고, 각 액션의 페이로드 타입도 명확합니다.

service/app/src/entities/game/model/state/create/state.ts (1)

1-41: 잘 구조화된 타입 정의입니다.

게임 생성 상태를 위한 인터페이스들이 논리적으로 잘 분리되어 있고, 각 인터페이스의 책임이 명확합니다. 상태 관리에 필요한 모든 필드들이 적절히 정의되어 있습니다.

Comment on lines 12 to 222
export const gameCreationReducer = (
state: GameCreationState,
action: GameCreationAction
): GameCreationState => {
switch (action.type) {
case 'SET_GAME_NAME':
return {
...state,
gameName: action.payload
};

case 'SET_GAME_NAME_FOCUS':
return {
...state,
isGameNameFocused: action.payload
};

case 'SET_GAME_NAME_EDITING':
return {
...state,
isGameNameEditing: action.payload
};

case 'ADD_QUESTION': {
const newQuestions = addQuestion(state.questions);
const newQuestion = newQuestions[newQuestions.length - 1];

return {
...state,
questions: newQuestions,
selectedQuestionId: newQuestion.id
};
}

case 'DELETE_QUESTION': {
const questionId = action.payload;
const updatedQuestions = deleteQuestion(state.questions, questionId);

let selectedQuestionId = state.selectedQuestionId;
if (selectedQuestionId === questionId) {
selectedQuestionId = updatedQuestions.length > 0 ? updatedQuestions[0].id : null;
}

return {
...state,
questions: updatedQuestions,
selectedQuestionId
};
}

case 'UPDATE_QUESTION': {
const { id, updates } = action.payload;
const updatedQuestions = updateQuestion(state.questions, id, updates);

return {
...state,
questions: updatedQuestions
};
}

case 'SELECT_QUESTION':
return {
...state,
selectedQuestionId: action.payload
};

case 'MOVE_QUESTION': {
const { id, direction } = action.payload;
const updatedQuestions = moveQuestionInArray(state.questions, id, direction);

return {
...state,
questions: updatedQuestions
};
}

case 'UPLOAD_IMAGE_START': {
const { questionId, file } = action.payload;
const validation = validateImageFile(file);
const imageError = validation.isValid ? null : validation.error;

if (imageError) {
return {
...state,
questions: updateQuestion(state.questions, questionId, {
imageUploadError: imageError
})
};
}

return {
...state,
loading: {
...state.loading,
isUploading: true
},
questions: updateQuestion(state.questions, questionId, {
imageFile: file,
imageUploadError: null
})
};
}

case 'UPLOAD_IMAGE_SUCCESS': {
const { questionId, imageUrl } = action.payload;

return {
...state,
loading: {
...state.loading,
isUploading: false
},
questions: updateQuestion(state.questions, questionId, {
imageUrl,
imageUploadError: null
})
};
}

case 'UPLOAD_IMAGE_ERROR': {
const { questionId, error } = action.payload;

return {
...state,
loading: {
...state.loading,
isUploading: false
},
questions: updateQuestion(state.questions, questionId, {
imageUploadError: error
})
};
}

case 'SET_IMAGE_HOVER':
return {
...state,
questions: updateQuestion(state.questions, action.payload.questionId, {
isImageHovered: action.payload.isHovered
})
};

case 'SHOW_POPUP': {
const popupType = action.payload;
return {
...state,
popups: {
...state.popups,
[popupType]: true
}
};
}

case 'HIDE_POPUP': {
const popupType = action.payload;
return {
...state,
popups: {
...state.popups,
[popupType]: false
}
};
}

case 'SAVE_GAME_START':
return {
...state,
loading: {
...state.loading,
isSaving: true
}
};

case 'SAVE_GAME_SUCCESS':
return {
...state,
loading: {
...state.loading,
isSaving: false
}
};

case 'SAVE_GAME_ERROR':
return {
...state,
loading: {
...state.loading,
isSaving: false
},
errors: {
...state.errors,
globalError: action.payload
}
};

case 'SET_GLOBAL_ERROR':
return {
...state,
errors: {
...state.errors,
globalError: action.payload
}
};

case 'RESET_FORM':
return createInitialState();

default:
return state;
}
}; No newline at end of file
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

reducer 로직이 이전 파일과 동일합니다.

이 파일이 service/app/src/entities/game/model/gameCreationReducer.ts와 중복된 내용인지 확인이 필요합니다. 같은 기능의 파일이 두 개 있다면 하나를 제거해야 합니다.

다음 스크립트로 중복 파일 여부를 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# Description: 중복된 reducer 파일 확인

# gameCreationReducer 관련 파일들 검색
fd -t f "reducer" --exec echo "Reducer 파일: {}"
fd -t f "gameCreationReducer" --exec echo "GameCreationReducer 파일: {}"

# 두 파일의 내용 비교
if [ -f "service/app/src/entities/game/model/gameCreationReducer.ts" ] && [ -f "service/app/src/entities/game/model/state/create/reducer.ts" ]; then
  echo "=== 파일 내용 비교 ==="
  diff service/app/src/entities/game/model/gameCreationReducer.ts service/app/src/entities/game/model/state/create/reducer.ts
fi

Length of output: 1236


중복 리듀서 정리 필요
두 파일에 동일한 로직이 중복되어 있습니다. 하나로 통합하고 불필요한 파일을 제거해주세요.

  • 중복 대상

    • service/app/src/entities/game/model/gameCreationReducer.ts
    • service/app/src/entities/game/model/state/create/reducer.ts
  • 작업 안내

    1. 실제로 사용 중인 파일 하나를 선택해 남기고, 다른 파일은 제거
    2. 남긴 파일에서 사용하는 validateImageFile, GameCreationAction, GameCreationState, addQuestion 등 import 경로(actions, state, utils)를 일관된 구조로 수정
    3. 코드베이스 전반의 관련 import 경로를 새 파일 위치로 업데이트

이렇게 중복을 제거하면 유지보수성과 코드 가독성이 크게 향상됩니다.

🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/reducer.ts lines 12 to 222,
the gameCreationReducer logic is duplicated in
service/app/src/entities/game/model/gameCreationReducer.ts. Choose one file to
keep (preferably the one actively used), then delete the other file entirely. In
the retained file, update all imports such as validateImageFile,
GameCreationAction, GameCreationState, addQuestion, etc., to use consistent and
correct paths reflecting the unified structure. Finally, update all other parts
of the codebase that import from the removed file to now import from the
retained file's location to maintain consistency and avoid broken references.

Comment on lines 82 to 84
hasChanges: (state: GameCreationState): boolean => {
return state.gameName !== '게임1' || state.questions.length > 0;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

hasChanges 선택자의 로직 오류를 수정해주세요.

state.questions.length > 0 조건은 항상 true입니다(초기 상태에서도 질문이 1개 있음). 이는 변경 사항을 올바르게 감지하지 못합니다.

다음과 같이 수정해주세요:

   hasChanges: (state: GameCreationState): boolean => {
-    return state.gameName !== '게임1' || state.questions.length > 0;
+    return state.gameName !== '게임1' || 
+           state.questions.length !== 1 || 
+           state.questions[0].text !== '' || 
+           state.questions[0].answer !== '' ||
+           state.questions[0].imageFile !== null;
   }
📝 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.

Suggested change
hasChanges: (state: GameCreationState): boolean => {
return state.gameName !== '게임1' || state.questions.length > 0;
}
hasChanges: (state: GameCreationState): boolean => {
return state.gameName !== '게임1' ||
state.questions.length !== 1 ||
state.questions[0].text !== '' ||
state.questions[0].answer !== '' ||
state.questions[0].imageFile !== null;
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/selectors.ts around lines 82
to 84, the hasChanges selector incorrectly returns true because
state.questions.length > 0 is always true due to the initial state having one
question. To fix this, update the condition to properly detect changes by
comparing the current questions array with the initial questions array or by
implementing a more accurate check that reflects actual modifications rather
than just length.

Comment on lines 66 to 74
export const createImageUrl = (file: File): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target?.result as string);
};
reader.readAsDataURL(file);
});
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

FileReader 에러 처리가 누락되었습니다.

createImageUrl 함수에서 FileReader의 onerror 핸들러가 없어서 파일 읽기 실패 시 Promise가 영원히 pending 상태가 될 수 있습니다.

다음과 같이 에러 처리를 추가해주세요:

 export const createImageUrl = (file: File): Promise<string> => {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     const reader = new FileReader();
     reader.onload = (e) => {
       resolve(e.target?.result as string);
     };
+    reader.onerror = () => {
+      reject(new Error('파일을 읽는 중 오류가 발생했습니다.'));
+    };
     reader.readAsDataURL(file);
   });
 };
📝 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.

Suggested change
export const createImageUrl = (file: File): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target?.result as string);
};
reader.readAsDataURL(file);
});
};
export const createImageUrl = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target?.result as string);
};
reader.onerror = () => {
reject(new Error('파일을 읽는 중 오류가 발생했습니다.'));
};
reader.readAsDataURL(file);
});
};
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/utils.ts around lines 66 to
74, the createImageUrl function lacks error handling for the FileReader. Add an
onerror event handler to the FileReader instance that rejects the Promise with
the error event, ensuring the Promise does not remain pending if file reading
fails.

SecondaryGhostIconButton,
} from "@shared/design/src/components/button"
import { Control, Field, Root } from "@shared/design/src/components/input"
import { ErrorText } from "@shared/design/src/components/input"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

ErrorText 컴포넌트가 이미 import된 상태에서 중복 import가 있습니다.

Line 7에서 이미 ErrorText를 import하고 있는데, Line 8에서 다시 import하고 있습니다.

다음과 같이 수정하세요:

-import { Control, Field, Root } from "@shared/design/src/components/input"
-import { ErrorText } from "@shared/design/src/components/input"
+import { Control, Field, Root, ErrorText } from "@shared/design/src/components/input"
🤖 Prompt for AI Agents
In service/app/src/entities/game/ui/components/createGameNavigation.tsx at line
8, there is a duplicate import of the ErrorText component which is already
imported at line 7. Remove the redundant import statement on line 8 to avoid
duplication.

Comment on lines +43 to +60
{/* Image - 오른쪽 */}
<div className="absolute right-14 top-5">
{image ? (
<img
src={image}
alt="질문 이미지"
className="size-[78px] rounded-[7px] object-cover"
/>
) : (
<div className="flex size-[78px] items-center justify-center rounded-[7px] bg-gray-200">
<img
src="/checker.svg"
alt="기본 이미지"
className="size-[78px] rounded-[7px]"
/>
</div>
)}
</div>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 표시 로직에서 보안 및 접근성 개선이 필요합니다.

현재 구현에서 몇 가지 개선점이 있습니다:

  1. img 태그 대신 Next.js Image 컴포넌트 사용 권장
  2. 기본 이미지 경로 검증 필요
  3. 이미지 로딩 에러 처리 부재
+import Image from "next/image"

-        {image ? (
-          <img 
-            src={image} 
-            alt="질문 이미지" 
-            className="size-[78px] rounded-[7px] object-cover"
-          />
-        ) : (
-          <div className="flex size-[78px] items-center justify-center rounded-[7px] bg-gray-200">
-            <img 
-              src="/checker.svg" 
-              alt="기본 이미지" 
-              className="size-[78px] rounded-[7px]"
-            />
-          </div>
-        )}
+        {image ? (
+          <Image 
+            src={image} 
+            alt="질문 이미지" 
+            width={78}
+            height={78}
+            className="size-[78px] rounded-[7px] object-cover"
+          />
+        ) : (
+          <div className="flex size-[78px] items-center justify-center rounded-[7px] bg-gray-200">
+            <Image 
+              src="/checker.svg" 
+              alt="기본 이미지" 
+              width={78}
+              height={78}
+              className="size-[78px] rounded-[7px]"
+            />
+          </div>
+        )}
📝 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.

Suggested change
{/* Image - 오른쪽 */}
<div className="absolute right-14 top-5">
{image ? (
<img
src={image}
alt="질문 이미지"
className="size-[78px] rounded-[7px] object-cover"
/>
) : (
<div className="flex size-[78px] items-center justify-center rounded-[7px] bg-gray-200">
<img
src="/checker.svg"
alt="기본 이미지"
className="size-[78px] rounded-[7px]"
/>
</div>
)}
</div>
import Image from "next/image"
{/* Image - 오른쪽 */}
<div className="absolute right-14 top-5">
{image ? (
<Image
src={image}
alt="질문 이미지"
width={78}
height={78}
className="size-[78px] rounded-[7px] object-cover"
/>
) : (
<div className="flex size-[78px] items-center justify-center rounded-[7px] bg-gray-200">
<Image
src="/checker.svg"
alt="기본 이미지"
width={78}
height={78}
className="size-[78px] rounded-[7px]"
/>
</div>
)}
</div>
🤖 Prompt for AI Agents
In shared/design/src/components/question/index.tsx around lines 43 to 60,
replace the img tags with Next.js Image components to improve performance and
security. Validate the default image path to ensure it is correct and
accessible. Add error handling for image loading failures, such as using onError
to fallback to a default image or display an alternative UI, enhancing
robustness and accessibility.

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: 4

♻️ Duplicate comments (1)
service/app/src/entities/game/model/state/create/utils.ts (1)

120-126: 질문 추가 시 ID 생성 방식을 통일하세요.

앞서 언급한 것처럼, 여기서 사용하는 ID 생성 방식을 createInitialQuestion과 통일하는 것이 좋겠습니다.

🧹 Nitpick comments (19)
service/app/src/entities/game/model/state/index.ts (1)

1-1: 배럴 파일의 구조적 준비가 좋습니다.

현재는 빈 배럴 파일이지만, 향후 상태 관리 모듈들의 통합 내보내기를 위한 구조적 준비로 보입니다. 다만 현재 사용되지 않는 파일이므로 실제 내보내기가 필요할 때까지 추가를 연기하는 것도 고려해볼 수 있습니다.

service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1)

3-3: 임포트 구문에서 공백 누락

useContext 앞에 공백이 누락되었습니다.

-import { createContext, ReactNode,useContext } from 'react';
+import { createContext, ReactNode, useContext } from 'react';
service/app/src/entities/game/ui/components/questionInputForm.tsx (1)

11-21: 이벤트 핸들러에서 null 체크 개선 제안

현재 selectedQuestion이 null일 때 조기 반환하고 있지만, 이 경우 사용자에게 피드백을 제공하는 것이 좋겠습니다.

  const handleQuestionChange = (value: string) => {
    if (selectedQuestion) {
      actions.updateQuestion(selectedQuestion.id, { text: value });
+    } else {
+      // 선택된 질문이 없을 때의 처리 로직 추가 고려
    }
  };
service/app/src/entities/game/ui/components/fileUploadArea.tsx (2)

20-21: 파일 배열 처리 개선 제안

현재 첫 번째 파일만 처리하고 있는데, 이에 대한 명시적인 로깅이나 사용자 피드백을 고려해보세요.

  const file = files[0];
- if (!file) return;
+ if (!file) {
+   console.warn('선택된 파일이 없습니다.');
+   return;
+ }

48-54: 이미지 접근성 개선 제안

이미지 alt 텍스트를 더 구체적으로 만들면 접근성이 향상됩니다.

              <Image 
                src={selectedQuestion.imageUrl} 
-               alt="업로드된 이미지" 
+               alt={`질문 "${selectedQuestion.text}"에 대한 업로드된 이미지`}
                width={256}
                height={256}
                className="max-h-64 max-w-full object-contain"
              />
shared/design/src/components/question/index.tsx (1)

43-60: 이미지 표시 로직 개선

이미지 URL이 있을 때와 없을 때를 적절히 처리하고 있습니다. 하지만 몇 가지 개선점이 있습니다:

  1. 이미지 로드 실패에 대한 에러 처리가 없습니다
  2. alt 텍스트가 하드코딩되어 있습니다

다음과 같이 개선할 수 있습니다:

        {image ? (
          <img 
            src={image} 
-           alt="질문 이미지" 
+           alt={`${title} 질문 이미지`}
            className="size-[78px] rounded-[7px] object-cover"
+           onError={(e) => {
+             e.currentTarget.src = "/checker.svg";
+           }}
          />
        ) : (
          <div className="flex size-[78px] items-center justify-center rounded-[7px] bg-gray-200">
            <img 
              src="/checker.svg" 
-             alt="기본 이미지" 
+             alt="기본 질문 이미지"
              className="size-[78px] rounded-[7px]"
            />
          </div>
        )}
service/app/src/entities/game/model/state/create/reducer.ts (1)

46-60: 질문 삭제 시 선택된 질문 처리 로직 개선 필요

현재 로직에서 삭제된 질문이 선택된 질문이었을 때 첫 번째 질문을 자동 선택하는데, 더 직관적인 선택 로직을 고려해보세요.

다음과 같이 개선할 수 있습니다:

    case 'DELETE_QUESTION': {
      const questionId = action.payload;
      const updatedQuestions = deleteQuestion(state.questions, questionId);
      
      let selectedQuestionId = state.selectedQuestionId;
      if (selectedQuestionId === questionId) {
-       selectedQuestionId = updatedQuestions.length > 0 ? updatedQuestions[0].id : null;
+       // 삭제된 질문의 다음 질문을 선택하거나, 없으면 이전 질문 선택
+       const deletedIndex = state.questions.findIndex(q => q.id === questionId);
+       if (updatedQuestions.length > 0) {
+         const nextIndex = Math.min(deletedIndex, updatedQuestions.length - 1);
+         selectedQuestionId = updatedQuestions[nextIndex].id;
+       } else {
+         selectedQuestionId = null;
+       }
      }
      
      return {
        ...state,
        questions: updatedQuestions,
        selectedQuestionId
      };
    }
service/app/src/entities/game/model/state/create/useGameCreation.ts (1)

108-113: 질문별 셀렉터 함수 개선 제안

현재 구조에서 getQuestionSelectors가 매번 새로운 객체를 반환하여 성능 이슈가 있을 수 있습니다.

메모이제이션을 고려해보세요:

- const getQuestionSelectors = (questionId: string) => ({
-   canDelete: questionSelectors.canDelete(state),
-   canMoveUp: questionSelectors.canMoveUp(state, questionId),
-   canMoveDown: questionSelectors.canMoveDown(state, questionId),
-   isSelected: questionSelectors.isSelected(state, questionId)
- });

+ const getQuestionSelectors = useCallback((questionId: string) => ({
+   canDelete: questionSelectors.canDelete(state),
+   canMoveUp: questionSelectors.canMoveUp(state, questionId),
+   canMoveDown: questionSelectors.canMoveDown(state, questionId),
+   isSelected: questionSelectors.isSelected(state, questionId)
+ }), [state]);
service/app/src/entities/game/model/state/create/actions.ts (1)

19-20: PopupState 타입 참조 개선 제안

현재 keyof PopupState를 사용하고 있는데, PopupState를 export하지 않았습니다.

타입 일관성을 위해 export를 추가하세요:

- interface PopupState {
+ export interface PopupState {
    showExitConfirmation: boolean;
    showSaveConfirmation: boolean;
    showImageUploadError: boolean;
    showFileSizeError: boolean;
    showFileTypeError: boolean;
  }
service/app/src/entities/game/model/gameCreationReducer.ts (3)

1-4: 가져오기 구조를 개선하세요.

utility 함수들과 타입들이 서로 다른 경로에서 가져와지고 있습니다. 더 명확한 구조를 위해 가져오기를 정리하는 것을 고려해보세요.

-import { validateImageFile } from "../utils/fileValidation";
-import { GameCreationAction, GameCreationState } from "./state/create";
-import { addQuestion, createInitialState, deleteQuestion, moveQuestionInArray, updateQuestion } from "./state/create";
+import { validateImageFile } from "../utils/fileValidation";
+import { 
+  GameCreationAction, 
+  GameCreationState,
+  addQuestion, 
+  createInitialState, 
+  deleteQuestion, 
+  moveQuestionInArray, 
+  updateQuestion 
+} from "./state/create";

39-53: 질문 삭제 후 선택 상태 처리를 개선하세요.

현재 로직은 삭제된 질문이 선택되어 있을 때 첫 번째 질문을 선택하지만, 더 나은 UX를 위해 삭제된 질문의 이전 또는 다음 질문을 선택하는 것을 고려해보세요.

 case 'DELETE_QUESTION': {
   const questionId = action.payload;
   const updatedQuestions = deleteQuestion(state.questions, questionId);
   
   let selectedQuestionId = state.selectedQuestionId;
   if (selectedQuestionId === questionId) {
-    selectedQuestionId = updatedQuestions.length > 0 ? updatedQuestions[0].id : null;
+    // 삭제된 질문의 위치를 기반으로 다음 질문 선택
+    const deletedIndex = state.questions.findIndex(q => q.id === questionId);
+    const nextIndex = Math.min(deletedIndex, updatedQuestions.length - 1);
+    selectedQuestionId = updatedQuestions.length > 0 ? updatedQuestions[nextIndex].id : null;
   }
   
   return {
     ...state,
     questions: updatedQuestions,
     selectedQuestionId
   };
 }

187-198: 게임 저장 에러 처리 로직을 확인하세요.

저장 실패 시 로딩 상태를 false로 설정하는 것은 올바르지만, 글로벌 에러만 설정하고 있습니다. 저장 관련 특정 에러 상태도 고려해보세요.

service/app/src/entities/game/model/state/create/state.ts (1)

30-41: 게임 생성 상태 인터페이스를 개선하세요.

전체적인 구조는 좋지만, 일부 필드의 순서와 그룹핑을 개선할 수 있습니다.

 export interface GameCreationState {
+  // 게임 기본 정보
   gameName: string;
+  isGameNameEditing: boolean;
+  isGameNameFocused: boolean;
+  
+  // 질문 관련 상태
   questions: Question[];
   selectedQuestionId: string | null;
   
+  // UI 상태
   popups: PopupState;
   loading: LoadingState;
   errors: ErrorState;
-  
-  isGameNameEditing: boolean;
-  isGameNameFocused: boolean;
 }
service/app/src/entities/game/model/state/create/utils.ts (3)

3-12: 질문 ID 생성 방식을 개선하세요.

현재 createInitialQuestion에서는 간단한 문자열 기반 ID를 사용하지만, addQuestion에서는 timestamp와 random을 조합합니다. 일관성을 위해 ID 생성 로직을 통일하는 것을 고려해보세요.

+const generateQuestionId = (): string => `question-${Date.now()}-${Math.random()}`;
+
 export const createInitialQuestion = (order: number): Question => ({
-  id: `question-${order}`,
+  id: generateQuestionId(),
   text: '',
   answer: '',
   imageFile: null,
   imageUrl: null,
   imageUploadError: null,
   isImageHovered: false,
   order
 });

37-57: 질문 이동 로직에서 경계 검사를 개선하세요.

현재 로직은 올바르지만, 더 명확한 경계 검사와 에러 처리를 추가할 수 있습니다.

 export const moveQuestionInArray = (
   questions: Question[], 
   questionId: string, 
   direction: 'up' | 'down'
 ): Question[] => {
   const questionIndex = questions.findIndex(q => q.id === questionId);
   if (questionIndex === -1) return questions;

+  const targetIndex = direction === 'up' ? questionIndex - 1 : questionIndex + 1;
+  
+  // 경계 검사를 더 명확하게
+  if (targetIndex < 0 || targetIndex >= questions.length) {
+    return questions;
+  }

   const newQuestions = [...questions];
-  const targetIndex = direction === 'up' ? questionIndex - 1 : questionIndex + 1;
-  
-  if (targetIndex < 0 || targetIndex >= questions.length) return questions;
   
   [newQuestions[questionIndex], newQuestions[targetIndex]] = 
   [newQuestions[targetIndex], newQuestions[questionIndex]];
   
   return newQuestions.map((question, index) => ({
     ...question,
     order: index
   }));
 };

82-93: 게임명 생성 로직의 국제화를 고려하세요.

현재 하드코딩된 '게임' 문자열을 사용하고 있습니다. 국제화 지원을 위해 매개변수로 받거나 설정에서 가져오는 것을 고려해보세요.

-export const generateGameName = (existingNames: string[]): string => {
-  const baseName = '게임';
+export const generateGameName = (existingNames: string[], baseName: string = '게임'): string => {
   let counter = 1;
   let newName = `${baseName}${counter}`;
   
   while (existingNames.includes(newName)) {
     counter++;
     newName = `${baseName}${counter}`;
   }
   
   return newName;
 };
service/app/src/entities/game/model/state/create/selectors.ts (3)

37-43: 질문 개수 제한을 확인하세요.

최대 50개의 질문을 허용하는 것이 비즈니스 요구사항에 맞는지 확인이 필요합니다. 이런 상수는 설정으로 관리하는 것이 좋겠습니다.

+const MAX_QUESTIONS = 50;
+
 canAddQuestion: (state: GameCreationState): boolean => {
-  return state.questions.length < 50;
+  return state.questions.length < MAX_QUESTIONS;
 },

55-61: 저장 가능 여부 검증 로직을 개선하세요.

현재 로직은 올바르지만, 성능을 위해 조기 반환을 고려하고 더 명확하게 구조화할 수 있습니다.

 canSave: (state: GameCreationState): boolean => {
   const hasValidGameName = !validateGameName(state.gameName);
+  if (!hasValidGameName) return false;
+  
   const hasValidQuestions = state.questions.length > 0 && 
                            state.questions.every(validateQuestion);
   
-  return hasValidGameName && hasValidQuestions;
+  return hasValidQuestions;
 },

87-103: 중복된 셀렉터 로직을 정리하세요.

questionSelectors의 함수들이 메인 selectors 객체의 함수들과 중복됩니다. 일관성을 위해 하나로 통일하거나 명확한 역할 분리를 하는 것이 좋겠습니다.

 export const questionSelectors = {
   canDelete: (state: GameCreationState): boolean => {
-    return state.questions.length > 1;
+    return selectors.canDeleteQuestion(state);
   },

   canMoveUp: (state: GameCreationState, questionId: string): boolean => {
-    return selectors.canMoveQuestionUp(state, questionId);
+    return selectors.canMoveQuestionUp(state, questionId);
   },

   canMoveDown: (state: GameCreationState, questionId: string): boolean => {
-    return selectors.canMoveQuestionDown(state, questionId);
+    return selectors.canMoveQuestionDown(state, questionId);
   },

   isSelected: (state: GameCreationState, questionId: string): boolean => {
     return state.selectedQuestionId === questionId;
   }
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf4738f and 580e590.

⛔ Files ignored due to path filters (1)
  • service/app/public/checker.svg is excluded by !**/*.svg
📒 Files selected for processing (19)
  • service/app/src/app/create/page.tsx (2 hunks)
  • service/app/src/entities/game/model/gameCreationReducer.ts (1 hunks)
  • service/app/src/entities/game/model/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/actions.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1 hunks)
  • service/app/src/entities/game/model/state/create/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/reducer.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/selectors.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/state.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/useGameCreation.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/utils.ts (1 hunks)
  • service/app/src/entities/game/model/state/index.ts (1 hunks)
  • service/app/src/entities/game/ui/components/createGameNavigation.tsx (2 hunks)
  • service/app/src/entities/game/ui/components/fileUploadArea.tsx (2 hunks)
  • service/app/src/entities/game/ui/components/questionInputForm.tsx (1 hunks)
  • service/app/src/entities/game/ui/components/questionList.tsx (1 hunks)
  • service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (2 hunks)
  • service/app/src/entities/game/utils/fileValidation.ts (2 hunks)
  • shared/design/src/components/question/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (1)
service/app/src/entities/game/utils/fileValidation.ts (2)
  • validateImageFile (18-40)
  • validateMultipleFiles (48-56)
shared/design/src/components/question/index.tsx (2)
shared/design/src/components/button/destructiveSolidIconButton.tsx (2)
  • DestructiveSolidIconButton (32-45)
  • DestructiveSolidIconButtonProps (26-30)
service/app/src/entities/game/ui/questionList.tsx (1)
  • QuestionList (5-30)
service/app/src/entities/game/model/state/create/state.ts (3)
service/app/src/entities/game/model/gameRequest.ts (2)
  • GameCreateQuestion (21-26)
  • GameUpdateQuestion (28-34)
service/app/src/entities/game/model/game.ts (1)
  • GameQuestion (20-27)
shared/design/src/components/upload/upload.tsx (1)
  • FileState (36-41)
🔇 Additional comments (54)
service/app/src/entities/game/utils/fileValidation.ts (5)

8-8: 파일 크기 제한 감소가 적절합니다.

10MB에서 2MB로 파일 크기 제한을 줄인 것은 업로드 성능과 사용자 경험을 개선하는 좋은 변경입니다.


10-10: 타입 안전성이 향상되었습니다.

FileValidationError 유니온 타입을 도입하여 가능한 에러 코드를 명시적으로 정의한 것은 타입 안전성과 코드 가독성을 크게 향상시킵니다.


14-15: 구조화된 에러 응답이 개선되었습니다.

error 필드를 타입화하고 message 필드를 추가하여 에러 정보를 더 체계적으로 제공하는 것은 훌륭한 개선입니다. 이를 통해 클라이언트에서 에러를 더 정확하게 처리할 수 있습니다.


26-27: 에러 메시지의 일관성을 확인하세요.

에러 코드와 함께 사용자 친화적인 한국어 메시지를 제공하는 것은 좋습니다. 다만 애플리케이션 전반의 다국어 지원 정책과 일치하는지 확인해보세요.


34-35: 동적 메시지 구성이 잘 구현되었습니다.

파일 크기 제한을 동적으로 계산하여 메시지에 포함하는 것은 유지보수성을 높이는 좋은 접근입니다.

service/app/src/entities/game/utils/__tests__/fileValidation.test.ts (5)

25-26: 타입화된 에러 검증이 향상되었습니다.

에러 코드를 직접 확인하고 메시지 내용도 검증하는 것은 테스트의 정확성을 크게 향상시킵니다.


30-30: 파일 크기 테스트가 적절합니다.

3MB 파일을 사용하여 2MB 제한을 테스트하는 것은 경계값 테스트로서 적절합니다.


33-34: 에러 응답 구조를 완전히 검증합니다.

에러 코드와 메시지를 모두 검증하여 새로운 구조화된 에러 응답이 올바르게 작동하는지 확인하는 것이 좋습니다.


52-53: 다중 파일 검증 테스트가 정확합니다.

validateMultipleFiles 함수에서도 동일한 에러 구조를 반환하는지 확인하는 것은 일관성 테스트로서 중요합니다.


57-61: 테스트 케이스가 포괄적입니다.

파일 크기 초과에 대한 다중 파일 검증 테스트도 새로운 에러 구조를 올바르게 검증하고 있습니다.

service/app/src/entities/game/model/state/create/index.ts (1)

1-6: 게임 생성 상태 관리 모듈이 잘 구성되었습니다.

상태 관리에 필요한 모든 요소들(액션, 리듀서, 셀렉터, 상태 타입, 훅, 유틸리티)을 체계적으로 분리하고 하나의 진입점으로 통합한 것은 훌륭한 구조입니다. 이는 관심사의 분리와 사용 편의성을 모두 만족시킵니다.

service/app/src/entities/game/model/index.ts (2)

22-22: 새로운 상태 관리 시스템이 잘 통합되었습니다.

게임 생성 상태 관리 모듈을 기존 모델 내보내기에 추가한 것은 적절합니다. 이를 통해 클라이언트 코드에서 게임 관련 모든 기능을 하나의 모듈을 통해 접근할 수 있게 되었습니다.


1-24: 코드 정리가 깔끔하게 되었습니다.

주석을 제거하여 코드가 더 깔끔해졌고, 새로운 상태 관리 시스템의 통합도 기존 구조와 일관성 있게 처리되었습니다.

service/app/src/app/create/page.tsx (2)

1-1: Context 임포트가 올바르게 추가되었습니다.

게임 생성 상태 관리를 위한 Context Provider 임포트가 적절하게 추가되었습니다.


10-27: Context Provider 래핑이 올바르게 구현되었습니다.

GameCreationProvider가 최상위에 위치하여 모든 하위 컴포넌트들이 게임 생성 상태에 접근할 수 있도록 구성되었습니다. PopupManager와의 래핑 순서도 적절합니다.

service/app/src/entities/game/model/state/create/gameCreationContext.tsx (2)

7-9: 타입 정의가 적절합니다.

useGameCreation 훅의 반환 타입을 활용한 Context 타입 정의가 올바르게 구현되었습니다.


21-27: Context 사용 시 에러 핸들링이 우수합니다.

Provider 외부에서 Context를 사용할 때 명확한 에러 메시지를 제공하는 가드 로직이 잘 구현되었습니다.

service/app/src/entities/game/ui/components/questionInputForm.tsx (3)

5-5: Context 통합이 적절합니다.

게임 생성 컨텍스트가 올바르게 임포트되어 사용되고 있습니다.


8-10: Context 사용법이 우수합니다.

액션과 셀렉터를 구조 분해 할당으로 추출하여 깔끔하게 사용하고 있습니다.


28-28: 유효성 검사 로직이 우수합니다.

입력 필드의 상태가 값의 존재 여부에 따라 동적으로 변경되어 사용자에게 명확한 피드백을 제공합니다.

Also applies to: 44-44

service/app/src/entities/game/ui/components/createGameNavigation.tsx (5)

8-8: ErrorText 임포트가 적절합니다.

에러 메시지 표시를 위한 컴포넌트 임포트가 올바르게 추가되었습니다.


16-16: Context 구조 분해 할당이 깔끔합니다.

상태, 액션, 셀렉터를 명확하게 분리하여 사용하고 있습니다.


18-28: 이벤트 핸들러들이 잘 구현되었습니다.

게임명 변경, 포커스, 블러 이벤트에 대한 핸들러들이 Context 액션과 적절히 연결되어 있습니다.


38-38: 동적 상태 관리가 우수합니다.

게임명 에러 상태에 따른 입력 필드 스타일 변경과 에러 메시지 표시가 잘 구현되었습니다.

Also applies to: 49-51


64-65: 버튼 비활성화 로직이 적절합니다.

셀렉터를 통해 질문 추가 가능 여부를 확인하고 버튼을 비활성화하는 로직이 잘 구현되었습니다.

service/app/src/entities/game/ui/components/fileUploadArea.tsx (4)

4-4: 필요한 임포트가 적절히 추가되었습니다.

Next.js Image 컴포넌트와 Context, 파일 유효성 검사 유틸리티가 올바르게 임포트되었습니다.

Also applies to: 6-7


15-18: 선택된 질문 확인 로직이 우수합니다.

파일 업로드 전에 선택된 질문이 있는지 확인하는 가드 로직이 잘 구현되었습니다.


23-31: 파일 유효성 검사와 에러 처리가 우수합니다.

구조화된 유효성 검사 결과를 기반으로 적절한 팝업을 표시하는 로직이 잘 구현되었습니다.


46-56: 이미지 미리보기 기능이 잘 구현되었습니다.

조건부 렌더링과 Next.js Image 컴포넌트를 사용한 이미지 미리보기가 적절합니다.

shared/design/src/components/question/index.tsx (3)

12-14: 인터페이스 확장이 잘 구현되었습니다

새로운 optional props들이 적절하게 추가되었고, canDelete의 기본값 설정도 합리적입니다.


32-36: 클릭 핸들러 통합이 적절합니다

cursor-pointer 클래스와 onClick 핸들러가 잘 연동되어 있습니다.


64-67: 삭제 버튼 비활성화 로직이 올바릅니다

canDelete prop을 통한 조건부 비활성화가 UX 관점에서 적절하게 구현되었습니다.

service/app/src/entities/game/model/state/create/reducer.ts (3)

88-113: 이미지 업로드 시 검증 로직이 효율적입니다

업로드 시작 시점에서 파일 검증을 수행하여 불필요한 업로드를 방지하는 것이 좋은 접근입니다.


154-174: 팝업 상태 관리가 깔끔하게 구현되었습니다

동적 키를 사용한 팝업 상태 토글링이 재사용 가능하고 확장성이 좋습니다.


216-218: 폼 리셋 구현이 간단하고 효과적입니다

createInitialState()를 사용한 상태 초기화가 일관성 있고 유지보수하기 좋습니다.

service/app/src/entities/game/model/state/create/useGameCreation.ts (3)

17-91: 액션 래퍼들이 적절하게 메모이제이션되었습니다

모든 액션 크리에이터가 useCallback으로 래핑되어 불필요한 리렌더링을 방지합니다.


93-106: 파생 상태 계산이 효율적입니다

셀렉터를 통한 파생 상태 계산이 잘 구조화되어 있습니다.


115-144: 훅 반환 구조가 명확하고 체계적입니다

state, actions, selectors로 그룹화된 반환 구조가 사용하기 편리합니다.

service/app/src/entities/game/ui/components/questionList.tsx (2)

5-8: 컨텍스트 통합이 깔끔하게 구현되었습니다

useGameCreationContext 훅을 통한 상태와 액션 접근이 적절합니다.


24-27: 이벤트 핸들러들이 적절하게 연결되었습니다

질문 선택, 삭제, 이동 액션들이 올바르게 바인딩되어 있습니다.

service/app/src/entities/game/model/state/create/actions.ts (3)

3-28: 액션 타입 정의가 포괄적이고 체계적입니다

게임 생성 프로세스의 모든 액션이 적절하게 타입화되어 있습니다.


30-36: PopupState 인터페이스가 명확합니다

팝업 상태들이 명확하게 정의되어 있고 의미 있는 이름을 가지고 있습니다.


38-129: 액션 크리에이터들이 일관성 있게 구현되었습니다

모든 액션 크리에이터가 동일한 패턴을 따르고 있어 예측 가능하고 유지보수하기 좋습니다.

service/app/src/entities/game/model/gameCreationReducer.ts (4)

28-37: 질문 추가 로직이 올바릅니다.

새 질문을 추가하고 자동으로 선택하는 로직이 잘 구현되어 있습니다. 배열의 마지막 요소를 새로 선택된 질문으로 설정하는 것이 직관적입니다.


108-137: 이미지 업로드 성공/실패 처리가 올바릅니다.

로딩 상태를 적절히 관리하고, 에러 상태를 올바르게 초기화하거나 설정하는 로직이 잘 구현되어 있습니다.


209-214: 폼 리셋과 기본 케이스 처리가 올바릅니다.

createInitialState()를 사용한 폼 리셋과 기본 케이스에서 현재 상태를 반환하는 것이 적절합니다.


81-106: 이미지 유효성 검증 로직 확인 완료: 수정 불필요

validateImageFile 함수는 실패 시 isValid: false만 반환하며, 리듀서도 에러가 있을 때 imageFile을 설정하지 않고 있습니다. 성공 시에만 imageFile을 설정하는 현재 로직이 의도된 동작으로 보이므로 별도 변경은 필요하지 않습니다.

Likely an incorrect or invalid review comment.

service/app/src/entities/game/model/state/create/state.ts (3)

12-18: 팝업 상태 인터페이스가 잘 정의되어 있습니다.

다양한 팝업 상태를 명확하게 구분하여 정의한 것이 좋습니다. 에러 타입별로 세분화된 것도 UX 관점에서 유용합니다.


20-28: 로딩 및 에러 상태 구조가 적절합니다.

비동기 작업 상태를 명확히 구분하고, 에러 상태를 게임명과 글로벌 에러로 분리한 것이 좋은 설계입니다.


1-10: UI 상태용 Question 인터페이스는 기존 게임 모델과 의도적으로 다릅니다

현재 service/app/src/entities/game/model/state/create/state.ts에 정의된 Question 인터페이스는 클라이언트 상태 관리용으로 로컬 ID(string), 이미지 파일/업로드 에러/호버 상태 등을 포함하기 때문에, 백엔드 모델(GameQuestion, GameCreateQuestion)과 필드명·타입이 다르게 설계되어 있습니다. 이는 의도된 구조이므로 API 요청 전 매핑 로직만 구현하시면 됩니다.

  • QuestionGameCreateQuestion 변환함수 작성
    예시:
    function toCreateQuestionPayload(q: Question): GameCreateQuestion {
      return {
        questionOrder: q.order,
        imageUrl: q.imageUrl ?? '',
        questionText: q.text,
        questionAnswer: q.answer,
      };
    }
  • 로컬 ID(string)와 백엔드 questionId(number) 매핑 처리

위 구조는 의도된 설계이므로 변경하지 않으셔도 됩니다.

Likely an incorrect or invalid review comment.

service/app/src/entities/game/model/state/create/utils.ts (2)

14-35: 초기 상태 생성이 잘 구현되어 있습니다.

모든 필드가 적절한 기본값으로 초기화되어 있고, 첫 번째 질문이 자동으로 선택되는 것이 좋습니다.


107-118: 질문 삭제 시 최소 개수 제한이 올바릅니다.

최소 1개의 질문을 유지하는 로직과 순서를 재정렬하는 것이 적절합니다.

service/app/src/entities/game/model/state/create/selectors.ts (2)

3-26: 검증 함수들이 잘 구현되어 있습니다.

게임명과 질문 텍스트, 답안에 대한 검증 로직이 명확하고 일관성 있게 구현되어 있습니다. 길이 제한과 필수 입력 검증이 적절합니다.


28-35: 선택된 질문 셀렉터가 올바릅니다.

find 메서드를 사용하여 선택된 질문을 찾는 로직이 간단하고 정확합니다.

Comment on lines 147 to 167
case 'SHOW_POPUP': {
const popupType = action.payload;
return {
...state,
popups: {
...state.popups,
[popupType]: true
}
};
}

case 'HIDE_POPUP': {
const popupType = action.payload;
return {
...state,
popups: {
...state.popups,
[popupType]: false
}
};
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

팝업 상태 관리 로직을 개선하세요.

현재 팝업 타입을 동적으로 설정하는 방식은 타입 안전성을 보장하지 않습니다. 허용되는 팝업 타입을 명시적으로 제한하는 것을 고려해보세요.

 case 'SHOW_POPUP': {
   const popupType = action.payload;
+  // 타입 안전성을 위한 검증 추가
+  if (!Object.keys(state.popups).includes(popupType)) {
+    console.warn(`Unknown popup type: ${popupType}`);
+    return state;
+  }
   return {
     ...state,
     popups: {
       ...state.popups,
       [popupType]: true
     }
   };
 }
📝 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.

Suggested change
case 'SHOW_POPUP': {
const popupType = action.payload;
return {
...state,
popups: {
...state.popups,
[popupType]: true
}
};
}
case 'HIDE_POPUP': {
const popupType = action.payload;
return {
...state,
popups: {
...state.popups,
[popupType]: false
}
};
}
case 'SHOW_POPUP': {
const popupType = action.payload;
// 타입 안전성을 위한 검증 추가
if (!Object.keys(state.popups).includes(popupType)) {
console.warn(`Unknown popup type: ${popupType}`);
return state;
}
return {
...state,
popups: {
...state.popups,
[popupType]: true
}
};
}
case 'HIDE_POPUP': {
const popupType = action.payload;
return {
...state,
popups: {
...state.popups,
[popupType]: false
}
};
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/gameCreationReducer.ts around lines 147
to 167, the popup state management uses dynamic keys from action.payload without
type safety. To fix this, define a TypeScript union type or enum for allowed
popup types and restrict action.payload to this type. Update the reducer to use
this typed popupType to ensure only valid popup types are handled, improving
type safety and preventing invalid keys.

Comment on lines 82 to 84
hasChanges: (state: GameCreationState): boolean => {
return state.gameName !== '게임1' || state.questions.length > 0;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

변경사항 감지 로직에 오류가 있습니다.

hasChanges 셀렉터의 로직이 잘못되어 있습니다. 질문이 0개보다 많은 경우를 확인하고 있는데, 초기 상태에서도 질문이 1개 있으므로 항상 true를 반환할 것입니다.

 hasChanges: (state: GameCreationState): boolean => {
-  return state.gameName !== '게임1' || state.questions.length > 0;
+  const hasGameNameChanged = state.gameName !== '게임1';
+  const hasQuestionsChanged = state.questions.length > 1 || 
+    state.questions.some(q => q.text.trim() !== '' || q.answer.trim() !== '' || q.imageUrl);
+  
+  return hasGameNameChanged || hasQuestionsChanged;
 }
📝 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.

Suggested change
hasChanges: (state: GameCreationState): boolean => {
return state.gameName !== '게임1' || state.questions.length > 0;
}
hasChanges: (state: GameCreationState): boolean => {
const hasGameNameChanged = state.gameName !== '게임1';
const hasQuestionsChanged =
state.questions.length > 1 ||
state.questions.some(
q =>
q.text.trim() !== '' ||
q.answer.trim() !== '' ||
Boolean(q.imageUrl)
);
return hasGameNameChanged || hasQuestionsChanged;
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/selectors.ts around lines 82
to 84, the hasChanges selector incorrectly checks if the number of questions is
greater than 0, which always returns true since the initial state has one
question. Update the logic to correctly detect changes by comparing the current
questions array with the initial questions or by using a more precise condition
that reflects actual changes rather than just length.

Comment on lines 66 to 80
export const createImageUrl = (file: File): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target?.result as string);
};
reader.readAsDataURL(file);
});
};

export const removeImageUrl = (url: string): void => {
if (url.startsWith('blob:')) {
URL.revokeObjectURL(url);
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 URL 처리 방식을 재고하세요.

createImageUrl이 FileReader를 사용하는 것은 괜찮지만, removeImageUrl에서 blob URL만 확인하는 것은 제한적입니다. 또한 FileReader 사용 시 에러 처리가 없습니다.

 export const createImageUrl = (file: File): Promise<string> => {
   return new Promise((resolve, reject) => {
     const reader = new FileReader();
     reader.onload = (e) => {
       resolve(e.target?.result as string);
     };
+    reader.onerror = () => {
+      reject(new Error('파일을 읽을 수 없습니다.'));
+    };
     reader.readAsDataURL(file);
   });
 };

 export const removeImageUrl = (url: string): void => {
-  if (url.startsWith('blob:')) {
+  if (url && url.startsWith('blob:')) {
     URL.revokeObjectURL(url);
   }
 };
📝 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.

Suggested change
export const createImageUrl = (file: File): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target?.result as string);
};
reader.readAsDataURL(file);
});
};
export const removeImageUrl = (url: string): void => {
if (url.startsWith('blob:')) {
URL.revokeObjectURL(url);
}
};
export const createImageUrl = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target?.result as string);
};
reader.onerror = () => {
reject(new Error('파일을 읽을 수 없습니다.'));
};
reader.readAsDataURL(file);
});
};
export const removeImageUrl = (url: string): void => {
if (url && url.startsWith('blob:')) {
URL.revokeObjectURL(url);
}
};
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/utils.ts between lines 66
and 80, the createImageUrl function lacks error handling for the FileReader, and
removeImageUrl only revokes blob URLs, which is limited. Add error handling to
createImageUrl by listening for the reader's onerror event and rejecting the
promise accordingly. For removeImageUrl, expand the logic to handle other URL
types if necessary or clarify its scope, ensuring proper cleanup of URLs created
by createImageUrl.

Comment on lines 13 to 30
{state.questions.map((question) => {
const questionSelectors = getQuestionSelectors(question.id);
const isSelected = questionSelectors.isSelected;

return (
<Question
key={question.id}
title={question.text || "질문을 입력해주세요"}
image={question.imageUrl}
state={isSelected ? "selected" : question.text ? "default" : "error"}
canDelete={state.questions.length > 1}
onClick={() => actions.selectQuestion(question.id)}
onDelete={() => actions.deleteQuestion(question.id)}
onMoveUp={() => actions.moveQuestion(question.id, 'up')}
onMoveDown={() => actions.moveQuestion(question.id, 'down')}
/>
);
})}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

동적 질문 렌더링 로직 개선 필요

질문 상태 매핑과 삭제 가능 여부 로직에 일관성 문제가 있습니다.

다음과 같이 개선하세요:

        {state.questions.map((question) => {
          const questionSelectors = getQuestionSelectors(question.id);
          const isSelected = questionSelectors.isSelected;
          
          return (
            <Question
              key={question.id}
              title={question.text || "질문을 입력해주세요"}
              image={question.imageUrl}
              state={isSelected ? "selected" : question.text ? "default" : "error"}
-             canDelete={state.questions.length > 1}
+             canDelete={questionSelectors.canDelete}
              onClick={() => actions.selectQuestion(question.id)}
              onDelete={() => actions.deleteQuestion(question.id)}
              onMoveUp={() => actions.moveQuestion(question.id, 'up')}
              onMoveDown={() => actions.moveQuestion(question.id, 'down')}
            />
          );
        })}

이렇게 하면 셀렉터를 통한 일관된 로직을 사용할 수 있습니다.

📝 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.

Suggested change
{state.questions.map((question) => {
const questionSelectors = getQuestionSelectors(question.id);
const isSelected = questionSelectors.isSelected;
return (
<Question
key={question.id}
title={question.text || "질문을 입력해주세요"}
image={question.imageUrl}
state={isSelected ? "selected" : question.text ? "default" : "error"}
canDelete={state.questions.length > 1}
onClick={() => actions.selectQuestion(question.id)}
onDelete={() => actions.deleteQuestion(question.id)}
onMoveUp={() => actions.moveQuestion(question.id, 'up')}
onMoveDown={() => actions.moveQuestion(question.id, 'down')}
/>
);
})}
{state.questions.map((question) => {
const questionSelectors = getQuestionSelectors(question.id);
const isSelected = questionSelectors.isSelected;
return (
<Question
key={question.id}
title={question.text || "질문을 입력해주세요"}
image={question.imageUrl}
state={isSelected ? "selected" : question.text ? "default" : "error"}
canDelete={questionSelectors.canDelete}
onClick={() => actions.selectQuestion(question.id)}
onDelete={() => actions.deleteQuestion(question.id)}
onMoveUp={() => actions.moveQuestion(question.id, 'up')}
onMoveDown={() => actions.moveQuestion(question.id, 'down')}
/>
);
})}
🤖 Prompt for AI Agents
In service/app/src/entities/game/ui/components/questionList.tsx around lines 13
to 30, the current dynamic question rendering logic has inconsistencies in
mapping question states and determining delete availability. Refactor the code
to consistently use selectors for both the question state and delete permission
by retrieving these values from getQuestionSelectors(question.id). Replace the
inline conditional logic with these selector values to ensure uniform handling
across the component.

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: 0

🧹 Nitpick comments (1)
service/app/src/entities/game/ui/components/questionInputForm.tsx (1)

10-14: 컨텍스트 사용 시 에러 처리 추가를 고려해보세요.

컨텍스트 통합이 잘 구현되어 있습니다. 다만 GameCreationProvider 외부에서 이 컴포넌트가 사용될 경우를 대비한 에러 처리를 추가하는 것을 고려해보세요.

export function QuestionInputForm() {
+ if (!useGameCreationContext) {
+   throw new Error('QuestionInputForm must be used within GameCreationProvider')
+ }
  const { actions, selectors } = useGameCreationContext()
  const selectedQuestion = selectors.selectedQuestion
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 580e590 and 8ce68e9.

📒 Files selected for processing (16)
  • service/app/src/app/create/page.tsx (2 hunks)
  • service/app/src/entities/game/model/gameCreationReducer.ts (1 hunks)
  • service/app/src/entities/game/model/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/actions.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1 hunks)
  • service/app/src/entities/game/model/state/create/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/reducer.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/selectors.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/state.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/useGameCreation.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/utils.ts (1 hunks)
  • service/app/src/entities/game/model/state/index.ts (1 hunks)
  • service/app/src/entities/game/ui/components/createGameNavigation.tsx (3 hunks)
  • service/app/src/entities/game/ui/components/fileUploadArea.tsx (2 hunks)
  • service/app/src/entities/game/ui/components/questionInputForm.tsx (1 hunks)
  • service/app/src/entities/game/ui/components/questionList.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • service/app/src/entities/game/model/state/create/gameCreationContext.tsx
🚧 Files skipped from review as they are similar to previous changes (14)
  • service/app/src/entities/game/model/index.ts
  • service/app/src/entities/game/model/state/index.ts
  • service/app/src/entities/game/model/state/create/index.ts
  • service/app/src/entities/game/model/gameCreationReducer.ts
  • service/app/src/entities/game/model/state/create/actions.ts
  • service/app/src/entities/game/ui/components/fileUploadArea.tsx
  • service/app/src/app/create/page.tsx
  • service/app/src/entities/game/ui/components/createGameNavigation.tsx
  • service/app/src/entities/game/model/state/create/useGameCreation.ts
  • service/app/src/entities/game/model/state/create/reducer.ts
  • service/app/src/entities/game/model/state/create/utils.ts
  • service/app/src/entities/game/model/state/create/state.ts
  • service/app/src/entities/game/ui/components/questionList.tsx
  • service/app/src/entities/game/model/state/create/selectors.ts
⏰ 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 (3)
service/app/src/entities/game/ui/components/questionInputForm.tsx (3)

16-26: 훌륭한 이벤트 핸들러 구현입니다!

null 체크와 함께 적절한 액션 디스패치가 구현되어 있어 안전하고 예측 가능한 동작을 보장합니다. 제어 컴포넌트 패턴을 올바르게 따르고 있습니다.


28-44: 제어 컴포넌트 구현이 완벽합니다!

동적 상태 관리와 적절한 fallback 값 처리로 안전하고 사용자 친화적인 입력 필드가 구현되어 있습니다. 빈 값일 때 에러 상태를 보여주는 것도 좋은 UX입니다.


46-60: 일관성 있는 답안 필드 구현입니다!

질문 필드와 동일한 패턴을 사용하여 일관성을 유지하고 있으며, 제어 컴포넌트 구현이 올바르게 되어 있습니다. 필수 필드에 대한 검증 로직도 적절합니다.

export const addQuestion = (questions: Question[]): Question[] => {
const newQuestion = {
...createInitialQuestion(questions.length),
id: `question-${Date.now()}-${Math.random()}`
Copy link
Contributor

Choose a reason for hiding this comment

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

uuid 쓰셔도 괜찮지 않을까요?

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: 4

♻️ Duplicate comments (1)
service/app/src/entities/game/model/state/create/selectors.ts (1)

99-101: 변경사항 감지 로직이 잘못되어 있습니다.

이전 리뷰에서 지적된 문제가 여전히 남아있습니다. state.questions.length > 0 조건은 초기 상태에서도 질문이 1개 있으므로 항상 true를 반환하여 변경사항을 올바르게 감지하지 못합니다.

다음과 같이 수정해주세요:

 hasChanges: (state: GameCreationState): boolean => {
-  return state.gameName !== "게임1" || state.questions.length > 0
+  const hasGameNameChanged = state.gameName !== "게임1"
+  const hasQuestionsChanged = state.questions.length > 1 || 
+    state.questions.some(q => q.text.trim() !== '' || q.answer.trim() !== '' || q.imageFile !== null)
+  
+  return hasGameNameChanged || hasQuestionsChanged
 }
🧹 Nitpick comments (2)
service/app/src/entities/game/utils/gameSave.ts (1)

33-108: 복잡한 저장 로직을 더 작은 함수로 분리하는 것을 고려하세요.

saveGame 함수가 너무 많은 책임을 가지고 있어 테스트와 유지보수가 어렵습니다.

다음과 같이 분리를 고려해보세요:

  • uploadImagesIfNeeded()
  • createGameWithData()
  • handleImageUrlMapping()
service/app/src/entities/game/model/state/create/selectors.ts (1)

104-120: questionSelectors의 중복성을 검토해주세요.

questionSelectors 객체의 함수들이 주로 메인 selectors 객체의 래퍼 역할을 하고 있습니다. 이런 중복성이 필요한지 검토해보세요.

만약 questionSelectors가 특별한 목적이 없다면, 메인 selectors 객체만 사용하는 것을 고려해보세요. 또는 각각의 목적을 명확히 하기 위해 JSDoc 주석을 추가하는 것도 좋습니다:

+/**
+ * Question-specific selectors for UI components
+ */
 export const questionSelectors = {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86ecb3f and ef61c81.

📒 Files selected for processing (16)
  • service/app/src/entities/game/api/createGame.ts (1 hunks)
  • service/app/src/entities/game/api/index.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/actions.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/reducer.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/selectors.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/state.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/useGameCreation.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/utils.ts (1 hunks)
  • service/app/src/entities/game/ui/components/fileUploadArea.tsx (1 hunks)
  • service/app/src/entities/game/ui/components/questionInputForm.tsx (1 hunks)
  • service/app/src/entities/game/ui/components/questionList.tsx (1 hunks)
  • service/app/src/entities/game/ui/popupManager.tsx (2 hunks)
  • service/app/src/entities/game/utils/gameSave.ts (1 hunks)
  • service/app/src/entities/game/utils/index.ts (1 hunks)
  • service/app/src/entities/game/utils/s3Upload.ts (3 hunks)
  • shared/lib/fetchClient.ts (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • service/app/src/entities/game/utils/index.ts
  • service/app/src/entities/game/api/index.ts
  • shared/lib/fetchClient.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • service/app/src/entities/game/ui/components/questionInputForm.tsx
  • service/app/src/entities/game/model/state/create/reducer.ts
  • service/app/src/entities/game/model/state/create/useGameCreation.ts
  • service/app/src/entities/game/ui/components/questionList.tsx
  • service/app/src/entities/game/model/state/create/state.ts
  • service/app/src/entities/game/model/state/create/actions.ts
  • service/app/src/entities/game/model/state/create/utils.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
service/app/src/entities/game/model/state/create/selectors.ts (3)
service/app/src/entities/game/model/state/create/state.ts (2)
  • Question (1-9)
  • GameCreationState (27-38)
service/app/src/entities/game/model/game.ts (1)
  • GameDetailData (29-35)
service/app/src/entities/game/model/gameRequest.ts (1)
  • GameUpdateRequest (44-50)
🔇 Additional comments (6)
service/app/src/entities/game/api/createGame.ts (1)

7-20: API 함수 단순화가 적절하게 구현되었습니다.

이미지 처리 로직을 다른 유틸리티로 분리한 것은 좋은 리팩터링입니다. 단일 책임 원칙을 잘 따르고 있습니다.

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

52-69: 이미지 미리보기 구현이 잘 되었습니다.

조건부 렌더링과 호버 효과를 활용한 이미지 변경 UI가 사용자 친화적입니다.

service/app/src/entities/game/utils/gameSave.ts (1)

76-84: presignedUrls 배열 접근 시 안전성 검증이 필요합니다.

인덱스로 배열에 접근할 때 배열 길이 검증이 없어 런타임 오류가 발생할 수 있습니다.

다음과 같이 안전한 접근으로 수정하세요:

    if (presignedResponse && presignedResponse.data) {
      gameData.questions = gameData.questions.map((question, index) => {
-       const presignedUrl = presignedResponse.data!.presignedUrls[index]
+       const presignedUrl = presignedResponse.data!.presignedUrls.find(
+         (url, urlIndex) => urlIndex === index
+       )
        return {
          ...question,
          imageUrl: presignedUrl ? presignedUrl.key : "",
        }
      })
    }

Likely an incorrect or invalid review comment.

service/app/src/entities/game/utils/s3Upload.ts (2)

42-52: 파일별 presigned URL 처리가 개선되었습니다.

누락된 URL에 대해 개별적으로 처리하는 로직이 좋습니다. 전체 업로드를 실패시키지 않고 부분적 실패를 허용합니다.


3-6: S3UploadResult 인터페이스가 적절합니다.

구조화된 결과 타입으로 에러 처리가 개선되었습니다. 예외 던지기 대신 결과 객체 반환하는 패턴이 좋습니다.

service/app/src/entities/game/model/state/create/selectors.ts (1)

63-70: cleanedQuestions 선택자에서 모든 필드를 보존하지 않고 있습니다.

cleanedQuestions 선택자가 텍스트와 답안만 정리하고 있지만, Question 인터페이스의 다른 필드들(imageFile, imageUrl, previewImageUrl 등)은 유지하지 않고 있습니다.

스프레드 연산자를 사용하여 모든 필드를 보존하도록 수정하세요:

 cleanedQuestions: (state: GameCreationState): Question[] => {
   return state.questions
     .map(q => ({
       ...q,
       text: q.text.trim(),
       answer: q.answer.trim()
     }))
 },

이미 올바르게 구현되어 있지만, 명시적으로 확인했습니다.

Likely an incorrect or invalid review comment.

Comment on lines +9 to +16
export const validateQuestion = (question: Question): boolean => {
return (
question.text.trim().length > 0 &&
question.text.length <= 50 &&
question.answer.trim().length > 0 &&
question.answer.length <= 50
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

검증 로직의 일관성을 확인해주세요.

validateQuestion 함수에서 question.text.trim().length > 0으로 공백을 제거한 후 길이를 확인하지만, question.text.length <= 50에서는 원본 문자열 길이를 확인합니다. validateQuestionTextvalidateAnswerText에서도 동일한 패턴이 반복됩니다.

다음과 같이 수정하여 일관성을 확보하세요:

 export const validateQuestion = (question: Question): boolean => {
   return (
-    question.text.trim().length > 0 &&
-    question.text.length <= 50 &&
-    question.answer.trim().length > 0 &&
-    question.answer.length <= 50
+    question.text.trim().length > 0 &&
+    question.text.trim().length <= 50 &&
+    question.answer.trim().length > 0 &&
+    question.answer.trim().length <= 50
   )
 }
📝 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.

Suggested change
export const validateQuestion = (question: Question): boolean => {
return (
question.text.trim().length > 0 &&
question.text.length <= 50 &&
question.answer.trim().length > 0 &&
question.answer.length <= 50
)
}
export const validateQuestion = (question: Question): boolean => {
return (
question.text.trim().length > 0 &&
question.text.trim().length <= 50 &&
question.answer.trim().length > 0 &&
question.answer.trim().length <= 50
)
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/selectors.ts around lines 9
to 16, the validateQuestion function inconsistently checks the trimmed length
for non-empty validation but uses the original string length for the maximum
length check. To fix this, ensure both checks use the trimmed string
consistently by trimming once and then validating both the non-empty condition
and the length limit on the trimmed string. Apply the same consistent trimming
approach to validateQuestionText and validateAnswerText functions as well.

Comment on lines +41 to 47
const reader = new FileReader()
reader.onload = (e) => {
const previewUrl = e.target?.result as string
actions.uploadImageStart(selectedQuestion.id, file, previewUrl)
}
reader.readAsDataURL(file)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

FileReader 에러 처리가 누락되었습니다.

FileReader.readAsDataURL() 실행 중 오류가 발생할 수 있지만 에러 핸들러가 없습니다.

다음과 같이 에러 처리를 추가하세요:

    const reader = new FileReader()
    reader.onload = (e) => {
      const previewUrl = e.target?.result as string
      actions.uploadImageStart(selectedQuestion.id, file, previewUrl)
    }
+   reader.onerror = () => {
+     showPopup("fileTypeError") // 또는 적절한 에러 팝업
+   }
    reader.readAsDataURL(file)
📝 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.

Suggested change
const reader = new FileReader()
reader.onload = (e) => {
const previewUrl = e.target?.result as string
actions.uploadImageStart(selectedQuestion.id, file, previewUrl)
}
reader.readAsDataURL(file)
}
const reader = new FileReader()
reader.onload = (e) => {
const previewUrl = e.target?.result as string
actions.uploadImageStart(selectedQuestion.id, file, previewUrl)
}
reader.onerror = () => {
showPopup("fileTypeError") // 또는 적절한 에러 팝업
}
reader.readAsDataURL(file)
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/ui/components/fileUploadArea.tsx around lines
41 to 47, the FileReader instance lacks error handling for readAsDataURL. Add an
onerror event handler to the FileReader to properly handle and respond to any
errors that occur during the file reading process, ensuring robust error
management.

Comment on lines 22 to 44
const handleSaveConfirm = async () => {
try {
actions.saveGameStart()

const cleanedQuestions = selectors.cleanedQuestions(state)

const result = await saveGame({
...state,
questions: cleanedQuestions
})

if (result.success) {
actions.saveGameSuccess()
hidePopup("saveConfirm")
} else {
actions.saveGameError(result.error || "저장에 실패했습니다.")
hidePopup("saveConfirm")
}
} catch (error) {
actions.saveGameError("알 수 없는 오류가 발생했습니다.")
hidePopup("saveConfirm")
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

비동기 저장 로직이 잘 구현되었지만 사용자 경험 개선이 필요합니다.

저장 중 로딩 상태 표시와 중복 클릭 방지 로직이 없어 사용자가 여러 번 저장 버튼을 클릭할 수 있습니다.

다음과 같이 개선하는 것을 권장합니다:

  const handleSaveConfirm = async () => {
+   if (state.saveStatus === 'saving') return // 중복 클릭 방지
+   
    try {
      actions.saveGameStart()
      
      const cleanedQuestions = selectors.cleanedQuestions(state)
      
      const result = await saveGame({
        ...state,
        questions: cleanedQuestions
      })
      
      if (result.success) {
        actions.saveGameSuccess()
        hidePopup("saveConfirm")
      } else {
        actions.saveGameError(result.error || "저장에 실패했습니다.")
        hidePopup("saveConfirm")
      }
    } catch (error) {
      actions.saveGameError("알 수 없는 오류가 발생했습니다.")
      hidePopup("saveConfirm")
    }
  }
📝 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.

Suggested change
const handleSaveConfirm = async () => {
try {
actions.saveGameStart()
const cleanedQuestions = selectors.cleanedQuestions(state)
const result = await saveGame({
...state,
questions: cleanedQuestions
})
if (result.success) {
actions.saveGameSuccess()
hidePopup("saveConfirm")
} else {
actions.saveGameError(result.error || "저장에 실패했습니다.")
hidePopup("saveConfirm")
}
} catch (error) {
actions.saveGameError("알 수 없는 오류가 발생했습니다.")
hidePopup("saveConfirm")
}
}
const handleSaveConfirm = async () => {
if (state.saveStatus === 'saving') return // 중복 클릭 방지
try {
actions.saveGameStart()
const cleanedQuestions = selectors.cleanedQuestions(state)
const result = await saveGame({
...state,
questions: cleanedQuestions
})
if (result.success) {
actions.saveGameSuccess()
hidePopup("saveConfirm")
} else {
actions.saveGameError(result.error || "저장에 실패했습니다.")
hidePopup("saveConfirm")
}
} catch (error) {
actions.saveGameError("알 수 없는 오류가 발생했습니다.")
hidePopup("saveConfirm")
}
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/ui/popupManager.tsx around lines 22 to 44, the
handleSaveConfirm function lacks loading state management and duplicate click
prevention, allowing multiple save attempts. Introduce a loading state variable
to track when saving is in progress, disable the save button or ignore clicks
during this state, and update the loading state appropriately before and after
the async saveGame call to improve user experience and prevent multiple
submissions.

return {
gameId,
gameTitle: state.gameName,
gameCreatorEmail: "[email protected]",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

하드코딩된 이메일 주소를 제거해야 합니다.

사용자 이메일이 하드코딩되어 있어 실제 사용자 정보를 반영하지 못합니다.

사용자 컨텍스트나 인증 정보에서 실제 이메일을 가져오도록 수정하세요:

  return {
    gameId,
    gameTitle: state.gameName,
-   gameCreatorEmail: "[email protected]",
+   gameCreatorEmail: getCurrentUserEmail(), // 실제 사용자 이메일 조회
    gameThumbnailUrl: "",
🤖 Prompt for AI Agents
In service/app/src/entities/game/utils/gameSave.ts at line 22, the email address
is hardcoded as "[email protected]", which does not reflect the actual user's
email. Replace this hardcoded string by retrieving the user's email dynamically
from the user context or authentication information available in the
application, ensuring the saved game data correctly associates with the current
user.

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 (2)
service/app/src/entities/game/model/state/create/utils.ts (2)

66-74: FileReader 에러 처리가 여전히 누락되었습니다.

이전 리뷰에서 지적된 FileReader 에러 처리 문제가 해결되지 않았습니다. Promise가 영원히 pending 상태가 될 수 있습니다.

다음과 같이 에러 처리를 추가해주세요:

 export const createImageUrl = (file: File): Promise<string> => {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     const reader = new FileReader()
     reader.onload = (e) => {
       resolve(e.target?.result as string)
     }
+    reader.onerror = () => {
+      reject(new Error('파일을 읽는 중 오류가 발생했습니다.'))
+    }
     reader.readAsDataURL(file)
   })
 }

76-80: URL 해제 로직에 null 체크를 추가하세요.

removeImageUrl 함수에서 null/undefined URL에 대한 처리가 없습니다.

다음과 같이 개선해주세요:

 export const removeImageUrl = (url: string): void => {
-  if (url.startsWith("blob:")) {
+  if (url && url.startsWith("blob:")) {
     URL.revokeObjectURL(url)
   }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef61c81 and 4be8a3f.

📒 Files selected for processing (1)
  • service/app/src/entities/game/model/state/create/utils.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
service/app/src/entities/game/model/state/create/utils.ts (3)
service/app/src/entities/game/model/state/create/state.ts (2)
  • Question (1-9)
  • GameCreationState (27-38)
service/app/src/entities/game/model/gameRequest.ts (4)
  • GameUpdateQuestion (28-34)
  • GameCreateQuestion (21-26)
  • GameUpdateRequest (44-50)
  • GameCreateRequest (36-42)
service/app/src/entities/game/model/game.ts (1)
  • GameQuestion (20-27)
🔇 Additional comments (5)
service/app/src/entities/game/model/state/create/utils.ts (5)

5-13: LGTM! 초기 질문 생성 로직이 명확합니다.

createInitialQuestion 함수의 구현이 간결하고 명확합니다. 모든 필수 속성이 적절한 기본값으로 초기화되어 있습니다.


15-34: 초기 상태 구조가 잘 설계되었습니다.

createInitialState 함수가 게임 생성에 필요한 모든 상태를 포괄적으로 초기화하고 있습니다. 팝업, 로딩, 에러 상태까지 체계적으로 관리하는 점이 좋습니다.


82-93: 게임명 생성 로직이 효율적입니다.

generateGameName 함수의 구현이 간단하고 명확합니다. 중복 방지 로직도 적절합니다.


105-114: 질문 삭제 시 안전장치가 잘 구현되었습니다.

최소 1개 질문을 유지하는 안전장치와 삭제 후 순서 업데이트 로직이 적절합니다.


116-138: UUID 사용으로 고유성이 보장됩니다.

이전 리뷰에서 제안된 UUID 사용이 잘 적용되었습니다. 질문 추가 로직도 삽입 위치 처리와 순서 업데이트가 올바르게 구현되어 있습니다.

Comment on lines +36 to +57
export const moveQuestionInArray = (
questions: Question[],
questionId: string,
direction: "up" | "down",
): Question[] => {
const questionIndex = questions.findIndex((q) => q.id === questionId)
if (questionIndex === -1) return questions

const newQuestions = [...questions]
const targetIndex = direction === "up" ? questionIndex - 1 : questionIndex + 1

if (targetIndex < 0 || targetIndex >= questions.length) return questions
;[newQuestions[questionIndex], newQuestions[targetIndex]] = [
newQuestions[targetIndex],
newQuestions[questionIndex],
]

return newQuestions.map((question, index) => ({
...question,
order: index,
}))
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

질문 순서 변경 로직의 경계 처리를 개선하세요.

로직은 전반적으로 올바르지만 일부 개선이 필요합니다.

다음과 같이 개선해주세요:

 export const moveQuestionInArray = (
   questions: Question[],
   questionId: string,
   direction: "up" | "down",
 ): Question[] => {
   const questionIndex = questions.findIndex((q) => q.id === questionId)
-  if (questionIndex === -1) return questions
+  if (questionIndex === -1) {
+    console.warn(`Question with id ${questionId} not found`)
+    return questions
+  }

   const newQuestions = [...questions]
   const targetIndex = direction === "up" ? questionIndex - 1 : questionIndex + 1

-  if (targetIndex < 0 || targetIndex >= questions.length) return questions
+  if (targetIndex < 0 || targetIndex >= questions.length) {
+    return questions // 이미 첫 번째/마지막 위치인 경우
+  }
+  
   ;[newQuestions[questionIndex], newQuestions[targetIndex]] = [
     newQuestions[targetIndex],
     newQuestions[questionIndex],
   ]

   return newQuestions.map((question, index) => ({
     ...question,
     order: index,
   }))
 }
📝 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.

Suggested change
export const moveQuestionInArray = (
questions: Question[],
questionId: string,
direction: "up" | "down",
): Question[] => {
const questionIndex = questions.findIndex((q) => q.id === questionId)
if (questionIndex === -1) return questions
const newQuestions = [...questions]
const targetIndex = direction === "up" ? questionIndex - 1 : questionIndex + 1
if (targetIndex < 0 || targetIndex >= questions.length) return questions
;[newQuestions[questionIndex], newQuestions[targetIndex]] = [
newQuestions[targetIndex],
newQuestions[questionIndex],
]
return newQuestions.map((question, index) => ({
...question,
order: index,
}))
}
export const moveQuestionInArray = (
questions: Question[],
questionId: string,
direction: "up" | "down",
): Question[] => {
const questionIndex = questions.findIndex((q) => q.id === questionId)
if (questionIndex === -1) {
console.warn(`Question with id ${questionId} not found`)
return questions
}
const newQuestions = [...questions]
const targetIndex = direction === "up" ? questionIndex - 1 : questionIndex + 1
if (targetIndex < 0 || targetIndex >= questions.length) {
return questions // 이미 첫 번째/마지막 위치인 경우
}
;[newQuestions[questionIndex], newQuestions[targetIndex]] = [
newQuestions[targetIndex],
newQuestions[questionIndex],
]
return newQuestions.map((question, index) => ({
...question,
order: index,
}))
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/utils.ts between lines 36
and 57, improve the boundary checks in the moveQuestionInArray function to
handle edge cases more robustly. Specifically, ensure that when moving a
question up or down, the function correctly prevents out-of-bound index access
by validating the targetIndex before attempting the swap. Adjust the conditions
to return the original array unchanged if the move is not possible due to
boundary limits.

@kimnamheeee kimnamheeee merged commit be55384 into dev Aug 4, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨feature 구현, 개선 사항 관련 부분 👩🏻‍💻frontend 프론트엔드 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

implement initial game creation form

3 participants