diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx new file mode 100644 index 0000000..0aeafba --- /dev/null +++ b/src/app/admin/users/page.tsx @@ -0,0 +1,71 @@ +"use client"; +import useSWR from "swr"; +import { useState } from "react"; + +const fetcher = (url: string) => fetch(url).then((r) => r.json()); + +export default function AdminUsersPage() { + const [q, setQ] = useState(""); + const { data, mutate } = useSWR<{ users: any[] }>(`/api/admin/users?q=${encodeURIComponent(q)}`, fetcher); + const users = data?.users ?? []; + return ( +
+

사용자 관리

+
+ setQ(e.target.value)} /> +
+ + + + + + + + + + + + + {users.map((u) => ( + + ))} + +
닉네임이름전화상태권한
+
+ ); +} + +function Row({ u, onChanged }: { u: any; onChanged: () => void }) { + const [status, setStatus] = useState(u.status); + const [roles, setRoles] = useState(u.roles ?? []); + async function save() { + await fetch(`/api/admin/users/${u.userId}/status`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ status }) }); + await fetch(`/api/admin/users/${u.userId}/roles`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ roles }) }); + onChanged(); + } + const allRoles = ["admin", "editor", "user"] as const; + return ( + + {u.nickname} + {u.name} + {u.phone} + + + + + {allRoles.map((r) => ( + + ))} + + + + ); +} + + diff --git a/src/app/api/admin/users/[id]/roles/route.ts b/src/app/api/admin/users/[id]/roles/route.ts new file mode 100644 index 0000000..157f195 --- /dev/null +++ b/src/app/api/admin/users/[id]/roles/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function PATCH(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const body = await req.json().catch(() => ({})); + const roles = Array.isArray(body?.roles) ? (body.roles as string[]) : []; + const all = await prisma.role.findMany({ select: { roleId: true, name: true } }); + const wanted = new Set(roles); + // 현재 매핑 + const existing = await prisma.userRole.findMany({ where: { userId: id } }); + const toKeep = new Set( + existing.filter((ur) => all.find((r) => r.roleId === ur.roleId && wanted.has(r.name))).map((ur) => ur.roleId) + ); + // 삭제 + await prisma.userRole.deleteMany({ where: { userId: id, roleId: { notIn: Array.from(toKeep) } } }); + // 추가 + for (const r of all) { + if (wanted.has(r.name) && !toKeep.has(r.roleId)) { + await prisma.userRole.create({ data: { userId: id, roleId: r.roleId } }); + } + } + return NextResponse.json({ ok: true }); +} + + diff --git a/src/app/api/admin/users/[id]/status/route.ts b/src/app/api/admin/users/[id]/status/route.ts new file mode 100644 index 0000000..efe7d44 --- /dev/null +++ b/src/app/api/admin/users/[id]/status/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function PATCH(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const body = await req.json().catch(() => ({})); + const status = body?.status as "active" | "suspended" | "withdrawn" | undefined; + if (!status) return NextResponse.json({ error: "invalid status" }, { status: 400 }); + const user = await prisma.user.update({ where: { userId: id }, data: { status } }); + return NextResponse.json({ user }); +} + + diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..f146f23 --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const q = searchParams.get("q") || ""; + const users = await prisma.user.findMany({ + where: q + ? { + OR: [ + { nickname: { contains: q } }, + { phone: { contains: q } }, + { name: { contains: q } }, + ], + } + : {}, + orderBy: { createdAt: "desc" }, + select: { + userId: true, + nickname: true, + name: true, + phone: true, + status: true, + authLevel: true, + createdAt: true, + userRoles: { select: { role: { select: { name: true } } } }, + }, + take: 100, + }); + const items = users.map((u) => ({ + ...u, + roles: u.userRoles.map((r) => r.role.name), + })); + return NextResponse.json({ users: items }); +} + + diff --git a/todolist.txt b/todolist.txt index e364e4f..06f222f 100644 --- a/todolist.txt +++ b/todolist.txt @@ -71,7 +71,7 @@ [관리자(Admin)] 10.1 대시보드 핵심 지표 위젯 o 10.2 게시판 스키마/설정 관리 UI o -10.3 사용자 검색/정지/권한 변경 +10.3 사용자 검색/정지/권한 변경 o 10.4 공지/배너 등록 및 노출 설정 10.5 감사 이력/신고 내역/열람 로그 10.6 카테고리 유형/설정 관리(일반/특수/승인/레벨/익명/태그)