feat : 관리자용 신고 내역 관리 페이지 추가 (#221)#222
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Review Summary by QodoAdd admin report management, user reporting system, PWA setup, and TWA workspace
WalkthroughsDescription• Admin report management page with approve/reject workflow - GET /admin/reports with status filtering (pending/applied/rejected/all) - PATCH /admin/reports/[id] to approve (update song data) or reject reports - Admin authorization via ADMIN_USER_ID environment variable • User-facing song error reporting system - POST/GET/DELETE /songs/report endpoints for users to submit/view/delete reports - Report modal in search results with category selection and suggested value input - User report history page at /info/reports • PWA setup with Service Worker and manifest - Serwist integration for offline support and precaching - PWA manifest with maskable icons for Android - Service Worker registration component • TWA (Trusted Web Activity) workspace for Android Play Store deployment - Bubblewrap CLI configuration with Digital Asset Links - Replaces deprecated Expo wrapper app (apps/mobile) • Shared report display utilities and types - reportDisplay.ts with badge styling and date formatting - Comprehensive report type definitions and constants Diagramflowchart LR
A["User Reports Song Error"] -->|POST /songs/report| B["song_reports table"]
B -->|GET /songs/report| C["User Report History"]
C -->|DELETE /songs/report| B
D["Admin Dashboard"] -->|GET /admin/reports| E["Fetch Reports with JOIN"]
E -->|PATCH /admin/reports/[id]| F{Action?}
F -->|approve| G["Update songs table + set status=applied"]
F -->|reject| H["Set status=rejected"]
I["PWA Manifest"] -->|Service Worker| J["Offline Support"]
K["Bubblewrap"] -->|Build| L["Android .aab/.apk"]
L -->|Deploy| M["Google Play Store"]
File Changes1. apps/web/src/app/api/songs/report/route.ts
|
Code Review by Qodo
Context used✅ Tickets:
🎫 관리자용 신고 내역 관리 페이지 추가 1. Non-atomic approve updates
|
| if (action === 'approve') { | ||
| const column = CATEGORY_TO_SONG_COLUMN[report.category]; | ||
| const { error: songUpdateError } = await supabase | ||
| .from('songs') | ||
| .update({ [column]: report.suggested_value }) | ||
| .eq('id', report.song_id); | ||
|
|
||
| if (songUpdateError) throw songUpdateError; | ||
|
|
||
| const { error: statusUpdateError } = await supabase | ||
| .from('song_reports') | ||
| .update({ status: 'applied' }) | ||
| .eq('id', reportId); | ||
|
|
||
| if (statusUpdateError) throw statusUpdateError; |
There was a problem hiding this comment.
1. Non-atomic approve updates 📎 Requirement gap ☼ Reliability
Approving a report performs the songs update and the song_reports.status update as two separate operations without a transaction/RPC. If the second update fails, the song can be changed while the report remains pending, violating the atomicity requirement.
Agent Prompt
## Issue description
Approving a report updates `songs` and `song_reports` in two separate calls, which can leave the system in a partially-applied state.
## Issue Context
Compliance requires the song update and report status update to be atomic (single transaction or RPC), so failures cannot create mismatched state.
## Fix Focus Areas
- apps/web/src/app/api/admin/reports/[id]/route.ts[64-78]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| 'use client'; | ||
|
|
||
| import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; | ||
| import { CircleIcon } from 'lucide-react'; | ||
| import * as React from 'react'; | ||
|
|
||
| import { cn } from '@/utils/cn'; | ||
|
|
||
| function RadioGroup({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { | ||
| return ( | ||
| <RadioGroupPrimitive.Root | ||
| data-slot="radio-group" | ||
| className={cn('grid gap-3', className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function RadioGroupItem({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { | ||
| return ( | ||
| <RadioGroupPrimitive.Item | ||
| data-slot="radio-group-item" | ||
| className={cn( | ||
| 'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', | ||
| className, | ||
| )} | ||
| {...props} | ||
| > | ||
| <RadioGroupPrimitive.Indicator | ||
| data-slot="radio-group-indicator" | ||
| className="relative flex items-center justify-center" | ||
| > | ||
| <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" /> | ||
| </RadioGroupPrimitive.Indicator> | ||
| </RadioGroupPrimitive.Item> | ||
| ); | ||
| } | ||
|
|
||
| export { RadioGroup, RadioGroupItem }; |
There was a problem hiding this comment.
2. Shadcn/ui file added 📘 Rule violation ⚙ Maintainability
This PR adds apps/web/src/components/ui/radio-group.tsx, which is inside the protected shadcn/ui source directory. Direct additions/edits under apps/web/src/components/ui/ violate the policy intended to preserve upgradability.
Agent Prompt
## Issue description
A new component file was added under `apps/web/src/components/ui/`, which is disallowed for shadcn/ui sources.
## Issue Context
To keep shadcn/ui upgradable, customizations/wrappers must live outside `apps/web/src/components/ui/`.
## Fix Focus Areas
- apps/web/src/components/ui/radio-group.tsx[1-45]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (action === 'approve') { | ||
| const column = CATEGORY_TO_SONG_COLUMN[report.category]; | ||
| const { error: songUpdateError } = await supabase | ||
| .from('songs') | ||
| .update({ [column]: report.suggested_value }) | ||
| .eq('id', report.song_id); |
There was a problem hiding this comment.
3. Approve writes null song fields 🐞 Bug ≡ Correctness
Admin approve updates songs.<column> with song_reports.suggested_value even when it is null (e.g., “데이터 없음” reports for num_tj/num_ky), which can turn songs.num_tj/num_ky into NULL while multiple APIs/types assume these are non-null strings.
Agent Prompt
### Issue description
Approving a report can write `NULL` into `songs.num_tj`/`songs.num_ky` (and potentially other columns) because the admin PATCH handler uses `report.suggested_value` as-is. Other parts of the codebase treat these fields as non-null `string`.
### Issue Context
- `POST /api/songs/report` allows `suggested_value: null` for `num_tj`/`num_ky` to represent “데이터 없음”.
- `Song` type currently models `num_tj`/`num_ky` as required `string`.
### Fix Focus Areas
- apps/web/src/app/api/admin/reports/[id]/route.ts[64-79]
- apps/web/src/app/api/songs/report/route.ts[123-154]
- apps/web/src/types/song.ts[1-15]
- apps/web/src/app/api/search/route.ts[178-186]
### Suggested fix options (pick one, but be consistent)
1) **Keep songs fields non-null (minimal impact):** when approving, coalesce `null` to `''` for `num_tj/num_ky`, and (optionally) also null-coalesce in APIs that read from `songs`.
2) **Make songs fields nullable (larger change):** update `Song`/`DBSong` types and all API mappers/UI renderers to handle `null` explicitly.
3) **Disallow approving null suggested_value:** return 400/409 if `action==='approve'` and `suggested_value` is null (then require reject instead).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
관리자(본인 1인)가 사용자가 등록한 곡 오류 신고를 검토하고 승인/거부할 수 있는 전용 페이지를 추가했습니다.
ADMIN_USER_ID환경변수 화이트리스트 방식 (DB 스키마 변경 없음, 비밀번호 게이트 불필요)/info메뉴에 노출 안 함 —/admin/reports직접 진입만 가능 (서버측 가드가 본질)songs.<category>컬럼을suggested_value로 업데이트 +song_reports.status = 'applied'reportDisplay.ts) 추출, 기존/info/reports/ReportItem.tsx도 마이그레이션 (DRY)주요 변경 파일
apps/web/src/utils/getAdminUser.ts— admin 가드 (forbidden cause throw)apps/web/src/app/api/admin/reports/route.ts—GET(status 필터, songs/users JOIN, 최신순)apps/web/src/app/api/admin/reports/[id]/route.ts—PATCH(approve/reject, pending 아닌 신고는 409)apps/web/src/app/admin/reports/{page,AdminReportItem,ReviewActionModal}.tsx— 페이지 UIapps/web/src/queries/adminReportQuery.ts,apps/web/src/lib/api/adminReport.ts— 클라이언트 측apps/web/src/utils/reportDisplay.ts— 공통 유틸 (badge classes, formatReportDate)turbo.json—ADMIN_USER_IDenv 등록배포 전 필수 작업
Vercel 환경변수 + 로컬 `.env.development.local` 에 다음 추가:
```
ADMIN_USER_ID=<본인 Supabase auth user_id>
```
→ 미설정 시 모든 사용자가 admin API 에 대해 403 응답을 받습니다 (안전한 기본값).
Test plan
ADMIN_USER_ID미설정 상태에서/admin/reports진입 → toast +/리다이렉트/admin/reports진입 → 동일하게 차단/admin/reports진입 → 신고 목록 표시songs테이블 해당 컬럼 업데이트 + 상태applied로 변경 + invalidaterejected로 변경 + invalidate/info/reports(사용자 측 페이지) 회귀 없음 확인Closes #221