Skip to content

관리자 페이지 신고탭 추가#270

Merged
kimnamheeee merged 37 commits into
devfrom
add-admin-layout
Jan 23, 2026
Merged

관리자 페이지 신고탭 추가#270
kimnamheeee merged 37 commits into
devfrom
add-admin-layout

Conversation

@kimnamheeee

@kimnamheeee kimnamheeee commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

📝 설명

🛠️ 주요 변경 사항

리뷰 시 고려해야 할 사항

Summary by CodeRabbit

  • 새 기능
    • 관리자 섹션 추가: 신고관리·게임관리·회원관리 탭 및 상단 네비게이션
    • 신고 목록 페이지(페이징) 및 신고 상세 미리보기(관리자용) 제공
    • 신고 처리 액션: 신고 무시, 게임 삭제, 회원 차단/차단 해제(확인 모달 포함)
    • 게임 미리보기 UI(사용자/관리자용), 질문 슬라이더 및 관리자 표/페이징 컴포넌트 추가
  • 테스트
    • 관리자 신고 페이지 전체 흐름을 검증하는 Playwright 테스트 추가

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

@coderabbitai

coderabbitai Bot commented Jan 23, 2026

Copy link
Copy Markdown

Walkthrough

관리자 페이지(레이아웃·네비게이션), 신고 조회/상세/뮤테이션 API·훅, 게임 미리보기(Admin/User 분리), 관리자 테이블 UI, 차단/해제 모달 및 MSW 목핸들러와 관련 타입·유틸을 추가하고 기존 fetchClientFactory 및 이전 GamePreview를 제거했습니다.

Changes

Cohort / File(s) 변경 요약
관리자 레이아웃·네비게이션
service/app/src/app/admin/layout.tsx, service/app/src/app/admin/_components/adminNavigation.tsx, service/app/src/app/admin/page.tsx
AdminLayout에서 validateSession 호출로 ADMIN 권한 검사 및 리다이렉트, AdminNavigation 컴포넌트 추가, /admin → /admin/reports 리다이렉트
관리자 페이지 스텁
service/app/src/app/admin/reports/page.tsx, service/app/src/app/admin/games/page.tsx, service/app/src/app/admin/users/page.tsx
Admin reports/games/users 페이지(Reports는 실제 테이블·페이징 연동) 추가
신고 API·훅·모델·유틸
service/app/src/entities/report/api/*, service/app/src/entities/report/hooks/*, service/app/src/entities/report/model/types.ts, service/app/src/entities/report/utils/*
getAdminReports/getReportDetail/updateAdminReportStatus, useAdminReports/useReportDetail/useAdminReportMutations, 타입·Zod 스키마·포맷터·매핑 유틸 추가
게임 미리보기 리팩터링
service/app/src/entities/game/ui/GamePreview/*, service/app/src/entities/game/hooks/useGameReportDetail.tsx, service/app/src/entities/game/hooks/useGamePreview.tsx, removed .../gamePreview.tsx
기존 단일 GamePreview 제거 후 Shell/User/Admin/Title/QuestionList/ReportTable 분리 및 useAdminGamePreview 오버레이 훅 추가, 기존 훅 import 경로·매핑 조정
관리자 테이블 UI 및 위젯
service/app/src/shared/ui/admin-table/*, service/app/src/widgets/admin/reported-games-table/*
AdminTableRoot/Row/Group/Text 컴포넌트와 ReportedGamesTable/ReportedGameRow 위젯 추가
차단 관련 API·모델·모달
service/app/src/entities/suspension/*, service/app/src/app/admin/users/_components/dialog/*
blockUsers API, Zod 스키마·타입, openBlockConfirmModal/openUnblockConfirmModal 모달 유틸 추가
MSW 목데이터·핸들러·응답헬퍼
service/app/src/mocks/data/*, service/app/src/mocks/fixtures/*, service/app/src/mocks/handlers/*, service/app/src/mocks/utils/responseHelpers.ts
admin 관련 MSW 핸들러(신고 목록/상세/삭제, 사용자 차단)·목데이터·에러 헬퍼 추가; auth에 /users/auth/me 핸들러 추가
페이지네이션 컴포넌트(디자인 시스템)
shared/design/src/components/pagination/*
Pagination 컴포넌트와 타입·인덱스 재수출 추가
인증 모델·validateSession 변경
service/app/src/entities/auth/model/auth.ts, service/app/src/entities/auth/api/validateSession.ts, service/app/src/mocks/handlers/auth.ts
UserRole/UserSession 타입 추가 및 validateSession 엔드포인트 /users/auth/me 및 반환타입 명시로 변경, MSW 인증 핸들러 연동
삭제된 파일
service/app/src/shared/api/fetchClientFactory.ts, service/app/src/entities/game/ui/gamePreview.tsx
기존 fetchClientFactory(인터셉터 지원)와 이전 GamePreview 컴포넌트 제거

Sequence Diagram(s)

sequenceDiagram
    actor Admin as 관리자
    participant Browser as 브라우저
    participant AdminLayout as AdminLayout
    participant AuthAPI as 인증 API
    participant MockServer as MSW/백엔드

    Admin->>Browser: /admin 접근
    Browser->>AdminLayout: 마운트 -> validateSession()
    AdminLayout->>AuthAPI: GET /users/auth/me
    AuthAPI->>MockServer: 세션 확인
    alt 비인증 또는 권한없음
        MockServer-->>AuthAPI: 401 / null
        AdminLayout-->>Browser: "/"로 리다이렉트
    else ADMIN
        MockServer-->>AuthAPI: UserSession(role="ADMIN")
        AdminLayout-->>Browser: 관리자 네비게이션 및 children 렌더
    end
Loading
sequenceDiagram
    actor Admin as 관리자
    participant ReportsPage as AdminReportsPage
    participant useAdminReports as useAdminReports
    participant ReportAPI as Report API
    participant Overlay as Overlay Kit
    participant Preview as GamePreviewAdmin

    Admin->>ReportsPage: 신고 페이지 접속
    ReportsPage->>useAdminReports: getAdminReports(page)
    useAdminReports->>ReportAPI: GET /admin/games
    ReportAPI-->>useAdminReports: reports list
    ReportsPage-->>Admin: ReportedGamesTable 렌더

    Admin->>ReportedGamesTable: 행 클릭(reportId)
    ReportedGamesTable->>Preview: openAdminPreview(reportId)
    Preview->>ReportAPI: GET /admin/games/{reportId}
    ReportAPI-->>Preview: reportDetail
    Preview->>Overlay: overlay.open(GamePreviewAdmin)
    Admin->>Preview: "게임 삭제" 클릭
    Preview->>ReportAPI: POST /admin/games/delete {reportId, status: DELETE_GAME}
    ReportAPI-->>Preview: success
    Preview->>useAdminReports: invalidateQuery("adminReports")
    useAdminReports->>ReportAPI: GET /admin/games (refetch)
    ReportAPI-->>useAdminReports: updated list
    Preview->>Overlay: overlay.close()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

✨feature, 👩🏻‍💻frontend

Suggested reviewers

  • whdgur5717

Poem

"나는 토끼, 코드 당근 쫓아왔네 🥕
관리자 탭 번쩍, 네비 반짝 빛나네,
신고 보고, 게임 보고, 유저도 관찰해요,
모달은 '예/아니요' 탁! 눌러 닫고 열고,
토끼가 축하해요 — 새 관리창 오픈!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 템플릿의 모든 필수 섹션이 비어있으며, 실제 내용이 작성되지 않았습니다. 작업 내용 요약, 주요 변경 사항(커밋 SHA 포함), 그리고 리뷰 시 고려사항을 작성해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 정확히 반영합니다. 관리자 페이지에 신고탭을 추가하는 변경사항과 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

Copy link
Copy Markdown

테스트 실패

테스트가 실패했습니다. Actions 로그를 확인해주세요.

@github-actions

Copy link
Copy Markdown

테스트 실패

테스트가 실패했습니다. Actions 로그를 확인해주세요.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In `@service/app/src/entities/auth/api/validateSession.ts`:
- Around line 3-8: The current validateSession implementation calls
fetchClient.get<null> which discards the session type and never returns null on
failure; change the generic to fetchClient.get<UserSessionResponse> and add a
failure branch (check response.ok or catch network/errors) so that on non-2xx
responses or exceptions the function returns null; update validateSession to
parse and return the UserSessionResponse on success and null on failure
(referencing validateSession, fetchClient.get, and UserSessionResponse).

In `@service/app/src/entities/game/hooks/useGameReportDetail.tsx`:
- Around line 58-67: The GamePreviewAdmin prop is receiving a misspelled field
(data.quetionCount) so the displayed question count is empty; in
useGameReportDetail (where GamePreviewAdmin is returned) change the prop
assignment to pass data.questionCount instead of data.quetionCount so the
correct value flows into the GamePreviewAdmin component (verify the prop name
questionCount and the data property questionCount align).

In `@service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx`:
- Around line 26-106: The outer row containers are missing role="row" (you used
role="table" elsewhere), so add role="row" to each of the three top-level divs
that wrap each row (the divs starting with className "flex w-full items-center
..." — e.g., the blocks for 제작자, 신고자, 항목) so the row semantics match the
existing role="table" and the inner headers/cells (role="rowheader" and
role="cell") are correctly associated.

In `@service/app/src/entities/report/api/getReportDetail.ts`:
- Around line 5-10: The function getReportDetail uses a .then() chain on
response.json(), violating the async/await guideline; update it to await the
JSON parsing instead: after calling fetchClient.get in getReportDetail, await
response.json() into a variable (e.g., const res = await response.json()) and
return res.data, removing the .then() usage so the function consistently uses
async/await.

In `@service/app/src/entities/report/model/types.ts`:
- Around line 58-65: The ReportDetailData interface has a typo: the field is
named quetionCount but should be questionCount; update the property name in the
ReportDetailData interface to questionCount and then update all
references/usages (tests, serializers, mappers, consumers) that read or write
ReportDetailData so they use ReportDetailData.questionCount instead of
quetionCount to ensure the API/consumer fields match.

In `@service/app/src/entities/report/utils/formatReportDate.ts`:
- Around line 1-6: formatReportDate currently uses local-date getters
(getFullYear/getMonth/getDate) which can shift the day in non-UTC timezones;
change it to use UTC getters: getUTCFullYear, getUTCMonth (plus 1), and
getUTCDate, and keep the existing zero-padding logic so the returned string is
consistently YYYY-MM-DD in UTC.

In `@service/app/src/mocks/handlers/admin.ts`:
- Around line 39-50: The page parsing currently sets page to parseInt(pageParam,
10) without handling NaN which can break pagination; change the logic around
pageParam/parseInt so you parse into a temporary variable (e.g., const parsed =
parseInt(pageParam, 10)) and then set page = pageParam ? (Number.isNaN(parsed) ?
0 : Math.max(0, parsed)) : 0 so NaN falls back to 0 and negative values are
clamped; update references to page (used with PAGE_SIZE, startIndex, endIndex,
hasNext, totalPages) accordingly.

In `@service/app/src/shared/ui/admin-table/AdminTableRoot.tsx`:
- Around line 1-3: Rename the file from AdminTableRoot.tsx to camelCase (e.g.,
adminTableRoot.tsx) and update all imports and barrel exports that reference
AdminTableRoot to the new filename; keep the internal React component/identifier
(AdminTableRoot) PascalCase inside the file but change all import paths (and any
export lists) that used "AdminTableRoot" as a module path to the new
"adminTableRoot" path so module resolution stays correct.

In `@service/app/src/shared/ui/admin-table/AdminTableRow.tsx`:
- Line 1: Rename the file AdminTableRow.tsx to adminTableRow.tsx and update all
imports and barrel exports that reference "AdminTableRow" (e.g., any import
paths or index/barrel files exporting AdminTableRow) to use the new camelCase
filename "adminTableRow". Ensure the component's exported symbol name (if
default or named) remains unchanged so no other code changes are needed beyond
adjusting the import paths and barrel entries.

In `@service/app/src/shared/ui/admin-table/AdminTableText.tsx`:
- Around line 1-32: Rename the component file from AdminTableText.tsx to
adminTableText.tsx to follow camelCase file naming; update all imports/usages of
AdminTableText (e.g., import/exports in barrels and any files importing the
component) to reference the new filename while keeping the exported symbol name
AdminTableText unchanged, and verify barrel files (index.ts/exports) and any
path-based imports are updated so builds/tests pass.

In `@service/app/src/shared/ui/admin-table/index.ts`:
- Line 5: index.ts re-exports a missing module "useRowSelection", causing
TS2307; either add the missing module file (implementing the hook named
useRowSelection), correct the import path/casing in the export statement in
service/app/src/shared/ui/admin-table/index.ts to match the actual filename, or
remove the export if the hook is unused. Locate the export line "export * from
\"./useRowSelection\"" in index.ts and then (a) add a corresponding
useRowSelection.ts(x) that exports the hook, or (b) fix the relative
path/filename capitalization to the real file, or (c) delete this export when
there are no consumers.

In `@service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx`:
- Around line 13-20: The ReportedGameRow component's AdminTableRow is focusable
but doesn't handle keyboard activation; update ReportedGameRow to call the same
onClick handler when Enter or Space is pressed (e.g., implement an onKeyDown
that checks for "Enter" or " " / "Spacebar" and invokes onClick?.(item)), add
role="button" to AdminTableRow if it isn't a semantic button, and ensure you
preventDefault for Space to avoid page scroll; keep aria-label and tabIndex
as-is so screen reader and keyboard users can activate the row.

In
`@service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx`:
- Around line 1-52: The filename uses PascalCase instead of the project's
camelCase filename convention; rename the file containing the ReportedGamesTable
React component to reportedGamesTable.tsx and update any imports referencing
ReportedGamesTable to use the new filename (e.g., imports of ReportedGamesTable
or ReportedGameRow where this component is imported). Ensure the exported
component name ReportedGamesTable remains unchanged, update any related
barrel/index exports if present, and run a quick typecheck to catch any stale
import paths.
🧹 Nitpick comments (14)
service/app/src/entities/report/api/updateAdminReportStatus.ts (1)

5-10: 엔드포인트 명명은 개선 가능하지만 기능은 정상 작동합니다.

updateAdminReportStatus는 payload의 status 필드("IGNORE_REPORT" 또는 "DELETE_GAME")에 따라 두 가지 액션을 모두 처리합니다. 백엔드 mock handler가 두 경우를 모두 정상 처리하고 있으며, hook에서도 ignore와 delete 모두에 동일하게 사용하고 있습니다. 따라서 무시/차단 기능이 깨지지 않습니다.

다만 /admin/games/delete 엔드포인트 명이 여러 액션을 담당한다는 의도를 드러내지 못해, 더 명확한 명칭(예: /admin/games/report-action)으로 개선하는 것을 검토해볼 수 있습니다.

service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx (1)

1-1: 파일명이 PascalCase입니다 — camelCase로 변경 필요
코딩 가이드라인에 따라 ReportedGameRow.tsxreportedGameRow.tsx처럼 camelCase로 맞춰 주세요. As per coding guidelines, ...

service/app/src/entities/game/ui/GamePreview/gamePreviewShell.tsx (2)

20-25: onOpenChange 핸들러가 전달받은 상태 값을 무시합니다.

onOpenChange는 다이얼로그 상태 변경 시 boolean 값을 전달받습니다. 현재 구현에서는 이 값을 무시하고 항상 onClose를 호출하므로, 다이얼로그가 열리는 경우에도 onClose가 호출될 수 있습니다.

♻️ 제안하는 수정사항
-  const handleClose = () => {
-    onClose?.()
-  }
-
   return (
-    <Dialog open={isOpen} onOpenChange={handleClose}>
+    <Dialog open={isOpen} onOpenChange={(open) => !open && onClose?.()}>

26-28: cn() 유틸리티 사용을 권장합니다.

템플릿 리터럴 대신 cn() 유틸리티를 사용하면 클래스 병합이 더 안전하고 가독성이 향상됩니다. 특히 외부에서 전달된 className이 기존 스타일을 오버라이드해야 할 때 유용합니다.

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

27-28: blurDataURL을 상수로 추출하는 것을 고려해주세요.

긴 base64 문자열이 JSX 내에 인라인으로 포함되어 있어 가독성이 저하됩니다. 상수나 공용 유틸리티로 추출하면 재사용성과 가독성이 향상됩니다.

service/app/src/entities/report/hooks/useAdminReportMutations.ts (2)

4-4: useQueryClient() 훅 사용을 권장합니다.

직접 import한 queryClient를 사용하는 대신 useQueryClient() 훅을 사용하는 것이 React Query의 권장 패턴입니다. 이 방식은 SSR 환경에서 더 안전하고 테스트 시 모킹이 용이합니다.

♻️ 제안하는 수정사항
-import { useMutation } from "@tanstack/react-query"
+import { useMutation, useQueryClient } from "@tanstack/react-query"

 import { blockUsers } from "@/entities/suspension/api/blockUsers"
-import { queryClient } from "@/shared/lib/queryClient"

 import { updateAdminReportStatus } from "../api/updateAdminReportStatus"

 export const useAdminReportMutations = () => {
+  const queryClient = useQueryClient()
+
   const ignoreReportMutation = useMutation({

17-23: 게임 삭제 후 reportDetail 쿼리도 무효화가 필요할 수 있습니다.

deleteGameMutation 성공 시 adminReports만 무효화하고 있습니다. 신고 상세 팝업이 열린 상태에서 게임을 삭제하면 reportDetail 데이터도 갱신이 필요할 수 있습니다. ignoreReportMutation도 동일한 상황입니다.

♻️ 제안하는 수정사항
   const deleteGameMutation = useMutation({
     mutationFn: (reportId: number) =>
       updateAdminReportStatus({ status: "DELETE_GAME", reportId }),
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ["adminReports"] })
+      queryClient.invalidateQueries({ queryKey: ["reportDetail"] })
     },
   })
service/app/src/entities/report/api/getAdminReports.ts (1)

9-17: Promise 체인 대신 async/await 사용 권장

이미 async 함수이므로 response.json() 결과를 await로 처리하면 흐름이 단순해집니다. 코딩 가이드라인에 따라.

♻️ 개선안
   const response = await fetchClient.get<AdminReportsResponse>(
     `admin/games${queryString}`,
   )
-  return response.json().then((res) => res.data)
+  const { data } = await response.json()
+  return data
service/app/src/app/admin/layout.tsx (1)

18-30: 세션 검증 로직을 async/await로 정리

Promise 체인 대신 비동기 함수로 처리하면 흐름이 단순해지고 규칙에도 맞습니다. 코딩 가이드라인에 따라.

♻️ 개선안
   useEffect(() => {
-    validateSession()
-      .then((user) => {
-        if (!user || user.data?.role !== "ADMIN") {
-          router.replace("/")
-          return
-        }
-        setChecked(true)
-      })
-      .catch(() => {
-        router.replace("/")
-      })
+    const checkSession = async () => {
+      try {
+        const user = await validateSession()
+        if (!user || user.data?.role !== "ADMIN") {
+          router.replace("/")
+          return
+        }
+        setChecked(true)
+      } catch {
+        router.replace("/")
+      }
+    }
+    void checkSession()
   }, [router])
service/app/src/shared/ui/admin-table/AdminTableGroup.tsx (1)

1-2: 파일명 PascalCase → camelCase 정리 필요

파일명이 AdminTableGroup.tsx로 PascalCase입니다. adminTableGroup.tsx로 변경하고 배럴/임포트를 함께 갱신하세요. 코딩 가이드라인에 따라.

service/app/src/app/admin/reports/page.tsx (1)

4-12: Zustand 사용 규칙 준수 여부 확인 필요 (Line 4-12)
가이드라인에 “클라이언트 상태는 Zustand 사용”이 명시되어 있는데, 여기서는 useState로 페이지 상태를 관리하고 있습니다. 로컬 상태 예외인지 확인하고, 필요하다면 Zustand로 이전하거나 예외 기준을 문서화해 주세요.

service/app/src/shared/ui/admin-table/AdminTableRoot.tsx (1)

4-20: 접근성: aria-label 기본값 또는 필수화 필요 (Line 4-20)
role="table" 요소에 접근 가능한 이름이 없으면 스크린리더 접근성이 떨어질 수 있습니다. ariaLabel을 필수로 하거나 기본값을 부여하는 쪽을 권장합니다.

♿️ 제안 수정안
-interface AdminTableRootProps {
+interface AdminTableRootProps {
   children: ReactNode
   className?: string
-  ariaLabel?: string
+  ariaLabel?: string
 }

 export const AdminTableRoot = ({
   children,
   className,
-  ariaLabel,
+  ariaLabel = "관리자 테이블",
 }: AdminTableRootProps) => {
service/app/src/mocks/data/admin.ts (1)

42-81: 전역 Math.random 덮어쓰기는 피하세요.
동기 함수라도 전역 변경은 테스트 간 간섭 위험이 있습니다. 로컬 PRNG 함수로 대체하는 편이 안전합니다.

♻️ 제안 변경
 export const generateMockAdminReports = (
   count: number,
   seed?: number,
 ): AdminReport[] => {
-  let originalRandom: (() => number) | undefined
-
-  if (seed !== undefined) {
-    originalRandom = Math.random
-    let currentSeed = seed
-    Math.random = () => {
-      const x = Math.sin(currentSeed++) * 10000
-      return x - Math.floor(x)
-    }
-  }
+  let random = Math.random
+  if (seed !== undefined) {
+    let currentSeed = seed
+    random = () => {
+      const x = Math.sin(currentSeed++) * 10000
+      return x - Math.floor(x)
+    }
+  }

   const result: AdminReport[] = []
   for (let i = 0; i < count; i++) {
-    const randomIndex = Math.floor(Math.random() * gameNames.length)
-    const creatorIndex = Math.floor(Math.random() * creatorNames.length)
-    const reporterIndex = Math.floor(Math.random() * reporterNames.length)
-    const daysAgo = Math.floor(Math.random() * 30)
+    const randomIndex = Math.floor(random() * gameNames.length)
+    const creatorIndex = Math.floor(random() * creatorNames.length)
+    const reporterIndex = Math.floor(random() * reporterNames.length)
+    const daysAgo = Math.floor(random() * 30)
@@
-  if (originalRandom) {
-    Math.random = originalRandom
-  }
-
   return result
 }
shared/design/src/components/pagination/pagination.tsx (1)

49-88: currentPage 범위 보호를 추가해주세요.

상위에서 범위를 벗어난 값이 들어오면 계산/핸들러가 일관되지 않을 수 있어, 내부에서 최소/최대 클램프를 두는 편이 안전합니다.

♻️ 제안 변경
+    const safeCurrentPage = Math.min(
+      Math.max(currentPage, 1),
+      totalPages,
+    )
+
     const handlePrev = () => {
-      if (currentPage > 1) {
-        onPageChange(currentPage - 1)
+      if (safeCurrentPage > 1) {
+        onPageChange(safeCurrentPage - 1)
       }
     }

     const handleNext = () => {
-      if (currentPage < totalPages) {
-        onPageChange(currentPage + 1)
+      if (safeCurrentPage < totalPages) {
+        onPageChange(safeCurrentPage + 1)
       }
     }

     const handlePageClick = (page: number) => {
-      if (page !== currentPage) {
+      if (page !== safeCurrentPage) {
         onPageChange(page)
       }
     }

     const getVisiblePages = () => {
       const pages: number[] = []
       const half = Math.floor(maxVisible / 2)

-      let start = Math.max(1, currentPage - half)
+      let start = Math.max(1, safeCurrentPage - half)
       const end = Math.min(totalPages, start + maxVisible - 1)

Comment thread service/app/src/entities/auth/api/validateSession.ts Outdated
Comment thread service/app/src/entities/game/hooks/useGameReportDetail.tsx
Comment thread service/app/src/entities/report/api/getReportDetail.ts
Comment thread service/app/src/entities/report/model/types.ts
@@ -0,0 +1,29 @@
import { cn } from "@ject-5-fe/design/utils/cn"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

파일명이 camelCase 규칙을 위반합니다.

AdminTableRow.tsx는 PascalCase라서 규칙 위반입니다. adminTableRow.tsx로 변경하고 배럴/임포트 경로도 함께 갱신해 주세요. 코딩 가이드라인에 따라, 반드시 camelCase 파일명을 사용해야 합니다.

🤖 Prompt for AI Agents
In `@service/app/src/shared/ui/admin-table/AdminTableRow.tsx` at line 1, Rename
the file AdminTableRow.tsx to adminTableRow.tsx and update all imports and
barrel exports that reference "AdminTableRow" (e.g., any import paths or
index/barrel files exporting AdminTableRow) to use the new camelCase filename
"adminTableRow". Ensure the component's exported symbol name (if default or
named) remains unchanged so no other code changes are needed beyond adjusting
the import paths and barrel entries.

Comment on lines +1 to +32
import { cn } from "@ject-5-fe/design/utils/cn"
import type { ReactNode } from "react"

type AdminTableTextTone = "header" | "body"

interface AdminTableTextProps {
children: ReactNode
className?: string
tone?: AdminTableTextTone
}

export const AdminTableText = ({
children,
className,
tone = "body",
}: AdminTableTextProps) => {
const header =
"typography-heading-md-medium text-text-interactive-secondary-hovered"
const body = "typography-heading-md-medium text-text-interactive-input-filled"

return (
<span
className={cn(
"min-w-0 truncate",
tone === "header" ? header : body,
className,
)}
role={tone === "header" ? "columnheader" : "cell"}
>
{children}
</span>
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

파일명이 camelCase 규칙을 위반합니다.

가이드라인상 파일명은 camelCase여야 합니다. AdminTableText.tsxadminTableText.tsx로 변경하고 관련 import/barrel도 함께 수정해 주세요.

🤖 Prompt for AI Agents
In `@service/app/src/shared/ui/admin-table/AdminTableText.tsx` around lines 1 -
32, Rename the component file from AdminTableText.tsx to adminTableText.tsx to
follow camelCase file naming; update all imports/usages of AdminTableText (e.g.,
import/exports in barrels and any files importing the component) to reference
the new filename while keeping the exported symbol name AdminTableText
unchanged, and verify barrel files (index.ts/exports) and any path-based imports
are updated so builds/tests pass.

Comment thread service/app/src/shared/ui/admin-table/index.ts Outdated
Comment on lines +13 to +20
export const ReportedGameRow = ({ item, onClick }: ReportedGameRowProps) => {
return (
<AdminTableRow
className="cursor-pointer gap-72"
onClick={() => onClick?.(item)}
aria-label={`게임명 ${item.title} 제작자 ${item.creatorName} 신고자 ${item.reporterName} 신고일자 ${item.reportedAt} 처리 여부 ${item.statusLabel}`}
tabIndex={0}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

키보드로 행 클릭이 불가합니다 (접근성).
tabIndex는 있지만 Enter/Space 처리 없어서 키보드 사용자에게는 동작하지 않습니다. 키보드 활성화를 추가해 주세요.

🛠️ 제안 수정
-    <AdminTableRow
-      className="cursor-pointer gap-72"
-      onClick={() => onClick?.(item)}
+    <AdminTableRow
+      className={onClick ? "cursor-pointer gap-72" : "gap-72"}
+      onClick={() => onClick?.(item)}
+      onKeyDown={(event) => {
+        if (!onClick) return
+        if (event.key === "Enter" || event.key === " ") {
+          event.preventDefault()
+          onClick(item)
+        }
+      }}
       aria-label={`게임명 ${item.title} 제작자 ${item.creatorName} 신고자 ${item.reporterName} 신고일자 ${item.reportedAt} 처리 여부 ${item.statusLabel}`}
-      tabIndex={0}
+      tabIndex={onClick ? 0 : -1}
     >
📝 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 ReportedGameRow = ({ item, onClick }: ReportedGameRowProps) => {
return (
<AdminTableRow
className="cursor-pointer gap-72"
onClick={() => onClick?.(item)}
aria-label={`게임명 ${item.title} 제작자 ${item.creatorName} 신고자 ${item.reporterName} 신고일자 ${item.reportedAt} 처리 여부 ${item.statusLabel}`}
tabIndex={0}
>
export const ReportedGameRow = ({ item, onClick }: ReportedGameRowProps) => {
return (
<AdminTableRow
className={onClick ? "cursor-pointer gap-72" : "gap-72"}
onClick={() => onClick?.(item)}
onKeyDown={(event) => {
if (!onClick) return
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
onClick(item)
}
}}
aria-label={`게임명 ${item.title} 제작자 ${item.creatorName} 신고자 ${item.reporterName} 신고일자 ${item.reportedAt} 처리 여부 ${item.statusLabel}`}
tabIndex={onClick ? 0 : -1}
>
🤖 Prompt for AI Agents
In `@service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx`
around lines 13 - 20, The ReportedGameRow component's AdminTableRow is focusable
but doesn't handle keyboard activation; update ReportedGameRow to call the same
onClick handler when Enter or Space is pressed (e.g., implement an onKeyDown
that checks for "Enter" or " " / "Spacebar" and invokes onClick?.(item)), add
role="button" to AdminTableRow if it isn't a semantic button, and ensure you
preventDefault for Space to avoid page scroll; keep aria-label and tabIndex
as-is so screen reader and keyboard users can activate the row.

Comment on lines +1 to +52
import type { ReportedGame } from "@/entities/report/model/types"
import {
AdminTableGroup,
AdminTableRoot,
AdminTableRow,
AdminTableText,
} from "@/shared/ui/admin-table"

import { ReportedGameRow } from "./ReportedGameRow"

interface ReportedGamesTableProps {
items: ReportedGame[]
onRowClick?: (item: ReportedGame) => void
}

export const ReportedGamesTable = ({
items,
onRowClick,
}: ReportedGamesTableProps) => {
return (
<AdminTableRoot ariaLabel="신고접수 테이블">
<AdminTableRow
variant="header"
className="gap-72"
aria-label="신고접수 테이블 헤더"
>
<AdminTableGroup className="w-full gap-56 md:min-w-[400px] lg:min-w-[800px]">
<AdminTableText tone="header" className="min-w-[45px]">
번호
</AdminTableText>
<AdminTableText tone="header">게임명</AdminTableText>
</AdminTableGroup>

<AdminTableGroup className="w-full gap-60">
<AdminTableText tone="header">제작자</AdminTableText>
<AdminTableText tone="header">신고자</AdminTableText>
</AdminTableGroup>

<AdminTableGroup className="w-full gap-24">
<AdminTableText tone="header" className="min-w-[200px]">
신고일자
</AdminTableText>
<AdminTableText tone="header">처리 여부</AdminTableText>
</AdminTableGroup>
</AdminTableRow>

{items.map((item) => (
<ReportedGameRow key={item.id} item={item} onClick={onRowClick} />
))}
</AdminTableRoot>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

파일명은 camelCase 규칙을 따라야 합니다.
현재 파일명 ReportedGamesTable.tsx는 PascalCase입니다. 가이드에 맞게 reportedGamesTable.tsx로 변경해 주세요.

🤖 Prompt for AI Agents
In `@service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx`
around lines 1 - 52, The filename uses PascalCase instead of the project's
camelCase filename convention; rename the file containing the ReportedGamesTable
React component to reportedGamesTable.tsx and update any imports referencing
ReportedGamesTable to use the new filename (e.g., imports of ReportedGamesTable
or ReportedGameRow where this component is imported). Ensure the exported
component name ReportedGamesTable remains unchanged, update any related
barrel/index exports if present, and run a quick typecheck to catch any stale
import paths.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@service/app/src/app/admin/reports/__tests__/reports.spec.ts`:
- Around line 122-132: The assertion is checking the wrong row after deleting
the clicked game: update the "삭제되지 않은 게임..." test to verify the same row you
clicked (use reportedGameRows.first() or reportedGameRows.nth(0) instead of
reportedGameRows.nth(1)) so it matches clickReportedGameRow(0); also ensure you
await any state update after clickDeleteGame (the existing await
expect(pom.reportDetail).not.toBeVisible() can remain) before asserting the
row's aria-label to avoid flakiness.
🧹 Nitpick comments (2)
service/app/src/app/admin/reports/__tests__/reports.spec.ts (2)

14-20: 주석 처리된 테스트 코드를 추적하거나 구현하세요.

관리자가 아닌 로그인 사용자에 대한 리다이렉트 테스트가 주석 처리되어 있습니다. RBAC(역할 기반 접근 제어) 검증에 중요한 테스트이므로, 구현 계획이 있다면 이슈로 추적하고 주석 코드는 제거하는 것이 좋습니다.

이 테스트 케이스 구현을 위한 이슈를 생성해 드릴까요?


24-24: 관리자 테스트에 admin.json과 같이 역할을 명확히 나타내는 인증 상태 파일명을 사용하세요.

현재 관리자 페이지 테스트들이 모두 playwright/.auth/user.json을 사용하고 있는데, 파일명이 일반 사용자를 나타내어 혼동을 초래할 수 있습니다. 특히 코드의 17번 줄 주석에서 "USER role로 로그인 상태 설정 필요"라고 명시되어 있어, 향후 USER 역할과 ADMIN 역할을 분리할 때 admin.json으로 이름을 변경하면 역할 구분을 더 명확하게 할 수 있습니다.

Comment thread service/app/src/app/admin/reports/__tests__/reports.spec.ts
@github-actions

github-actions Bot commented Jan 23, 2026

Copy link
Copy Markdown

Deploy preview for re-creation ready!

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

Built with commit 318b593.
This pull request is being automatically deployed with vercel-action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@service/app/src/app/admin/reports/__tests__/reports.spec.ts`:
- Around line 5-21: The commented-out authorization test inside the
reports.spec.ts ("로그인 사용자라도 ADMIN이 아닌 경우 홈으로 리다이렉트 되어야 한다") should not remain as
a raw comment; either (A) open a tracked issue referencing that test and remove
the commented block, or (B) restore it as a managed test by converting the
comment to a skipped or TODO test (e.g., use test.skip with a short reason) or
implement it properly using your auth helpers (e.g., loginAsUser /
setAuthCookie) then assert navigation from page.goto("/admin/reports") to
page.waitForURL("/"); update the test suite around the "인증 및 권한" describe block
accordingly so the missing-case is discoverable and actionable.

Comment on lines +5 to +21
test.describe("신고접수 페이지", () => {
test.describe("인증 및 권한", () => {
test("비로그인 사용자는 홈으로 리다이렉트 되어야 한다", async ({
page,
}) => {
await page.goto("/admin/reports")
await page.waitForURL("/")
})

// test("로그인 사용자라도 ADMIN이 아닌 경우 홈으로 리다이렉트 되어야 한다", async ({
// page,
// }) => {
// // TODO: USER role로 로그인 상태 설정 필요
// await page.goto("/admin/reports")
// await page.waitForURL("/")
// })
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

주석 처리된 권한 테스트는 추적이 어렵습니다.
해당 케이스는 주석 블록 대신 이슈로 등록하거나 테스트 형태로 관리해 주세요. 필요하시면 구현 지원 가능합니다.

🤖 Prompt for AI Agents
In `@service/app/src/app/admin/reports/__tests__/reports.spec.ts` around lines 5 -
21, The commented-out authorization test inside the reports.spec.ts ("로그인 사용자라도
ADMIN이 아닌 경우 홈으로 리다이렉트 되어야 한다") should not remain as a raw comment; either (A)
open a tracked issue referencing that test and remove the commented block, or
(B) restore it as a managed test by converting the comment to a skipped or TODO
test (e.g., use test.skip with a short reason) or implement it properly using
your auth helpers (e.g., loginAsUser / setAuthCookie) then assert navigation
from page.goto("/admin/reports") to page.waitForURL("/"); update the test suite
around the "인증 및 권한" describe block accordingly so the missing-case is
discoverable and actionable.

@kimnamheeee kimnamheeee merged commit b5c45d2 into dev Jan 23, 2026
2 checks passed
@whdgur5717

Copy link
Copy Markdown
Contributor

4354cb9 이 커밋은 reset하는게 좋을것 같습니다. 이부분이랑 테스트코드 연관된부분은 제가 수정할게요

그리고 aria태그나 role은 정말 최소한만 사용하는걸로 하면 좋을것 같습니다. html 시멘틱태그를 우선 사용하고, 모르는 부분은 그냥 안 건드는게 접근성 측면에서는 더 좋을 것 같습니다. 만약 테스트에 필요하다면 차라리 data-test-id같은 data attribute를 심는게 aria태그 오용보다는 더 좋을 것 같습니다.

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.

2 participants