Skip to content

Conversation

@kimnamheeee
Copy link
Contributor

@kimnamheeee kimnamheeee commented Aug 14, 2025

📝 설명

자잘한 버그를 수정했습니다.

🛠️ 주요 변경 사항

  • 공유 상태에 따라 표시되는 아이콘 구분 (공유, 공유취소) 5ad8e28
  • 홈 화면 네비게이션에 useAuth 상태 연동 25c6998
  • searchParams 폴백처리 811367f

리뷰 시 고려해야 할 사항

  • ci관련 커밋은 rebase로 drop함

Summary by CodeRabbit

  • New Features
    • 카카오 로그인 플로우 추가: 로그인 페이지/콜백 페이지 도입, 홈/게임 목록에서 로그인 버튼이 카카오 로그인으로 이동.
    • 아바타 드롭다운 추가: 홈·대시보드에서 아바타 클릭 시 로그아웃 버튼 표시 및 로그아웃 동작.
    • 게임 카드 공유 메뉴 개선: 공유/공유취소 토글 아이콘과 라벨 표시.
    • 게임 생성 페이지 로딩 상태 추가(Suspense).
    • 기존 게임 수정 저장 지원: 저장 버튼이 상황에 따라 “게임 저장”/“게임 수정”으로 표시, 저장 후 대시보드로 이동.
    • 홈 화면에서 실시간 인증 상태 반영 및 내 게임 이동 경로 개선.

@coderabbitai
Copy link

coderabbitai bot commented Aug 14, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

게임 업데이트 워크플로우가 추가·분기되었습니다(신규/기존 게임 저장 분리). 게임 생성 상태에 버전 및 상세 초기화 로직이 도입되었고, create 페이지는 Suspense로 감쌌습니다. 카카오 로그인 페이지/콜백이 추가되고, 헤더/대시보드/홈 내 인증 UI가 갱신되었습니다. 모델/모킹/테스트는 이메일 필드 제거에 맞춰 정리되었습니다.

Changes

Cohort / File(s) Change Summary
Game 업데이트 플로우 확장
service/app/src/entities/game/api/updateGame.ts, service/app/src/entities/game/utils/gameSave.ts, service/app/src/entities/game/ui/interactions/saveButton.tsx
기존 이미지 업로드 포함 업데이트와 별도로, 신규 이미지 없는 PUT 전용 updateGameWithoutNewImages 추가. 저장 버튼이 gameId 유무로 생성/수정 분기하고, 수정 시 버전 포함 업데이트 실행. 유틸에 prepareUpdateGameData/updateExistingGame 추가.
게임 생성 상태 관리 확장
.../model/state/create/actions.ts, .../reducer.ts, .../state.ts, .../useGameCreation.ts, .../gameCreationContext.tsx, service/app/src/app/create/page.tsx
INITIALIZE_FROM_GAME_DETAIL/SET_GAME_VERSION 액션과 디스패처 추가, state에 gameVersion? 도입. Provider가 gameId로 상세 조회 후 상태 초기화(로딩 표시). create 페이지를 Suspense 래핑.
Auth/NAV UI 갱신
service/app/src/widgets/HomeNavigation.tsx, service/app/src/app/page.tsx, service/app/src/app/dashboard/page.tsx, service/app/src/app/games/page.tsx
useAuth 반환에 logout 사용, isAuthenticated 기반 렌더링 전환. 아바타 드롭다운에 로그아웃 추가. 홈은 클라이언트 컴포넌트로 전환. games 페이지는 /login/kakao로 라우팅.
카카오 로그인 페이지 추가
service/app/src/app/login/kakao/page.tsx, service/app/src/app/login/kakao/callback/page.tsx
인가 URL로 리다이렉트하는 로그인 페이지와 code 처리 콜백 페이지 추가. 에러/로딩 UI 및 성공 시 리다이렉트 구현.
게임 요청 모델 정리
service/app/src/entities/game/model/gameRequest.ts
GameCreate/UpdateRequest에서 gameCreatorEmail 제거. GameUpdateQuestion의 version 제거.
디자인: 공유 토글 아이템
shared/design/src/components/gameCard/index.tsx
공유 메뉴가 공유/공유취소를 아이콘/텍스트로 토글 표기.
MSW/테스트 정리
service/app/src/mocks/__tests__/gameHandlers.test.ts, service/app/src/mocks/utils/gameHandlers.ts, service/app/src/mocks/handlers/game.ts
이메일 필드 검증/페이로드 제거, Question 내부 version 제거, 디버그 로그 삭제.
공용 fetch 인터셉터
shared/lib/fetchClient.ts
개발용 cookie 로그 제거.

Sequence Diagram(s)

sequenceDiagram
  participant UI as SaveButton
  participant State as GameCreationState
  participant Utils as gameSave.ts
  participant API as update/create API
  UI->>State: 현재 상태 조회(이름/질문/버전)
  UI->>UI: 유효성 검증(이름/질문)
  alt 신규 생성 (no gameId)
    UI->>Utils: saveGame(state)
    Utils->>API: POST /games (필요 시 이미지 업로드 후 URL 포함)
  else 기존 수정 (with gameId)
    UI->>Utils: updateExistingGame(state, gameId, version)
    Utils->>API: PUT /games/{id} (신규 이미지 업로드 후 URL 매핑)
  end
  API-->>UI: 결과(success/failure)
  UI->>UI: 성공 시 쿼리 무효화 후 /dashboard 이동
Loading
sequenceDiagram
  participant Page as /login/kakao
  participant Kakao as Kakao OAuth
  participant Cb as /login/kakao/callback
  participant Auth as useAuth.login
  Page->>Kakao: redirect to authorize (client_id, redirect_uri, code)
  Kakao-->>Cb: redirect with ?code=...
  Cb->>Auth: login(code)
  alt success
    Auth-->>Cb: ok
    Cb->>Cb: 홈으로 라우팅
  else error
    Auth-->>Cb: error
    Cb->>Cb: 에러 메시지 표시
  end
Loading
sequenceDiagram
  participant Provider as GameCreationProvider
  participant API as getGameDetail
  participant Store as useGameCreation.actions
  Provider->>Provider: gameId 존재? (가드)
  alt gameId 있음
    Provider->>API: GET /games/{id}
    API-->>Provider: game detail (title, questions, version)
    Provider->>Store: initializeFromGameDetail(title, mappedQuestions, version)
  else 없음
    Provider->>Provider: 초기 상태 유지
  end
  Provider->>UI: 로딩 완료 후 children 렌더
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

🐛bug, 👩🏻‍💻frontend

Poem

작은 발로 저장 버튼 툭, 달빛처럼 반짝 저장 경로 두 갈래
캐롯 주머니엔 버전 하나, 질문들 줄 맞춰 총총총
카카오 바람 타고 훅— 로그인 띵동!
아바타 밑에 살포시, 로그아웃 한 잎 더
오늘도 코드를 씹작씹작, 토끼는 대시보드로 뛰어간다 🥕✨


📜 Recent review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae6321 and eaeabc1.

