Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7a8f5cb
chore(assets): remove unused files
kimnamheeee Jan 15, 2026
f9b8724
chore: remove unused file
kimnamheeee Jan 15, 2026
38fc9a3
feat(admin): add navigation ui
kimnamheeee Jan 15, 2026
eff0943
feat(admin): add auth guard
kimnamheeee Jan 15, 2026
2ef3e4c
feat(admin): add link / separate className
kimnamheeee Jan 15, 2026
f8ad64f
feat(admin): use /admin as alias
kimnamheeee Jan 15, 2026
72ffc7f
feat(admin/users): add block, unblock confirm dialog
kimnamheeee Jan 17, 2026
ce65ea9
feat(msw): add auth handler
kimnamheeee Jan 17, 2026
a7f87e5
fix(figma-plugin): apply sanitation for figma.mixed
kimnamheeee Jan 17, 2026
b773d40
feat(admin): add admin report handling
kimnamheeee Jan 17, 2026
0cfea3f
feat(admin): add basic routes
kimnamheeee Jan 17, 2026
ec9b7f8
Revert "fix(figma-plugin): apply sanitation for figma.mixed"
kimnamheeee Jan 19, 2026
d6bfd7d
refactor(gamepreview): update gamepreview component using composition
kimnamheeee Jan 19, 2026
899ff75
feat(components): add pagination
kimnamheeee Jan 19, 2026
5dc73d2
style(pagination): update selected style
kimnamheeee Jan 20, 2026
985b716
feat(admin): add reported games table
kimnamheeee Jan 20, 2026
273521a
feat(admin): add table ui to reports page
kimnamheeee Jan 20, 2026
75aad17
feat(msw): add report detail handler
kimnamheeee Jan 20, 2026
6422647
fix(msw): change endpoint
kimnamheeee Jan 20, 2026
65cdcd9
feat(admin): open preview on row click
kimnamheeee Jan 20, 2026
ab4b5e3
feat(msw): add report delete handler
kimnamheeee Jan 20, 2026
0721e20
feat(msw): add user block, delete game api
kimnamheeee Jan 20, 2026
575f22b
feat(admin): match api call on admin game preview
kimnamheeee Jan 21, 2026
234b7eb
fix(admin): fix report id type to number, fix enum type for report
kimnamheeee Jan 21, 2026
d1a0b8a
fix(admin): move directory
kimnamheeee Jan 21, 2026
4354cb9
feat(reports): add a11y tags
kimnamheeee Jan 23, 2026
02d8496
test(reports): add e2e tests for reports page
kimnamheeee Jan 23, 2026
c582a16
refactor(msw): change admin handler to be fixture based
kimnamheeee Jan 23, 2026
f60de70
feat(reportdetail): add block state and report status
kimnamheeee Jan 23, 2026
89ea011
feat(reports): use react query on reportDetail api
kimnamheeee Jan 23, 2026
a4568f4
merge: resolve conflict with dev branch
kimnamheeee Jan 23, 2026
61dc5dd
fix: remove unused export
kimnamheeee Jan 23, 2026
4039928
test(reports): update test logic
kimnamheeee Jan 23, 2026
00f2b8f
fix(session): update type
kimnamheeee Jan 23, 2026
4e9b7c1
fix: fix typo
kimnamheeee Jan 23, 2026
e5ea0a6
refactor(report): use async / await
kimnamheeee Jan 23, 2026
318b593
test(reports): update test selector
kimnamheeee Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed service/app/public/profiles/프로필01.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필02.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필03.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필04.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필05.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필06.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필07.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필08.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필09.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필10.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필11.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필12.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필13.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필14.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필15.png
Binary file not shown.
Binary file removed service/app/public/profiles/프로필16.png
Binary file not shown.
65 changes: 65 additions & 0 deletions service/app/src/app/admin/_components/adminNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { SecondaryPlainBoxButton } from "@ject-5-fe/design/components/button"
import { Navigation } from "@ject-5-fe/design/components/navigation"
import { cn } from "@ject-5-fe/design/utils/cn"
import Link from "next/link"
import { usePathname } from "next/navigation"

import { HomeButton } from "@/widgets/components/homeButton"

export const AdminNavigation = () => {
const pathname = usePathname()

const isActive = (href: string) =>
pathname === href || pathname.startsWith(`${href}/`)

const getAriaCurrent = (active: boolean) => (active ? "page" : undefined)

const navItemClass = (active: boolean) =>
cn(
"typography-heading-md-semibold",
active
? "text-text-interactive-secondary"
: "text-text-interactive-tertiary",
)
return (
<Navigation
leftContent={
<div className="flex shrink-0 items-center gap-12">
<HomeButton />
<span className="typography-heading-md-semibold text-text-interactive-secondary-hovered">
관리자페이지
</span>
</div>
}
rightContent={
<div
className="flex shrink-0 items-center gap-52"
role="group"
aria-label="관리자 섹션 탭"
>
<SecondaryPlainBoxButton
asChild
className={navItemClass(isActive("/admin/reports"))}
aria-current={getAriaCurrent(isActive("/admin/reports"))}
>
<Link href="/admin/reports">신고접수</Link>
</SecondaryPlainBoxButton>
<SecondaryPlainBoxButton
asChild
className={navItemClass(isActive("/admin/games"))}
aria-current={getAriaCurrent(isActive("/admin/games"))}
>
<Link href="/admin/games">게임관리</Link>
</SecondaryPlainBoxButton>
<SecondaryPlainBoxButton
asChild
className={navItemClass(isActive("/admin/users"))}
aria-current={getAriaCurrent(isActive("/admin/users"))}
>
<Link href="/admin/users">회원관리</Link>
</SecondaryPlainBoxButton>
</div>
}
/>
)
}
3 changes: 3 additions & 0 deletions service/app/src/app/admin/games/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function AdminGamesPage() {
return <div>AdminPage-Games</div>
}
42 changes: 42 additions & 0 deletions service/app/src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client"

import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"

import { validateSession } from "@/entities/auth/api/validateSession"

import { AdminNavigation } from "./_components/adminNavigation"

export default function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
const router = useRouter()
const [checked, setChecked] = useState(false)

useEffect(() => {
validateSession()
.then((user) => {
if (!user || user.data?.role !== "ADMIN") {
router.replace("/")
return
}
setChecked(true)
})
.catch(() => {
router.replace("/")
})
}, [router])

if (!checked) {
return null
}

return (
<div>
<AdminNavigation />
{children}
</div>
)
}
5 changes: 5 additions & 0 deletions service/app/src/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation"

export default function AdminPage() {
redirect("/admin/reports")
}
196 changes: 196 additions & 0 deletions service/app/src/app/admin/reports/__tests__/reports.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { expect, test } from "@playwright/test"

import { ReportsPOM } from "./reportsPOM"

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("/")
// })
})
Comment on lines +5 to +21

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.


test.describe("초기 UI 요소", () => {
test.use({ storageState: "playwright/.auth/user.json" })

let pom: ReportsPOM

test.beforeEach(async ({ page }) => {
pom = new ReportsPOM(page)
await pom.navigateToReports()
})

test("초기 UI 요소가 잘 표시되어야 한다", async () => {
await pom.checkNavigationHighlight()
await pom.checkReportedTable()
})
})

test.describe("네비게이션", () => {
test.use({ storageState: "playwright/.auth/user.json" })

let pom: ReportsPOM

test.beforeEach(async ({ page }) => {
pom = new ReportsPOM(page)
await pom.navigateToReports()
})

test("게임 관리 클릭시 게임 관리 페이지로 이동한다", async ({ page }) => {
await pom.clickGamesTab()
await page.waitForURL("/admin/games")
})

test("회원 관리 클릭시 회원 관리 페이지로 이동한다", async ({ page }) => {
await pom.clickUsersTab()
await page.waitForURL("/admin/users")
})
})

test.describe("기본 리스트 렌더링", () => {
test.use({ storageState: "playwright/.auth/user.json" })

let pom: ReportsPOM

test.beforeEach(async ({ page }) => {
pom = new ReportsPOM(page)
await pom.navigateToReports()
})

test("한 화면에 표시된 row 수가 7개 이하이다", async () => {
await pom.checkRowCount()
})

test("각 row에 컬럼이 존재한다 (번호, 게임명, 제작자, 신고자, 신고일자, 처리여부)", async () => {
await pom.checkRowColumns()
})
})

