Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 밸런스 게임 수정 및 삭제 로직 구현 #268

Open
wants to merge 50 commits into
base: dev
Choose a base branch
from

Conversation

WonJuneKim
Copy link
Collaborator

@WonJuneKim WonJuneKim commented Dec 19, 2024

💡 작업 내용

  • 밸런스 게임 수정 로직 구현
  • 밸런스 게임 삭제 api 연동
  • 밸런스 게임 생성 시 games의 상태를 페이지가 관리하도록 설정

💡 자세한 설명

✅ 밸런스 게임 수정, 생성 시 공통 로직

  • 이미지 추가, 삭제 는 밸런스게임 수정, 그리고 생성 시에 공통적으로 사용되는 부분이라고 생각합니다. 따라서 상태관리는 각 페이지에서 수행하되, 이미지 생성 그리고 삭제하는 로직은 공통 훅을 통해 코드의 중복 사용을 제어하였습니다.

  • 스타일 파일 역시 중복 처리되는 부분이 많기 때문에 수정 페이지는 생성 페이지의 스타일 파일을 그대로 import 해서 사용합니다.

✅ 밸런스 게임 생성(및 수정) 시 games 배열의 상태 관리

  • 더 이상 하위 organisms 컴포넌트에서 중복으로 games의 상태를 관리하지 않습니다. 이중으로 관리되는 상태에서 페이지에서 games 상태를 관리하고 자식 컴포넌트에 주입해주는 형태로 수정하였습니다.

✅ 밸런스 게임 수정 로직

  • 밸런스 게임의 수정의 경우 수정 페이지 접근 시 밸런스 게임 조회 api를 호출 후에 해당 데이터를 수정 인터페이스(생성과 동일)에 맞춰 가공 후 ui를 랜더링합니다.
  • 이 후 게임 생성 로직의 임시 저장 불러오기 이후 동작과 동일하게 원하는 부분을 수정 후 하단의 버튼을 통해서 수정을 완료합니다.
  • 태그 수정 버튼, 그리고 게임 마지막 스테이지의 버튼은 메인태그, 서브 태그를 변경하는 용도로만 사용됩니다.

기본적인 수정 로직(imgUrl은 api가 수정이 되어야함)

default.mp4

상단 태그 선택 버튼 클릭 시

default.mp4

마지막 스테이지에서 태그 선택(버튼의 label 명은 수정하기 까다로워 일단 보류) 클릭 시

default.mp4

✅ 모달 공용 훅 생성

  • src/hooks/modal 디렉토리에 모달과 상태를 관리 할 수 있는 useModal 훅을 생성하였습니다.
  • 밸런스 게임 생성, 수정 부분에서는 해당 훅을 통해 모달 상태를 관리할 수 있도록 수정하였습니다.

📗 참고 자료 (선택)

📢 리뷰 요구 사항 (선택)

🚩 후속 작업 (선택)

  • 준수님께 요청드린 밸런스 게임 조회 시 fileId 값도 같이 반환되는 수정 사항 반영 후 수정 페이지에서 이미지가 정상적으로 로드 되는지 확인 필요!! (구현은 되어있음)

  • 밸런스 게임 삭제 시 올바른 요청을 보냈는데도 500이 반환되는 문제 확인 필요.

  • 밸런스 게임 신고 api는 아직 미구현

✅ 셀프 체크리스트

  • PR 제목을 형식에 맞게 작성했나요?
  • 브랜치 전략에 맞는 브랜치에 PR을 올리고 있나요? (master/main이 아닙니다.)
  • 이슈는 close 했나요?
  • Reviewers, Labels, Projects를 등록했나요?
  • 작업 도중 문서 수정이 필요한 경우 잘 수정했나요?
  • 테스트는 잘 통과했나요?
  • 불필요한 코드는 제거했나요?

closes #231

Summary by CodeRabbit

릴리즈 노트

  • 새로운 기능

    • 밸런스 게임 편집 기능 추가
    • 게임 삭제 및 신고 기능 도입
    • 이미지 업로드 및 관리 기능 개선
    • 태그 편집 및 모달 관리 기능 추가
    • 새로운 라우팅 경로 및 API 기능 추가
  • 버그 수정

    • 게임 생성 및 편집 시 상태 관리 안정화
    • 모달 상호작용 개선
  • 개선 사항

    • 라우팅 구조 최적화
    • 사용자 인터페이스 유연성 향상
    • 이미지 및 태그 관리 기능 강화
  • 기타

    • 코드 리팩토링 및 유지보수성 개선
    • 훅(Hook) 및 유틸리티 함수 추가

@WonJuneKim WonJuneKim self-assigned this Dec 19, 2024
Copy link
Contributor

coderabbitai bot commented Dec 19, 2024

Walkthrough

이 풀 리퀘스트는 밸런스 게임의 수정 기능을 구현하기 위한 광범위한 변경 사항을 포함하고 있습니다. 주요 변경 사항은 라우팅 구조 업데이트, 게임 수정을 위한 새로운 페이지 및 컴포넌트 추가, 이미지 업로드 및 게임 데이터 관리를 위한 새로운 훅 도입, 그리고 관련 API 및 유틸리티 함수의 확장을 포함합니다.

Changes

파일 변경 요약
src/App.tsx 밸런스 게임 수정 라우트 추가
src/api/game.ts 게임 수정 및 삭제를 위한 API 함수 추가
src/components/molecules/TagModal/TagModal.tsx 초기 태그 값 설정을 위한 선택적 props 추가
src/components/organisms/BalanceGameCreation/BalanceGameCreation.style.ts 새로운 스타일 컴포넌트 추가
src/components/organisms/BalanceGameCreation/BalanceGameCreation.tsx 게임 생성 컴포넌트의 상태 관리 및 함수 업데이트
src/components/organisms/BalanceGameSection/BalanceGameSection.tsx 게임 수정 및 삭제 기능 추가
src/constants/message.ts 게임 수정 및 삭제 관련 메시지 추가
src/constants/path.ts 밸런스 게임 경로 구조 업데이트
src/hooks/api/game/useDeleteGameSetMutation.ts 게임 세트 삭제를 위한 훅 추가
src/hooks/api/game/useEditGamesMutation.ts 게임 수정 훅 추가
src/hooks/game/useImageHandlers.ts 이미지 업로드 및 관리를 위한 새로운 훅 추가
src/pages/BalanceGameCreationPage/BalanceGameCreationPage.tsx 상태 관리 및 모달 처리 개선
src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx 새로운 게임 수정 페이지 컴포넌트 생성
src/types/game.ts 게임 옵션 인터페이스 수정
src/utils/balanceGameUtils.ts 게임 세트를 밸런스 게임으로 변환하는 함수 추가

Assessment against linked issues

목표 해결 여부 설명
밸런스게임 수정 기능 추가 완전히 구현됨

Possibly related PRs

Suggested labels

✔︎pull requests, 🪄refactor

Suggested reviewers

  • alwubin
  • areumH
  • hsgh085

Poem

🐰 코드의 토끼, 수정의 춤을 추네
라우트와 훅으로 게임을 물들이고
새로운 기능, 창의의 물결 위에
밸런스 게임, 더욱 빛나는 모습으로
사용자의 즐거움을 노래하네! 🎮✨


📜 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 c5bedf4 and ce3fe30.

📒 Files selected for processing (1)
  • src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

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. (Beta)
  • @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.

Copy link
Contributor

@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: 13

🧹 Nitpick comments (25)
src/components/organisms/BalanceGameSection/BalanceGameSection.tsx (1)

91-113: 게스트 투표 로직 개선 제안

현재 게스트 투표 로직이 다소 복잡합니다. 다음과 같이 단순화할 수 있습니다:

 const handleGuestGameVote = (
   selectedOption: MyVoteOption,
   voteOption: VoteOption,
 ) => {
-  const updatedVotes = [...guestVotedList];
-  const currentVoteIndex = updatedVotes.findIndex(
-    (vote) => vote.gameId === game?.gameDetailResponses[currentStage]?.id,
-  );
+  const gameId = game?.gameDetailResponses[currentStage]?.id;
+  if (!gameId) return;
 
-  if (!selectedOption) {
-    updatedVotes.push({
-      gameId: game?.gameDetailResponses[currentStage]?.id as number,
-      votedOption: voteOption,
-    });
-  } else if (selectedOption === voteOption) {
-    updatedVotes.splice(currentVoteIndex, 1);
-  } else {
-    updatedVotes[currentVoteIndex].votedOption = voteOption;
-  }
+  const updatedVotes = guestVotedList.filter(vote => vote.gameId !== gameId);
+  if (!selectedOption || selectedOption !== voteOption) {
+    updatedVotes.push({ gameId, votedOption: voteOption });
+  }
 
   setGuestVotedList(updatedVotes);
   localStorage.setItem(`game_${gameSetId}`, JSON.stringify(updatedVotes));
 };
src/pages/BalanceGamePage/BalanceGamePage.tsx (4)

1-19: 불필요한 반복 Import 방지 제안
현재 많은 훅과 컴포넌트를 한 파일에서 임포트 중입니다. 관리 편의를 위해 관련 기능(예: API 훅)들을 하나의 인덱스 파일에서 통합 임포트하여 import 구문을 간소화하면 유지보수성이 향상될 수 있습니다.


54-76: 삭제 처리 로직 예외 케이스 검토
삭제 로직 실행에 성공 또는 실패 시 토스트 모달로 처리하고 있습니다. 네트워크 실패(오프라인 등)나 API 예상치 못한 응답 코드 같은 예외 상황에 대해서도 다양한 테스트를 진행하는 것을 권장드립니다.