📒 Files selected for processing (5)
  • service/app/src/app/games/page.tsx (1 hunks)
  • service/app/src/app/login/kakao/callback/page.tsx (1 hunks)
  • service/app/src/app/login/kakao/page.tsx (1 hunks)
  • service/app/src/entities/game/ui/interactions/saveButton.tsx (3 hunks)
  • service/app/src/widgets/HomeNavigation.tsx (3 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 106-implement-fix-game-functionality

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.
    • 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.
  • 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 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/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

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 (1)
shared/design/src/components/gameCard/index.tsx (1)

219-227: 공유 토글 핸들러 의미를 명확히 하세요 (onShare → onToggleShare 권장)

현재 shared 상태에 따라 아이콘/라벨은 토글되지만, 콜백 이름은 onShare로 고정되어 의미가 모호합니다. 명확성을 위해 토글 의미가 드러나도록 핸들러를 변경하는 것을 권장합니다. next 상태를 인자로 넘기면 상위 컴포넌트에서 의도를 파악하기 쉽습니다.

아래처럼 onClick에서 next 상태를 전달하도록 바꾸는 것을 제안합니다:

-                <DropdownMenuItem type="icon" onClick={(e) => {
-                  e.stopPropagation()
-                  props.onShare?.()
-                }}>
+                <DropdownMenuItem type="icon" onClick={(e) => {
+                  e.stopPropagation()
+                  props.onToggleShare?.(!shared)
+                }}>

선택적으로, MyGameCardProps 정의도 다음과 같이 업데이트해 주세요(파일 상단 타입 정의 부):

type MyGameCardProps = BaseGameCardProps & {
  type: "myGame"
  optionView?: boolean
  onEdit?: () => void
  onToggleShare?: (nextShared: boolean) => void
  onDelete?: () => void
  onMoreClick?: () => void
}
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4383c12 and 25c6998.

📒 Files selected for processing (3)
  • service/app/src/entities/game/api/updateGame.ts (0 hunks)
  • service/app/src/widgets/HomeNavigation.tsx (2 hunks)
  • shared/design/src/components/gameCard/index.tsx (2 hunks)
💤 Files with no reviewable changes (1)
  • service/app/src/entities/game/api/updateGame.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
service/app/src/widgets/HomeNavigation.tsx (1)
service/app/src/entities/auth/hooks/useAuth.ts (1)
  • useAuth (17-68)
🔇 Additional comments (2)
shared/design/src/components/gameCard/index.tsx (1)

4-4: 확인 완료: Upload와 Unshare가 icons 배럴에서 재내보내기되고 있습니다

다음에서 각각 re-export되고 있어 빌드 에러 우려 없습니다:

  • shared/design/src/icons/index.ts:17 — export { default as Unshare } from "./unshare"
  • shared/design/src/icons/index.ts:18 — export { default as Upload } from "./upload"
service/app/src/widgets/HomeNavigation.tsx (1)

72-90: isAuthenticated 기반 조건부 렌더링 전환 LGTM

훅의 단일 출처 상태를 사용해 로그인 UI 분기를 처리하는 방향이 깔끔합니다. 상단의 authLoading 비활성화 처리와도 일관됩니다.

Comment on lines 20 to 25
export const HomeNavigation = ({
isLoggedIn = false,
className = "",
}: HomeNavigationProps) => {
const router = useRouter()
const { user, isLoading: authLoading, login, logout } = useAuth()
const { user, isLoading: authLoading, isAuthenticated, login, logout } = useAuth()
const [listButton, setListButton] = useState(false)
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

isAuthenticated 훅 전환 좋습니다. 남아있는 isLoggedIn prop 타입은 제거 고려

컴포넌트는 더 이상 isLoggedIn prop를 사용하지 않으므로 HomeNavigationProps에서 해당 필드를 제거해 API 표면을 정리하는 것이 좋습니다. 단, 외부 사용처가 있다면 같이 정리하십시오.

  • 타입 정리(파일 내 인터페이스 정의 부):
interface HomeNavigationProps {
  className?: string
}
  • 사용처 점검 스크립트:

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# isLoggedIn 식별자 전역 검색 (prop/변수/타입 모두)
rg -n -S '\bisLoggedIn\b' -A 2

# HomeNavigation JSX에서 isLoggedIn prop 전달 유무 확인
rg -n '<HomeNavigation[^>]*isLoggedIn' -A 1

Length of output: 647


isLoggedIn prop 제거 필요 — HomeNavigation은 useAuth로 대체됨

HomeNavigation은 useAuth 훅을 사용하므로 isLoggedIn prop는 불필요합니다. 검색 결과 해당 prop 정의와 전달이 아래 파일들에서 확인되었습니다. 제거 및 호출부 정리하세요.

  • 수정 필요 파일:
    • service/app/src/widgets/HomeNavigation.tsx — 인터페이스에서 isLoggedIn 제거
    • service/app/src/app/page.tsx — 컴포넌트 호출부에서 isLoggedIn 전달 제거

추천 변경(diff):

- interface HomeNavigationProps {
-   isLoggedIn?: boolean
-   className?: string
- }
+ interface HomeNavigationProps {
+   className?: string
+ }
- <HomeNavigation isLoggedIn={isAuthenticated} />
+ <HomeNavigation />
📝 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 HomeNavigation = ({
isLoggedIn = false,
className = "",
}: HomeNavigationProps) => {
const router = useRouter()
const { user, isLoading: authLoading, login, logout } = useAuth()
const { user, isLoading: authLoading, isAuthenticated, login, logout } = useAuth()
const [listButton, setListButton] = useState(false)
interface HomeNavigationProps {
className?: string
}
export const HomeNavigation = ({
className = "",
}: HomeNavigationProps) => {
const router = useRouter()
const { user, isLoading: authLoading, isAuthenticated, login, logout } = useAuth()
const [listButton, setListButton] = useState(false)
Suggested change
export const HomeNavigation = ({
isLoggedIn = false,
className = "",
}: HomeNavigationProps) => {
const router = useRouter()
const { user, isLoading: authLoading, login, logout } = useAuth()
const { user, isLoading: authLoading, isAuthenticated, login, logout } = useAuth()
const [listButton, setListButton] = useState(false)
// In service/app/src/app/page.tsx
- <HomeNavigation isLoggedIn={isAuthenticated} />
+ <HomeNavigation />
🤖 Prompt for AI Agents
In service/app/src/widgets/HomeNavigation.tsx around lines 20 to 25, remove the
unused isLoggedIn prop from the component's props/interface and from the
function signature since HomeNavigation reads auth state via useAuth; update the
HomeNavigationProps type to drop isLoggedIn and remove any default/usage of that
prop. Also update service/app/src/app/page.tsx to stop passing isLoggedIn when
rendering HomeNavigation (remove the prop from the JSX), and run a quick
build/TS check to ensure no remaining references to isLoggedIn remain.

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

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

🔭 Outside diff range comments (1)
service/app/src/app/create/page.tsx (1)

31-40: gameId 정규화 적용 권장 — GameCreationProvider 타입 확인 완료

확인 결과 GameCreationProvider의 props는 service/app/src/entities/game/model/state/create/gameCreationContext.tsx에 선언된대로
gameId?: string | null 입니다. (즉 undefined 전달이 가능하지만 null도 허용됨)
따라서 searchParams에서 받은 값이 null/빈 문자열/공백일 경우 이를 undefined로 정규화해 넘기면 분기 처리가 더 명확해집니다.

수정 위치(권장)

  • service/app/src/app/create/page.tsx (CreateGamePageContent, 현재 31-40행)
  • 참고 타입 선언: service/app/src/entities/game/model/state/create/gameCreationContext.tsx (GameCreationProviderProps: gameId?: string | null)

제안된 변경(diff):

 function CreateGamePageContent() {
   const searchParams = useSearchParams()
-  const gameId = searchParams.get("gameId")
+  // null 또는 공백 문자열을 undefined로 정규화
+  const gameIdParam = searchParams.get("gameId")?.trim() || undefined
 
   return (
-    <GameCreationProvider gameId={gameId}>
+    <GameCreationProvider gameId={gameIdParam}>
       <CreateGameContent />
     </GameCreationProvider>
   )
 }
🧹 Nitpick comments (1)
service/app/src/app/create/page.tsx (1)

42-48: Suspense fallback 접근성/일관성 개선 (+ route-level loading.tsx 고려)

  • 단순 텍스트 대신 접근성 속성을 추가하면 보조기기 대응이 좋아집니다.
  • 디자인 시스템의 로더/스피너 또는 app/create/loading.tsx(route-level fallback)로 일관성 있는 로딩 UI를 제공하는 것도 고려해 주세요.

권장 최소 수정:

-    <Suspense fallback={<div>로딩 중</div>}>
+    <Suspense fallback={<div role="status" aria-live="polite">로딩 중</div>}>
       <CreateGamePageContent />
     </Suspense>

route-level fallback 사용 예시(선택):

// service/app/src/app/create/loading.tsx
export default function Loading() {
  return <div role="status" aria-live="polite">로딩 중</div>
}
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 25c6998 and 8b6a3c6.

📒 Files selected for processing (1)
  • service/app/src/app/create/page.tsx (3 hunks)
⏰ 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 (1)
service/app/src/app/create/page.tsx (1)

4-4: Suspense import 추가: 방향 OK입니다

useSearchParams 사용 시(클라이언트 전용) 초기 하이드레이션 타이밍 문제를 완화하기 위해 Suspense를 도입하는 방향은 적절합니다.

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8b6a3c6 and 677dc24.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml (1 hunks)

Comment on lines 78 to 80
yarn vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
yarn vercel build --token=${{ secrets.VERCEL_TOKEN }}
url=$(yarn vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

토큰을 명령행 인자에 노출하지 말고 env로 주입하세요

명령행 인자는 로그나 프로세스 리스트에 노출될 수 있습니다. GitHub Actions가 시크릿 값을 마스킹하지만, 보수적으로 환경변수로 주입 후 --token 옵션을 생략하는 것을 권장합니다.

변경 예시(해당 스텝 블록에 env 추가; 참고용 코드):

- name: Deploy to Vercel
  if: success()
  working-directory: service/app
  id: deploy
  env:
    VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
  run: |
    vercel pull --yes --environment=preview
    vercel build
    url=$(vercel deploy --prebuilt --yes | tail -n1)
    echo "PREVIEW_URL=$url" >> $GITHUB_OUTPUT
    echo "🚀 Preview deployed to: $url"

적용 후 배포 로그에 토큰 문자열이 등장하지 않는지 확인 부탁드립니다.


토큰을 명령행 인자에 노출하지 말고 env로 주입하세요

명령행 인자는 로그나 프로세스 리스트에 노출될 수 있습니다. GitHub Actions에서는 시크릿을 env로 주입하고 --token 옵션을 제거하세요.

  • 파일: .github/workflows/ci.yml — 라인 78-80 (현재 yarn vercel ... --token=${{ secrets.VERCEL_TOKEN }} 사용)

권장 변경 예시:

- name: Deploy to Vercel
  if: success()
  working-directory: service/app
  id: deploy
  env:
    VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
  run: |
    yarn vercel pull --yes --environment=preview
    yarn vercel build
    url=$(yarn vercel deploy --prebuilt --yes | tail -n1)
    echo "PREVIEW_URL=$url" >> $GITHUB_OUTPUT
    echo "🚀 Preview deployed to: $url"

적용 후 배포 로그에 토큰 문자열이 노출되지 않는지 확인해 주세요.

🤖 Prompt for AI Agents
.github/workflows/ci.yml lines 78-80: current steps pass the VERCEL_TOKEN via
the --token CLI arg which can leak in logs/process lists; remove the --token
flags and instead set VERCEL_TOKEN in the job/step env, then call vercel
commands without the --token option (e.g., yarn vercel pull --yes
--environment=preview, yarn vercel build, and yarn vercel deploy --prebuilt
--yes), capture the deployment URL from the deploy command output and write it
to GITHUB_OUTPUT; ensure the step includes env: VERCEL_TOKEN: ${{
secrets.VERCEL_TOKEN }} so the token is injected securely and verify logs after
applying the change to confirm the token is not printed.

⚠️ Potential issue

yarn vercel은 Yarn Berry(2+) 환경에서 실행 불가 가능성 큼 — 전역 CLI 설치와 불일치

위 단계에서 npm install -g vercel@latest로 전역 설치했는데, 그 다음에 yarn vercel ...로 호출하면 Yarn이 전역 npm 바이너리를 인식하지 못해 “Command not found”가 발생할 수 있습니다. 전역 설치를 유지하려면 직접 vercel을 호출하세요. 또한 배포 시 대화형 프롬프트 방지를 위해 --yes가 필요하고, URL 캡처의 안정성을 위해 tail -n1을 권장합니다.

다음 diff로 수정 제안(전역 설치된 vercel CLI 직접 사용):

-          yarn vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
-          yarn vercel build --token=${{ secrets.VERCEL_TOKEN }}
-          url=$(yarn vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
+          vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
+          vercel build --token=${{ secrets.VERCEL_TOKEN }}
+          url=$(vercel deploy --prebuilt --yes --token=${{ secrets.VERCEL_TOKEN }} | tail -n1)

참고: Yarn을 꼭 사용하고 싶다면 전역 설치를 제거하고 yarn dlx vercel ...로 일회성 실행을 사용하는 방법도 있습니다.

🤖 Prompt for AI Agents
.github/workflows/ci.yml lines 78-80: calling `yarn vercel` after installing the
Vercel CLI globally can fail in Yarn Berry environments because Yarn may not see
global npm binaries; replace the `yarn vercel ...` invocations with the direct
`vercel` binary (e.g., `vercel pull --yes --environment=preview --token=...`,
`vercel build --token=...`, and capture the deploy URL with `url=$(vercel deploy
--prebuilt --token=... | tail -n1)`), ensure `--yes` is present to avoid
interactive prompts, or if you prefer Yarn, remove the global install and use
`yarn dlx vercel ...` instead.

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@whdgur5717 whdgur5717 force-pushed the 106-implement-fix-game-functionality branch from dc55773 to 8b6a3c6 Compare August 14, 2025 11:29
@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@whdgur5717 whdgur5717 force-pushed the 106-implement-fix-game-functionality branch from 8b6a3c6 to e1966f3 Compare August 14, 2025 11:32
@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@whdgur5717 whdgur5717 force-pushed the 106-implement-fix-game-functionality branch from e1966f3 to 3bfefeb Compare August 14, 2025 11:43
@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@whdgur5717 whdgur5717 force-pushed the 106-implement-fix-game-functionality branch from 3bfefeb to 28380dd Compare August 14, 2025 11:50
@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

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

🔭 Outside diff range comments (1)
service/app/src/entities/game/utils/gameSave.ts (1)

121-134: 이미지 Presigned URL 매핑 오동작: 인덱스 기준 매핑으로 잘못된 질문에 이미지가 할당됩니다

현재 전체 questions를 인덱스로 순회하며 presignedUrls[index]를 적용하고 있어, 이미지가 없는 질문이 끼어 있는 경우 앞선/뒤선 질문에 다른 이미지 키가 엮일 수 있습니다. 업로드 요청을 만든 questionsWithImages 순서와 동일한 순서로만 매핑해야 합니다.

아래와 같이 이미지가 있는 질문에서만 포인터를 증가시키는 방식으로 수정해주세요.

-    if (presignedResponse && presignedResponse.data) {
-      gameData.questions = gameData.questions.map((question, index) => {
-        const presignedUrl = presignedResponse.data!.presignedUrls[index]
-        return {
-          ...question,
-          imageUrl: presignedUrl ? presignedUrl.key : "",
-        }
-      })
-
-      const firstImageUrl = presignedResponse.data.presignedUrls[0]?.key
-      if (firstImageUrl) {
-        gameData.gameThumbnailUrl = firstImageUrl
-      }
-    }
+    if (presignedResponse && presignedResponse.data) {
+      const presignedUrls = presignedResponse.data.presignedUrls
+      let p = 0
+      gameData.questions = gameData.questions.map((question, index) => {
+        // state.questions와 gameData.questions는 동일한 순서라는 전제
+        if (state.questions[index].imageFile) {
+          const presignedUrl = presignedUrls[p++]
+          return {
+            ...question,
+            imageUrl: presignedUrl ? presignedUrl.key : "",
+          }
+        }
+        return question
+      })
+
+      const firstImageUrl = presignedResponse.data.presignedUrls[0]?.key
+      if (firstImageUrl) {
+        gameData.gameThumbnailUrl = firstImageUrl
+      }
+    }
♻️ Duplicate comments (2)
service/app/src/app/page.tsx (1)

14-14: HomeNavigation에 isLoggedIn 전달 제거 (내부 useAuth로 대체됨)

HomeNavigation이 내부에서 useAuth를 사용하므로 외부에서 isLoggedIn을 전달할 필요가 없습니다. 호출부에서 prop를 제거해 API를 일관되게 유지하세요.

적용 diff:

-      <HomeNavigation isLoggedIn={isAuthenticated} />
+      <HomeNavigation />
service/app/src/widgets/HomeNavigation.tsx (1)

15-18: 불필요한 isLoggedIn prop 타입 제거

컴포넌트가 useAuth로 내부 상태를 사용하므로 isLoggedIn prop는 더 이상 필요하지 않습니다. 타입 표면을 정리해 혼동을 줄이세요.

적용 diff:

 interface HomeNavigationProps {
-  isLoggedIn?: boolean
   className?: string
 }

그리고 사용처(page.tsx)의 전달도 함께 제거하세요.

🧹 Nitpick comments (14)
service/app/src/app/page.tsx (1)

1-1: (옵션) Home 컴포넌트를 서버 컴포넌트로 되돌리기 — 불필요한 클라이언트 전환 제거

위 변경으로 Home에서 더 이상 훅을 사용하지 않는다면, "use client"와 useAuth 관련 코드를 제거해 서버 컴포넌트로 되돌릴 수 있습니다. 초기 페인트/번들 크기 측면의 이점이 있습니다.

적용 diff:

-"use client"
-
-import { useAuth } from "@/entities/auth"
+// server component (no client hooks used here)

-  const { isAuthenticated } = useAuth()

참고: HomeNavigation은 클라이언트 컴포넌트이므로 서버 컴포넌트인 Home에서 그대로 렌더링해도 문제 없습니다.

Also applies to: 3-3, 10-10

service/app/src/app/dashboard/page.tsx (4)

160-162: 아바타 메뉴 토글 setState는 함수형 업데이트 사용 권장

현재 상태값을 바로 캡처해 토글하고 있어 불필요한 의존이 생길 수 있습니다. 함수형 업데이트로 안전하게 토글하세요.

적용 diff:

-  const handleAvatarClick = () => {
-    setListButton(!listButton)
-  }
+  const handleAvatarClick = () => {
+    setListButton((prev) => !prev)
+  }

164-168: 로그아웃 후 네비게이션 처리 OK — 필요 시 replace 사용 고려

현재 push("/") 동작은 적절합니다. 로그인 상태가 필요한 페이지에서 뒤로가기 방지를 원하면 replace("/")를 고려해도 됩니다.

예:

-    router.push("/")
+    router.replace("/")

199-223: 아바타 드롭다운: 외부 클릭/ESC 닫힘 및 접근성 개선 제안

현재 드롭다운은 아바타 클릭으로만 열고 닫을 수 있습니다. 외부 클릭/ESC 닫힘과 button/aria 속성 적용으로 접근성과 UX를 개선하세요.

예시 구현(요약):

// 상단 imports 근처
import { useEffect, useRef } from "react"

// 컴포넌트 내부
const menuRef = useRef<HTMLDivElement>(null)

useEffect(() => {
  if (!listButton) return
  const onDocClick = (e: MouseEvent) => {
    if (!menuRef.current?.contains(e.target as Node)) setListButton(false)
  }
  const onKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Escape") setListButton(false)
  }
  document.addEventListener("mousedown", onDocClick)
  document.addEventListener("keydown", onKeyDown)
  return () => {
    document.removeEventListener("mousedown", onDocClick)
    document.removeEventListener("keydown", onKeyDown)
  }
}, [listButton])

// JSX
<div className="relative" ref={menuRef}>
  <button
    type="button"
    className="flex size-[42px] items-center justify-center rounded-full bg-gray-300"
    onClick={handleAvatarClick}
    aria-haspopup="menu"
    aria-expanded={listButton}
  >
    <Image ... />
  </button>
  {listButton && (
    <div className="absolute right-0 top-full z-10 mt-2" role="menu">
      <SecondaryOutlineBoxButton role="menuitem" onClick={handleLogoutClick}>로그아웃</SecondaryOutlineBoxButton>
    </div>
  )}
</div>

199-223: 중복 UI 추출 제안 — AvatarMenu 컴포넌트로 공통화

HomeNavigation에도 유사한 아바타/드롭다운 UI가 존재합니다. 공통 AvatarMenu(예: entities/auth/ui/AvatarMenu.tsx)로 추출해 중복 제거와 유지보수성을 높이세요.

원하시면 두 파일에서 사용하는 범용 AvatarMenu 컴포넌트를 생성하는 PR용 패치를 제안드리겠습니다.

service/app/src/widgets/HomeNavigation.tsx (2)

37-39: 토글 시 함수형 업데이트 사용 권장

현재 상태 캡처 기반 토글입니다. 함수형 업데이트로 안전하게 토글하세요.

적용 diff:

-  const handleAvatarClick = () => {
-    setListButton(!listButton)
-  }
+  const handleAvatarClick = () => {
+    setListButton((prev) => !prev)
+  }

91-115: 아바타 드롭다운: 외부 클릭/ESC 닫힘 및 접근성 개선

Dashboard와 동일하게 외부 클릭/ESC 닫힘과 button/aria 속성을 적용하면 UX/접근성이 향상됩니다. 또한 클릭 가능한 div 대신 button 요소 사용을 권장합니다.

예시 패턴(요약):

// useRef/useEffect로 외부 클릭/ESC 처리 (대시보드 코멘트 참조)
// 버튼 요소 및 aria 적용
<button
  type="button"
  className="flex size-[42px] items-center justify-center rounded-full bg-gray-300"
  onClick={handleAvatarClick}
  aria-haspopup="menu"
  aria-expanded={listButton}
>
  <Image ... />
</button>
{listButton && (
  <div className="absolute right-0 top-full z-10 mt-2" role="menu">
    <SecondaryOutlineBoxButton role="menuitem" onClick={handleLogoutClick}>로그아웃</SecondaryOutlineBoxButton>
  </div>
)}
service/app/src/entities/game/model/state/create/state.ts (1)

30-30: gameVersion의 optional 처리 일관성 점검 및 기본값 전략 정리 제안

  • 현재 gameVersion?: number로 optional입니다. 편집(수정) 모드에서만 필요하다면 OK이지만, 사용 측(saveButton.tsx)에서 || 1로 폴백하고 있어 0도 1로 간주되는 문제가 있습니다. nullish 병합(??) 또는 초기 상태에서 명시적 기본값 설정(예: 로딩 시 setGameVersion) 중 하나로 일관성 있게 가져가는 것을 권장합니다.
service/app/src/entities/game/ui/interactions/saveButton.tsx (1)

56-56: 버전 폴백은 nullish 병합(??) 사용 권장

state.gameVersion || 101로 처리합니다. 의도된 값이 아니라면 ??를 사용해 undefined/null일 때만 폴백되도록 해주세요.

-            state.gameVersion || 1,
+            state.gameVersion ?? 1,
service/app/src/entities/game/utils/gameSave.ts (1)

160-257: 중복 로직(검증/Presigned/업로드/매핑) 추출로 DRY화 제안

saveGameupdateExistingGame 간에 검증, Presigned 발급, 업로드, 매핑 로직이 대부분 중복됩니다. 공통 유틸(예: prepareAndUploadImages(state) 또는 applyPresignedUrls(state, gameData, presignedResponse))로 추출하면 유지보수성과 테스트 용이성이 크게 향상됩니다.

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

66-68: 로딩 UI 개선은 선택 사항

간단한 div 대신 스켈레톤/스피너 컴포넌트를 적용하면 UX가 개선됩니다.

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

163-184: 게임 상세 초기화 매핑은 전반적으로 적절합니다. 선택 ID/버전 설정 OK. 선택 유지 로직 고려 가능

현재 구현은:

  • 외부 질문을 내부 스키마로 일괄 매핑(id/text/answer/imageUrl/order).
  • 첫 번째 질문을 선택.
  • 버전(version)을 state에 세팅.

개선 제안(선택 사항):

  • 사용자가 특정 질문을 보고 있는 중 재초기화되는 경우를 고려해, 가능한 경우 이전 selectedQuestionId를 유지하고, 없으면 첫 번째로 폴백하는 편이 UX에 유리합니다.

예시 수정안:

     case "INITIALIZE_FROM_GAME_DETAIL": {
       const { gameTitle, questions, version } = action.payload

       // 기존 질문들을 새로운 데이터로 교체
       const newQuestions = questions.map((question, index) => ({
         id: question.questionId,
         text: question.questionText,
         answer: question.answer,
         imageFile: null,
         imageUrl: question.imageUrl,
         previewImageUrl: null,
         order: index,
       }))

+      const nextSelectedQuestionId =
+        newQuestions.find((q) => q.id === state.selectedQuestionId)?.id ??
+        (newQuestions[0]?.id ?? null)

       return {
         ...state,
         gameName: gameTitle,
         questions: newQuestions,
-        selectedQuestionId: newQuestions.length > 0 ? newQuestions[0].id : null,
+        selectedQuestionId: nextSelectedQuestionId,
         gameVersion: version,
       }
     }

또한 초기화 시 편집/포커스/팝업/로딩 등의 UI 상태를 초기 상태로 재설정해야 한다면(제품 요구에 따라) 함께 리셋하는 것이 안전합니다. 필요 시 초기 상태 스키마를 확인해 반영하도록 하겠습니다.

초기화 이후 UI 상태(편집/포커스/팝업/로딩)를 유지할지 초기 상태로 되돌릴지 요구사항을 알려주시면 반영안을 드리겠습니다.

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

24-36: INITIALIZE_FROM_GAME_DETAIL payload: 불변 배열 타입으로 좁혀 타입 안정성 향상 제안

런타임 동작에는 영향 없지만, payload가 외부 데이터 스냅샷이라는 성격상 ReadonlyArray로 좁히면 호출 측에서 불필요한 변경을 시도하는 실수를 줄일 수 있습니다.

-        questions: Array<{
+        questions: ReadonlyArray<{
           questionId: string
           questionText: string
           answer: string
           imageUrl: string | null
         }>

121-134: initializeFromGameDetail 시그니처: 불변 배열로 좁히고, 재사용 가능한 타입 별칭 도입 고려

  • questions 파라미터도 ReadonlyArray로 좁혀 호출 측 변이를 방지하면 좋습니다.
  • 동일한 질문 페이로드 형태가 유니온과 크리에이터에서 반복되므로, 별칭 타입(예: GameDetailQuestionPayload)을 도입해 중복을 줄이길 권장합니다.
   initializeFromGameDetail: (
     gameTitle: string,
-    questions: Array<{
+    questions: ReadonlyArray<{
       questionId: string
       questionText: string
       answer: string
       imageUrl: string | null
     }>,
     version: number,
 ): GameCreationAction => ({

참고: 타입 중복 제거 예시

// 파일 상단 근처
export type GameDetailQuestionPayload = {
  questionId: string
  questionText: string
  answer: string
  imageUrl: string | null
}

// 유니온/크리에이터에서
questions: ReadonlyArray<GameDetailQuestionPayload>
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ece5eeb and 28380dd.

📒 Files selected for processing (18)
  • service/app/src/app/create/page.tsx (2 hunks)
  • service/app/src/app/dashboard/page.tsx (4 hunks)
  • service/app/src/app/page.tsx (1 hunks)
  • service/app/src/entities/game/api/updateGame.ts (1 hunks)
  • service/app/src/entities/game/model/gameRequest.ts (0 hunks)
  • service/app/src/entities/game/model/state/create/actions.ts (3 hunks)
  • service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1 hunks)
  • service/app/src/entities/game/model/state/create/reducer.ts (2 hunks)
  • service/app/src/entities/game/model/state/create/state.ts (1 hunks)
  • service/app/src/entities/game/model/state/create/useGameCreation.ts (2 hunks)
  • service/app/src/entities/game/ui/interactions/saveButton.tsx (3 hunks)
  • service/app/src/entities/game/utils/gameSave.ts (3 hunks)
  • service/app/src/mocks/__tests__/gameHandlers.test.ts (0 hunks)
  • service/app/src/mocks/handlers/game.ts (0 hunks)
  • service/app/src/mocks/utils/gameHandlers.ts (1 hunks)
  • service/app/src/widgets/HomeNavigation.tsx (3 hunks)
  • shared/design/src/components/gameCard/index.tsx (2 hunks)
  • shared/lib/fetchClient.ts (0 hunks)
💤 Files with no reviewable changes (4)
  • shared/lib/fetchClient.ts
  • service/app/src/mocks/handlers/game.ts
  • service/app/src/entities/game/model/gameRequest.ts
  • service/app/src/mocks/tests/gameHandlers.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • shared/design/src/components/gameCard/index.tsx
  • service/app/src/app/create/page.tsx
  • service/app/src/entities/game/api/updateGame.ts
🧰 Additional context used
🧬 Code Graph Analysis (9)
service/app/src/app/dashboard/page.tsx (2)
service/app/src/entities/auth/hooks/useAuth.ts (1)
  • useAuth (17-68)
shared/design/src/components/button/secondaryOutlineBoxButton.tsx (1)
  • SecondaryOutlineBoxButton (32-45)
service/app/src/app/page.tsx (2)
service/app/src/entities/auth/hooks/useAuth.ts (1)
  • useAuth (17-68)
service/app/src/widgets/HomeNavigation.tsx (1)
  • HomeNavigation (20-143)
service/app/src/entities/game/model/state/create/useGameCreation.ts (1)
service/app/src/entities/game/model/state/create/actions.ts (1)
  • gameCreationActions (45-139)
service/app/src/entities/game/utils/gameSave.ts (4)
service/app/src/entities/game/model/state/create/state.ts (1)
  • GameCreationState (26-38)
service/app/src/entities/game/model/gameRequest.ts (1)
  • GameUpdateRequest (42-47)
service/app/src/entities/game/utils/index.ts (1)
  • uploadMultipleFilesToS3 (11-11)
service/app/src/entities/game/api/updateGame.ts (1)
  • updateGameWithoutNewImages (73-87)
service/app/src/entities/game/model/state/create/actions.ts (1)
service/app/src/entities/game/model/state/create/state.ts (1)
  • PopupState (11-15)
service/app/src/widgets/HomeNavigation.tsx (2)
service/app/src/entities/auth/hooks/useAuth.ts (1)
  • useAuth (17-68)
shared/design/src/components/button/secondaryOutlineBoxButton.tsx (1)
  • SecondaryOutlineBoxButton (32-45)
service/app/src/entities/game/ui/interactions/saveButton.tsx (4)
service/app/src/entities/game/ui/interactions/index.ts (1)
  • SaveButton (2-2)
service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1)
  • useGameCreationContext (77-85)
service/app/src/entities/game/model/useGamePopupActions.tsx (1)
  • useGamePopupActions (13-134)
service/app/src/entities/game/utils/gameSave.ts (2)
  • updateExistingGame (160-257)
  • saveGame (63-158)
service/app/src/mocks/utils/gameHandlers.ts (2)
service/app/src/entities/game/model/gameRequest.ts (1)
  • GameUpdateRequest (42-47)
service/app/src/entities/game/model/index.ts (1)
  • GameUpdateRequest (18-18)
service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1)
service/app/src/entities/game/model/state/create/useGameCreation.ts (1)
  • useGameCreation (11-160)
🔇 Additional comments (8)
service/app/src/widgets/HomeNavigation.tsx (2)

24-25: useAuth 연동 및 상태 분기 LGTM

user 렌더링, authLoading 비활성화, isAuthenticated 조건부 UI가 일관적으로 연결되었습니다.


119-133: 로그인 버튼 상태 연동/비활성화 처리 LGTM

authLoading과의 바인딩 및 에러 로깅 처리가 적절합니다.

service/app/src/entities/game/ui/interactions/saveButton.tsx (2)

47-64: 수정/생성 분기 로직은 명확하고 검증 경로도 적절합니다

  • 검증 실패 시 팝업, 성공 시 쿼리 무효화 및 대시보드 이동 플로우가 일관적입니다.
  • cleanedQuestions를 양쪽 경로에 동일 적용한 점도 좋습니다.

88-89: 버튼 라벨 동적 처리 LGTM

gameId 존재 여부로 "게임 수정"/"게임 저장"을 구분하는 UX가 명확합니다.

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

86-106: 게임 상세로부터 초기화 액션 래퍼 추가 LGTM

  • 페이로드 구조가 액션 정의와 일치하며 useCallback 최적화도 적절합니다.

108-110: 버전 설정 액션 래퍼 추가 LGTM

  • 단순한 디스패치 래퍼로 명확합니다.
service/app/src/entities/game/model/state/create/reducer.ts (1)

186-191: SET_GAME_VERSION 처리 적절

payload를 그대로 gameVersion에 반영하는 단순/명확한 처리로 보입니다. 다른 필드에 영향이 없고 불변성도 잘 지켜졌습니다.

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

135-138: setGameVersion 액션 크리에이터 추가 👍

간결하고 명확합니다. 리듀서 처리와도 일치합니다.

Comment on lines +98 to 106
showPopup: (popup: keyof PopupState): GameCreationAction => ({
type: "SHOW_POPUP",
payload: popupType,
payload: popup,
}),

hidePopup: (popupType: keyof PopupState): GameCreationAction => ({
hidePopup: (popup: keyof PopupState): GameCreationAction => ({
type: "HIDE_POPUP",
payload: popupType,
payload: popup,
}),
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

PopupState 중복 정의 방지: state.ts의 타입을 직접 참조하도록 변경 권장

현재 이 파일 내부에 PopupState 인터페이스가 별도로 존재하는데, state.ts에도 동일 이름의 타입이 있습니다. 두 소스의 스키마가 어긋나면 곧바로 타입 드리프트가 발생합니다. 이 라인에서는 inline import 타입을 사용해 중복을 제거할 수 있습니다.

-  showPopup: (popup: keyof PopupState): GameCreationAction => ({
+  showPopup: (popup: keyof import("./state").PopupState): GameCreationAction => ({
     type: "SHOW_POPUP",
     payload: popup,
   }),

-  hidePopup: (popup: keyof PopupState): GameCreationAction => ({
+  hidePopup: (popup: keyof import("./state").PopupState): GameCreationAction => ({
     type: "HIDE_POPUP",
     payload: popup,
   }),

추가로, 액션 유니온 정의 부분(라인 19~20 부근)도 동일하게 교체하고, 이 파일 내부의 interface PopupState는 제거하거나 state.ts에서 명시적으로 import하는 방식으로 단일 소스화하기를 권장합니다.

예) 파일 상단/하단 정리(참고용):

// 상단
import type { PopupState } from "./state"

// ...유니온 정의 내
| { type: "SHOW_POPUP"; payload: keyof PopupState }
| { type: "HIDE_POPUP"; payload: keyof PopupState }

// 하단의 로컬 interface PopupState { ... } 제거

중복 정의 여부를 확인하려면 다음을 실행해 주세요:


🏁 Script executed:

#!/bin/bash
# PopupState가 정의된 위치와 사용처 확인
rg -n -A2 -B2 $'\binterface\s+PopupState\b|keyof\s+PopupState'

Length of output: 1560


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="service/app/src/entities/game/model/state/create/actions.ts"

echo "=== Showing file with line numbers: $FILE ==="
nl -ba "$FILE" | sed -n '1,240p'

echo
echo "=== rg search for PopupState across repo ==="
rg -n "PopupState" || true

echo
echo "=== rg search for 'interface PopupState' and 'type PopupState' ==="
rg -n -S "interface\s+PopupState|type\s+PopupState|PopupState\s*=" || true

Length of output: 473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== find state.ts files under service/app ==="
find service/app -type f -name state.ts -print || true

echo
echo "=== rg for 'PopupState' across repo ==="
rg -n "PopupState" || true

echo
echo "=== Show actions.ts (around PopupState usage) ==="
sed -n '1,240p' service/app/src/entities/game/model/state/create/actions.ts || true

Length of output: 4919


PopupState 중복 정의 제거 — actions.ts가 state.ts의 타입을 참조하도록 수정 필요

state.ts에서 이미 export된 PopupState가 존재(service/app/src/entities/game/model/state/create/state.ts, 약 11행). actions.ts에 동일한 인터페이스가 로컬로 중복 선언되어 있어 타입 드리프트 위험이 있습니다. 아래 위치들을 수정해 주세요.

  • 수정 대상

    • service/app/src/entities/game/model/state/create/state.ts
      • export interface PopupState (단일 출처)
    • service/app/src/entities/game/model/state/create/actions.ts
      • 라인 ~19-20: GameCreationAction 유니온의 { type: "SHOW_POPUP" | "HIDE_POPUP" } 항목 (payload: keyof PopupState) — 외부 타입 참조로 변경
      • 라인 ~39: 로컬 interface PopupState { ... } — 제거
      • 라인 ~98, ~103: showPopup / hidePopup 액션 생성기 시그니처 (popup: keyof PopupState) — 외부 타입 참조로 변경
  • 권장 변경 (대표안: 파일 상단에서 타입 import 후 로컬 인터페이스 제거)

+ import type { PopupState } from "./state"
- interface PopupState {
-   showExitConfirmation: boolean
-   showSaveConfirmation: boolean
-   showFileUploadError: boolean
- }

(유니온·액션 생성기 쪽 타입들은 상단에서 import한 PopupState를 그대로 사용하면 됩니다.)

  • 대안 (inline import 사용)
-  | { type: "SHOW_POPUP"; payload: keyof PopupState }
-  | { type: "HIDE_POPUP"; payload: keyof PopupState }
+  | { type: "SHOW_POPUP"; payload: keyof import("./state").PopupState }
+  | { type: "HIDE_POPUP"; payload: keyof import("./state").PopupState }

또는

-  showPopup: (popup: keyof PopupState): GameCreationAction => ({
+  showPopup: (popup: keyof import("./state").PopupState): GameCreationAction => ({

변경 후에는 타입체크/빌드로 타입 충돌이 없는지 확인해 주세요.

📝 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
showPopup: (popup: keyof PopupState): GameCreationAction => ({
type: "SHOW_POPUP",
payload: popupType,
payload: popup,
}),
hidePopup: (popupType: keyof PopupState): GameCreationAction => ({
hidePopup: (popup: keyof PopupState): GameCreationAction => ({
type: "HIDE_POPUP",
payload: popupType,
payload: popup,
}),
showPopup: (popup: keyof import("./state").PopupState): GameCreationAction => ({
type: "SHOW_POPUP",
payload: popup,
}),
hidePopup: (popup: keyof import("./state").PopupState): GameCreationAction => ({
type: "HIDE_POPUP",
payload: popup,
}),
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/actions.ts (refs around
lines ~19-20, ~39, and the action creators at ~98-106), remove the local
duplicate PopupState interface, import the exported PopupState from
service/app/src/entities/game/model/state/create/state.ts at the top, update the
GameCreationAction union members that currently use the local type to reference
keyof PopupState from the imported type, and change the showPopup/hidePopup
action creator signatures to use (popup: keyof PopupState); finally run
typecheck/build to ensure no type conflicts.

Comment on lines +3 to +10
import {
createContext,
ReactNode,
useContext,
useEffect,
useRef,
useState,
} from "react"
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

불필요한 useRef 제거 및 의존성 안정화 권장

useEffect의 deps에 gameCreation.actions 객체 전체를 넣어 매 렌더마다 참조가 바뀌고 있어 hasLoaded로 억지로 1회 실행을 보장하고 있습니다. initializeFromGameDetail만 의존하도록 추출하면 useRef가 불필요해지고 로직이 단순해집니다.

-import {
-  createContext,
-  ReactNode,
-  useContext,
-  useEffect,
-  useRef,
-  useState,
-} from "react"
+import {
+  createContext,
+  ReactNode,
+  useContext,
+  useEffect,
+  useState,
+} from "react"
📝 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
import {
createContext,
ReactNode,
useContext,
useEffect,
useRef,
useState,
} from "react"
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react"
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/gameCreationContext.tsx
around lines 3 to 10, remove the unnecessary useRef and change the useEffect
dependency from the whole gameCreation.actions object to only the
initializeFromGameDetail function: extract or destructure
initializeFromGameDetail from gameCreation.actions (or memoize it where it’s
defined) so its reference is stable, update the useEffect to depend on
initializeFromGameDetail (and other true primitives) and remove the
hasLoaded/useRef guard; this ensures the effect runs only when the actual
initializer changes and simplifies the logic.

Comment on lines +30 to +32
const [isLoading, setIsLoading] = useState(false)
const hasLoaded = useRef(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

hasLoaded 제거

hasLoaded 가드는 위 개선으로 불필요해집니다.

-  const [isLoading, setIsLoading] = useState(false)
-  const hasLoaded = useRef(false)
+  const [isLoading, setIsLoading] = useState(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
const [isLoading, setIsLoading] = useState(false)
const hasLoaded = useRef(false)
const [isLoading, setIsLoading] = useState(false)
🤖 Prompt for AI Agents
In service/app/src/entities/game/model/state/create/gameCreationContext.tsx
around lines 30 to 32, remove the now-unnecessary hasLoaded guard: delete the
useRef declaration const hasLoaded = useRef(false) and remove any checks or
assignments referencing hasLoaded throughout this file, ensuring related logic
relies on the existing isLoading/useState flow or alternative guards already
introduced.

Comment on lines +33 to +65
useEffect(() => {
if (gameId && !hasLoaded.current) {
hasLoaded.current = true
setIsLoading(true)

const loadGameData = async () => {
try {
const response = await getGameDetail(gameId)
if (response.result === "SUCCESS" && response.data) {
const gameDetail = response.data

gameCreation.actions.initializeFromGameDetail(
gameDetail.gameTitle,
gameDetail.questions.map((question) => ({
questionId: question.questionId.toString(),
questionText: question.questionText,
answer: question.questionAnswer,
imageUrl: question.imageUrl,
})),
gameDetail.version,
)
}
} catch (error) {
console.error("Failed to load game data:", error)
} finally {
setIsLoading(false)
}
}

loadGameData()
}
}, [gameId, gameCreation.actions])

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

데이터 로드 useEffect 의존성/해제 처리 개선 및 gameId 변경 대응

  • actions 객체 전체 의존 제거, initializeFromGameDetail만 의존
  • 언마운트/변경 중 상태 업데이트 방지를 위한 cancel 플래그 추가
  • gameId 변경 시 재로딩 지원 (현행은 최초 1회만 로드)
-  useEffect(() => {
-    if (gameId && !hasLoaded.current) {
-      hasLoaded.current = true
-      setIsLoading(true)
-      
-      const loadGameData = async () => {
-        try {
-          const response = await getGameDetail(gameId)
-          if (response.result === "SUCCESS" && response.data) {
-            const gameDetail = response.data
-
-            gameCreation.actions.initializeFromGameDetail(
-              gameDetail.gameTitle,
-              gameDetail.questions.map((question) => ({
-                questionId: question.questionId.toString(),
-                questionText: question.questionText,
-                answer: question.questionAnswer,
-                imageUrl: question.imageUrl,
-              })),
-              gameDetail.version,
-            )
-          }
-        } catch (error) {
-          console.error("Failed to load game data:", error)
-        } finally {
-          setIsLoading(false)
-        }
-      }
-
-      loadGameData()
-    }
-  }, [gameId, gameCreation.actions])
+  useEffect(() => {
+    if (!gameId) return
+    let cancelled = false
+    setIsLoading(true)
+    ;(async () => {
+      try {
+        const response = await getGameDetail(gameId)
+        if (!cancelled && response.result === "SUCCESS" && response.data) {
+          const gameDetail = response.data
+          gameCreation.actions.initializeFromGameDetail(
+            gameDetail.gameTitle,
+            gameDetail.questions.map((question) => ({
+              questionId: question.questionId.toString(),
+              questionText: question.questionText,
+              answer: question.questionAnswer,
+              imageUrl: question.imageUrl,
+            })),
+            gameDetail.version,
+          )
+        }
+      } catch (error) {
+        if (!cancelled) {
+          console.error("Failed to load game data:", error)
+        }
+      } finally {
+        if (!cancelled) {
+          setIsLoading(false)
+        }
+      }
+    })()
+    return () => {
+      cancelled = true
+    }
+  }, [gameId, gameCreation.actions.initializeFromGameDetail])
📝 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
useEffect(() => {
if (gameId && !hasLoaded.current) {
hasLoaded.current = true
setIsLoading(true)
const loadGameData = async () => {
try {
const response = await getGameDetail(gameId)
if (response.result === "SUCCESS" && response.data) {
const gameDetail = response.data
gameCreation.actions.initializeFromGameDetail(
gameDetail.gameTitle,
gameDetail.questions.map((question) => ({
questionId: question.questionId.toString(),
questionText: question.questionText,
answer: question.questionAnswer,
imageUrl: question.imageUrl,
})),
gameDetail.version,
)
}
} catch (error) {
console.error("Failed to load game data:", error)
} finally {
setIsLoading(false)
}
}
loadGameData()
}
}, [gameId, gameCreation.actions])
useEffect(() => {
if (!gameId) return
let cancelled = false
setIsLoading(true)
;(async () => {
try {
const response = await getGameDetail(gameId)
if (!cancelled && response.result === "SUCCESS" && response.data) {
const gameDetail = response.data
gameCreation.actions.initializeFromGameDetail(
gameDetail.gameTitle,
gameDetail.questions.map((question) => ({
questionId: question.questionId.toString(),
questionText: question.questionText,
answer: question.questionAnswer,
imageUrl: question.imageUrl,
})),
gameDetail.version,
)
}
} catch (error) {
if (!cancelled) {
console.error("Failed to load game data:", error)
}
} finally {
if (!cancelled) {
setIsLoading(false)
}
}
})()
return () => {
cancelled = true
}
}, [gameId, gameCreation.actions.initializeFromGameDetail])

Comment on lines +98 to +106
const updatedQuestions = state.questions.map((question) =>
question.id === questionId
? {
...question,
imageFile: file,
previewImageUrl: previewUrl,
}
: question,
)
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

이미지 업로드 시작: per-question 업데이트는 좋지만, 유틸 재사용과 업로드 진행상태 관리 점검이 필요합니다

  • 동일 기능을 하는 updateQuestion 유틸이 이미 존재하므로, map으로 직접 갱신하기보다 유틸을 사용하면 중복을 줄이고 일관성이 좋아집니다.
  • 또한 START만 있고 진행 상태 플래그(예: isUploading) 관리나 SUCCESS/ERROR 분기 처리가 없다면, UI 로딩 표시나 에러 핸들링이 어려울 수 있습니다. 기존 구현에서 로딩 플래그를 사용했다면 회귀가 될 가능성이 있어 확인이 필요합니다.

아래처럼 유틸로 간결화하는 것을 제안합니다.

-      const updatedQuestions = state.questions.map((question) =>
-        question.id === questionId
-          ? {
-              ...question,
-              imageFile: file,
-              previewImageUrl: previewUrl,
-            }
-          : question,
-      )
+      const updatedQuestions = updateQuestion(state.questions, questionId, {
+        imageFile: file,
+        previewImageUrl: previewUrl,
+      })

추가로, 업로드 성공/실패 액션을 도입해 로딩/에러/완료 상태를 명확히 관리하는 것을 권장합니다. 필요하시면 액션/리듀서/액션 크리에이터 초안까지 함께 드리겠습니다.

다음 스크립트로 기존 코드베이스에서 업로드 관련 로딩 플래그 또는 SUCCESS/ERROR 액션이 있는지 점검해 주세요:

Also applies to: 110-110


🏁 Script executed:

#!/bin/bash
# 업로드 로딩/성공/실패 관련 키워드 서치
rg -n -A2 -B2 $'isUploading|UPLOAD_IMAGE_SUCCESS|UPLOAD_IMAGE_ERROR|uploadImageSuccess|uploadImageError'

Length of output: 871


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Searching for updateQuestion occurrences ==="
rg -n --hidden --no-ignore 'updateQuestion' || true

echo
echo "=== reducer.ts (first 240 lines) ==="
sed -n '1,240p' service/app/src/entities/game/model/state/create/reducer.ts || true

echo
echo "=== utils.ts (first 240 lines) ==="
sed -n '1,240p' service/app/src/entities/game/model/state/create/utils.ts || true

echo
echo "=== state.ts (first 240 lines) ==="
sed -n '1,240p' service/app/src/entities/game/model/state/create/state.ts || true

Length of output: 10332


UPLOAD_IMAGE_START: updateQuestion 유틸 재사용 및 업로드 진행 상태(SUCCESS/ERROR/isUploading) 처리 필요

검증 결과: updateQuestion 유틸은 service/app/src/entities/game/model/state/create/utils.ts에 존재하고(확인됨), reducer의 UPLOAD_IMAGE_START는 아직 inline map을 사용하며 loading.isUploading 플래그를 토글하지 않습니다. UPLOAD_IMAGE_SUCCESS / UPLOAD_IMAGE_ERROR 핸들러도 코드베이스에서 발견되지 않았습니다. 아래 수정 권장사항을 적용하세요.

수정 포인트

  • service/app/src/entities/game/model/state/create/reducer.ts
    • case "UPLOAD_IMAGE_START": inline map → updateQuestion 재사용 및 loading.isUploading = true 설정
    • UPLOAD_IMAGE_SUCCESS, UPLOAD_IMAGE_ERROR 액션 핸들러 추가(성공 시 imageUrl 반영, 실패 시 에러 처리 및 isUploading false)
  • service/app/src/entities/game/model/state/create/utils.ts
    • updateQuestion(questions, questionId, updates) 존재 — 재사용 권장
  • service/app/src/entities/game/model/state/create/actions.ts
    • UPLOAD_IMAGE_* 타입/액션 크리에이터가 없으면 추가
  • UI: 업로드 진행/오류 표시를 위해 컴포넌트가 해당 액션/플래그를 구독하는지 검토

권장 변경(간결한 예시)

-    case "UPLOAD_IMAGE_START": {
-      const { questionId, file, previewUrl } = action.payload
-      const updatedQuestions = state.questions.map((question) =>
-        question.id === questionId
-          ? {
-              ...question,
-              imageFile: file,
-              previewImageUrl: previewUrl,
-            }
-          : question,
-      )
-
-      return {
-        ...state,
-        questions: updatedQuestions,
-      }
-    }
+    case "UPLOAD_IMAGE_START": {
+      const { questionId, file, previewUrl } = action.payload
+      const updatedQuestions = updateQuestion(state.questions, questionId, {
+        imageFile: file,
+        previewImageUrl: previewUrl,
+      })
+
+      return {
+        ...state,
+        questions: updatedQuestions,
+        loading: { ...state.loading, isUploading: true },
+      }
+    }

추가로 reducer에 아래 같은 핸들러를 추가해 업로드 완료/실패를 명확히 처리하세요:

+    case "UPLOAD_IMAGE_SUCCESS": {
+      const { questionId, imageUrl } = action.payload
+      const updatedQuestions = updateQuestion(state.questions, questionId, {
+        imageUrl,
+        imageFile: null,
+        previewImageUrl: null,
+      })
+      return {
+        ...state,
+        questions: updatedQuestions,
+        loading: { ...state.loading, isUploading: false },
+      }
+    }
+
+    case "UPLOAD_IMAGE_ERROR":
+      return {
+        ...state,
+        loading: { ...state.loading, isUploading: false },
+        popups: { ...state.popups, showFileUploadError: true },
+      }

원하시면 액션 타입/액션 크리에이터 초안과 reducer 패치 패치를 바로 만들어 드리겠습니다.

📝 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 updatedQuestions = state.questions.map((question) =>
question.id === questionId
? {
...question,
imageFile: file,
previewImageUrl: previewUrl,
}
: question,
)
case "UPLOAD_IMAGE_START": {
const { questionId, file, previewUrl } = action.payload
const updatedQuestions = updateQuestion(state.questions, questionId, {
imageFile: file,
previewImageUrl: previewUrl,
})
return {
...state,
questions: updatedQuestions,
loading: { ...state.loading, isUploading: true },
}
}
case "UPLOAD_IMAGE_SUCCESS": {
const { questionId, imageUrl } = action.payload
const updatedQuestions = updateQuestion(state.questions, questionId, {
imageUrl,
imageFile: null,
previewImageUrl: null,
})
return {
...state,
questions: updatedQuestions,
loading: { ...state.loading, isUploading: false },
}
}
case "UPLOAD_IMAGE_ERROR":
return {
...state,
loading: { ...state.loading, isUploading: false },
popups: { ...state.popups, showFileUploadError: true },
}

Comment on lines 1 to 4
import { PrimaryBoxButton } from "@shared/design/src/components/button"
import { useQueryClient } from "@tanstack/react-query"
import { useRouter } from "next/navigation"
import { useRouter, useSearchParams } from "next/navigation"

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Next.js App Router 훅 사용 컴포넌트에 'use client' 지시어 필요

이 파일에서 useRouter, useSearchParams를 사용하므로 클라이언트 컴포넌트임을 명시해야 합니다. 상단에 "use client"가 보이지 않습니다. SSR 환경에서 런타임 오류가 발생할 수 있으니 다음과 같이 추가해주세요.

+"use client"
 import { PrimaryBoxButton } from "@shared/design/src/components/button"
 import { useQueryClient } from "@tanstack/react-query"
 import { useRouter, useSearchParams } from "next/navigation"
📝 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
import { PrimaryBoxButton } from "@shared/design/src/components/button"
import { useQueryClient } from "@tanstack/react-query"
import { useRouter } from "next/navigation"
import { useRouter, useSearchParams } from "next/navigation"
"use client"
import { PrimaryBoxButton } from "@shared/design/src/components/button"
import { useQueryClient } from "@tanstack/react-query"
import { useRouter, useSearchParams } from "next/navigation"
🤖 Prompt for AI Agents
In service/app/src/entities/game/ui/interactions/saveButton.tsx around lines 1
to 4, this component uses Next.js App Router hooks (useRouter, useSearchParams)
but lacks the "use client" directive; add a single line "use client" at the very
top of the file (before any imports) so the file is treated as a client
component and runtime errors in SSR are avoided.

Comment on lines +53 to +60
questions: state.questions.map((question, index) => ({
questionId: parseInt(question.id),
questionOrder: index,
imageUrl: question.imageUrl || "",
questionText: question.text.trim(),
questionAnswer: question.answer.trim(),
})),
}
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

questionId 숫자 변환 시 NaN → JSON null 전파 위험; 신규 질문 처리 전략 확인 필요

parseInt(question.id)는 숫자가 아닌 UUID 등의 문자열에 대해 NaN을 반환하며, JSON 직렬화 시 null로 전송됩니다. 서버가 정수만 기대한다면 400 에러가 발생할 수 있고, 신규로 추가된 질문(수정 모드에서의 새 질문)에 대한 처리 방식이 불명확합니다.

  • 제안: 숫자형 ID로 안전하게 판별되는 경우에만 questionId를 포함하고, 그렇지 않으면 누락(신규 생성으로 간주)하는 패턴을 사용하세요. 이 경우 타입 GameUpdateQuestion에서 questionId가 optional이어야 합니다.
-    questions: state.questions.map((question, index) => ({
-      questionId: parseInt(question.id),
-      questionOrder: index,
-      imageUrl: question.imageUrl || "",
-      questionText: question.text.trim(),
-      questionAnswer: question.answer.trim(),
-    })),
+    questions: state.questions.map((question, index) => {
+      const numericId =
+        /^\d+$/.test(question.id) ? Number(question.id) : undefined
+      const base = {
+        questionOrder: index,
+        imageUrl: question.imageUrl || "",
+        questionText: question.text.trim(),
+        questionAnswer: question.answer.trim(),
+      }
+      return numericId !== undefined ? { ...base, questionId: numericId } : base
+    }),

다음 스크립트로 GameUpdateQuestion 타입에서 questionId가 optional인지 확인 부탁드립니다.


🏁 Script executed:

#!/bin/bash
# GameUpdateQuestion 정의 및 questionId 선택(optional) 여부 확인
rg -n -A 5 -B 2 'interface\s+GameUpdateQuestion' service | sed -n '1,120p'
rg -n 'questionId\??:\s*number' service/app/src/entities/game/model | sed -n '1,200p'

Length of output: 801


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== gameRequest.ts ==="
sed -n '1,240p' service/app/src/entities/game/model/gameRequest.ts || true
echo
echo "=== game.ts ==="
sed -n '1,240p' service/app/src/entities/game/model/game.ts || true

Length of output: 2398


questionId 전파 불일치 — GameUpdateQuestion 인터페이스에 questionId 정의 없음

검증 결과:

  • service/app/src/entities/game/model/gameRequest.ts의 GameUpdateQuestion에는 questionId가 선언되어 있지 않습니다.
  • service/app/src/entities/game/utils/gameSave.ts는 parseInt(question.id)를 이용해 questionId를 페이로드에 포함하고 있어 NaN → JSON null 전파 및 타입/서버 불일치 위험이 있습니다.

조치(선택지):

  • 서버가 기존 질문 ID 전파를 허용하면 GameUpdateQuestion에 optional questionId?: number를 추가하고, 클라이언트에서는 숫자 ID일 때만 포함하세요(권장).
  • 서버가 ID 전파를 기대하지 않으면 gameSave.ts에서 questionId를 제거하세요.

권장 변경 예시(인터페이스에 optional 추가 + 조건부 포함):

파일: service/app/src/entities/game/model/gameRequest.ts

 export interface GameUpdateQuestion {
+  questionId?: number
   questionOrder: number
   imageUrl: string | null
   questionText: string
   questionAnswer: string
 }

파일: service/app/src/entities/game/utils/gameSave.ts

-    questions: state.questions.map((question, index) => ({
-      questionId: parseInt(question.id),
-      questionOrder: index,
-      imageUrl: question.imageUrl || "",
-      questionText: question.text.trim(),
-      questionAnswer: question.answer.trim(),
-    })),
+    questions: state.questions.map((question, index) => {
+      const numericId = /^\d+$/.test(question.id) ? Number(question.id) : undefined
+      const base = {
+        questionOrder: index,
+        imageUrl: question.imageUrl || "",
+        questionText: question.text.trim(),
+        questionAnswer: question.answer.trim(),
+      }
+      return numericId !== undefined ? { ...base, questionId: numericId } : base
+    }),

대안(서버가 ID를 기대하지 않는 경우 — 단순 제거):

-      questionId: parseInt(question.id),

수정 필요 파일:

  • service/app/src/entities/game/utils/gameSave.ts
  • (옵션) service/app/src/entities/game/model/gameRequest.ts
📝 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
questions: state.questions.map((question, index) => ({
questionId: parseInt(question.id),
questionOrder: index,
imageUrl: question.imageUrl || "",
questionText: question.text.trim(),
questionAnswer: question.answer.trim(),
})),
}
questions: state.questions.map((question, index) => {
const numericId = /^\d+$/.test(question.id) ? Number(question.id) : undefined
const base = {
questionOrder: index,
imageUrl: question.imageUrl || "",
questionText: question.text.trim(),
questionAnswer: question.answer.trim(),
}
return numericId !== undefined ? { ...base, questionId: numericId } : base
}),
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/utils/gameSave.ts around lines 53 to 60, the
code unconditionally sets questionId: parseInt(question.id) but
GameUpdateQuestion (service/app/src/entities/game/model/gameRequest.ts) does not
declare questionId, causing NaN/null and type/server mismatch; fix by either
(preferred) adding an optional questionId?: number to GameUpdateQuestion and
change gameSave.ts to only include questionId when question.id is a valid number
(parseInt yields a finite number), or (if server should not receive IDs) remove
questionId from the mapped payload in gameSave.ts; update both files accordingly
to keep types and runtime payloads consistent.

Comment on lines +220 to +233
if (presignedResponse && presignedResponse.data) {
gameData.questions = gameData.questions.map((question, index) => {
const presignedUrl = presignedResponse.data!.presignedUrls[index]
return {
...question,
imageUrl: presignedUrl ? presignedUrl.key : "",
}
})

const firstImageUrl = presignedResponse.data.presignedUrls[0]?.key
if (firstImageUrl) {
gameData.gameThumbnailUrl = firstImageUrl
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

업데이트 플로우에서도 동일한 이미지 매핑 버그 존재

위 생성 플로우와 동일하게 업데이트 로직도 매핑이 틀어질 수 있습니다. 동일한 방식으로 수정해주세요.

-    if (presignedResponse && presignedResponse.data) {
-      gameData.questions = gameData.questions.map((question, index) => {
-        const presignedUrl = presignedResponse.data!.presignedUrls[index]
-        return {
-          ...question,
-          imageUrl: presignedUrl ? presignedUrl.key : "",
-        }
-      })
-
-      const firstImageUrl = presignedResponse.data.presignedUrls[0]?.key
-      if (firstImageUrl) {
-        gameData.gameThumbnailUrl = firstImageUrl
-      }
-    }
+    if (presignedResponse && presignedResponse.data) {
+      const presignedUrls = presignedResponse.data.presignedUrls
+      let p = 0
+      gameData.questions = gameData.questions.map((question, index) => {
+        if (state.questions[index].imageFile) {
+          const presignedUrl = presignedUrls[p++]
+          return {
+            ...question,
+            imageUrl: presignedUrl ? presignedUrl.key : "",
+          }
+        }
+        return question
+      })
+
+      const firstImageUrl = presignedResponse.data.presignedUrls[0]?.key
+      if (firstImageUrl) {
+        gameData.gameThumbnailUrl = firstImageUrl
+      }
+    }
📝 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
if (presignedResponse && presignedResponse.data) {
gameData.questions = gameData.questions.map((question, index) => {
const presignedUrl = presignedResponse.data!.presignedUrls[index]
return {
...question,
imageUrl: presignedUrl ? presignedUrl.key : "",
}
})
const firstImageUrl = presignedResponse.data.presignedUrls[0]?.key
if (firstImageUrl) {
gameData.gameThumbnailUrl = firstImageUrl
}
}
if (presignedResponse && presignedResponse.data) {
const presignedUrls = presignedResponse.data.presignedUrls
let p = 0
gameData.questions = gameData.questions.map((question, index) => {
if (state.questions[index].imageFile) {
const presignedUrl = presignedUrls[p++]
return {
...question,
imageUrl: presignedUrl ? presignedUrl.key : "",
}
}
return question
})
const firstImageUrl = presignedResponse.data.presignedUrls[0]?.key
if (firstImageUrl) {
gameData.gameThumbnailUrl = firstImageUrl
}
}
🤖 Prompt for AI Agents
In service/app/src/entities/game/utils/gameSave.ts around lines 220 to 233, the
update flow has the same image-to-presigned-URL mapping bug as the create flow:
indices can become misaligned causing wrong imageUrl assignments and thumbnail
selection. Fix by reproducing the create-flow approach in the update branch:
build a lookup (e.g., map presignedResponse.data.presignedUrls by the original
image key or index you sent), then map gameData.questions by matching each
question's image identifier to the corresponding presigned URL (not relying on
array positions alone), set question.imageUrl from that matched presignedUrl.key
(or "" if missing), and set gameData.gameThumbnailUrl to the first valid
presignedUrl.key found. Ensure the update code handles missing entries safely
and mirrors the exact mapping logic used in the creation flow.

Comment on lines +34 to +35
const { gameId, gameTitle, questions } = body
return !!(gameId && gameTitle && questions)
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

생성 필드 검증: questions는 배열/비어있지 않음으로 검증하는 편이 안전합니다

현재는 truthy 체크만 하므로 빈 배열도 통과합니다. 최소한 배열 여부와 길이를 확인하는 편이 런타임 안정성이 높습니다. 또한 GameCreateRequest에 gameId가 실제로 포함되는지(서버가 생성하는 값이 아닌지) 확인이 필요합니다.

아래처럼 보강 제안드립니다:

-  const { gameId, gameTitle, questions } = body
-  return !!(gameId && gameTitle && questions)
+  const { gameId, gameTitle, questions } = body
+  return !!(gameId && gameTitle && Array.isArray(questions) && questions.length > 0)

GameCreateRequest 스키마 확인 스크립트:


🏁 Script executed:

#!/bin/bash
# GameCreateRequest에 gameId가 실제 포함되어 있는지 확인
rg -n -A 20 -B 2 $'export interface GameCreateRequest' | sed -n '1,40p'

Length of output: 1750


검증 완료 — GameCreateRequest에 gameId 포함됨; questions 검증은 배열/비어있지 않음으로 강화 필요

검증 결과: service/app/src/entities/game/model/gameRequest.ts에서 GameCreateRequest에 gameId: UUIDquestions: GameCreateQuestion[]가 선언되어 있어 gameId 포함은 확인됐습니다. 다만 mocks의 런타임 검증은 빈 배열을 통과시키므로 아래 변경이 필요합니다.

  • 수정 필요 파일: service/app/src/mocks/utils/gameHandlers.ts (현재 34–35번 라인)
  • 참조 스키마: service/app/src/entities/game/model/gameRequest.ts (export interface GameCreateRequest — gameId, questions 확인)

권장 변경(간단한 diff):

-  const { gameId, gameTitle, questions } = body
-  return !!(gameId && gameTitle && questions)
+  const { gameId, gameTitle, questions } = body
+  return !!(gameId && gameTitle && Array.isArray(questions) && questions.length > 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 { gameId, gameTitle, questions } = body
return !!(gameId && gameTitle && questions)
const { gameId, gameTitle, questions } = body
return !!(gameId && gameTitle && Array.isArray(questions) && questions.length > 0)
🤖 Prompt for AI Agents
In service/app/src/mocks/utils/gameHandlers.ts around lines 34–35, the current
runtime validation returns !!(gameId && gameTitle && questions) which allows an
empty questions array; update the check to ensure gameId and gameTitle are
present/non-empty and that questions is an array with at least one element
(e.g., use a check like gameId && gameTitle && Array.isArray(questions) &&
questions.length > 0) and return that boolean result.

Comment on lines +39 to +40
const { gameTitle, questions, version } = body
return !!(gameTitle && questions && version !== undefined)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

업데이트 필드 검증: gameThumbnailUrl 존재 여부와 version 타입을 엄격히 확인하세요

GameUpdateRequest에는 gameThumbnailUrl이 필수(값은 null 허용)입니다. 현재 검증은 이 필드 존재 여부를 확인하지 않아 undefined가 통과할 수 있습니다. 또한 version !== undefinednull도 통과시킬 수 있으므로 숫자 타입 검증으로 강화하는 편이 안전합니다. 참고: service/app/src/entities/game/model/gameRequest.ts Line 41-46.

아래처럼 보강 제안드립니다:

-  const { gameTitle, questions, version } = body
-  return !!(gameTitle && questions && version !== undefined)
+  const { gameTitle, questions, version } = body
+  return !!(
+    typeof gameTitle === "string" &&
+    gameTitle.trim() !== "" &&
+    Array.isArray(questions) &&
+    ("gameThumbnailUrl" in body) &&
+    typeof version === "number"
+  )

추가 메모:

  • questions에 대한 유효성은 이미 validateQuestionsArray가 있으므로, 중복을 피하려면 호출부에서 두 검증을 함께 사용하거나 이 함수 내부에서 해당 헬퍼를 호출하는 방향으로 일관성을 맞추는 것도 고려해볼 수 있습니다.
📝 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 { gameTitle, questions, version } = body
return !!(gameTitle && questions && version !== undefined)
const { gameTitle, questions, version } = body
return !!(
typeof gameTitle === "string" &&
gameTitle.trim() !== "" &&
Array.isArray(questions) &&
("gameThumbnailUrl" in body) &&
typeof version === "number"
)
🤖 Prompt for AI Agents
In service/app/src/mocks/utils/gameHandlers.ts around lines 39-40, the current
check returns true without verifying gameThumbnailUrl (which must exist but may
be null) and allows non-number versions; update the validation to ensure the
body has the gameTitle property, that gameThumbnailUrl is present (it may be
null but must not be undefined), that version is strictly a number (typeof
version === "number"), and that questions are validated either by calling the
existing validateQuestionsArray helper or ensuring questions is present if that
helper is used elsewhere; adjust the boolean expression to reflect these
stricter checks.

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

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

♻️ Duplicate comments (1)
.github/workflows/ci.yml (1)

73-84: CI가 멈출 수 있습니다: vercel env add는 대화형입니다 + 토큰을 CLI 인자로 넘기지 마세요

  • Line 78: vercel env add는 값/환경을 묻는 대화형 명령으로 CI가 대기 상태에 빠질 수 있습니다. 매 실행마다 새로운 값을 추가하려고 시도해 설정이 오염될 위험도 큽니다.
  • Lines 78-81: --token=${{ secrets.VERCEL_TOKEN }}처럼 토큰을 CLI 인자에 넘기면 로그/프로세스 리스트에 노출될 수 있어 보안상 안전하지 않습니다. 이전 리뷰에서도 동일 지적이 있었습니다.
  • Line 81: vercel deploy--yes가 없어 프롬프트가 발생할 수 있습니다. 또한 URL 파싱을 보장하려면 tail -n1로 마지막 줄만 취득하는 것이 안전합니다.

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

       - name: Deploy to Vercel
         if: success()
         id: deploy
-        run: |
-          cd service/app
-          vercel env add ENABLE_EXPERIMENTAL_COREPACK --token=${{ secrets.VERCEL_TOKEN }}
-          vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
-          vercel build --token=${{ secrets.VERCEL_TOKEN }}
-          url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
-          echo "PREVIEW_URL=$url" >> $GITHUB_OUTPUT
-          echo "🚀 Preview deployed to: $url"
+        working-directory: service/app
+        env:
+          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
+        run: |
+          # 대화형 env 추가 대신 --env 플래그로 비대화형 주입
+          vercel pull --yes --environment=preview
+          vercel build
+          url=$(vercel deploy --prebuilt --yes --env ENABLE_EXPERIMENTAL_COREPACK=1 | tail -n1)
+          echo "PREVIEW_URL=$url" >> $GITHUB_OUTPUT
+          echo "🚀 Preview deployed to: $url"

추가 메모:

  • ENABLE_EXPERIMENTAL_COREPACK가 Vercel 프로젝트에 이미 설정되어 있다면, --env 전달은 덮어쓰지 않고 해당 배포 컨텍스트에만 적용됩니다.
  • 장시간 대기 방지를 위해 이 스텝에 timeout-minutes: 10 같은 제한을 두는 것도 고려해 주세요.
🧹 Nitpick comments (5)
service/app/src/app/games/page.tsx (1)

88-90: 불필요한 async 및 디버그 로그 제거 제안

이 핸들러에서는 await를 사용하지 않으므로 async가 불필요하고, 콘솔 로그는 배포 환경에서 노이즈가 될 수 있습니다.

아래처럼 단순화하는 것을 제안합니다:

-  const handleLogin = async () => {
-    console.log("Login clicked")
+  const handleLogin = () => {
     router.push("/login/kakao")
   }
service/app/src/app/login/kakao/page.tsx (1)

3-8: 사용되지 않는 router 의존성 제거

router를 사용하지 않는데 import/변수/의존성 배열에 포함되어 있습니다. 제거하세요.

-import { useRouter } from "next/navigation"
 import { useEffect } from "react"
 
 export default function KakaoLoginPage() {
-  const router = useRouter()
 
   useEffect(() => {
@@
-  }, [router])
+  }, [])

Also applies to: 21-21

service/app/src/app/login/kakao/callback/page.tsx (1)

29-36: 성공 시 replace 사용 및 state 정리

성공 후 push 대신 replace를 사용하면 뒤로 가기 시 콜백 페이지로 돌아오지 않습니다. 또한 사용한 state를 정리하세요.

       try {
         await login(code)
-        // 로그인 성공 후 홈페이지로 리다이렉트
-        router.push("/")
+        // 사용한 state 제거
+        try { sessionStorage.removeItem("oauth_state") } catch {}
+        // 로그인 성공 후 홈페이지로 리다이렉트 (히스토리 치환)
+        router.replace("/")
       } catch (error) {
         console.error("Login error:", error)
         setError("로그인 처리 중 오류가 발생했습니다.")
       }
.github/workflows/ci.yml (2)

69-72: vercel CLI 버전 고정 권장 (latest 사용 지양)

vercel@latest는 릴리스 변동에 따라 워크플로가 예기치 않게 깨질 수 있습니다. 주/부 버전으로 고정하는 것을 권장합니다.

적용 예시:

-        run: npm install -g vercel@latest
+        run: npm install -g vercel@33

85-105: PR 코멘트 전 URL 검증 및 트리밍 추가 제안

steps.deploy.outputs.PREVIEW_URL이 비어있거나 잡다한 로그가 섞인 경우가 있습니다. 트리밍/검증 후 코멘트하도록 방어 로직을 권장합니다.

       - name: Comment PR with Preview URL
         if: success() && github.event_name == 'pull_request'
         uses: actions/github-script@v7
         with:
           script: |
-            const previewUrl = '${{ steps.deploy.outputs.PREVIEW_URL }}';
+            const previewUrl = ('${{ steps.deploy.outputs.PREVIEW_URL }}' || '').trim();
+            const isUrl = /^https?:\/\/\S+$/i.test(previewUrl);
+            if (!isUrl) {
+              core.warning('Preview URL이 비어있거나 올바르지 않습니다. 코멘트를 생략합니다.');
+              return;
+            }
             const commentBody = `## 🚀 Preview Deployment
 
             ✅ CI passed and preview has been deployed!
 
             **Preview URL:** ${previewUrl}
 
             📝 This preview will be updated automatically when you push new commits.`;
 
             github.rest.issues.createComment({
               issue_number: context.issue.number,
               owner: context.repo.owner,
               repo: context.repo.repo,
               body: commentBody
             });
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 28380dd and 6738aa7.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml (2 hunks)
  • service/app/src/app/games/page.tsx (1 hunks)
  • service/app/src/app/login/kakao/callback/page.tsx (1 hunks)
  • service/app/src/app/login/kakao/page.tsx (1 hunks)
  • service/app/src/widgets/HomeNavigation.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • service/app/src/widgets/HomeNavigation.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
service/app/src/app/login/kakao/callback/page.tsx (1)
service/app/src/entities/auth/hooks/useAuth.ts (1)
  • useAuth (17-68)
🔇 Additional comments (3)
service/app/src/app/games/page.tsx (1)

90-91: 라우트 기반 로그인 전환: 방향성 좋습니다

기존 programmatic login 호출 대신 카카오 로그인 경로로 라우팅하도록 바꾼 점, 현 구조(useAuth + 콜백 페이지)와 잘 맞습니다.

.github/workflows/ci.yml (2)

9-12: VERCEL_ORG_ID / VERCEL_PROJECT_ID 환경 주입 좋습니다

프로젝트/조직 ID를 글로벌 env로 노출해 CLI가 비대화형으로 동작하도록 한 점 합리적입니다.


106-123: CI 실패 알림 로직 적절합니다

실패 시 런 링크를 함께 남겨 triage를 돕는 구성 좋습니다.

Comment on lines +12 to +18
const [error, setError] = useState<string | null>(null)

useEffect(() => {
const handleCallback = async () => {
const code = searchParams.get("code")
const error = searchParams.get("error")

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

동일 식별자 재선언(변수명 충돌)로 컴파일 에러 발생

state 변수명 error를 이미 선언했는데, 동일한 이름으로 URL 파라미터를 다시 선언하고 있습니다. 변수명을 바꾸고, 이후 사용처도 변경하세요.

-  const [error, setError] = useState<string | null>(null)
+  const [error, setError] = useState<string | null>(null)
@@
-      const code = searchParams.get("code")
-      const error = searchParams.get("error")
+      const code = searchParams.get("code")
+      const oauthError = searchParams.get("error")
@@
-      if (error) {
+      if (oauthError) {
         setError("카카오 로그인 중 오류가 발생했습니다.")
         return
       }

Also applies to: 19-22

🤖 Prompt for AI Agents
In service/app/src/app/login/kakao/callback/page.tsx around lines 12-18 (also
applies to 19-22), you are shadowing the state variable error by declaring a
const error from searchParams; rename the local variable (e.g., callbackError or
urlError) and update all subsequent usages to use that new name, and when you
need to update the state call setError(callbackError) (or clear it) so there is
no duplicate identifier and no compile error.

Comment on lines +15 to +28
const handleCallback = async () => {
const code = searchParams.get("code")
const error = searchParams.get("error")

if (error) {
setError("카카오 로그인 중 오류가 발생했습니다.")
return
}

if (!code) {
setError("인증 코드를 받지 못했습니다.")
return
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

OAuth state 검증 누락: 콜백에서 state 검증 추가 필요

로그인 시작 시 생성한 state를 콜백에서 검증하지 않으면 CSRF 위험이 남습니다. 저장된 state와 일치하는지 확인하고, 불일치 시 에러 처리하세요.

   useEffect(() => {
     const handleCallback = async () => {
       const code = searchParams.get("code")
-      const oauthError = searchParams.get("error")
+      const oauthError = searchParams.get("error")
+      const state = searchParams.get("state")
+      let storedState: string | null = null
+      try {
+        storedState = sessionStorage.getItem("oauth_state")
+      } catch {}
@@
       if (!code) {
         setError("인증 코드를 받지 못했습니다.")
         return
       }
+
+      // CSRF 방지: state 일치 여부 검증
+      if (!state || !storedState || state !== storedState) {
+        setError("유효하지 않은 인증 요청입니다. 다시 시도해 주세요.")
+        return
+      }
📝 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 handleCallback = async () => {
const code = searchParams.get("code")
const error = searchParams.get("error")
if (error) {
setError("카카오 로그인 중 오류가 발생했습니다.")
return
}
if (!code) {
setError("인증 코드를 받지 못했습니다.")
return
}
const handleCallback = async () => {
const code = searchParams.get("code")
const error = searchParams.get("error")
const state = searchParams.get("state")
let storedState: string | null = null
try {
storedState = sessionStorage.getItem("oauth_state")
} catch {}
if (error) {
setError("카카오 로그인 중 오류가 발생했습니다.")
return
}
if (!code) {
setError("인증 코드를 받지 못했습니다.")
return
}
// CSRF 방지: state 일치 여부 검증
if (!state || !storedState || state !== storedState) {
setError("유효하지 않은 인증 요청입니다. 다시 시도해 주세요.")
return
}
🤖 Prompt for AI Agents
In service/app/src/app/login/kakao/callback/page.tsx around lines 15 to 28, the
callback handler reads code and error but does not validate the OAuth state;
retrieve the state param via searchParams.get("state") and compare it to the
state you saved when initiating login (from the same storage mechanism used
earlier — e.g., secure http-only cookie, session, or localStorage); if missing
or not equal, set an error (e.g., "Invalid OAuth state") and abort processing,
then remove/clear the stored state to prevent reuse. Ensure the comparison is
strict and use the same storage/key used when starting the OAuth flow.

Comment on lines +9 to +21
useEffect(() => {
const kakaoClientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID
const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI || `${window.location.origin}/login/kakao/callback`

if (!kakaoClientId) {
console.error("KAKAO_CLIENT_ID is not defined")
return
}

const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${kakaoClientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`

window.location.href = kakaoAuthUrl
}, [router])
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

OAuth CSRF 방지용 state 파라미터 누락: 보안상 반드시 추가 필요

Kakao OAuth 요청에 state 파라미터가 없어 CSRF/세션 고정 공격에 노출될 수 있습니다. 무작위 state를 생성해 저장하고 요청에 포함한 뒤, 콜백에서 검증해 주세요. 또한 로그인 페이지가 히스토리에 남지 않도록 replace 사용을 권장합니다.

아래와 같이 수정 제안합니다:

   useEffect(() => {
-    const kakaoClientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID
-    const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI || `${window.location.origin}/login/kakao/callback`
+    const kakaoClientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID
+    const redirectUri =
+      process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI ||
+      `${window.location.origin}/login/kakao/callback`
     
     if (!kakaoClientId) {
-      console.error("KAKAO_CLIENT_ID is not defined")
+      console.error("NEXT_PUBLIC_KAKAO_CLIENT_ID is not defined")
       return
     }
 
-    const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${kakaoClientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`
+    // CSRF 방지용 state 생성 및 저장
+    const state = (typeof crypto !== "undefined" && "randomUUID" in crypto)
+      ? crypto.randomUUID()
+      : Math.random().toString(36).slice(2)
+    try {
+      sessionStorage.setItem("oauth_state", state)
+    } catch {}
+
+    const kakaoAuthUrl =
+      `https://kauth.kakao.com/oauth/authorize` +
+      `?client_id=${encodeURIComponent(kakaoClientId)}` +
+      `&redirect_uri=${encodeURIComponent(redirectUri)}` +
+      `&response_type=code` +
+      `&state=${encodeURIComponent(state)}`
     
-    window.location.href = kakaoAuthUrl
-  }, [router])
+    // 히스토리에 남기지 않도록 replace 권장
+    window.location.replace(kakaoAuthUrl)
+  }, [])
📝 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
useEffect(() => {
const kakaoClientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID
const redirectUri = process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI || `${window.location.origin}/login/kakao/callback`
if (!kakaoClientId) {
console.error("KAKAO_CLIENT_ID is not defined")
return
}
const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${kakaoClientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`
window.location.href = kakaoAuthUrl
}, [router])
useEffect(() => {
const kakaoClientId = process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID
const redirectUri =
process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI ||
`${window.location.origin}/login/kakao/callback`
if (!kakaoClientId) {
console.error("NEXT_PUBLIC_KAKAO_CLIENT_ID is not defined")
return
}
// CSRF 방지용 state 생성 및 저장
const state = (typeof crypto !== "undefined" && "randomUUID" in crypto)
? crypto.randomUUID()
: Math.random().toString(36).slice(2)
try {
sessionStorage.setItem("oauth_state", state)
} catch {}
const kakaoAuthUrl =
`https://kauth.kakao.com/oauth/authorize` +
`?client_id=${encodeURIComponent(kakaoClientId)}` +
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
`&response_type=code` +
`&state=${encodeURIComponent(state)}`
// 히스토리에 남기지 않도록 replace 권장
window.location.replace(kakaoAuthUrl)
}, [])
🤖 Prompt for AI Agents
In service/app/src/app/login/kakao/page.tsx around lines 9 to 21, the Kakao
OAuth request is missing a CSRF-protection state parameter and uses
window.location.href (leaving the login page in history); generate a
cryptographically strong random state, store it (e.g., sessionStorage) before
redirecting, append the encoded state parameter to the kakaoAuthUrl, and use
router.replace (or window.location.replace) to navigate so the login page is not
kept in history; also ensure the OAuth callback route validates the returned
state against the stored value and clears it after validation.

@github-actions
Copy link

❌ CI Failed

The CI pipeline failed. Please check the logs and fix the issues before deployment.

View Details

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

♻️ Duplicate comments (2)
.github/workflows/ci.yml (1)

73-84: 토큰을 CLI 인자로 넘기지 말고 env로 주입 + deploy 비대화 옵션/URL 캡처 안정화 필요

  • secrets를 --token=...로 CLI 인자에 전달하면 로그/프로세스 리스트 노출 위험이 있습니다. 스텝 env에 VERCEL_TOKEN을 주입하고 --token 플래그를 제거하세요. [중복 지적: 이전 리뷰에서도 동일 사항 언급됨]
  • vercel deploy--yes가 없어 대화형으로 대기할 수 있습니다.
  • echo "1" | vercel env add ...는 매 실행마다 새 버전을 추가하여 비결정적 상태를 만들 수 있습니다. 이 값은 일시적/빌드타임 용도라면 스텝 env 또는 --build-env로 처리하세요.
  • cd 대신 working-directory 사용 권장.
  • 배포 URL 파싱은 tail -n1로 안정화하세요.

아래와 같이 수정 제안합니다.

-      - name: Deploy to Vercel
-        if: success()
-        id: deploy
-        run: |
-          cd service/app
-          echo "1" | vercel env add ENABLE_EXPERIMENTAL_COREPACK preview --token=${{ secrets.VERCEL_TOKEN }}
-          vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
-          vercel build --token=${{ secrets.VERCEL_TOKEN }}
-          url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
-          echo "PREVIEW_URL=$url" >> $GITHUB_OUTPUT
-          echo "🚀 Preview deployed to: $url"
+      - name: Deploy to Vercel
+        if: success()
+        id: deploy
+        working-directory: service/app
+        env:
+          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
+          ENABLE_EXPERIMENTAL_COREPACK: "1"
+        run: |
+          vercel pull --yes --environment=preview
+          vercel build
+          url=$(vercel deploy --prebuilt --yes | tail -n1)
+          echo "PREVIEW_URL=$url" >> $GITHUB_OUTPUT
+          echo "🚀 Preview deployed to: $url"

추가 권장: 포크 PR에는 시크릿이 주입되지 않아 스텝이 실패합니다. 다음과 같이 가드하면 안전합니다.

-        if: success()
+        if: success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
service/app/src/entities/game/ui/interactions/saveButton.tsx (1)

1-4: 클라이언트 훅 사용 컴포넌트에 'use client' 지시어가 누락되었습니다

useRouter, useSearchParams를 사용하므로 파일 최상단에 "use client"를 추가해야 런타임 오류를 방지할 수 있습니다.

+"use client"
 import { PrimaryBoxButton } from "@shared/design/src/components/button"
 import { useQueryClient } from "@tanstack/react-query"
 import { useRouter, useSearchParams } from "next/navigation"
🧹 Nitpick comments (6)
.github/workflows/ci.yml (2)

69-72: Vercel CLI 버전 고정 권장

latest는 런 간 재현성을 떨어뜨립니다. 메이저/마이너로 핀닝하면 예기치 못한 파손을 줄일 수 있습니다.

-        run: npm install -g vercel@latest
+        run: npm install -g vercel@33  # 또는 현재 검증된 메이저/마이너 버전으로 고정

85-105: 미리보기 URL 없을 때 코멘트 스킵 + 권한 명시 권장

  • 배포 URL이 비어있을 경우 불필요한 코멘트를 방지하기 위해 조건문을 추가하세요.
  • 저장소에서 기본 권한이 read-only인 경우를 대비해 pull-requests: write 권한을 명시하는 것이 안전합니다.
-      - name: Comment PR with Preview URL
-        if: success() && github.event_name == 'pull_request'
+      - name: Comment PR with Preview URL
+        if: success() && github.event_name == 'pull_request' && steps.deploy.outputs.PREVIEW_URL != ''

워크플로 혹은 잡 수준 권한(파일 외부 위치 수정 예시):

# workflow 수준
permissions:
  contents: read
  pull-requests: write
service/app/src/entities/game/ui/interactions/saveButton.tsx (4)

17-19: 편집 모드 판정의 안정성 보강 및 재사용을 위한 isEditing 도입 제안

빈 문자열(?gameId=)일 때 편집 모드로 잘못 인식되지 않도록 정규화하고, 모드 판정을 재사용할 수 있도록 isEditing을 도입하는 것을 제안드립니다.

-  const searchParams = useSearchParams()
-  const gameId = searchParams.get("gameId")
+  const searchParams = useSearchParams()
+  const rawGameId = searchParams.get("gameId")
+  const gameId = rawGameId?.trim() || null
+  const isEditing = !!gameId

47-58: 중복 제거 및 버전 값 안전 처리(nullish coalescing) 제안

공통 payload를 변수로 추출하면 가독성과 유지보수성이 좋아지고, 버전은 || 대신 ??로 처리해 0이 유효값인 경우를 보호할 수 있습니다.

-        let result
-        if (gameId) {
-          result = await updateExistingGame({
-            ...state,
-            questions: cleanedQuestions,
-          }, gameId, state.gameVersion || 1)
-        } else {
-          result = await saveGame({
-            ...state,
-            questions: cleanedQuestions,
-          })
-        }
+        const cleanedState = { ...state, questions: cleanedQuestions }
+        let result
+        if (isEditing && gameId) {
+          result = await updateExistingGame(
+            cleanedState,
+            gameId,
+            state.gameVersion ?? 1,
+          )
+        } else {
+          result = await saveGame(cleanedState)
+        }

60-64: 대시보드 이동 전에 캐시 무효화 완료를 보장

내비게이션 직전에 invalidateQueriesawait하면 도착 페이지에서 최신 목록을 더 안정적으로 볼 수 있습니다.

-          queryClient.invalidateQueries({ queryKey: ["infiniteMyGames"] })
-          router.push(`/dashboard`)
+          await queryClient.invalidateQueries({ queryKey: ["infiniteMyGames"] })
+          router.push("/dashboard")

82-82: 버튼 라벨 판정도 isEditing과 일치시키기

위에서 제안한 isEditing 도입과 라벨 판정 조건을 일치시켜 의도를 명확히 합니다.

-      {gameId ? "게임 수정" : "게임 저장"}
+      {isEditing ? "게임 수정" : "게임 저장"}
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6738aa7 and 8ae6321.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml (2 hunks)
  • service/app/src/entities/game/ui/interactions/saveButton.tsx (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
service/app/src/entities/game/ui/interactions/saveButton.tsx (3)
service/app/src/entities/game/model/state/create/gameCreationContext.tsx (1)
  • useGameCreationContext (77-85)
service/app/src/entities/game/model/useGamePopupActions.tsx (1)
  • useGamePopupActions (13-134)
service/app/src/entities/game/utils/gameSave.ts (2)
  • updateExistingGame (160-257)
  • saveGame (63-158)
🔇 Additional comments (3)
.github/workflows/ci.yml (2)

9-12: VERCEL_ORG_ID/PROJECT_ID 주입 방식 LGTM

조직/프로젝트 ID를 secrets에서 env로 주입하는 접근 적절합니다. 해당 시크릿이 리포 설정에 존재하는지만 확인해 주세요.


106-123: 실패 시 PR 알림 스텝 구성 적절

실패 링크 안내, 조건문 사용 모두 적절합니다.

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

9-9: updateExistingGame 도입 및 import 경로 적합

새 수정 플로우(updateExistingGame)와 생성 플로우(saveGame)를 함께 가져오는 구성 적절합니다. 상대 경로도 구현 파일(entities/game/utils/gameSave.ts)과 일치합니다.

@whdgur5717 whdgur5717 force-pushed the 106-implement-fix-game-functionality branch from 8ae6321 to eaeabc1 Compare August 14, 2025 13:13
@whdgur5717 whdgur5717 merged commit 98b8f28 into dev Aug 14, 2025
1 of 2 checks passed
@kimnamheeee kimnamheeee deleted the 106-implement-fix-game-functionality branch January 7, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants