"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 = {}; 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; phone?: 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, phone: input.phone?.trim(), // 전화번호 앞뒤 공백 제거 }); // 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 { 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 { const result = await db .select({ id: users.id }) // 굳이 모든 컬럼 필요 없으니 id만 .from(users) .where(eq(users.email, email)) .limit(1); return result.length > 0; // 1건 이상 있으면 true }