From e5cd10fda6d77c8b204175cdc6070a7955b55a7c Mon Sep 17 00:00:00 2001 From: koreacomp5 Date: Thu, 9 Oct 2025 14:49:31 +0900 Subject: [PATCH] =?UTF-8?q?2.4=20=ED=8E=98=EC=9D=B4=EC=A7=80/=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=80=EB=93=9C=20=ED=9B=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84(usePermission)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 24 +++++++++++++++++++++++- package.json | 5 +++-- src/app/api/auth/permissions/route.ts | 18 ++++++++++++++++++ src/lib/usePermission.ts | 24 ++++++++++++++++++++++++ todolist.txt | 2 +- 5 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 src/app/api/auth/permissions/route.ts create mode 100644 src/lib/usePermission.ts diff --git a/package-lock.json b/package-lock.json index 1279d5d..1c7ea5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "next": "15.5.4", "react": "19.1.0", "react-dom": "19.1.0", + "swr": "^2.3.6", "zod": "^3.23.8" }, "devDependencies": { @@ -3630,6 +3631,15 @@ "robust-predicates": "^3.0.2" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -6492,6 +6502,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -6782,7 +6805,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index 3b689f1..b691428 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,11 @@ }, "dependencies": { "@prisma/client": "^6.17.0", - "zod": "^3.23.8", "next": "15.5.4", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "swr": "^2.3.6", + "zod": "^3.23.8" }, "devDependencies": { "@biomejs/biome": "2.2.0", diff --git a/src/app/api/auth/permissions/route.ts b/src/app/api/auth/permissions/route.ts new file mode 100644 index 0000000..c5609a0 --- /dev/null +++ b/src/app/api/auth/permissions/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { getUserIdFromRequest } from "@/lib/auth"; + +export async function GET(req: Request) { + const userId = getUserIdFromRequest(req); + if (!userId) return NextResponse.json({ permissions: [] }); + const roles = await prisma.userRole.findMany({ where: { userId }, select: { roleId: true } }); + if (roles.length === 0) return NextResponse.json({ permissions: [] }); + const roleIds = roles.map((r) => r.roleId); + const permissions = await prisma.rolePermission.findMany({ + where: { roleId: { in: roleIds }, allowed: true }, + select: { resource: true, action: true }, + }); + return NextResponse.json({ permissions }); +} + + diff --git a/src/lib/usePermission.ts b/src/lib/usePermission.ts new file mode 100644 index 0000000..32897ac --- /dev/null +++ b/src/lib/usePermission.ts @@ -0,0 +1,24 @@ +"use client"; +import useSWR from "swr"; + +type Resource = "BOARD" | "POST" | "COMMENT" | "USER" | "ADMIN"; +type Action = "READ" | "CREATE" | "UPDATE" | "DELETE" | "MODERATE" | "ADMINISTER"; + +const fetcher = (url: string) => fetch(url).then((r) => r.json()); + +export function usePermission() { + const { data } = useSWR<{ permissions: { resource: Resource; action: Action }[] }>( + "/api/auth/permissions", + fetcher + ); + const perms = data?.permissions ?? []; + function can(resource: Resource, action: Action) { + return ( + perms.some((p) => p.resource === "ADMIN" && p.action === "ADMINISTER") || + perms.some((p) => p.resource === resource && p.action === action) + ); + } + return { can }; +} + + diff --git a/todolist.txt b/todolist.txt index 645ec50..9935e20 100644 --- a/todolist.txt +++ b/todolist.txt @@ -9,7 +9,7 @@ 2.1 역할·권한 시드(admin/editor/user) 추가 o 2.2 권한 enum/매핑 정의(리소스/액션) o 2.3 서버 권한 미들웨어 적용(API 보호 라우트 지정) o -2.4 페이지/컴포넌트 가드 훅 구현(usePermission) +2.4 페이지/컴포넌트 가드 훅 구현(usePermission) o 2.5 권한 기반 UI 노출 제어(빠른 액션/관리자 메뉴) [로그인/인증]