diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-27 01:16:20 +0000 |
| commit | e9897d416b3e7327bbd4d4aef887eee37751ae82 (patch) | |
| tree | bd20ce6eadf9b21755bd7425492d2d31c7700a0e /lib/vendor-users/service.ts | |
| parent | 3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff) | |
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'lib/vendor-users/service.ts')
| -rw-r--r-- | lib/vendor-users/service.ts | 491 |
1 files changed, 491 insertions, 0 deletions
diff --git a/lib/vendor-users/service.ts b/lib/vendor-users/service.ts new file mode 100644 index 00000000..428e8b73 --- /dev/null +++ b/lib/vendor-users/service.ts @@ -0,0 +1,491 @@ +"use server"; + +import { 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 { asc, desc, ilike, inArray, and, gte, lte, not, or, eq } from "drizzle-orm"; +import { headers } from 'next/headers'; + +// 레포지토리 함수들 (예시) - 아래처럼 작성했다고 가정 +import { + selectUsersWithCompanyAndRoles, + countUsers, + insertUser, + insertUserRole, + updateUser, deleteRolesByUserId, deleteRolesByUserIds, + deleteUserById, + deleteUsersByIds, + groupByRole, + findAllCompanies, getUserById, updateUsers, + findAllRoles +} from "./repository"; + +import { filterColumns } from "@/lib/filter-columns"; +import { getErrorMessage } from "@/lib/handle-error"; + +// types +import type { CreateVendorUserSchema, UpdateVendorUserSchema, GetVendorUsersSchema } from "./validations"; + +import { sendEmail } from "@/lib//mail/sendEmail"; +import { Vendor } from "@/db/schema/vendors"; +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" + +/** + * 복잡한 조건으로 User 목록을 조회 (+ pagination) 하고, + * 총 개수에 따라 pageCount를 계산해서 리턴. + * Next.js의 unstable_cache를 사용해 일정 시간 캐시. + */ + + +export async function getVendorUsers(input: GetVendorUsersSchema) { + try { + + const session = await getServerSession(authOptions) + + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + const companyId = session.user.companyId; + + 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, eq(userView.company_id , companyId)); + + // (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 }; + } +} + +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'); + } +}; + + +export async function createVendorUser(input: CreateVendorUserSchema & { language?: string }) { + unstable_noStore(); // Next.js 캐싱 방지 + + try { + + const session = await getServerSession(authOptions) + + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + const companyId = session.user.companyId; + + if(!companyId){ + throw new Error("인증이 필요합니다.") + } + + + // 예) 관리자 메일 알림 로직 + // roles에 'Vendor Admin'을 넣을 거라면, 사실상 input.roles.includes("admin") 체크 대신 + // 아래에서 직접 메일 보내도 됨. 질문 예시대로 유지하겠습니다. + const userLang = input.language || "en"; + const subject = userLang === "ko" + ? "[eVCP] 계정이 생성되었습니다." + : "[eVCP] Account Created"; + + + const headersList = await headers(); + const host = headersList.get('host') || 'localhost:3000'; + + const baseUrl = `http://${host}` + + const loginUrl = userLang === "ko" + ? `${baseUrl}/ko/partners` + : `${baseUrl}/en/partners`; + + await sendEmail({ + to: input.email, + subject, + template: "vendor-user-created", // 예: nodemailer + handlebars 등 + context: { + name: input.name, + loginUrl, + language: userLang, + }, + }); + + if(!companyId){ + throw new Error("인증이 필요합니다.") + } + // 트랜잭션 시작 + await db.transaction(async (tx) => { + + const [newUser] = await insertUser(tx, { + name: input.name, + email: input.email, + domain: input.domain, + phone: input.phone, + companyId:companyId, + // 기타 필요한 필드 추가 + }); + + }); + + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + + +/** + * 롤별 유저 개수 groupBy + */ +export async function getUserCountGroupByRoleAndVendor() { + try { + + const session = await getServerSession(authOptions) + + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + const companyId = session.user.companyId; + + if (!companyId) { + throw new Error("인증이 필요합니다.") + } + + const result = await db.transaction(async (tx) => { + const rows = await groupByRole(tx, companyId); + + 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 {}; + } +} +/** + * 단건 업데이트 + */ +export async function modifiVendorUser(input: UpdateVendorUserSchema & { id: number } & { language?: string }) { + unstable_noStore(); + + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + const companyId = session.user.companyId; + + // 🔒 1. 수정 대상이 같은 회사 사용자인지 검증 + const oldUser = await getUserById(input.id); + if (!oldUser) { + throw new Error("사용자를 찾을 수 없습니다."); + } + + if (oldUser.company_id !== companyId) { + throw new Error("수정 권한이 없는 사용자입니다."); + } + + + 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, + email: input.email, + }); + + // 2) 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; + }); + + // 4) 이메일 변경 알림 (기존 로직 유지) + 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", + }, + }); + } + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +/** 복수 업데이트 */ +export async function modifiVendorUsers(input: { + ids: number[]; + companyId?: User["companyId"]; + roles?: UserView["roles"]; +}) { + unstable_noStore() + + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + const companyId = session.user.companyId; + + if(!companyId){ + throw new Error("인증이 필요합니다.") + } + + await db.transaction(async (tx) => { + // 🔒 핵심: 대상 사용자들이 같은 회사인지 검증 + const targetUsers = await tx.select() + .from(users) + .where(and( + inArray(users.id, input.ids), + eq(users.companyId, companyId) // 같은 회사 사용자만 + )) + + // 요청한 ID 수와 실제 같은 회사 사용자 수가 다르면 에러 + if (targetUsers.length !== input.ids.length) { + throw new Error("권한이 없는 사용자가 포함되어 있습니다.") + } + + if (Array.isArray(input.roles)) { + await deleteRolesByUserIds(tx, input.ids) + + for (const userId of input.ids) { + for (const r of input.roles) { + await insertUserRole(tx, { + userId, + roleId: Number(r), + }) + } + } + } + }) + + return { data: null, error: null } + } catch (err) { + return { data: null, error: getErrorMessage(err) } + } +} +/** + * 단건 삭제 + */ +export async function removeVendorUser(input: { id: number }) { + unstable_noStore(); + + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + const companyId = session.user.companyId; + + if(!companyId){ + throw new Error("인증이 필요합니다.") + } + + await db.transaction(async (tx) => { + // 🔒 핵심: 삭제 대상이 같은 회사 사용자인지 검증 + const targetUser = await tx.select() + .from(users) + .where(and( + eq(users.id, input.id), + eq(users.companyId, companyId) + )) + .limit(1); + + if (targetUser.length === 0) { + throw new Error("삭제 권한이 없는 사용자입니다."); + } + + // 유저 삭제 + await deleteRolesByUserId(tx, input.id); + await deleteUserById(tx, input.id); + }); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +/** + * 복수 삭제 + */ +export async function removeVendorUsers(input: { ids: number[] }) { + unstable_noStore(); + + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + const companyId = session.user.companyId; + + if(!companyId){ + throw new Error("인증이 필요합니다.") + } + + await db.transaction(async (tx) => { + // 🔒 핵심: 삭제 대상들이 모두 같은 회사 사용자인지 검증 + const targetUsers = await tx.select() + .from(users) + .where(and( + inArray(users.id, input.ids), + eq(users.companyId, companyId) + )); + + // 요청한 ID 수와 실제 같은 회사 사용자 수가 다르면 에러 + if (targetUsers.length !== input.ids.length) { + throw new Error("삭제 권한이 없는 사용자가 포함되어 있습니다."); + } + + // user_roles도 있으면 먼저 삭제해야 할 수 있음 + await deleteRolesByUserIds(tx, input.ids); + await deleteUsersByIds(tx, input.ids); + }); + + return { data: null, error: null }; + } catch (err) { + return { data: null, error: getErrorMessage(err) }; + } +} + +export async function getAllRolesbyVendor(): Promise<Role[]> { + try { + + const session = await getServerSession(authOptions) + + if (!session?.user) { + throw new Error("인증이 필요합니다.") + } + + const companyId = session.user.companyId; + + if (!companyId) { + throw new Error("인증이 필요합니다.") + } + + return await findAllRoles(companyId); + } 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 +} |
