"use server"; import { revalidateTag, unstable_cache, unstable_noStore } from "next/cache"; import db from "@/db/db"; import { permissions, Role, rolePermissions, roles, RoleView, roleView, userRoles } from "@/db/schema/users"; import { and, or, asc, desc, ilike, eq, inArray } from "drizzle-orm"; import { filterColumns } from "@/lib/filter-columns"; import { selectRolesWithUserCount, countRoles, insertRole, getRoleById, updateRole, deleteRolesByIds, deleteUserRolesByIds, findAllRoleView, } from "./repository"; import { CreateRoleSchema, GetRolesSchema, UpdateRoleSchema } from "./validations"; import { getErrorMessage } from "@/lib/handle-error"; interface UpsertPermissionsInput { roleIds: number[]; permissionKeys: string[]; itemTitle?: string; } export async function getRolesWithCount(input: GetRolesSchema) { // unstable_cache: 특정 키와 함께 캐싱 return unstable_cache( async () => { try { // 1) pagination const offset = (input.page - 1) * input.perPage; // 2) advanced filter const advancedWhere = filterColumns({ table: roleView, // 또는 roleView filters: input.filters, joinOperator: input.joinOperator, }); // 3) 글로벌 검색 let globalWhere; if (input.search) { const s = `%${input.search}%`; // 예: roles.name 에 ilike 검색 globalWhere = or(ilike(roles.name, s)); } // 4) 최종 where const finalWhere = and(advancedWhere, globalWhere); // (5) 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc(roleView[item.id]) : asc(roleView[item.id]) ) : [desc(roleView.created_at)]; // 6) 트랜잭션 + Repository 호출 const { data, total } = await db.transaction(async (tx) => { // 실제 SELECT const data = await selectRolesWithUserCount(tx, { where: finalWhere, orderBy, offset, limit: input.perPage, }); // 전체 개수 const total = await countRoles(tx, finalWhere); return { data, total }; }); // 7) pageCount const pageCount = Math.ceil(total / input.perPage); return { data, pageCount }; } catch (err) { // 에러시 기본값 return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], // 캐싱 키 { revalidate: 3600, tags: ["roles"], // revalidateTag("roles")로 무효화 } )(); } export async function createRole(input: CreateRoleSchema) { unstable_noStore(); // 캐싱 방지(Next.js 서버 액션용) try { await db.transaction(async (tx) => { const [newRole] = await insertRole(tx, { name: input.name, domain: input.domain, description: input.description ?? "", companyId: input.domain === "partners" ? input.companyId ?? null : null, }); }); revalidateTag("roles"); return { data: null, error: null }; } catch (err) { return { data: null, error: getErrorMessage(err) }; } } export async function modifiRole(input: UpdateRoleSchema & { id: number }) { unstable_noStore(); try { const data = await db.transaction(async (tx) => { // 1) 먼저 User 테이블 업데이트 const [res] = await updateRole(tx, input.id, { name: input.name, description: input.description, domain: input.domain }); return res; }); // 3) 캐시 무효화 revalidateTag("roles"); return { data: null, error: null }; } catch (err) { return { data: null, error: getErrorMessage(err) }; } } export async function removeRoles(input: { ids: number[] }) { unstable_noStore(); try { await db.transaction(async (tx) => { // user_roles도 있으면 먼저 삭제해야 할 수 있음 await deleteUserRolesByIds(tx, input.ids); await deleteRolesByIds(tx, input.ids); }); revalidateTag("roles"); revalidateTag("user-role-counts"); revalidateTag("users"); return { data: null, error: null }; } catch (err) { return { data: null, error: getErrorMessage(err) }; } } export async function assignRolesToUsers(roleIds: number[], userIds: number[]) { // Next.js 서버 액션에서 캐싱 방지 unstable_noStore() try { await db.transaction(async (tx) => { // 1) 기존 userRoles 삭제: userIds, roleIds에 해당하는 레코드만 await tx .delete(userRoles) .where( and( inArray(userRoles.roleId, roleIds), inArray(userRoles.userId, userIds) ) ) // 2) 새로 삽입 if (roleIds.length > 0 && userIds.length > 0) { const newRows = [] for (const rid of roleIds) { for (const uid of userIds) { newRows.push({ roleId: rid, userId: uid }) } } await tx.insert(userRoles).values(newRows) } }) // 캐시 무효화 revalidateTag("users") revalidateTag("roles") return { data: null, error: null } } catch (err) { return { data: null, error: getErrorMessage(err) } } } export async function getAllRoleView(domain?: "evcp" | "partners"): Promise { try { return await findAllRoleView(domain) } catch (err) { throw new Error("Failed to get roles") } } export async function upsertPermissions(input: UpsertPermissionsInput) { unstable_noStore(); try { const { roleIds, permissionKeys, itemTitle } = input; if (!roleIds.length || !permissionKeys.length) { return; // nothing to do } const roleIdNums = roleIds await db.transaction(async (tx) => { for (const permKey of permissionKeys) { // A) Check if permissionKey exists in "permissions" table const [existingPerm] = await tx .select({ id: permissions.id }) .from(permissions) .where(eq(permissions.permissionKey, permKey)) .limit(1); let permissionId: number; if (!existingPerm) { // Insert new permission // description를 어떻게 만들지는 자유: itemTitle + permKey 등 const [inserted] = await tx .insert(permissions) .values({ permissionKey: permKey, description: itemTitle ? `Menu: ${itemTitle} perm: ${permKey}` : permKey, }) .returning({ id: permissions.id }); permissionId = inserted.id; } else { permissionId = existingPerm.id; } // B) now link (roleId, permissionId) in role_permissions for (const rId of roleIdNums) { // check if already exists const [rp] = await tx .select({ p: rolePermissions.permissionId }) .from(rolePermissions) .where(and(eq(rolePermissions.roleId, rId), eq(rolePermissions.permissionId, permissionId))) .limit(1); if (!rp) { // insert await tx.insert(rolePermissions).values({ roleId: rId, permissionId, }); } // if rp exists, skip } } }); return { data: null, error: null }; } catch (err) { return { data: null, error: getErrorMessage(err) }; } } export async function getMenuPermissions( itemKey: string ): Promise<{ roleId: number; permKey: string }[]> { // itemKey = "alert-dialog" // permKey = "alert-dialog.create", "alert-dialog.viewOwn", ... const pattern = `${itemKey}.%` // SELECT rp.role_id, p.permission_key // FROM role_permissions rp // JOIN permissions p ON p.id = rp.permissionId // WHERE p.permission_key LIKE 'alert-dialog.%' const rows = await db .select({ roleId: rolePermissions.roleId, permKey: permissions.permissionKey, }) .from(rolePermissions) .innerJoin(permissions, eq(permissions.id, rolePermissions.permissionId)) .where(ilike(permissions.permissionKey, pattern)); return rows; }