78-87: 신고 기능 구현 확인
현재 신고 기능이 모달 열기와 닫기 정도로 구현되어 있습니다. 추후 신고 API 연동 계획이 있으시다면, 에러 처리나 성공 시 유저에게 명확한 안내가 표시되도록 보강하시면 좋겠습니다.


89-129: 조건 분기 구조 간소화 제안
currentStage가 10인지 체크하여 다른 컴포넌트를 렌더링하는 로직은 읽기 쉽지만, 추후 조건이 늘어나면 분기문이 복잡해질 수 있습니다. 별도 함수나 객체 맵핑을 통해 분기를 관리하면 가독성을 더 높일 수 있을 것 같습니다.

src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx (1)

71-91: 게임 정보 수정 API 에러 처리
mutate 호출 시 onError 처리에서 에러 응답 정보를 좀 더 구체적으로 사용자에게 알려주면 좋을 것 같습니다. 예: 400인지, 500인지 등.

src/components/organisms/BalanceGameCreation/BalanceGameCreation.tsx (4)

17-26: 인터페이스 설계 검토
onDraftLoad, handleTagEditClick 같은 선택적 프로퍼티가 늘어나고 있는데, 향후 기능 확장 시 공통 인터페이스 분리나 컴포넌트 분해로 관리 부담을 낮출 것을 제안드립니다.


42-44: currentStage와 clearInput 동시 관리
두 상태를 각각 저장하고 있는데, setCurrentStage 시 clearInput을 초기화하는 식으로, 상호 의존 로직을 간단하게 관리할 수 있는지 한 번 고려해 보세요.


49-70: updateOption 함수의 가독성 제안
중첩 맵으로 인해 가독성이 약간 저하될 수 있습니다. 한 단계 더 나누거나 헬퍼 함수를 추출하여 “옵션 업데이트” 로직을 분명히 하는 방법을 고려해 보세요.


72-90: validateStage 함수의 리턴 구조
true 또는 에러 메시지로 분기하는 방식은 단순 명료합니다. 다만, 다양한 에러 종류가 생길 경우 상수를 한 파일에서 관리하거나, Error 객체를 던지는 구조로 확장하면 더 견고할 수 있겠습니다.

src/pages/BalanceGameCreationPage/BalanceGameCreationPage.tsx (2)

56-56: 이미지 처리 훅 도입
useImageHandlers 훅을 도입하여 업로드 및 삭제 로직을 중앙집중화했습니다. 이미지 업로드 후 에러가 나면 재시도 방안 등이 있는지 한 번 살펴보세요.


81-89: 이미지 삭제 로직 모달 처리
사용자가 삭제를 확정하기 전까지 confirm 과정을 거치도록 설계되었습니다. UX 차원에서 “삭제할 이미지를 미리보기” 등의 추가 기능을 검토해봐도 좋을 것 같습니다.

src/hooks/modal/useModal.ts (1)

3-7: 인터페이스 문서화 추가 제안

인터페이스가 잘 정의되어 있지만, 각 속성에 대한 JSDoc 문서화를 추가하면 더 좋을 것 같습니다.

 interface UseModalReturn {
+  /** 모달의 현재 표시 상태 */
   isOpen: boolean;
+  /** 모달을 여는 함수 */
   openModal: () => void;
+  /** 모달을 닫는 함수 */
   closeModal: () => void;
 }
src/hooks/api/game/useDeleteGameSetMutation.ts (1)

12-14: 불필요한 Promise 체이닝 제거

then(() => {}) 체이닝이 불필요합니다. 직접 deleteBySetId를 반환하는 것이 더 깔끔할 것 같습니다.

-    mutationFn: ({ gameSetId }) => deleteBySetId(gameSetId).then(() => {}),
+    mutationFn: ({ gameSetId }) => deleteBySetId(gameSetId),
src/components/organisms/BalanceGameCreation/BalanceGameCreation.style.ts (1)

45-53: z-index 값의 표준화가 필요합니다.

현재 하드코딩된 z-index 값(50)을 상수로 분리하여 관리하는 것이 좋습니다. 또한 위치 값(top: 16px, right: 21px)도 변수로 관리하면 유지보수가 더 용이할 것 같습니다.

다음과 같은 방식으로 개선해보세요:

+const Z_INDICES = {
+  TAG_EDIT_BUTTON: 50
+} as const;
+
+const POSITION_VALUES = {
+  TAG_EDIT_BUTTON: {
+    top: '16px',
+    right: '21px'
+  }
+} as const;

export const tagEditButtonContainer = css({
  position: 'absolute',
-  top: '16px',
-  right: '21px',
+  top: POSITION_VALUES.TAG_EDIT_BUTTON.top,
+  right: POSITION_VALUES.TAG_EDIT_BUTTON.right,
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
-  zIndex: '50',
+  zIndex: Z_INDICES.TAG_EDIT_BUTTON,
});
src/pages/BalanceGamePage/BalanceGamePage.style.ts (1)

36-42: 중앙 정렬 스타일이 적절하게 구현되었습니다.

모달 컴포넌트의 중앙 정렬을 위한 스타일이 잘 정의되어 있습니다. 다만, z-index 값의 관리가 필요해 보입니다.

z-index 값을 상수로 분리하여 관리하는 것을 추천드립니다.

src/types/game.ts (1)

7-7: 타입 정의에 JSDoc 문서화 추가 필요

fileId 필드에 대한 문서화가 누락되어 있습니다. 해당 필드의 용도와 null 값이 허용되는 상황에 대한 설명이 필요합니다.

다음과 같이 문서화를 추가할 것을 제안합니다:

 export interface GameOption {
   id: number;
   name: string;
   imgUrl: string | null;
+  /** 이미지 파일의 ID. 이미지가 업로드되지 않은 경우 null 또는 undefined */
   fileId?: number | null;
   description: string;
   optionType: 'A' | 'B';
 }
 export interface BalanceGameOption {
   id: number;
   name: string;
   imgUrl: string;
   description: string;
   optionType: 'A' | 'B';
   imageFile?: File | null;
+  /** 이미지 파일의 ID. 이미지가 업로드되지 않은 경우 null 또는 undefined */
   fileId?: number | null;
 }

Also applies to: 61-61

src/stories/organisms/BalanceGameCreation.stories.tsx (1)

28-43: 테스트 데이터 다양성 개선 필요

현재 더미 데이터가 기본적인 케이스만 다루고 있습니다. 다음과 같은 엣지 케이스들을 추가로 고려해주세요:

  1. 긴 텍스트가 포함된 케이스
  2. 특수 문자가 포함된 케이스
  3. 이미지 URL이 있는 케이스
  4. fileId가 있는 케이스

다음과 같이 더미 데이터를 확장할 것을 제안합니다:

 const dummyGames: BalanceGameSet[] = [
   {
     description: '첫 번째 스테이지 설명',
     gameOptions: [
-      { id: 1, name: '옵션 A', imgUrl: '', description: '', optionType: 'A' },
-      { id: 2, name: '옵션 B', imgUrl: '', description: '', optionType: 'B' },
+      {
+        id: 1,
+        name: '매우 긴 옵션 이름입니다. 이렇게 긴 텍스트가 들어가도 UI가 깨지지 않아야 합니다.',
+        imgUrl: 'https://example.com/image.jpg',
+        fileId: 123,
+        description: '특수문자 테스트: !@#$%^&*()',
+        optionType: 'A'
+      },
+      {
+        id: 2,
+        name: '옵션 B',
+        imgUrl: '',
+        description: '',
+        optionType: 'B'
+      },
     ],
   },
   // ... 나머지 더미 데이터
 ];
src/hooks/game/useImageHandlers.ts (2)

32-57: 이미지 변경 함수의 에러 처리를 구체화해주세요.

onImageChange 함수의 에러 처리가 너무 일반적입니다. 사용자에게 더 명확한 피드백을 제공할 수 있도록 개선이 필요합니다.

 const onImageChange = async (
   stageIndex: number,
   optionIndex: number,
   imageFile: File,
   updateGames: (
     updater: (games: BalanceGameSet[]) => BalanceGameSet[],
   ) => void,
-): Promise<boolean> => {
+): Promise<{ success: boolean; error?: string }> => {
   try {
     const { imgUrl, fileId } = await handleImageUpload(imageFile, {
       type: 'GAME_OPTION',
     });
     updateGames((prevGames) => {
       const updatedGames = [...prevGames];
       updatedGames[stageIndex].gameOptions[optionIndex] = {
         ...updatedGames[stageIndex].gameOptions[optionIndex],
         imgUrl,
         fileId,
       };
       return updatedGames;
     });
-    return true;
+    return { success: true };
   } catch (error) {
-    return false;
+    return {
+      success: false,
+      error: error instanceof Error ? error.message : '이미지 업로드 중 오류가 발생했습니다.',
+    };
   }
 };

59-78: 이미지 삭제 함수에 확인 로직 추가가 필요합니다.

이미지 삭제 시 실수로 인한 삭제를 방지하기 위한 검증 로직이 필요합니다.

 const deleteImage = (
   stageIndex: number,
   optionIndex: number,
   updateGames: (
     updater: (games: BalanceGameSet[]) => BalanceGameSet[],
   ) => void,
 ) => {
+  // 삭제 전 현재 이미지 존재 여부 확인
+  const currentGame = games[stageIndex];
+  const currentOption = currentGame?.gameOptions[optionIndex];
+  
+  if (!currentOption?.imgUrl) {
+    return;
+  }
+
   updateGames((prevGames) => {
     const updatedGames = [...prevGames];
     updatedGames[stageIndex] = {
       ...updatedGames[stageIndex],
       gameOptions: updatedGames[stageIndex].gameOptions.map((option, idx) =>
         idx === optionIndex
           ? { ...option, imgUrl: '', fileId: null }
           : option,
       ),
     };
     return updatedGames;
   });
 };
