diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-03-26 00:37:41 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-03-26 00:37:41 +0000 |
| commit | e0dfb55c5457aec489fc084c4567e791b4c65eb1 (patch) | |
| tree | 68543a65d88f5afb3a0202925804103daa91bc6f /lib/admin-users/service.ts | |
3/25 까지의 대표님 작업사항
Diffstat (limited to 'lib/admin-users/service.ts')
| -rw-r--r-- | lib/admin-users/service.ts | 531 |
1 files changed, 531 insertions, 0 deletions
diff --git a/lib/admin-users/service.ts b/lib/admin-users/service.ts new file mode 100644 index 00000000..5d738d38 --- /dev/null +++ b/lib/admin-users/service.ts @@ -0,0 +1,531 @@ +"use server"; + +import { revalidateTag, unstable_noStore } from "next/cache"; +import db from "@/db/db"; +import logger from '@/lib/logger'; + +import { Role, roles, users, userView, type User, type UserView } from "@/db/schema/users"; // User 테이블 +import { type Company } from "@/db/schema/companies"; // User 테이블 +import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq } from "drizzle-orm"; + +// 레포지토리 함수들 (예시) - 아래처럼 작성했다고 가정 +import { + selectUsersWithCompanyAndRoles, + countUsers, + insertUser, + insertUserRole, + updateUser, deleteRolesByUserId, deleteRolesByUserIds, + deleteUserById, + deleteUsersByIds, + groupByCompany, + groupByRole, + findAllCompanies, getUserById, updateUsers, + findAllRoles +} from "./repository"; + +import { filterColumns } from "@/lib/filter-columns"; +import { unstable_cache } from "@/lib/unstable-cache"; +import { getErrorMessage } from "@/lib/handle-error"; + +// types +import type { CreateUserSchema, UpdateUserSchema, GetUsersSchema } from "./validations"; + +import { sendEmail } from "@/lib//mail/sendEmail"; +import { Vendor } from "@/db/schema/vendors"; + +/** + * 복잡한 조건으로 User 목록을 조회 (+ pagination) 하고, + * 총 개수에 따라 pageCount를 계산해서 리턴. + * Next.js의 unstable_cache를 사용해 일정 시간 캐시. + */ + + +export async function getUsers(input: GetUsersSchema) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + + // (1) advancedWhere + const advancedWhere = filterColumns({ + table: userView, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + // (2) globalWhere + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(userView.user_name, s), + ilike(userView.user_email, s), + ilike(userView.company_name, s) + ); + } + + // (3) 디폴트 domainWhere = eq(userView.domain, "partners") + // 다만, 사용자가 이미 domain 필터를 줬다면 적용 X + let domainWhere; + const hasDomainFilter = input.filters?.some((f) => f.id === "user_domain"); + if (!hasDomainFilter) { + domainWhere = eq(userView.user_domain, "partners"); + } + + // (4) 최종 where + const finalWhere = and(advancedWhere, globalWhere, domainWhere); + + // (5) 정렬 + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(userView[item.id]) : asc(userView[item.id]) + ) + : [desc(users.createdAt)]; + + // ... + const { data, total } = await db.transaction(async (tx) => { + const data = await selectUsersWithCompanyAndRoles(tx, { + where: finalWhere, + orderBy, + offset, + limit: input.perPage, + }); + + const total = await countUsers(tx, finalWhere); + return { data, total }; + }); + + const pageCount = Math.ceil(total / input.perPage); + return { data, pageCount }; + } catch (err) { + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify(input)], + { + revalidate: 3600, + tags: ["users"], + } + )(); +} + +export async function findUserById(id: number) { + try { + logger.info({ id }, 'Fetching user by ID'); + const user = await getUserById(id); + if (!user) { + logger.warn({ id }, 'User not found'); + } else { + logger.debug({ user }, 'User fetched successfully'); + } + return user; + } catch (error) { + logger.error({ error }, 'Error fetching user by ID'); + throw new Error('Failed to fetch user'); + } +}; + +/** + * User 생성 + * 필요 시 companyId, roles, etc. 함께 처리 + */ +// export async function createUser(input: CreateUserSchema & { language?: string }) { +// unstable_noStore(); // 캐싱 방지(Next.js 서버 액션용) +// try { +// const userLang = input.language || "en"; // 클라이언트가 안 주면 기본 "en" +// // 예시 subject 분기 +// const subject = +// userLang === "ko" +// ? "[eVCP] 어드민 계정이 생성되었습니다." +// : "[eVCP] Admin Account Created"; + +// const loginUrl = +// userLang === "ko" +// ? "http://3.36.56.124:3000/ko/login" +// : "http://3.36.56.124:3000/en/login"; + +// // 실제 sendEmail +// await sendEmail({ +// to: input.email, +// subject, +// template: "admin-created", +// context: { +// name: input.name, +// loginUrl, // 위에서 분기한 URL +// language: userLang, // 템플릿에서 {{t ... lng=language}} 처럼 쓸 수도 +// }, +// }); + +// await db.transaction(async (tx) => { +// // insertUser는 단건 생성 +// const [newUser] = await insertUser(tx, { +// name: input.name, +// email: input.email, +// domain: input.domain, +// companyId: input.companyId ?? null, +// // 기타 필요한 필드 +// }); + +// // 만약 roles를 함께 생성하려면, +// await insertUserRole(tx, { userId: newUser.id, roleId: Number(r) }); +// } +// }); + +// // 캐시 무효화 +// revalidateTag("users"); +// revalidateTag("user-company-counts"); + + + +// return { data: null, error: null }; +// } catch (err) { +// return { data: null, error: getErrorMessage(err) }; +// } +// } + +export async function createAdminUser(input: CreateUserSchema & { language?: string }) { + unstable_noStore(); // Next.js 캐싱 방지 + + try { + // 예) 관리자 메일 알림 로직 + // roles에 'Vendor Admin'을 넣을 거라면, 사실상 input.roles.includes("admin") 체크 대신 + // 아래에서 직접 메일 보내도 됨. 질문 예시대로 유지하겠습니다. + const userLang = input.language || "en"; + const subject = userLang === "ko" + ? "[eVCP] 어드민 계정이 생성되었습니다." + : "[eVCP] Admin Account Created"; + + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://3.36.56.124:3000' + + const loginUrl = userLang === "ko" + ? `${baseUrl}/ko/partners` + : `${baseUrl}/en/partners`; + + await sendEmail({ + to: input.email, + subject, + template: "admin-created", // 예: nodemailer + handlebars 등 + context: { + name: input.name, + loginUrl, + language: userLang, + }, + }); + + // 트랜잭션 시작 + await db.transaction(async (tx) => { + // 1. 먼저 roles 테이블에서 name = "Vendor Admin" AND domain = input.domain 인 것을 찾는다. + let [vendorAdminRole] = await tx + .select() + .from(roles) + .where( + and( + eq(roles.name, "Vendor Admin"), + eq(roles.domain, input.domain), + eq(roles.companyId, input.companyId as number), + ) + ) + .limit(1); + + // 2. 만약 없다면, 새롭게 생성한다. + if (!vendorAdminRole) { + // companyId나 description 등은 필요에 따라 조정 + const insertedRoles = await tx + .insert(roles) + .values({ + name: "Vendor Admin", + domain: input.domain, + companyId: input.companyId ?? null, + description: "Auto created Vendor Admin role", + }) + .returning(); + vendorAdminRole = insertedRoles[0]; // 방금 insert한 row + } + + // 3. 유저 생성 + const [newUser] = await insertUser(tx, { + name: input.name, + email: input.email, + domain: input.domain, + companyId: input.companyId ?? null, + // 기타 필요한 필드 추가 + }); + + // 4. Vendor Admin role을 user_roles 에 할당 (반복문 없이 단일 insert) + await insertUserRole(tx, { + userId: newUser.id, + roleId: vendorAdminRole.id, // Number()로 캐스팅할 필요 없이 정수로 관리한다고 가정 + }); + }); + + // 캐시 무효화 + revalidateTag("users"); + revalidateTag("user-company-counts"); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +/** + * 회사별 유저 개수 groupBy + */ +export async function getUserCountGroupByCompany() { + return unstable_cache( + async () => { + try { + // 예: { [companyId: number]: number } + const result = await db.transaction(async (tx) => { + const rows = await groupByCompany(tx); + // groupByCompany(tx): SELECT companyId, COUNT(*) FROM users GROUP BY companyId HAVING COUNT(*) > 0 + // 예: [{ companyId: 1, count: 10 }, { companyId: 2, count: 3 }, ...] + + // reduce해서 {1: 10, 2: 3, ...} 형태로 만들거나 그대로 반환할 수 있음 + const obj: Record<number, number> = {}; + for (const row of rows) { + if (row.companyId !== null) { + obj[row.companyId] = row.count; + } else { + // companyId가 null인 유저 수 + obj[-1] = (obj[-1] ?? 0) + row.count; + } + } + return obj; + }); + return result; + } catch (err) { + return {}; + } + }, + ["user-company-counts"], + { + revalidate: 3600, + } + )(); +} + +/** + * 롤별 유저 개수 groupBy + */ +export async function getUserCountGroupByRole() { + return unstable_cache( + async () => { + try { + const result = await db.transaction(async (tx) => { + const rows = await groupByRole(tx); + + const obj: Record<number, number> = {}; + for (const row of rows) { + if (row.roleId !== null) { + obj[row.roleId] = row.count; + } else { + // roleId가 null인 유저 수 + obj[-1] = (obj[-1] ?? 0) + row.count; + } + } + return obj; + }); + + // 여기서 result를 반환해 줘야 함! + return result; + } catch (err) { + console.error("getUserCountGroupByRole error:", err); + return {}; + } + }, + ["user-role-counts"], + { + revalidate: 3600, + } + )(); +} +/** + * 단건 업데이트 + */ +export async function modifiUser(input: UpdateUserSchema & { id: number } & { language?: string }) { + unstable_noStore(); + + try { + + const oldUser = await getUserById(input.id) + const oldEmail = oldUser?.user_email ?? null; + + const data = await db.transaction(async (tx) => { + // 1) 먼저 User 테이블 업데이트 + const [res] = await updateUser(tx, input.id, { + name: input.name, + companyId: input.companyId, + email: input.email, + }); + + // 2) roles가 함께 왔다면, 기존 roles 삭제 → 새 roles 삽입 + if (input.roles) { + // 기존 roles 삭제 + await deleteRolesByUserId(tx, input.id); + + // 새 roles 삽입 + for (const r of input.roles) { + await insertUserRole(tx, { + userId: input.id, + roleId: Number(r), + }); + } + } + + return res; + }); + + // 3) 캐시 무효화 + revalidateTag("users"); + + // 4) 이메일이 변경되었고, roles 중에 "admin"이 있다면 → 메일 발송 + const isEmailChanged = oldEmail && input.email && oldEmail !== input.email; + const hasAdminRole = input.roles?.includes("admin") ?? false; + + if (isEmailChanged && hasAdminRole && input.email) { + await sendEmail({ + to: input.email, + subject: "[EVCP] Admin Email Changed", + template: "admin-email-changed", + context: { + name: input.name, + oldEmail, + newEmail: input.email, + language: input.language ?? "en", + }, + }); + } + + // 예: companyId 변경 시 회사별 count도 다시 계산해야 하는 경우 + if (data.companyId === input.companyId) { + revalidateTag("user-company-counts"); + } + + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + + +/** 복수 업데이트 */ +export async function modifiUsers(input: { + ids: number[]; // 업데이트 대상 유저 ID 배열 + companyId?: User["companyId"]; // 회사 ID (있으면 업데이트) + roles?: UserView["roles"]; // 새 roles 배열 (있으면 업데이트) +}) { + unstable_noStore() // Next.js 서버 액션 캐싱 방지 + + try { + await db.transaction(async (tx) => { + // 1) 회사 정보 업데이트 + if (typeof input.companyId !== "undefined") { + // companyId가 주어졌으면, 해당 사용자들의 companyId 변경 + await updateUsers(tx, input.ids, { companyId: input.companyId }) + } + + // 2) roles 업데이트 + // (있으면 기존 roles 삭제 → 새 roles 삽입) + if (Array.isArray(input.roles)) { + // (a) 기존 roles 전부 삭제 + await deleteRolesByUserIds(tx, input.ids) + + // (b) 새 roles 삽입 + for (const userId of input.ids) { + for (const r of input.roles) { + await insertUserRole(tx, { + userId, + roleId: Number(r), + }) + } + } + } + }) + + // 캐시 무효화 + revalidateTag("users") + + return { data: null, error: null } + } catch (err) { + return { data: null, error: getErrorMessage(err) } + } +} +/** + * 단건 삭제 + */ +export async function removeUser(input: { id: number }) { + unstable_noStore(); + + try { + await db.transaction(async (tx) => { + // 유저 삭제 + await deleteRolesByUserId(tx, input.id); + await deleteUserById(tx, input.id); + // roles, otps 등도 함께 삭제해야 하면 여기서 처리 + }); + + // 캐시 무효화 + revalidateTag("users"); + revalidateTag("user-company-counts"); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +/** + * 복수 삭제 + */ +export async function removeUsers(input: { ids: number[] }) { + unstable_noStore(); + + try { + await db.transaction(async (tx) => { + // user_roles도 있으면 먼저 삭제해야 할 수 있음 + await deleteRolesByUserIds(tx, input.ids); + await deleteUsersByIds(tx, input.ids); + }); + + revalidateTag("users"); + revalidateTag("user-company-counts"); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +export async function getAllCompanies(): Promise<Vendor[]> { + try { + return await findAllCompanies(); // Company[] + } catch (err) { + throw new Error("Failed to get companies"); + } +} + +export async function getAllRoles(): Promise<Role[]> { + try { + return await findAllRoles(); + } catch (err) { + throw new Error("Failed to get roles"); + } +} + +/** + * 이미 해당 이메일이 users 테이블에 존재하는지 확인하는 함수 + * @param email 확인할 이메일 + * @returns boolean - 존재하면 true, 없으면 false + */ +export async function checkEmailExists(email: string): Promise<boolean> { + const result = await db + .select({ id: users.id }) // 굳이 모든 컬럼 필요 없으니 id만 + .from(users) + .where(eq(users.email, email)) + .limit(1); + + return result.length > 0; // 1건 이상 있으면 true +} |