test.describe("신고 상세 팝업: 공통", () => {
test.use({ storageState: "playwright/.auth/user.json" })

let pom: ReportsPOM

test.beforeEach(async ({ page }) => {
pom = new ReportsPOM(page)
await pom.navigateToReports()
})

test("신고 항목을 클릭하면 신고 내용 확인 팝업이 표시된다", async () => {
await pom.clickReportedGameRow(0)
await pom.checkReportDetailPopup()
})

test("게임 정보가 표시된다 (게임명, 전체 문제 수, 신고 사유)", async () => {
await pom.clickReportedGameRow(0)
await pom.checkGameInfo()
})

test("팝업 닫기 버튼 클릭시 팝업이 닫힌다", async () => {
await pom.clickReportedGameRow(0)
await pom.checkReportDetailPopup()
await pom.clickCloseButton()
await expect(pom.reportDetail).not.toBeVisible()
})
})

test.describe("신고 상세 팝업: 삭제되지 않은 게임", () => {
test.use({ storageState: "playwright/.auth/user.json" })

let pom: ReportsPOM

test.beforeEach(async ({ page }) => {
pom = new ReportsPOM(page)
await pom.navigateToReports()
})

test("문제 목록이 캐러셀로 노출된다", async () => {
await pom.clickReportedGameRow(0)
await pom.checkCarousel()
})

test("삭제되지 않은 게임에서 게임 삭제 클릭시 팝업이 닫히고 상태가 삭제 완료로 업데이트된다", async () => {
await pom.clickReportedGameRow(0)

const row = pom.reportedGameRows.first()

await pom.clickDeleteGame()
await expect(pom.reportDetail).not.toBeVisible()

await expect(row).toHaveAttribute("aria-label", /처리 여부 삭제 완료/)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

test.describe("신고 상세 팝업: 삭제된 게임", () => {
test.use({ storageState: "playwright/.auth/user.json" })

let pom: ReportsPOM

test.beforeEach(async ({ page }) => {
pom = new ReportsPOM(page)
await pom.navigateToReports()
})
test("삭제된 게임의 경우 삭제 처리된 게임입니다 문구가 표시되고 캐러셀이 노출되지 않는다", async () => {
await pom.clickReportedGameRow(1)

await pom.checkDeletedGameMessage()
await expect(pom.gameQuestionList).not.toBeVisible()
})

test("삭제된 게임 클릭시 신고 무시 버튼과 게임 삭제 버튼이 비활성화된다", async () => {
await pom.clickReportedGameRow(1)

await pom.checkButtonDisabled(pom.reportIgnoreButton)
await pom.checkButtonDisabled(pom.gameDeleteButton)
})
})

test.describe("신고 무시", () => {
test.use({ storageState: "playwright/.auth/user.json" })

let pom: ReportsPOM

test.beforeEach(async ({ page }) => {
pom = new ReportsPOM(page)
await pom.navigateToReports()
})

test("신고 무시 클릭시 팝업이 닫히고 상태가 신고 무시로 업데이트된다", async () => {
await pom.clickReportedGameRow(0)
const row = pom.reportedGameRows.first()

await pom.clickIgnoreReport()
await expect(pom.reportDetail).not.toBeVisible()

await expect(row).toHaveAttribute("aria-label", /처리 여부 신고 무시/)
})
})

test.describe("사용자 차단", () => {
test.use({ storageState: "playwright/.auth/user.json" })

let pom: ReportsPOM

test.beforeEach(async ({ page }) => {
pom = new ReportsPOM(page)
await pom.navigateToReports()
})

test("사용자 차단 클릭시 사용자 차단 버튼이 비활성화된다", async () => {
await pom.clickReportedGameRow(0)
await pom.clickBlockCreator()
await expect(pom.reportDetail).toBeVisible()
await pom.checkButtonDisabled(pom.blockCreatorButton)
})
})
})
Loading
Loading