src/components/molecules/TagModal/TagModal.tsx (2)

23-24: 상태 초기화 로직 개선이 필요합니다.

상태 초기화 시 null 체크와 타입 일관성이 부족합니다. 또한 빈 문자열 처리가 불명확합니다.

-const [mainTag, setMainTag] = useState<string | null>(initialMainTag ?? null);
-const [subTag, setSubTag] = useState<string>(initialSubTag ?? '');
+const [mainTag, setMainTag] = useState<string>(() => {
+  return initialMainTag?.trim() || '';
+});
+const [subTag, setSubTag] = useState<string>(() => {
+  return initialSubTag?.trim() || '';
+});

26-31: useEffect 의존성 배열 최적화가 필요합니다.

useEffect의 의존성이 불필요하게 많이 포함되어 있습니다. 또한 상태 초기화 시점을 더 명확하게 제어할 필요가 있습니다.

 useEffect(() => {
-  if (isOpen) {
-    setMainTag(initialMainTag ?? null);
-    setSubTag(initialSubTag ?? '');
-  }
-}, [isOpen, initialMainTag, initialSubTag]);
+  const resetTags = () => {
+    setMainTag(initialMainTag?.trim() || '');
+    setSubTag(initialSubTag?.trim() || '');
+  };
+
+  if (isOpen) {
+    resetTags();
+  }
+}, [isOpen]);
src/constants/message.ts (2)

57-62: 에러 메시지 구조 개선이 필요합니다.

게임 수정 및 삭제 실패에 대한 구체적인 원인을 사용자에게 전달하면 좋을 것 같습니다.

다음과 같이 더 상세한 에러 메시지를 추가하는 것을 제안합니다:

 EDITGAME: {
   FAIL: '게임 수정에 실패했습니다.',
+  INVALID_DATA: '올바르지 않은 게임 데이터입니다.',
+  PERMISSION_DENIED: '게임을 수정할 권한이 없습니다.',
 },
 DELETEGAME: {
   FAIL: '게임 삭제에 실패했습니다.',
+  PERMISSION_DENIED: '게임을 삭제할 권한이 없습니다.',
 },

109-111: 태그 수정 메시지의 위치를 재고려해보세요.

태그 관련 메시지가 향후 늘어날 것을 고려하여, 다른 메시지들처럼 객체로 구조화하는 것이 좋을 것 같습니다.

다음과 같은 구조를 제안합니다:

-  TAG: {
-    EDIT: '태그 수정이 완료되었습니다!',
-  },
+  TAG: {
+    SUCCESS: {
+      EDIT: '태그 수정이 완료되었습니다!',
+      CREATE: '태그 생성이 완료되었습니다!',
+      DELETE: '태그 삭제가 완료되었습니다!',
+    },
+    ERROR: {
+      EDIT: '태그 수정에 실패했습니다.',
+      INVALID: '올바르지 않은 태그입니다.',
+    },
+  },
src/App.tsx (1)

10-10: import 문 정렬이 필요합니다.

새로 추가된 import 문이 알파벳 순서로 정렬되어 있지 않습니다.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 955bc16 and ee4d849.

📒 Files selected for processing (21)
  • src/App.tsx (2 hunks)
  • src/api/game.ts (1 hunks)
  • src/components/molecules/TagModal/TagModal.tsx (2 hunks)
  • src/components/organisms/BalanceGameCreation/BalanceGameCreation.style.ts (1 hunks)
  • src/components/organisms/BalanceGameCreation/BalanceGameCreation.tsx (3 hunks)
  • src/components/organisms/BalanceGameSection/BalanceGameSection.tsx (1 hunks)
  • src/constants/message.ts (2 hunks)
  • src/constants/path.ts (1 hunks)
  • src/hooks/api/game/useDeleteGameSetMutation.ts (1 hunks)
  • src/hooks/api/game/useUpdateGameMutation.ts (1 hunks)
  • src/hooks/game/useBalanceGameCreation.ts (0 hunks)
  • src/hooks/game/useImageHandlers.ts (1 hunks)
  • src/hooks/modal/useModal.ts (1 hunks)
  • src/pages/BalanceGameCreationPage/BalanceGameCreationPage.style.ts (1 hunks)
  • src/pages/BalanceGameCreationPage/BalanceGameCreationPage.tsx (7 hunks)
  • src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx (1 hunks)
  • src/pages/BalanceGamePage/BalanceGamePage.style.ts (1 hunks)
  • src/pages/BalanceGamePage/BalanceGamePage.tsx (1 hunks)
  • src/stories/organisms/BalanceGameCreation.stories.tsx (2 hunks)
  • src/types/game.ts (2 hunks)
  • src/utils/balanceGameUtils.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • src/hooks/game/useBalanceGameCreation.ts
🧰 Additional context used
🪛 eslint
src/components/organisms/BalanceGameCreation/BalanceGameCreation.tsx

[error] 153-153: Expected 'undefined' and instead saw 'void'.

(no-void)


[error] 171-171: Expected 'undefined' and instead saw 'void'.

(no-void)

🔇 Additional comments (17)
src/pages/BalanceGameCreationPage/BalanceGameCreationPage.style.ts (2)

5-5: 네이밍 컨벤션 변경이 적절합니다.

CSS 객체의 경우 camelCase를 사용하는 것이 일반적인 JavaScript/TypeScript 컨벤션에 더 잘 부합합니다. PageContainer에서 pageContainer로의 변경은 적절한 판단입니다.


Line range hint 5-11: 변수명 변경으로 인한 영향을 확인해주세요.

PageContainerpageContainer로 변경함에 따라 이를 임포트하는 다른 파일들의 수정이 필요할 수 있습니다.

다음 스크립트를 실행하여 영향을 받는 파일들을 확인하세요:

src/components/organisms/BalanceGameSection/BalanceGameSection.tsx (1)

23-34: Props 인터페이스가 잘 구현되었습니다!

콜백 함수들이 옵셔널로 잘 정의되어 있으며, 네이밍 컨벤션도 잘 지켜졌습니다.

src/pages/BalanceGamePage/BalanceGamePage.tsx (2)

26-29: useModal 훅의 북마크
useModal 훅을 잘 활용하여 모달 상태를 간소화하고 있습니다. 추가로, 모달 별로 useModal 훅을 재사용할 때 네이밍 충돌이 없도록 주의하면 좋을 것 같습니다.


50-52: 수정 기능 라우팅 처리 확인
수정 버튼 클릭 시 경로를 correct하게 연결하고 있습니다. 다른 곳에서 동일한 라우트를 사용하거나 혹은 잘못된 path 호출이 없는지도 점검 부탁드립니다.

src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx (3)

20-39: gameSet 미존재 시 처리 로직 제안
currentGameSetId로 조회했는데 gameSet이 없을 경우(404, 혹은 null), 화면에 적절한 안내를 띄우거나 에러 처리를 할 필요가 있어 보입니다.


51-52: 이미지 관리 훅의 사용
handleOptionImageChange와 deleteImage 기능 분리가 명확하여 유지보수성이 좋아 보입니다. 다만, 에러 시 어떻게 처리될지 혹은 로딩 상태는 어떻게 표시되는지 확인 부탁드립니다.


125-137: 이미지 변경 시 예외 상황 처리
비정상 파일 형식 혹은 파일 업로드 실패 시 사용자에게 안내가 필요한지 검토해 보세요.

src/pages/BalanceGameCreationPage/BalanceGameCreationPage.tsx (3)

30-32: 초기 스테이지 생성
createInitialGameStages(TOTAL_STAGE)를 람다로 감싸 useState 초기값으로 설정하여 렌더링 시마다 새 객체를 생성하지 않는 점은 좋습니다. 성능 측면에서도 적절해 보입니다.


156-160: 주석 유지보수
현재 주석이 잘 작성되어 있습니다. 향후 로직 변경 시 주석이 코드와 일치하도록 지속 관리 부탁드립니다.


170-179: onImageChange 반환값
onImageChange를 async로 선언하고 있어 반환값을 처리할 여지가 있는데, 실제 호출부에서 await를 사용하는지 확인 필요합니다.

src/hooks/modal/useModal.ts (1)

9-16: 훅 구현이 깔끔합니다!

상태 관리가 효율적으로 구현되어 있고, 불필요한 리렌더링이 없습니다.

src/hooks/api/game/useUpdateGameMutation.ts (1)

6-9: 타입 정의가 명확합니다

UpdateGameParams 인터페이스가 잘 정의되어 있습니다.

src/constants/path.ts (1)

23-25: 밸런스 게임 수정 경로가 적절하게 추가되었습니다!

라우트 패턴과 링크 생성 함수가 기존 코드 스타일을 잘 따르고 있으며, 타입 안전성도 보장되어 있습니다.

src/components/organisms/BalanceGameCreation/BalanceGameCreation.style.ts (1)

41-43: 컨테이너의 상대적 위치 지정이 적절합니다.

태그 편집 버튼의 위치 지정을 위한 기준점으로 잘 활용되고 있습니다.

src/pages/BalanceGamePage/BalanceGamePage.style.ts (1)

1-4: 타이포그래피와 색상 상수 사용이 잘 되어있습니다.

디자인 시스템의 타이포그래피와 색상 상수를 일관성 있게 잘 활용하고 있습니다.

src/constants/message.ts (1)

104-108: 성공 메시지가 일관성 있게 구성되었습니다.

게임 관련 메시지들이 GAME 객체 아래 잘 구조화되어 있습니다.

