// lib/users/service.ts "use server"; import { Otp } from '@/types/user'; import { getAllUsers, createUser, getUserById, updateUser, deleteUser, getUserByEmail, createOtp,getOtpByEmailAndToken, updateOtp, findOtpByEmail ,getOtpByEmailAndCode, findAllRoles, getRoleAssignedUsers} from './repository'; import logger from '@/lib/logger'; import { Role, roles, userRoles, users, userView, type User } from '@/db/schema/users'; import { GetSimpleUsersSchema, GetUsersSchema } from '../admin-users/validations'; import { revalidatePath, revalidateTag, unstable_cache, unstable_noStore } from 'next/cache'; import { filterColumns } from '../filter-columns'; import { countUsers, countUsersSimple, selectUsers, selectUsersWithCompanyAndRoles } from '../admin-users/repository'; import db from "@/db/db"; import { getErrorMessage } from "@/lib/handle-error"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { and, or, desc, asc, ilike, eq, isNull, isNotNull, sql, count, inArray, ne, not } from "drizzle-orm"; import { SaveFileResult, saveFile } from '@/lib/file-stroage'; interface AssignUsersArgs { roleId: number userIds: number[] } export const fetchAllUsers = async (): Promise => { try { logger.info('Fetching all users'); const users = await getAllUsers(); logger.debug({ count: users.length }, 'Fetched users successfully'); return users; } catch (error) { logger.error({ error }, 'Error fetching all users'); throw new Error('Failed to fetch users'); } }; export const fetchRoleAssignedUserID = async (roleId: number) => { try { logger.info('Fetching all users'); const users = await getRoleAssignedUsers(roleId); logger.debug({ count: users.length }, 'Fetched users successfully'); return users; } catch (error) { logger.error({ error }, 'Error fetching all users'); throw new Error('Failed to fetch users'); } }; export const addNewUser = async (name: string, email: string): Promise => { try { logger.info({ name, email }, 'Creating a new user'); const user = await createUser(name, email); logger.debug({ user }, 'User created successfully'); return user; } catch (error) { logger.error({ error }, 'Error creating a new user'); throw new Error('Failed to create user'); } }; export const findUserById = async (id: number): Promise => { 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 const findUserByEmail = async (email: string): Promise => { try { logger.info({ email }, 'Fetching user by Email'); const user = await getUserByEmail(email); if (!user) { logger.warn({ email }, '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 const modifyUser = async (id: number, data: Partial): Promise => { try { logger.info({ id, data }, 'Updating user'); const user = await updateUser(id, data); if (!user) { logger.warn({ id }, 'User not found for update'); } else { logger.debug({ user }, 'User updated successfully'); } return user; } catch (error) { logger.error({ error }, 'Error updating user'); throw new Error('Failed to update user'); } }; export const removeUser = async (id: number): Promise => { try { logger.info({ id }, 'Deleting user'); const success = await deleteUser(id); if (success) { logger.debug({ id }, 'User deleted successfully'); } else { logger.warn({ id }, 'User not found for deletion'); } return success; } catch (error) { logger.error({ error }, 'Error deleting user'); throw new Error('Failed to delete user'); } }; export const addNewOtp = async ( email: string, code: string, createdAt: Date, otpToken: string, otpExpires: Date ): Promise => { try { logger.info({ email }, 'Creating or updating an OTP record'); // 1) 먼저 email로 Otp가 있는지 조회 const existingOtp = await findOtpByEmail(email); // 2) 이미 있으면 update if (existingOtp) { const otp = await updateOtp(email, code, createdAt, otpToken, otpExpires); logger.debug({ otp }, 'OTP updated successfully'); return otp; } // 3) 없으면 새로 생성 else { const otp = await createOtp(email, code, createdAt, otpToken, otpExpires); logger.debug({ otp }, 'OTP created successfully'); return otp; } } catch (error) { logger.error({ error }, 'Error creating or updating OTP'); throw new Error('Failed to create or update OTP'); } }; export const findOtpByEmailandToken = async (email: string, otpToken: string): Promise => { try { logger.info({ email }, 'Fetching otp by Email'); const otp = await getOtpByEmailAndToken(email, otpToken); if (!otp) { logger.warn({ email }, 'Otp not found'); } else { logger.debug({ otp }, 'Otp fetched successfully'); } return otp; } catch (error) { logger.error({ error }, 'Error fetching user by ID'); throw new Error('Failed to fetch user'); } }; export async function findEmailandOtp(email: string, code: string) { try { // 1) otp 조회 const otpRecord: Otp | null = await getOtpByEmailAndCode(email, code) if (!otpRecord) { return null } // 2) 사용자 정보 추가로 조회 const userRecord: User | null = await getUserByEmail(email) if (!userRecord) { return null } // 3) 필요한 형태로 "통합된 객체"를 반환 return { otpExpires: otpRecord.otpExpires, email: userRecord.email, name: userRecord.name, // DB 에서 가져온 실제 이름 id: userRecord.id, // user id imageUrl:userRecord.imageUrl, companyId:userRecord.companyId, techCompanyId:userRecord.techCompanyId, domain:userRecord.domain // 기타 필요한 필드... } } catch (error) { // 에러 처리 throw new Error('Failed to fetch user & otp') } } export async function findEmailTemp(email: string) { try { // 2) 사용자 정보 추가로 조회 const userRecord: User | null = await getUserByEmail(email) if (!userRecord) { return null } // 3) 필요한 형태로 "통합된 객체"를 반환 return { email: userRecord.email, name: userRecord.name, // DB 에서 가져온 실제 이름 id: userRecord.id, // user id imageUrl:userRecord.imageUrl, companyId:userRecord.companyId, techCompanyId:userRecord.techCompanyId, domain:userRecord.domain, epId:userRecord.epId, // Knox 계정인 경우 // 기타 필요한 필드... } } catch (error) { // 에러 처리 throw new Error('Failed to fetch user & otp') } } /** * 프로필 업데이트 결과 인터페이스 */ interface ProfileUpdateResult { success: boolean; user?: any; error?: string; duration?: number; fileInfo?: { fileName: string; originalName: string; fileSize: string; publicPath: string; }; securityChecks?: { extensionCheck: boolean; fileNameCheck: boolean; sizeCheck: boolean; mimeTypeCheck: boolean; contentCheck: boolean; }; } /** * 사용자 데이터 검증 (프로필 업데이트용) */ const validateUserData = { validateEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }, validateName(name: string): boolean { return name.length >= 2 && name.length <= 50 && !/[<>:"'|?*]/.test(name); }, }; /** * 파일 크기를 읽기 쉬운 형태로 변환 */ const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; /** * 보안 강화된 사용자 프로필 이미지 업데이트 함수 * (file-storage.ts의 saveFile 함수 활용) */ export async function updateUserProfileImage(formData: FormData): Promise { const startTime = Date.now(); // 1) FormData에서 데이터 꺼내기 const file = formData.get("file") as File | null; const userId = Number(formData.get("userId")); const name = formData.get("name") as string; const email = formData.get("email") as string; try { // 2) 기본 유효성 검증 if (!file) { throw new Error("파일이 제공되지 않았습니다."); } if (!userId || userId <= 0) { throw new Error("유효한 사용자 ID가 필요합니다."); } // 3) 사용자 정보 검증 (파일 저장 전에 미리 체크) if (name && !validateUserData.validateName(name)) { throw new Error("유효하지 않은 이름입니다 (2-50자, 특수문자 제한)."); } if (email && !validateUserData.validateEmail(email)) { throw new Error("유효하지 않은 이메일 주소입니다."); } console.log("📤 프로필 이미지 업데이트 시작:", { fileName: file.name, fileSize: formatFileSize(file.size), userId, }); // 4) file-storage.ts의 saveFile 함수로 보안 검증 + 저장 (한 번에 처리!) const saveResult: SaveFileResult = await saveFile({ file, directory: 'profiles', // './public/profiles'에서 'profiles'로 변경 originalName: file.name, userId: userId.toString(), }); // 5) 파일 저장 실패 시 에러 처리 if (!saveResult.success) { throw new Error(saveResult.error || "파일 저장에 실패했습니다."); } console.log("✅ 파일 저장 성공:", { fileName: saveResult.fileName, publicPath: saveResult.publicPath, securityChecks: saveResult.securityChecks, }); // 6) DB 업데이트 (file-storage.ts에서 반환된 publicPath 사용) const imageUrl = saveResult.publicPath; // 웹에서 접근 가능한 경로 const data = { name, email, imageUrl }; const user = await updateUser(userId, data); if (!user) { // DB 업데이트 실패 시 업로드된 파일 정리 // TODO: file-storage.ts의 deleteFile 함수 사용하여 파일 삭제 고려 throw new Error(`사용자 ID=${userId}를 찾을 수 없거나 업데이트에 실패했습니다.`); } // 7) 성공 반환 const duration = Date.now() - startTime; console.log("🎉 프로필 업데이트 성공:", { userId, imageUrl, duration: `${duration}ms`, }); return { success: true, user, duration, fileInfo: { fileName: saveResult.fileName!, originalName: saveResult.originalName!, fileSize: formatFileSize(saveResult.fileSize!), publicPath: saveResult.publicPath!, }, securityChecks: saveResult.securityChecks, }; } catch (err: any) { const duration = Date.now() - startTime; const errorMessage = err.message ?? "프로필 업데이트 중 오류가 발생했습니다."; console.error("❌ [updateUserProfileImage] Error:", err); return { success: false, error: errorMessage, duration, }; } } export async function getUsersEVCP(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, "evcp"); } // (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 getUsersNotPartners(input: GetSimpleUsersSchema) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; // (1) advancedWhere const advancedWhere = filterColumns({ table: users, filters: input.filters, joinOperator: input.joinOperator, }); // (2) globalWhere let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(users.name, s), ilike(users.email, s), ilike(users.deptName, s), ); } // (3) 디폴트 domainWhere = eq(userView.domain, "partners") // 다만, 사용자가 이미 domain 필터를 줬다면 적용 X let domainWhere; const hasDomainFilter = input.filters?.some((f) => f.id === "domain"); if (!hasDomainFilter) { domainWhere = ne(users.domain, "partners"); } // (4) 최종 where const finalWhere = and(advancedWhere, globalWhere, domainWhere); // (5) 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc(users[item.id]) : asc(users[item.id]) ) : [desc(users.createdAt)]; // ... const { data, total } = await db.transaction(async (tx) => { const data = await selectUsers(tx, { where: finalWhere, orderBy, offset, limit: input.perPage, }); console.log(data) const total = await countUsersSimple(tx, finalWhere); return { data, total }; }); const pageCount = Math.ceil(total / input.perPage); return { data, pageCount }; } catch (err) { console.log(err) return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], { revalidate: 3600, tags: ["users-access-control"], } )(); } export async function getAllRoles(): Promise { try { return await findAllRoles(); } catch (err) { throw new Error("Failed to get roles"); } } export async function getUsersAll(input: GetUsersSchema, domain: string) { 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 - 무조건 들어가야 하는 domain 조건 const domainWhere = eq(userView.user_domain, domain); // (4) 최종 where // domainWhere과 advancedWhere, globalWhere를 모두 and로 묶는다. // (globalWhere가 존재하지 않을 수 있으니, and() 호출 시 undefined를 자동 무시할 수도 있음) const finalWhere = and(domainWhere, advancedWhere, globalWhere); // (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 }; } }, // (6) 캐시 종속성 배열에 domain도 추가 [JSON.stringify(input), domain], { revalidate: 3600, tags: ["users"], } )(); } export async function getUsersAllbyVendor(input: GetUsersSchema, domain: string) { 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), ); } // (3) domainWhere - 무조건 들어가야 하는 domain 조건 const domainWhere = eq(userView.user_domain, domain); // (4) 최종 where // domainWhere과 advancedWhere, globalWhere를 모두 and로 묶는다. // (globalWhere가 존재하지 않을 수 있으니, and() 호출 시 undefined를 자동 무시할 수도 있음) const finalWhere = and(domainWhere, advancedWhere, globalWhere, 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 assignUsersToRole(roleId: number, userIds: number[]) { unstable_noStore() // 캐싱 방지(Next.js 서버 액션용) try { if (userIds.length === 0) { return { data: null, error: "선택된 사용자가 없습니다." } } await db.transaction(async (tx) => { // 1) 이미 할당된 사용자들 확인 const existingAssignments = await tx .select({ userId: userRoles.userId }) .from(userRoles) .where( and( eq(userRoles.roleId, roleId), inArray(userRoles.userId, userIds) ) ) const existingUserIds = existingAssignments.map(item => item.userId) // 2) 새로 할당할 사용자들만 필터링 const newUserIds = userIds.filter(uid => !existingUserIds.includes(uid)) // 3) 새로운 할당만 추가 if (newUserIds.length > 0) { await tx.insert(userRoles).values( newUserIds.map((uid) => ({ userId: uid, roleId: roleId })) ) } }) revalidateTag("users") revalidateTag("roles") return { data: { assignedCount: userIds.length, message: `${userIds.length}명의 사용자가 성공적으로 할당되었습니다.` }, error: null } } catch (err) { return { data: null, error: getErrorMessage(err) } } } /** * 특정 롤에서 사용자들을 제거합니다 */ export async function removeUsersFromRole(roleId: number, userIds: number[]) { unstable_noStore() // 캐싱 방지(Next.js 서버 액션용) try { if (userIds.length === 0) { return { data: null, error: "선택된 사용자가 없습니다." } } await db.transaction(async (tx) => { // 해당 롤에서 특정 사용자들만 삭제 await tx .delete(userRoles) .where( and( eq(userRoles.roleId, roleId), inArray(userRoles.userId, userIds) ) ) }) revalidateTag("users") revalidateTag("roles") return { data: { removedCount: userIds.length, message: `${userIds.length}명의 사용자가 성공적으로 제거되었습니다.` }, error: null } } catch (err) { return { data: null, error: getErrorMessage(err) } } } /** * 롤의 모든 사용자 할당을 재설정합니다 (기존 함수와 동일) * 기존 할당을 모두 삭제하고 새로운 할당으로 교체합니다 */ export async function replaceRoleAssignments(roleId: number, userIds: number[]) { unstable_noStore() // 캐싱 방지(Next.js 서버 액션용) try { await db.transaction(async (tx) => { // 1) 기존 userRoles 레코드 삭제 await tx.delete(userRoles).where(eq(userRoles.roleId, roleId)) // 2) 새로 넣기 if (userIds.length > 0) { await tx.insert(userRoles).values( userIds.map((uid) => ({ userId: uid, roleId })) ) } }) revalidateTag("users") revalidateTag("roles") return { data: null, error: null } } catch (err) { return { data: null, error: getErrorMessage(err) } } } /** * 특정 롤에 할당된 사용자 목록을 가져옵니다 */ export async function getUsersAssignedToRole(roleId: number) { unstable_noStore() try { const assignedUsers = await db .select({ userId: userRoles.userId, // 필요한 다른 사용자 정보들도 join해서 가져올 수 있습니다 }) .from(userRoles) .where(eq(userRoles.roleId, roleId)) return { data: assignedUsers.map(u => u.userId), error: null } } catch (err) { return { data: [], error: getErrorMessage(err) } } } export type UserDomain = "pending" | "evcp" | "procurement" | "sales" | "engineering" | "partners" /** * 여러 사용자에게 도메인을 일괄 할당하는 함수 */ export async function assignUsersDomain( userIds: number[], domain: UserDomain ) { try { if (!userIds.length) { return { success: false, message: "할당할 사용자가 없습니다." } } if (!domain) { return { success: false, message: "도메인을 선택해주세요." } } // 사용자들의 도메인 업데이트 const result = await db .update(users) .set({ domain, updatedAt: new Date(), }) .where(inArray(users.id, userIds)) .returning({ id: users.id, name: users.name, email: users.email, domain: users.domain, }) // 관련 페이지들 revalidate revalidatePath("/evcp/user-management") revalidatePath("/") return { success: true, message: `${result.length}명의 사용자 도메인이 업데이트되었습니다.`, data: result } } catch (error) { console.error("사용자 도메인 할당 오류:", error) return { success: false, message: "도메인 할당 중 오류가 발생했습니다." } } } /** * 단일 사용자의 도메인을 변경하는 함수 */ export async function assignUserDomain( userId: number, domain: UserDomain ) { try { const result = await db .update(users) .set({ domain, updatedAt: new Date(), }) .where(eq(users.id, userId)) .returning({ id: users.id, name: users.name, email: users.email, domain: users.domain, }) if (result.length === 0) { return { success: false, message: "사용자를 찾을 수 없습니다." } } revalidatePath("/evcp/user-management") revalidatePath("/evcp/users") return { success: true, message: `${result[0].name}님의 도메인이 ${domain}으로 변경되었습니다.`, data: result[0] } } catch (error) { console.error("사용자 도메인 할당 오류:", error) return { success: false, message: "도메인 할당 중 오류가 발생했습니다." } } } /** * 도메인별 사용자 통계를 조회하는 함수 */ export async function getUserDomainStats() { try { const stats = await db .select({ domain: users.domain, count: count(), }) .from(users) .where(eq(users.isActive, true)) .groupBy(users.domain) return { success: true, data: stats } } catch (error) { console.error("도메인 통계 조회 오류:", error) return { success: false, message: "통계 조회 중 오류가 발생했습니다.", data: [] } } } /** * pending 도메인 사용자 목록을 조회하는 함수 (관리자용) */ export async function getPendingUsers() { try { const pendingUsers = await db .select({ id: users.id, name: users.name, email: users.email, createdAt: users.createdAt, domain: users.domain, }) .from(users) .where(and( eq(users.domain, "pending"), eq(users.isActive, true) )) .orderBy(desc(users.createdAt)) return { success: true, data: pendingUsers } } catch (error) { console.error("pending 사용자 조회 오류:", error) return { success: false, message: "사용자 조회 중 오류가 발생했습니다.", data: [] } } } // ✅ Role 정보 조회 함수 추가 export async function getUserRoles(userId: number): Promise { try { const userWithRoles = await db .select({ roleName: roles.name, }) .from(userRoles) .innerJoin(roles, eq(userRoles.roleId, roles.id)) .where(eq(userRoles.userId, userId)) return userWithRoles.map(r => r.roleName) } catch (error) { console.error('Error fetching user roles:', error) return [] } } /** * 사용자 선택기용 간단한 사용자 검색 함수 */ export async function searchUsersForSelector( query: string, page: number = 1, perPage: number = 10, domainFilter?: { type: "exclude" | "include"; domains: string[] } | null ) { try { const offset = (page - 1) * perPage; // 이름 검색 조건 let searchWhere; if (query.trim()) { const searchPattern = `%${query.trim()}%`; searchWhere = ilike(users.name, searchPattern); } // 도메인 필터 조건 let domainWhere; if (domainFilter && domainFilter.domains.length > 0) { if (domainFilter.type === "include") { domainWhere = inArray(users.domain, domainFilter.domains); } else if (domainFilter.type === "exclude") { domainWhere = not(inArray(users.domain, domainFilter.domains)); } } // 활성 사용자만 const activeWhere = eq(users.isActive, true); // 최종 조건 const finalWhere = and(searchWhere, domainWhere, activeWhere); const { data, total } = await db.transaction(async (tx) => { const data = await tx .select({ id: users.id, epId: users.epId, name: users.name, email: users.email, deptCode: users.deptCode, deptName: users.deptName, imageUrl: users.imageUrl, domain: users.domain, }) .from(users) .where(finalWhere) .orderBy(asc(users.name)) .limit(perPage) .offset(offset); const totalResult = await tx .select({ count: count() }) .from(users) .where(finalWhere); return { data, total: totalResult[0].count }; }); const pageCount = Math.ceil(total / perPage); return { success: true, data, pagination: { page, perPage, total, pageCount, hasNextPage: page < pageCount, hasPrevPage: page > 1, }, }; } catch (error) { console.error("사용자 검색 오류:", error); return { success: false, data: [], pagination: { page: 1, perPage, total: 0, pageCount: 0, hasNextPage: false, hasPrevPage: false, }, error: "사용자 검색 중 오류가 발생했습니다.", }; } } /** * 사번으로 사용자 조회 */ export async function getUserByEmployeeNumber(employeeNumber: string) { try { if(!employeeNumber) { throw new Error( '사번으로 유저 정보 반환 함수(getUserByEmployeeNumber): 받은 사번이 없음.' ); } const user = await db .select() .from(users) .where(eq(users.employeeNumber, employeeNumber)) .limit(1) return user[0] } catch (error) { console.error("사용자 조회 오류:", error) } }