summaryrefslogtreecommitdiff
path: root/lib/admin-users/service.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-03-25 15:55:45 +0900
committerjoonhoekim <26rote@gmail.com>2025-03-25 15:55:45 +0900
commit1a2241c40e10193c5ff7008a7b7b36cc1d855d96 (patch)
tree8a5587f10ca55b162d7e3254cb088b323a34c41b /lib/admin-users/service.ts
initial commit
Diffstat (limited to 'lib/admin-users/service.ts')
-rw-r--r--lib/admin-users/service.ts531
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
+}