summaryrefslogtreecommitdiff
path: root/lib/vendor-users/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-27 01:16:20 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-27 01:16:20 +0000
commite9897d416b3e7327bbd4d4aef887eee37751ae82 (patch)
treebd20ce6eadf9b21755bd7425492d2d31c7700a0e /lib/vendor-users/service.ts
parent3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff)
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'lib/vendor-users/service.ts')
-rw-r--r--lib/vendor-users/service.ts491
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
+}