Comment on lines 179 to 197
const handleBookmarkClick = () => {
if (!game) return;

if (isGuest) {
setLoginModalOpen(true);
return;
}

if (isMyGame) {
showToastModal(ERROR.BOOKMARK.MY_GAME);
return;
}

if (currentGame.myBookmark) {
deleteBookmark();
} else {
createBookmark();
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

북마크 성공 시 사용자 피드백 추가 필요

북마크 기능에 성공 케이스에 대한 피드백이 누락되어 있습니다:

 if (currentGame.myBookmark) {
-  deleteBookmark();
+  deleteBookmark();
+  showToastModal(SUCCESS.BOOKMARK.DELETED);
 } else {
-  createBookmark();
+  createBookmark();
+  showToastModal(SUCCESS.BOOKMARK.CREATED);
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +199 to +203
const myGameItem: MenuItem[] = [
{ label: '수정', onClick: onEdit },
{ label: '삭제', onClick: onDelete },
];
const otherGameItem: MenuItem[] = [{ label: '신고', onClick: onReport }];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

메뉴 아이템 콜백에 에러 처리 추가 필요

현재 메뉴 아이템의 콜백 함수들이 에러 처리 없이 직접 호출되고 있습니다. 다음과 같이 개선이 필요합니다:

 const myGameItem: MenuItem[] = [
-  { label: '수정', onClick: onEdit },
-  { label: '삭제', onClick: onDelete },
+  {
+    label: '수정',
+    onClick: () => {
+      try {
+        onEdit?.();
+      } catch (error) {
+        showToastModal(ERROR.EDIT_FAILED);
+        console.error('게임 수정 중 오류 발생:', error);
+      }
+    }
+  },
+  {
+    label: '삭제',
+    onClick: () => {
+      try {
+        onDelete?.();
+      } catch (error) {
+        showToastModal(ERROR.DELETE_FAILED);
+        console.error('게임 삭제 중 오류 발생:', error);
+      }
+    }
+  },
 ];

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 170 to 171
onImageChange={(file) => {
void onImageChange(currentStage, 1, file);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ESLint: no-void
동일하게, void 사용이 ESLint 룰에 의해 금지되어 있으므로 아래와 같이 수정 제안드립니다.

- void onImageChange(currentStage, 1, file);
+ onImageChange(currentStage, 1, 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
onImageChange={(file) => {
void onImageChange(currentStage, 1, file);
onImageChange={(file) => {
onImageChange(currentStage, 1, file);
🧰 Tools
🪛 eslint

[error] 171-171: Expected 'undefined' and instead saw 'void'.

(no-void)

Comment on lines 152 to 153
onImageChange={(file) => {
void onImageChange(currentStage, 0, file);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ESLint: no-void
void 키워드 대신 undefined 반환을 의도하신 거라면, 간단히 즉시 실행하고 반환값을 사용하지 않도록 수정하시는 게 좋습니다.

- void onImageChange(currentStage, 0, file);
+ onImageChange(currentStage, 0, 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
onImageChange={(file) => {
void onImageChange(currentStage, 0, file);
onImageChange={(file) => {
onImageChange(currentStage, 0, file);
🧰 Tools
🪛 eslint

[error] 153-153: Expected 'undefined' and instead saw 'void'.

(no-void)

Comment on lines +14 to +16
onSuccess: async (_, { gameSetId }) => {
await queryClient.invalidateQueries({ queryKey: ['gameSet', gameSetId] });
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

에러 처리 개선 필요

쿼리 무효화 과정에서 발생할 수 있는 에러에 대한 처리가 필요합니다.

   onSuccess: async (_, { gameSetId }) => {
-      await queryClient.invalidateQueries({ queryKey: ['gameSet', gameSetId] });
+      try {
+        await queryClient.invalidateQueries({ queryKey: ['gameSet', gameSetId] });
+      } catch (error) {
+        console.error('Failed to invalidate queries:', error);
+      }
   },
📝 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
onSuccess: async (_, { gameSetId }) => {
await queryClient.invalidateQueries({ queryKey: ['gameSet', gameSetId] });
},
onSuccess: async (_, { gameSetId }) => {
try {
await queryClient.invalidateQueries({ queryKey: ['gameSet', gameSetId] });
} catch (error) {
console.error('Failed to invalidate queries:', error);
}
},

Comment on lines +70 to +73
onGamesChange: () => {},
onImageChange: async () => true,
onImageDelete: () => {},
games: dummyGames,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

비동기 핸들러 에러 처리 필요

onImageChange 핸들러가 항상 true를 반환하고 있어 실제 에러 상황을 테스트할 수 없습니다.

다음과 같이 개선할 것을 제안합니다:

   args: {
     title: '밸런스 게임 제목',
     onTitleChange: () => {},
     handleCompleteClick: () => {},
     onGamesChange: () => {},
-    onImageChange: async () => true,
+    onImageChange: async (file: File) => {
+      // 파일 크기나 타입에 따라 다른 결과 반환
+      return file.size <= 5000000;
+    },
     onImageDelete: () => {},
     games: dummyGames,
   },

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +54 to +56
export const deleteBySetId = async (gameSetId: Id) => {
return axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

삭제 API의 응답 처리를 명확히 해주세요.

삭제 API의 응답 처리가 불명확합니다. 성공/실패 여부를 명확하게 반환하도록 개선이 필요합니다.

-export const deleteBySetId = async (gameSetId: Id) => {
-  return axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
+export const deleteBySetId = async (gameSetId: Id): Promise<boolean> => {
+  try {
+    await axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
+    return true;
+  } catch (error) {
+    console.error('게임 삭제 중 오류 발생:', error);
+    return false;
+  }
 };
📝 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 deleteBySetId = async (gameSetId: Id) => {
return axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
};
export const deleteBySetId = async (gameSetId: Id): Promise<boolean> => {
try {
await axiosInstance.delete(END_POINT.GAME_SET(gameSetId));
return true;
} catch (error) {
console.error('게임 삭제 중 오류 발생:', error);
return false;
}
};

src/api/game.ts Outdated
Comment on lines 43 to 52
export const updateGameBySetId = async (
gameSetId: Id,
gameData: BalanceGame,
) => {
const { data } = await axiosInstance.put<BalanceGame>(
END_POINT.GAME_SET(gameSetId),
gameData,
);
return data;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 응답 타입과 에러 처리 개선이 필요합니다.

게임 수정 API의 응답 처리가 불완전해 보입니다. 에러 상황에서의 타입 안전성을 보장하기 위해 다음과 같은 개선이 필요합니다.

 export const updateGameBySetId = async (
   gameSetId: Id,
   gameData: BalanceGame,
-) => {
+): Promise<BalanceGame | undefined> => {
+  try {
     const { data } = await axiosInstance.put<BalanceGame>(
       END_POINT.GAME_SET(gameSetId),
       gameData,
     );
     return data;
+  } catch (error) {
+    console.error('게임 수정 중 오류 발생:', error);
+    return undefined;
+  }
 };
📝 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 updateGameBySetId = async (
gameSetId: Id,
gameData: BalanceGame,
) => {
const { data } = await axiosInstance.put<BalanceGame>(
END_POINT.GAME_SET(gameSetId),
gameData,
);
return data;
};
export const updateGameBySetId = async (
gameSetId: Id,
gameData: BalanceGame,
): Promise<BalanceGame | undefined> => {
try {
const { data } = await axiosInstance.put<BalanceGame>(
END_POINT.GAME_SET(gameSetId),
gameData,
);
return data;
} catch (error) {
console.error('게임 수정 중 오류 발생:', error);
return undefined;
}
};

Comment on lines +16 to +30
const handleImageUpload = async (
imageFile: File,
type: FileUploadType,
): Promise<{ imgUrl: string; fileId: number }> => {
const resizedBlob = await resizeImage(imageFile, 577, 359);
const resizedFile = new File([resizedBlob], imageFile.name, {
type: imageFile.type,
});

const formData = new FormData();
formData.append('file', resizedFile);

const response = await uploadImage({ formData, params: type });
return { imgUrl: response.imgUrls[0], fileId: response.fileIds[0] };
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 업로드 처리 로직 개선이 필요합니다.

이미지 업로드 과정에서 파일 크기와 타입 검증이 누락되어 있습니다. 또한 에러 처리가 구체적이지 않습니다.

 const handleImageUpload = async (
   imageFile: File,
   type: FileUploadType,
 ): Promise<{ imgUrl: string; fileId: number }> => {
+  // 파일 크기 검증 (예: 최대 5MB)
+  if (imageFile.size > 5 * 1024 * 1024) {
+    throw new Error('파일 크기는 5MB를 초과할 수 없습니다.');
+  }
+
+  // 이미지 타입 검증
+  if (!imageFile.type.startsWith('image/')) {
+    throw new Error('이미지 파일만 업로드 가능합니다.');
+  }
+
   const resizedBlob = await resizeImage(imageFile, 577, 359);
   const resizedFile = new File([resizedBlob], imageFile.name, {
     type: imageFile.type,
   });

   const formData = new FormData();
   formData.append('file', resizedFile);

   const response = await uploadImage({ formData, params: type });
   return { imgUrl: response.imgUrls[0], fileId: response.fileIds[0] };
 };
📝 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 handleImageUpload = async (
imageFile: File,
type: FileUploadType,
): Promise<{ imgUrl: string; fileId: number }> => {
const resizedBlob = await resizeImage(imageFile, 577, 359);
const resizedFile = new File([resizedBlob], imageFile.name, {
type: imageFile.type,
});
const formData = new FormData();
formData.append('file', resizedFile);
const response = await uploadImage({ formData, params: type });
return { imgUrl: response.imgUrls[0], fileId: response.fileIds[0] };
};
const handleImageUpload = async (
imageFile: File,
type: FileUploadType,
): Promise<{ imgUrl: string; fileId: number }> => {
// 파일 크기 검증 (예: 최대 5MB)
if (imageFile.size > 5 * 1024 * 1024) {
throw new Error('파일 크기는 5MB를 초과할 수 없습니다.');
}
// 이미지 타입 검증
if (!imageFile.type.startsWith('image/')) {
throw new Error('이미지 파일만 업로드 가능합니다.');
}
const resizedBlob = await resizeImage(imageFile, 577, 359);
const resizedFile = new File([resizedBlob], imageFile.name, {
type: imageFile.type,
});
const formData = new FormData();
formData.append('file', resizedFile);
const response = await uploadImage({ formData, params: type });
return { imgUrl: response.imgUrls[0], fileId: response.fileIds[0] };
};

src/App.tsx Outdated
Comment on lines 79 to 82
<Route
path={PATH.BALANCEGAME_EDIT_ROUTE}
element={<BalanceGameEditPage />}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

인증된 사용자만 접근 가능하도록 수정이 필요합니다.

게임 수정 기능은 인증된 사용자만 접근할 수 있어야 합니다. 현재 라우트가 보호되지 않은 영역에 위치해 있습니다.

다음과 같이 보호된 라우트 영역으로 이동하는 것을 제안합니다:

- <Route
-   path={PATH.BALANCEGAME_EDIT_ROUTE}
-   element={<BalanceGameEditPage />}
- />

  <Route element={<ProtectedRoutes isLoggedIn={isLoggedIn} />}>
    <Route path={PATH.MYPAGE} element={<LayoutNoFooter />}>
      <Route index element={<MyPage />} />
    </Route>
    <Route path="/" element={<Layout />}>
      <Route path={PATH.CREATE.TALK_PICK} element={<CreatePostPage />} />
      <Route
        path={PATH.CREATE.GAME}
        element={<BalanceGameCreationPage />}
      />
+     <Route
+       path={PATH.BALANCEGAME_EDIT_ROUTE}
+       element={<BalanceGameEditPage />}
+     />
      <Route
        path={PATH.CHANGE.PROFILE}
        element={<ChangeUserInfoPage />}
      />
    </Route>
  </Route>
📝 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
<Route
path={PATH.BALANCEGAME_EDIT_ROUTE}
element={<BalanceGameEditPage />}
/>
<Route element={<ProtectedRoutes isLoggedIn={isLoggedIn} />}>
<Route path={PATH.MYPAGE} element={<LayoutNoFooter />}>
<Route index element={<MyPage />} />
</Route>
<Route path="/" element={<Layout />}>
<Route path={PATH.CREATE.TALK_PICK} element={<CreatePostPage />} />
<Route
path={PATH.CREATE.GAME}
element={<BalanceGameCreationPage />}
/>
<Route
path={PATH.BALANCEGAME_EDIT_ROUTE}
element={<BalanceGameEditPage />}
/>
<Route
path={PATH.CHANGE.PROFILE}
element={<ChangeUserInfoPage />}
/>
</Route>
</Route>

Copy link
Contributor

@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

♻️ Duplicate comments (1)
src/utils/balanceGameUtils.ts (1)

36-66: ⚠️ Potential issue

데이터 변환 시 유효성 검사 강화 필요

데이터 변환 과정에서 예외 상황에 대한 처리가 부족합니다. 이전 리뷰에서 지적된 사항이 아직 반영되지 않았습니다.

다음과 같이 개선할 것을 제안합니다:

 export const transformGameSetToBalanceGame = (
   gameSet: GameSet,
 ): BalanceGameSet[] => {
+  if (!gameSet?.gameDetailResponses) {
+    throw new Error('유효하지 않은 게임 데이터입니다');
+  }
+
   return gameSet.gameDetailResponses.map(
-    ({ description = '', gameOptions }) => ({
+    ({ description = '', gameOptions }) => {
+      if (!Array.isArray(gameOptions) || gameOptions.length !== 2) {
+        throw new Error('게임 옵션은 정확히 2개여야 합니다');
+      }
+      return {
       description,
       gameOptions: gameOptions.map(
         ({
           id,
           name,
           description: optionDescription,
           optionType,
           imgUrl,
           fileId,
         }) => {
+          if (!id || !name || !optionType) {
+            throw new Error('필수 게임 옵션 필드가 누락되었습니다');
+          }
+          if (!['A', 'B'].includes(optionType)) {
+            throw new Error('옵션 타입은 A 또는 B여야 합니다');
+          }
           return {
             id,
             name,
             description: optionDescription,
             optionType,
-            imgUrl: imgUrl ?? '',
+            imgUrl: imgUrl || '',  // 빈 문자열이나 null 모두 처리
             fileId: fileId ?? null,
           };
         },
       ),
-    }),
+    }),
   );
 };

추가로 다음 사항도 고려해보세요:

  1. try-catch로 전체 변환 과정을 감싸서 에러 처리를 일관되게 관리
  2. 변환된 데이터의 형식을 검증하는 별도의 유틸리티 함수 작성
  3. 에러 메시지를 상수로 분리하여 관리
🧹 Nitpick comments (1)
src/utils/balanceGameUtils.ts (1)

36-40: JSDoc 문서 개선 필요

현재 JSDoc에 중요한 정보가 누락되어 있습니다.

다음과 같이 개선할 것을 제안합니다:

 /**
  * GameSet 데이터를 BalanceGameSet 형식으로 변환하는 함수
  * @param gameSet GameSet API에서 받은 원본 데이터
  * @returns BalanceGameSet[]로 가공된 데이터
+ * @throws {Error} gameSet이 유효하지 않은 경우
+ * @throws {Error} gameOptions가 2개가 아닌 경우
+ * @throws {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 ee4d849 and 58211a0.

📒 Files selected for processing (1)
  • src/utils/balanceGameUtils.ts (2 hunks)
🔇 Additional comments (1)
src/utils/balanceGameUtils.ts (1)

1-1: LGTM! 타입 임포트가 적절히 추가되었습니다.

GameSet 타입이 새로운 변환 함수에 필요하여 적절하게 추가되었습니다.

Copy link
Collaborator

@areumH areumH left a comment

Choose a reason for hiding this comment

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

밸런스 게임 수정 로직 구현 수고 많으셨어요!!! 밸런스 게임 페이지로 이동되는 navigate 함수 경로들 상수로 처리 부탁드립니다!! 코멘트도 확인 부탁드려용🙌

Comment on lines 22 to 25
BALANCEGAME: (setId: number | string = ':setId') => `balancegame/${setId}`,
BALANCEGAME_EDIT_ROUTE: 'balancegame/edit/:gameSetId',
BALANCEGAME_EDIT_LINK: (gameSetId: number | string) =>
`balancegame/edit/${gameSetId}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

위의 BALANCEGAME처럼 한번에 선언이 가능한데 두 가지로 분리하신 이유가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

경로 변경이나 유지보수를 할 때 좀 더 용이하게 관리하기 위해서 해당 방식을 시도해보았는데, 크게 복잡한 정도의 라우팅 처리는 아니기 때문에 BALANCEGAME 관련 라우팅은

Suggested change
BALANCEGAME: (setId: number | string = ':setId') => `balancegame/${setId}`,
BALANCEGAME_EDIT_ROUTE: 'balancegame/edit/:gameSetId',
BALANCEGAME_EDIT_LINK: (gameSetId: number | string) =>
`balancegame/edit/${gameSetId}`,
export const BALANCEGAME = {
EDIT: 'balancegame/edit/:gameSetId',
CREATE: 'balancegame/create',
};

이런 식으로 그룹핑하는게 현재 규모에서는 더 적합해 보이네요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

추가로, 정적 값과 동적 값을 동시에 사용한 이유는
동적 값: 실제 navigate 로직에서 사용, 정적 값: App.tsx에서 라우팅 설정을 선언하기 위함 이었습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

c0a54ef 에서 라우팅 상수 및 라우트 설정, efbfa97 에서 네비게이션 상수 처리를 하였습니다!

src/api/game.ts Outdated
Comment on lines 43 to 46
export const updateGameBySetId = async (
gameSetId: Id,
gameData: BalanceGame,
) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

api 함수의 경우 함수명을 서버로의 요청인 post / put / delete로 시작하도록 통일하면 좋을 것 같아요!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 함수를 사용하는 쿼리 훅도 useEdit~로 통일 부탁드려요!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

사실 네이밍의 경우에는 비즈니스 로직의 역할에 충실하는게 더 맞다고는 생각이 드는데요(후속되는 get post put 은 함수 내부에서 확인할 수도 있기 때문), 현재 대부분의 호출 함수들이 말씀해주신 방식으로 되어있다보니, 요청하신 방식으로 수정하도록 하겠습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

1595b75178f4ed 에서 처리했습니다!
useEditGameMutation의 경우 이미 존재해서 useEditGamesMutation 으로 명명했습니다!

Comment on lines 25 to 26

const TOTAL_STAGE = 10;
Copy link
Collaborator

Choose a reason for hiding this comment

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

상수 값은 contants/game.ts에 정의하여 사용하는게 어떨까요?!?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

해당 페이지에서만 사용하기 때문에 이러한 처리를 했는데, 확장성을 위해 969b9ca 에 처리하였습니다!

src/App.tsx Outdated
Comment on lines 105 to 108
<Route
path={PATH.BALANCEGAME_EDIT_ROUTE}
element={<BalanceGameEditPage />}
/>
Copy link
Collaborator

Choose a reason for hiding this comment

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

로그인 상태일 때 해당 페이지에 접근할 수 있도록 넣어두셨는데 혹시 본인이 작성한 게 아닌 게임의 수정 페이지에도 접근이 가능한 상태라면 수정이 필요해보입니다!!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

url로도 해당 페이지에 접근 가능하다는 사실을 간과 하고 있었네요!
ce3fe30에서 해결했습니다!

Copy link
Contributor

@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: 3

🧹 Nitpick comments (12)
src/pages/BalanceGamePage/BalanceGamePage.tsx (5)

1-20: 타입 안전성 개선을 위한 제안

컴포넌트의 props 타입들을 명시적으로 import하면 타입 안전성이 향상될 것 같습니다.

다음과 같이 타입을 추가하는 것을 고려해보세요:

+import type { BalanceGameSectionProps } from '@/components/organisms/BalanceGameSection/BalanceGameSection';
+import type { BalanceGameEndingSectionProps } from '@/components/organisms/BalanceGameEndingSection/BalanceGameEndingSection';

22-41: 에러 처리 강화 제안

현재 구현은 잘 되어있지만, 에러 바운더리를 추가하면 더 안정적인 에러 처리가 가능할 것 같습니다.

다음과 같은 에러 바운더리 구현을 고려해보세요:

class BalanceGameErrorBoundary extends React.Component {
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // 에러 로깅 및 처리
  }
  
  render() {
    return this.props.children;
  }
}

42-49: 매직 넘버 상수화 제안

게임 스테이지 관련 매직 넘버를 상수로 분리하면 유지보수성이 향상될 것 같습니다.

다음과 같이 상수를 추가하는 것을 추천드립니다:

+const GAME_CONSTANTS = {
+  MIN_STAGE: 0,
+  MAX_STAGE: 10,
+} as const;

const handleNextGame = () => {
-  setCurrentStage((stage) => (stage < 10 ? stage + 1 : stage));
+  setCurrentStage((stage) => (stage < GAME_CONSTANTS.MAX_STAGE ? stage + 1 : stage));
};

54-76: 삭제 중 로딩 상태 처리 제안

삭제 작업 중에 사용자에게 로딩 상태를 표시하면 더 나은 사용자 경험을 제공할 수 있을 것 같습니다.

다음과 같이 로딩 상태를 추가하는 것을 고려해보세요:

+const [isDeleting, setIsDeleting] = useState(false);

const handleDeleteClick = () => {
  setModalProps({
    text: '정말 삭제하시겠습니까?',
    onConfirm: () => {
+      setIsDeleting(true);
      deleteGameSet(
        { gameSetId },
        {
          onSuccess: () => {
+            setIsDeleting(false);
            closeModal();
            showToastModal(SUCCESS.GAME.DELETE);
            navigate('/');
          },
          onError: () => {
+            setIsDeleting(false);
            closeModal();
            showToastModal(ERROR.DELETEGAME.FAIL);
          },
        },
      );
    },
    onClose: closeModal,
  });
  openModal();
};

89-129: 성능 최적화를 위한 메모이제이션 제안

컴포넌트 리렌더링을 최적화하기 위해 메모이제이션을 적용하면 좋을 것 같습니다.

다음과 같이 메모이제이션을 적용하는 것을 고려해보세요:

+const MemoizedBalanceGameSection = React.memo(BalanceGameSection);
+const MemoizedBalanceGameEndingSection = React.memo(BalanceGameEndingSection);

return (
  <div css={S.pageStyle}>
    {currentStage === 10 ? (
-      <BalanceGameEndingSection
+      <MemoizedBalanceGameEndingSection
        title={gameSet?.title ?? ''}
        gameSetId={gameSetId}
        isMyGame={isMyGame}
        isMyEndBookmark={gameSet?.isEndBookmarked ?? false}
      />
    ) : (
-      <BalanceGameSection
+      <MemoizedBalanceGameSection
        gameSetId={gameSetId}
        game={gameSet}
        isMyGame={isMyGame}
        currentStage={currentStage}
        setCurrentStage={setCurrentStage}
        handleNextGame={handleNextGame}
        handlePrevGame={handlePrevGame}
        onEdit={handleEditClick}
        onDelete={handleDeleteClick}
        onReport={handleReportClick}
      />
    )}
  </div>
);
src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx (1)

54-61: [useEffect 내부에서 초기 값 설정 주의]
gameSet 값이 존재할 때 setTitle, setGames, setMainTag, setSubTag 등을 호출하는 것은 적절합니다. 다만, API 응답이 지연되어 늦게 세팅되는 경우 UI 깜빡임이 발생할 수 있으므로, 로딩 상태(isLoading 등)를 별도로 관리하면 사용자 경험 개선에 도움이 될 수 있습니다.

src/components/organisms/BalanceGameCreation/BalanceGameCreation.tsx (2)

72-90: [validateStage 함수 검증 로직 적절]
게임 옵션의 이름과 이미지를 검사해 유효성을 체크하는 방식이 명확해 보입니다. 다만, 확장될 수 있는 검증 항목(예: 문자열 길이, 특정 규칙 등)에 대비해 조건을 추가하기 쉽게 재사용 가능한 헬퍼 함수로 분리해두면 유지보수에 유리할 듯합니다.


127-146: [태그 수정 버튼 표시 조건 간소화 가능]
handleTagEditClick가 존재할 때만 버튼을 렌더링하는 로직은 올바릅니다. 필요하다면 props 단에 기본 함수를 제공하여 조건부 분기를 제거할 수도 있습니다. 현재 구현 그대로도 충분히 명확하므로 선택 사항으로 참고하시기 바랍니다.

src/pages/BalanceGameCreationPage/BalanceGameCreationPage.tsx (4)

50-53: 타입 정의를 더 명확하게 개선해보세요.

popupData의 타입을 별도의 인터페이스로 분리하여 재사용성을 높이고 의도를 더 명확하게 표현할 수 있습니다.

+interface ImageDeletePopupData {
+  stageIndex: number;
+  optionIndex: number;
+}

-const [popupData, setPopupData] = useState<{
-  stageIndex: number;
-  optionIndex: number;
-} | null>(null);
+const [popupData, setPopupData] = useState<ImageDeletePopupData | null>(null);

80-88: 에러 처리를 개선해보세요.

이미지 삭제 과정에서 발생할 수 있는 예외 상황에 대한 처리가 필요합니다.

 const handleConfirmDeleteImage = () => {
   if (popupData) {
-    deleteImage(popupData.stageIndex, popupData.optionIndex, (updater) => {
-      setGames(updater);
-    });
-    showToastModal(SUCCESS.IMAGE.DELETE);
+    try {
+      deleteImage(popupData.stageIndex, popupData.optionIndex, (updater) => {
+        setGames(updater);
+      });
+      showToastModal(SUCCESS.IMAGE.DELETE);
+    } catch (error) {
+      showToastModal(ERROR.IMAGE.DELETE);
+    }
   }
   setPopupData(null);
   closeTextModal();
 };

Line range hint 114-122: 에러 타입을 구체화하여 처리해보세요.

에러 타입에 따라 다른 메시지를 표시하면 사용자에게 더 명확한 피드백을 제공할 수 있습니다.

 try {
   const gameId = await handleCreateBalanceGame(gameData);
   showToastModal(SUCCESS.GAME.CREATE);
   navigate(`/balancegame/${gameId}`);
 } catch (error) {
-  showToastModal(ERROR.CREATEGAME.FAIL);
+  if (error instanceof NetworkError) {
+    showToastModal(ERROR.NETWORK);
+  } else if (error instanceof ValidationError) {
+    showToastModal(ERROR.VALIDATION);
+  } else {
+    showToastModal(ERROR.CREATEGAME.FAIL);
+  }
 }

155-162: JSDoc 문서를 더 상세하게 작성해보세요.

매개변수와 반환값에 대한 설명을 추가하면 함수의 사용 방법을 더 명확하게 이해할 수 있습니다.

 /**
  * 상태 변경 요청을 처리하는 콜백
  * 자식이 상태를 갱신하고자 할 때 이 함수를 호출하면
  * 부모에서 setGames를 통해 상태를 갱신
+ * @param updatedGames - 갱신할 게임 상태 배열
+ * @returns void
  */
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 58211a0 and efbfa97.

📒 Files selected for processing (10)
  • src/App.tsx (3 hunks)
  • src/api/game.ts (1 hunks)
  • src/components/organisms/BalanceGameCreation/BalanceGameCreation.tsx (3 hunks)
  • src/components/organisms/BalanceGameList/BalanceGameList.tsx (2 hunks)
  • src/constants/game.ts (1 hunks)
  • src/constants/path.ts (1 hunks)
  • src/hooks/api/game/useEditGamesMutation.ts (1 hunks)
  • src/pages/BalanceGameCreationPage/BalanceGameCreationPage.tsx (7 hunks)
  • src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx (1 hunks)
  • src/pages/BalanceGamePage/BalanceGamePage.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/App.tsx
  • src/constants/path.ts
  • src/api/game.ts
🔇 Additional comments (8)
src/components/organisms/BalanceGameList/BalanceGameList.tsx (2)

11-11: PATH 상수 도입으로 라우팅 유지보수성 향상
PATH 상수를 통해 경로를 일관성 있게 관리하므로, 추후 라우트 변경 시 수정 범위가 최소화되어 유지보수에 유리합니다.


40-40: PATH 상수 활용을 통한 동적 경로 구성
기존에 문자열로 직접 경로를 작성하던 방식을 PATH 상수로 대체하여, 경로 정의를 모듈화하고 중복을 줄일 수 있습니다.

src/constants/game.ts (1)

6-6: [TOTAL_STAGE 상수 선언]
새로운 상수를 추가하여 반복되는 값을 제거한 점이 좋습니다. 다만, 추후 게임 단계가 동적으로 변동될 필요가 있다면 환경 변수나 설정 파일에서 관리하는 것도 고려해보세요.

src/hooks/api/game/useEditGamesMutation.ts (2)

6-9: [UpdateGameParams 인터페이스 잘 정의됨]
UpdateGameParams 인터페이스에서 gameSetIddata를 명확히 분리해 타입 안정성을 높였습니다. 현재 구조로는 충분히 가독성이 좋으며, 확장성도 우수해 보입니다.


11-20: [useEditGamesMutation 훅 설계 간단명료]
react-query를 활용해 수정 로직을 훅으로 잘 추상화하셨습니다. onSuccessinvalidateQueries를 통해 최신 데이터를 보장하는 부분도 깔끔합니다. 에러 상황에 대한 추가 처리(예: 실패 토스트 노출 등) 역시 별도의 콜백으로 필요에 따라 확장할 수 있어 보입니다.

src/pages/BalanceGameEditPage/BalanceGameEditPage.tsx (1)

71-91: [handleSaveEdit: 게임 수정 로직 효율적]
mutate 호출 시 onSuccess, onError 콜백을 명시적으로 전달하여, 성공/실패 시나리오를 각각 처리하고 있습니다. 이 패턴은 추적이 용이하고 코드 가독성도 높여주므로 바람직합니다.

src/components/organisms/BalanceGameCreation/BalanceGameCreation.tsx (1)

152-154: [onImageChange 비동기 처리 확인]
현재 onImageChangePromise<boolean>을 반환하므로, 호출부에서 비동기 완료 후 추가적인 로직(에러 처리 등)을 진행할 수 있습니다. 이 반환값에 대한 후속 처리가 필요하다면, await onImageChange(...) 패턴을 사용하는 등 명시적으로 활용하길 권장합니다.

src/pages/BalanceGameCreationPage/BalanceGameCreationPage.tsx (1)

21-31: 코드 모듈화가 잘 되어있습니다! 👍

커스텀 훅과 상수를 활용하여 코드를 모듈화하고 재사용성을 높였습니다.

Comment on lines +78 to +87
const handleReportClick = () => {
setModalProps({
text: '정말 신고하시겠습니까?',
onConfirm: () => {
closeModal();
},
onClose: closeModal,
});
openModal();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

밸런스 게임 신고 API 구현 누락

현재 코드베이스에는 댓글 신고 기능(postCommentReport)만 구현되어 있고, 밸런스 게임 신고를 위한 API가 구현되어 있지 않습니다. 다음 사항들이 필요합니다:

  • src/api/report.ts에 밸런스 게임 신고를 위한 API 엔드포인트 구현
  • 해당 API를 사용하는 mutation hook 구현 (useGameReportMutation 또는 useBalanceReportMutation)
  • BalanceGamePage.tsxhandleReportClick 함수에 실제 신고 로직 연동
🔗 Analysis chain

신고 API 구현 필요

신고 기능이 아직 완전히 구현되지 않은 것 같습니다. PR 목표에 언급된 대로 신고 API를 통합해야 합니다.

신고 API 구현이 필요합니다. 다음 스크립트로 현재 구현 상태를 확인할 수 있습니다:


Let me check the report API implementation further.


Let me check one final thing about the game report API implementation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for report API implementation

# Search for report API related files
echo "Searching for report API implementation..."
rg -g "*.ts" -g "*.tsx" "report.*api|api.*report"

# Search for report mutation hooks
echo "Searching for report mutation hooks..."
rg -g "*.ts" -g "*.tsx" "useReport.*Mutation"

Length of output: 829


Script:

#!/bin/bash
# Check the implementation details of report API and mutations

# Check report API implementation
echo "Checking report API implementation..."
rg -g "*.ts" -g "*.tsx" "postCommentReport" -A 5

# Check if there's any game report API
echo "Checking game report API..."
fd "api" --type f --exec rg "game.*report|report.*game"

# Check the actual implementation of useReportCommentMutation
echo "Checking report mutation implementation..."
cat src/hooks/api/report/useReportCommentMutation.ts

Length of output: 3116


Script:

#!/bin/bash
# Check for game report API implementation
echo "Checking game report API implementation..."
cat src/api/report.ts

# Check for any other report-related mutations
echo "Checking for game report mutations..."
fd "hooks" --type f --exec rg "useGameReport|useBalanceReport"

Length of output: 713

Comment on lines +97 to +111
const handleImageDelete = (stageIndex: number, optionIndex: number) => {
setPopupData({ stageIndex, optionIndex });
openTextModal();
};

const handleConfirmDeleteImage = () => {
if (popupData) {
deleteImage(popupData.stageIndex, popupData.optionIndex, (updater) => {
setGames(updater);
});
showToastModal(SUCCESS.IMAGE.DELETE);
}
setPopupData(null);
closeTextModal();
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

[이미지 삭제 시 예외 처리 보완 권장]
deleteImage 후 결과 핸들링은 정상적으로 보이지만, 삭제 요청이 실패할 경우(네트워크 오류 등)에 대한 예외 처리가 보완되면 좋겠습니다. 작성된 showToastModal로 상황을 알리거나 재시도 메커니즘을 추가해 사용자 혼선을 줄일 수 있습니다.

Comment on lines +169 to +177
const handleImageChange = async (
stageIndex: number,
optionIndex: number,
file: File,
) => {
return onImageChange(stageIndex, optionIndex, file, (updater) => {
setGames(updater);
});
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

파일 유효성 검사를 추가해보세요.

이미지 파일의 크기, 형식 등을 검증하여 잘못된 파일 업로드를 방지할 수 있습니다.

 const handleImageChange = async (
   stageIndex: number,
   optionIndex: number,
   file: File,
 ) => {
+  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+  const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
+
+  if (file.size > MAX_FILE_SIZE) {
+    showToastModal(ERROR.IMAGE.SIZE_EXCEED);
+    return;
+  }
+
+  if (!ALLOWED_TYPES.includes(file.type)) {
+    showToastModal(ERROR.IMAGE.INVALID_TYPE);
+    return;
+  }
+
   return onImageChange(stageIndex, optionIndex, file, (updater) => {
     setGames(updater);
   });
 };

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/components/organisms/BalanceGameSection/BalanceGameSection.tsx (1)

137-141: ⚠️ Potential issue

메뉴 아이템 콜백에 에러 처리가 필요합니다.

수정 및 삭제 작업 중 발생할 수 있는 오류에 대한 처리가 누락되어 있습니다.

이전 리뷰 피드백과 동일하게 다음과 같이 에러 처리를 추가해주세요:

 const myGameItem: MenuItem[] = [
-  { label: '수정', onClick: onEdit },
-  { label: '삭제', onClick: onDelete },
+  {
+    label: '수정',
+    onClick: () => {
+      try {
+        onEdit?.();
+      } catch (error) {
+        showToastModal(ERROR.EDIT_FAILED);
+        console.error('게임 수정 중 오류 발생:', error);
+      }
+    }
+  },
+  {
+    label: '삭제',
+    onClick: () => {
+      try {
+        onDelete?.();
+      } catch (error) {
+        showToastModal(ERROR.DELETE_FAILED);
+        console.error('게임 삭제 중 오류 발생:', error);
+      }
+    }
+  },
 ];
🧹 Nitpick comments (2)
src/pages/BalanceGamePage/BalanceGamePage.tsx (1)

80-80: 불필요한 공백을 제거해주세요.

줄 끝의 불필요한 공백을 제거해주세요.

🧰 Tools
🪛 eslint

[error] 80-80: Delete ····

(prettier/prettier)

src/components/organisms/BalanceGameSection/BalanceGameSection.tsx (1)

1-22: 사용하지 않는 import 구문을 정리해주세요.

다음 import들이 사용되지 않고 있습니다:

  • ERROR from '@/constants/message'
  • useCreateGameBookmarkMutation
  • useDeleteGameBookmarkMutation
  • MyVoteOption
  • VoteOption
-import { ERROR, SUCCESS } from '@/constants/message';
+import { SUCCESS } from '@/constants/message';
-import { useCreateGameBookmarkMutation } from '@/hooks/api/bookmark/useCreateGameBookmarkMutation';
-import { useDeleteGameBookmarkMutation } from '@/hooks/api/bookmark/useDeleteGameBookmarkMutation';
-import { MyVoteOption, VoteOption, VoteRecord } from '@/types/vote';
+import { VoteRecord } from '@/types/vote';
🧰 Tools
🪛 eslint

[error] 3-3: 'ERROR' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 17-17: 'useCreateGameBookmarkMutation' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 18-18: 'useDeleteGameBookmarkMutation' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 21-21: 'MyVoteOption' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 21-21: 'VoteOption' is defined but never used.

(@typescript-eslint/no-unused-vars)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between efbfa97 and 10eec60.

📒 Files selected for processing (7)
  • src/App.tsx (3 hunks)
  • src/api/game.ts (1 hunks)
  • src/components/organisms/BalanceGameList/BalanceGameList.tsx (2 hunks)
  • src/components/organisms/BalanceGameSection/BalanceGameSection.tsx (1 hunks)
  • src/constants/message.ts (2 hunks)
  • src/pages/BalanceGamePage/BalanceGamePage.tsx (1 hunks)
  • src/types/game.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/organisms/BalanceGameList/BalanceGameList.tsx
  • src/types/game.ts
  • src/api/game.ts
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/organisms/BalanceGameSection/BalanceGameSection.tsx

[error] 68-71: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)

🪛 eslint
src/components/organisms/BalanceGameSection/BalanceGameSection.tsx

[error] 3-3: 'ERROR' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 17-17: 'useCreateGameBookmarkMutation' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 18-18: 'useDeleteGameBookmarkMutation' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 21-21: 'MyVoteOption' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 21-21: 'VoteOption' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 66-66: Delete ····

(prettier/prettier)


[error] 68-71: Unexpected empty object pattern.

(no-empty-pattern)


[error] 68-71: Delete ·⏎··⏎··⏎··

(prettier/prettier)


[error] 127-127: Delete ··

(prettier/prettier)


[error] 246-246: Delete ·

(prettier/prettier)

src/pages/BalanceGamePage/BalanceGamePage.tsx

[error] 80-80: Delete ····

(prettier/prettier)

🔇 Additional comments (6)
src/constants/message.ts (2)

59-64: 메시지 상수가 잘 정의되어 있습니다!

게임 수정 및 삭제 관련 에러 메시지가 기존 상수들과 일관된 패턴으로 잘 정의되어 있습니다.


106-114: 성공 메시지가 잘 구조화되어 있습니다!

게임 및 태그 관련 성공 메시지가 체계적으로 구조화되어 있습니다.

src/pages/BalanceGamePage/BalanceGamePage.tsx (2)

70-79: 신고 기능 구현이 필요합니다.

신고 기능이 아직 완전히 구현되지 않았습니다. 모달만 표시되고 실제 신고 처리가 이루어지지 않고 있습니다.


40-68: 삭제 기능이 잘 구현되어 있습니다!

게임 삭제 기능이 다음과 같이 잘 구현되어 있습니다:

  • 삭제 확인 모달 표시
  • 성공/실패 처리
  • 적절한 에러 핸들링
  • 삭제 후 홈 페이지로 이동
src/App.tsx (1)

111-114: 라우팅이 올바르게 구현되어 있습니다!

밸런스 게임 수정 페이지가 ProtectedRoutes 내부에 위치하여 인증된 사용자만 접근 가능하도록 잘 구현되어 있습니다.

src/components/organisms/BalanceGameSection/BalanceGameSection.tsx (1)

24-34: 인터페이스 변경이 잘 구현되었습니다!

수정, 삭제, 신고 기능을 위한 콜백 props가 적절하게 추가되었습니다.

Comment on lines 68 to 77
const {


} = useGuestGameVote(
guestVotedList,
setGuestVotedList,
gameSetId,
currentStage,
game,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

useGuestGameVote 훅의 반환값이 사용되지 않고 있습니다.

훅을 호출하고 있지만 반환값을 구조분해할당하지 않고 있습니다. 이는 잠재적인 버그가 될 수 있습니다.

다음과 같이 필요한 값들을 추가해주세요:

- const { 
-  
-  
-  } = useGuestGameVote(
+ const { handleGuestGameVote } = useGuestGameVote(
    guestVotedList,
    setGuestVotedList,
    gameSetId,
    currentStage,
    game,
  );

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (1.9.4)

[error] 68-71: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)

🪛 eslint

[error] 68-71: Unexpected empty object pattern.

(no-empty-pattern)


[error] 68-71: Delete ·⏎··⏎··⏎··

(prettier/prettier)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/pages/BalanceGamePage/BalanceGamePage.tsx (1)

70-79: ⚠️ Potential issue

밸런스 게임 신고 API 구현 필요

신고 기능이 아직 구현되지 않았습니다. 이전 리뷰에서 지적된 사항이 아직 해결되지 않았습니다.

신고 기능 구현을 위해 다음 작업이 필요합니다:

  1. 신고 API 엔드포인트 구현
  2. useGameReportMutation 훅 구현
  3. handleReportClick 함수에 실제 신고 로직 연동
🧹 Nitpick comments (3)
src/pages/BalanceGamePage/BalanceGamePage.tsx (3)

1-20: 타입 안전성 향상을 위한 제안

컴포넌트의 props 타입과 API 응답 타입을 명시적으로 import하면 타입 안전성이 향상될 것 같습니다. 다음과 같은 타입들의 import를 추가하는 것을 고려해보세요:

  • GameSet 타입
  • Member 타입
  • DeleteGameSetMutationResponse 타입

81-83: 매직 넘버 상수화 필요

스테이지 관련 매직 넘버(0, 10)를 의미 있는 상수로 추출하면 코드의 가독성과 유지보수성이 향상될 것 같습니다.

+const MIN_STAGE = 0;
+const MAX_STAGE = 10;

const changeStage = (step: number) => {
-  setCurrentStage((stage) => Math.min(10, Math.max(0, stage + step)));
+  setCurrentStage((stage) => Math.min(MAX_STAGE, Math.max(MIN_STAGE, stage + step)));
};

85-124: 에러 바운더리 추가 권장

컴포넌트에서 발생할 수 있는 예외 상황(데이터 로딩 실패, 게임 세트 없음 등)을 처리하기 위한 에러 바운더리 추가를 권장드립니다.

class BalanceGameErrorBoundary extends React.Component {
  // 에러 바운더리 구현
  // 게임 로딩 실패, 존재하지 않는 게임 등의 상황 처리
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 10eec60 and c5bedf4.

📒 Files selected for processing (2)
  • src/components/organisms/BalanceGameSection/BalanceGameSection.tsx (1 hunks)
  • src/pages/BalanceGamePage/BalanceGamePage.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/organisms/BalanceGameSection/BalanceGameSection.tsx

Comment on lines +22 to +40
const BalanceGamePage = () => {
const { setId } = useParams<{ setId: string }>();
const gameSetId = Number(setId);
const navigate = useNavigate();
const { isOpen, openModal, closeModal } = useModal();
const [modalProps, setModalProps] = useState<Omit<TextModalProps, 'isOpen'>>({
text: '',
});

const { gameSet } = useGameBySetId(gameSetId);
const [currentStage, setCurrentStage] = useState<number>(0);

const accessToken = useNewSelector(selectAccessToken);
const { member } = useMemberQuery(useParseJwt(accessToken).memberId);
const { isVisible, modalText, showToastModal } = useToastModal();

const isMyGame: boolean = member?.nickname === gameSet?.member;

const { mutate: deleteGameSet } = useDeleteGameSetMutation();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

게임 세트 ID 유효성 검사 추가 필요

setId가 유효하지 않은 경우(NaN)에 대한 처리가 필요합니다. 사용자가 잘못된 URL을 입력했을 때 적절한 에러 처리나 리다이렉션이 이루어져야 합니다.

const gameSetId = Number(setId);
+if (isNaN(gameSetId)) {
+  navigate('/404');
+  return 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
const BalanceGamePage = () => {
const { setId } = useParams<{ setId: string }>();
const gameSetId = Number(setId);
const navigate = useNavigate();
const { isOpen, openModal, closeModal } = useModal();
const [modalProps, setModalProps] = useState<Omit<TextModalProps, 'isOpen'>>({
text: '',
});
const { gameSet } = useGameBySetId(gameSetId);
const [currentStage, setCurrentStage] = useState<number>(0);
const accessToken = useNewSelector(selectAccessToken);
const { member } = useMemberQuery(useParseJwt(accessToken).memberId);
const { isVisible, modalText, showToastModal } = useToastModal();
const isMyGame: boolean = member?.nickname === gameSet?.member;
const { mutate: deleteGameSet } = useDeleteGameSetMutation();
const BalanceGamePage = () => {
const { setId } = useParams<{ setId: string }>();
const gameSetId = Number(setId);
if (isNaN(gameSetId)) {
navigate('/404');
return null;
}
const navigate = useNavigate();
const { isOpen, openModal, closeModal } = useModal();
const [modalProps, setModalProps] = useState<Omit<TextModalProps, 'isOpen'>>({
text: '',
});
const { gameSet } = useGameBySetId(gameSetId);
const [currentStage, setCurrentStage] = useState<number>(0);
const accessToken = useNewSelector(selectAccessToken);
const { member } = useMemberQuery(useParseJwt(accessToken).memberId);
const { isVisible, modalText, showToastModal } = useToastModal();
const isMyGame: boolean = member?.nickname === gameSet?.member;
const { mutate: deleteGameSet } = useDeleteGameSetMutation();

Copy link
Collaborator

@areumH areumH left a comment

Choose a reason for hiding this comment

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

수정까지 고생 많으셨습니다!! 🙌

Copy link
Collaborator

@alwubin alwubin left a comment

Choose a reason for hiding this comment

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

이미 아름님께서 피드백을 한참 주신뒤였군요.. 상수 수정만 작성해두었어요..! 구현하시느라 수고 많으셨습니다👏

@@ -19,7 +19,11 @@ export const PATH = {
},
TODAY_TALKPICK: 'todaytalkpick',
TALKPICK_PLACE: 'talkpickplace',
BALANCEGAME: (setId: number | string = ':setId') => `balancegame/${setId}`,
BALANCEGAME: {
VIEW: (setId: number | string = ':setId') => `balancegame/${setId}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분 머지되면 저도 수정해야겠네용!!

navigate(`/balancegame/${gameId}`);
});
showToastModal(SUCCESS.GAME.CREATE);
navigate(`/balancegame/${gameId}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분도 BALANCEGAME.VIEW(gameId)로 수정하셔도 될 거 같습니당

{
onSuccess: () => {
showToastModal(SUCCESS.GAME.EDIT);
navigate(`/balancegame/${currentGameSetId}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

위와 동일하게 처리하면 어떨까용

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Todo
Development

Successfully merging this pull request may close these issues.

밸런스게임 수정 api 연동
3 participants