"use server"; import { revalidatePath, revalidateTag } from "next/cache"; import { unstable_cache, unstable_noStore } from "next/cache"; import db from "@/db/db"; import { departmentDomainAssignments, departmentDomainAssignmentHistory } from "@/db/schema/departmentDomainAssignments"; import { users } from "@/db/schema/users"; import { and, eq, inArray, desc, sql, ne } from "drizzle-orm"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { getErrorMessage } from "@/lib/handle-error"; import { getCurrentCompanyCode } from "@/lib/users/knox-service"; // 도메인 타입 export type UserDomain = "pending" | "evcp" | "procurement" | "sales" | "engineering" | "partners"; // 부서별 도메인 할당 정보 조회 export async function getDepartmentDomainAssignments() { return unstable_cache( async () => { try { const assignments = await db .select({ id: departmentDomainAssignments.id, companyCode: departmentDomainAssignments.companyCode, departmentCode: departmentDomainAssignments.departmentCode, departmentName: departmentDomainAssignments.departmentName, assignedDomain: departmentDomainAssignments.assignedDomain, isActive: departmentDomainAssignments.isActive, description: departmentDomainAssignments.description, createdAt: departmentDomainAssignments.createdAt, updatedAt: departmentDomainAssignments.updatedAt, }) .from(departmentDomainAssignments) .where(eq(departmentDomainAssignments.isActive, true)) .orderBy( desc(departmentDomainAssignments.updatedAt) ); return assignments; } catch (error) { console.error("부서별 도메인 할당 정보 조회 실패:", error); return []; } }, ["department-domain-assignments"], { revalidate: 3600, // 1시간 캐시 tags: ["department-domain-assignments"], } )(); } // 특정 부서들의 도메인 할당 정보 조회 export async function getDepartmentDomainAssignmentsByDepartments(departmentCodes: string[]) { return unstable_cache( async () => { try { if (departmentCodes.length === 0) return []; const assignments = await db .select() .from(departmentDomainAssignments) .where( and( inArray(departmentDomainAssignments.departmentCode, departmentCodes), eq(departmentDomainAssignments.isActive, true) ) ); return assignments; } catch (error) { console.error("부서별 도메인 할당 정보 조회 실패:", error); return []; } }, [`department-assignments-${departmentCodes.sort().join(',')}`], { revalidate: 3600, tags: ["department-domain-assignments"], } )(); } // 부서별 도메인 할당 export async function assignDomainToDepartments(params: { departmentCodes: string[]; domain: UserDomain; description?: string; departmentNames?: Record; // departmentCode -> departmentName 매핑 }) { unstable_noStore(); try { // 세션 확인 const session = await getServerSession(authOptions); if (!session?.user?.id) { return { success: false, message: "인증이 필요합니다.", }; } const { departmentCodes, domain, description, departmentNames = {} } = params; const userId = parseInt(session.user.id); if (!departmentCodes.length || !domain) { return { success: false, message: "부서 코드와 도메인이 필요합니다.", }; } // 현재 회사 코드 가져오기 const companyCode = await getCurrentCompanyCode(); await db.transaction(async (tx) => { // 기존 할당 정보를 비활성화 (soft delete) const existingAssignments = await tx .select() .from(departmentDomainAssignments) .where( and( inArray(departmentDomainAssignments.departmentCode, departmentCodes), eq(departmentDomainAssignments.isActive, true) ) ); // 기존 할당이 있으면 비활성화하고 히스토리 기록 for (const existing of existingAssignments) { // 히스토리 기록 await tx.insert(departmentDomainAssignmentHistory).values({ assignmentId: existing.id, action: "deactivated", previousValues: JSON.stringify({ assignedDomain: existing.assignedDomain, isActive: true, description: existing.description, }), newValues: JSON.stringify({ isActive: false, }), changedBy: userId, changeReason: `새로운 도메인 할당으로 인한 기존 할당 비활성화: ${domain}`, }); // 기존 할당 비활성화 await tx .update(departmentDomainAssignments) .set({ isActive: false, updatedBy: userId, updatedAt: new Date(), }) .where(eq(departmentDomainAssignments.id, existing.id)); } // 새로운 할당 생성 const newAssignments = departmentCodes.map(departmentCode => { return { companyCode, departmentCode, departmentName: departmentNames[departmentCode] || departmentCode, assignedDomain: domain, description, isActive: true, createdBy: userId, updatedBy: userId, }; }); const insertedAssignments = await tx .insert(departmentDomainAssignments) .values(newAssignments) .returning(); // 신규 생성 히스토리 기록 for (let i = 0; i < insertedAssignments.length; i++) { const assignment = insertedAssignments[i]; await tx.insert(departmentDomainAssignmentHistory).values({ assignmentId: assignment.id, action: "created", newValues: JSON.stringify({ companyCode: assignment.companyCode, departmentCode: assignment.departmentCode, assignedDomain: assignment.assignedDomain, description: assignment.description, }), changedBy: userId, changeReason: description || "부서별 도메인 할당", }); } }); // 캐시 무효화 revalidateTag("department-domain-assignments"); revalidatePath("/evcp/menu-access-dept"); return { success: true, message: `${departmentCodes.length}개 부서에 ${domain} 도메인이 성공적으로 할당되었습니다.`, }; } catch (error) { console.error("부서별 도메인 할당 실패:", error); return { success: false, message: getErrorMessage(error), }; } } // 부서별 도메인 할당 수정 export async function updateDepartmentDomainAssignment(params: { assignmentId: number; domain: UserDomain; description?: string; isActive?: boolean; }) { unstable_noStore(); try { const session = await getServerSession(authOptions); if (!session?.user?.id) { return { success: false, message: "인증이 필요합니다.", }; } const { assignmentId, domain, description, isActive = true } = params; const userId = parseInt(session.user.id); await db.transaction(async (tx) => { // 기존 할당 정보 조회 const existing = await tx .select() .from(departmentDomainAssignments) .where(eq(departmentDomainAssignments.id, assignmentId)) .limit(1); if (existing.length === 0) { throw new Error("존재하지 않는 할당 정보입니다."); } const currentAssignment = existing[0]; // 히스토리 기록 await tx.insert(departmentDomainAssignmentHistory).values({ assignmentId, action: "updated", previousValues: JSON.stringify({ assignedDomain: currentAssignment.assignedDomain, description: currentAssignment.description, isActive: currentAssignment.isActive, }), newValues: JSON.stringify({ assignedDomain: domain, description, isActive, }), changedBy: userId, changeReason: description || "부서별 도메인 할당 수정", }); // 할당 정보 업데이트 await tx .update(departmentDomainAssignments) .set({ assignedDomain: domain, description, isActive, updatedBy: userId, updatedAt: new Date(), }) .where(eq(departmentDomainAssignments.id, assignmentId)); }); // 캐시 무효화 revalidateTag("department-domain-assignments"); revalidatePath("/evcp/menu-access-dept"); return { success: true, message: "도메인 할당 정보가 성공적으로 수정되었습니다.", }; } catch (error) { console.error("부서별 도메인 할당 수정 실패:", error); return { success: false, message: getErrorMessage(error), }; } } // 부서별 도메인 할당 삭제 (soft delete) export async function deleteDepartmentDomainAssignment(assignmentId: number) { unstable_noStore(); try { const session = await getServerSession(authOptions); if (!session?.user?.id) { return { success: false, message: "인증이 필요합니다.", }; } const userId = parseInt(session.user.id); await db.transaction(async (tx) => { // 기존 할당 정보 조회 const existing = await tx .select() .from(departmentDomainAssignments) .where(eq(departmentDomainAssignments.id, assignmentId)) .limit(1); if (existing.length === 0) { throw new Error("존재하지 않는 할당 정보입니다."); } const currentAssignment = existing[0]; // 히스토리 기록 await tx.insert(departmentDomainAssignmentHistory).values({ assignmentId, action: "deleted", previousValues: JSON.stringify({ assignedDomain: currentAssignment.assignedDomain, description: currentAssignment.description, isActive: currentAssignment.isActive, }), newValues: JSON.stringify({ isActive: false, }), changedBy: userId, changeReason: "부서별 도메인 할당 삭제", }); // 할당 정보 비활성화 await tx .update(departmentDomainAssignments) .set({ isActive: false, updatedBy: userId, updatedAt: new Date(), }) .where(eq(departmentDomainAssignments.id, assignmentId)); }); // 캐시 무효화 revalidateTag("department-domain-assignments"); revalidatePath("/evcp/menu-access-dept"); return { success: true, message: "도메인 할당 정보가 성공적으로 삭제되었습니다.", }; } catch (error) { console.error("부서별 도메인 할당 삭제 실패:", error); return { success: false, message: getErrorMessage(error), }; } } // 부서별 도메인 할당 히스토리 조회 export async function getDepartmentDomainAssignmentHistory(assignmentId?: number) { return unstable_cache( async () => { try { const query = db .select({ id: departmentDomainAssignmentHistory.id, assignmentId: departmentDomainAssignmentHistory.assignmentId, action: departmentDomainAssignmentHistory.action, previousValues: departmentDomainAssignmentHistory.previousValues, newValues: departmentDomainAssignmentHistory.newValues, changedBy: departmentDomainAssignmentHistory.changedBy, changeReason: departmentDomainAssignmentHistory.changeReason, createdAt: departmentDomainAssignmentHistory.createdAt, }) .from(departmentDomainAssignmentHistory); if (assignmentId) { query.where(eq(departmentDomainAssignmentHistory.assignmentId, assignmentId)); } const history = await query .orderBy(desc(departmentDomainAssignmentHistory.createdAt)) .limit(100); // 최근 100개 제한 return history; } catch (error) { console.error("부서별 도메인 할당 히스토리 조회 실패:", error); return []; } }, [`department-domain-assignment-history-${assignmentId || 'all'}`], { revalidate: 1800, // 30분 캐시 tags: ["department-domain-assignment-history"], } )(); } // 도메인별 통계 조회 export async function getDepartmentDomainStats() { return unstable_cache( async () => { try { const stats = await db .select({ domain: departmentDomainAssignments.assignedDomain, count: db.$count(departmentDomainAssignments), }) .from(departmentDomainAssignments) .where(eq(departmentDomainAssignments.isActive, true)) .groupBy(departmentDomainAssignments.assignedDomain); return stats; } catch (error) { console.error("도메인별 통계 조회 실패:", error); return []; } }, ["department-domain-stats"], { revalidate: 3600, tags: ["department-domain-assignments"], } )(); } // 도메인 사용자들의 부서별 도메인 자동 할당 export async function autoAssignUsersDomains() { unstable_noStore(); try { console.log('[DOMAIN-AUTO-ASSIGN] 사용자 도메인 자동 할당 시작'); // 1. 모든 활성 부서별 도메인 할당 정보 조회 const domainAssignments = await db .select({ companyCode: departmentDomainAssignments.companyCode, departmentCode: departmentDomainAssignments.departmentCode, assignedDomain: departmentDomainAssignments.assignedDomain, }) .from(departmentDomainAssignments) .where(eq(departmentDomainAssignments.isActive, true)); console.log(`[DOMAIN-AUTO-ASSIGN] ${domainAssignments.length}개 부서의 도메인 할당 정보 로드됨`); if (domainAssignments.length === 0) { console.log('[DOMAIN-AUTO-ASSIGN] 활성 부서별 도메인 할당 정보가 없습니다.'); return { success: true, processedCount: 0, assignedCount: 0, skippedCount: 0, message: '활성 부서별 도메인 할당 정보가 없습니다.', }; } let totalAssignedCount = 0; let totalSkippedCount = 0; // 2. 각 부서별로 partners가 아닌 도메인 사용자들을 업데이트 for (const assignment of domainAssignments) { try { // 해당 부서의 partners가 아닌 도메인 사용자들 중 도메인이 다른 사용자만 업데이트 const updateResult = await db .update(users) .set({ domain: assignment.assignedDomain as UserDomain, updatedAt: new Date(), }) .where(and( ne(users.domain, 'partners'), // partners가 아닌 모든 도메인 사용자 ne(users.domain, assignment.assignedDomain), // 이미 할당된 도메인과 다른 경우만 eq(users.deptCode, assignment.departmentCode) // 해당 부서의 사용자 )); const affectedRows = updateResult.rowCount || 0; if (affectedRows > 0) { totalAssignedCount += affectedRows; console.log(`[DOMAIN-AUTO-ASSIGN] 부서 ${assignment.companyCode}-${assignment.departmentCode} -> ${assignment.assignedDomain} 도메인으로 ${affectedRows}명 업데이트`); } else { totalSkippedCount++; console.log(`[DOMAIN-AUTO-ASSIGN] 부서 ${assignment.companyCode}-${assignment.departmentCode} -> 업데이트할 사용자가 없음`); } } catch (error) { console.error(`[DOMAIN-AUTO-ASSIGN] 부서 ${assignment.companyCode}-${assignment.departmentCode} 처리 실패:`, error); totalSkippedCount++; } } // 3. partners가 아닌 도메인을 가진 총 사용자 수 확인 const totalNonPartnersUsers = await db .select({ count: sql`count(*)` }) .from(users) .where(ne(users.domain, 'partners')); const totalNonPartnersCount = totalNonPartnersUsers[0]?.count || 0; // 4. 부서 코드가 없어서 pending으로 남아있는 사용자 조회 및 로깅 const pendingUsersWithoutDept = await db .select({ id: users.id, name: users.name, email: users.email, employeeNumber: users.employeeNumber, deptCode: users.deptCode, deptName: users.deptName, }) .from(users) .where(and( eq(users.domain, 'pending'), ne(users.domain, 'partners') )); // 부서 코드가 없는 pending 사용자 필터링 및 로깅 const usersWithoutDeptCode = pendingUsersWithoutDept.filter(u => !u.deptCode); if (usersWithoutDeptCode.length > 0) { console.warn(`[DOMAIN-AUTO-ASSIGN] ⚠️ 부서 코드가 없어 pending 상태로 남은 사용자: ${usersWithoutDeptCode.length}명`); console.warn(`[DOMAIN-AUTO-ASSIGN] 부서 코드 없는 사용자 목록:`); usersWithoutDeptCode.forEach(user => { console.warn(` - ID: ${user.id}, 이름: ${user.name}, 이메일: ${user.email}, 사번: ${user.employeeNumber || 'N/A'}, 부서코드: ${user.deptCode || 'NULL'}, 부서명: ${user.deptName || 'N/A'}`); }); } // 부서가 있지만 할당 규칙이 없어서 pending인 사용자도 로깅 const usersWithDeptButNoAssignment = pendingUsersWithoutDept.filter(u => !!u.deptCode); if (usersWithDeptButNoAssignment.length > 0) { console.info(`[DOMAIN-AUTO-ASSIGN] ℹ️ 부서는 있지만 도메인 할당 규칙이 없어 pending 상태인 사용자: ${usersWithDeptButNoAssignment.length}명`); console.info(`[DOMAIN-AUTO-ASSIGN] 할당 규칙 없는 부서의 사용자 목록:`); usersWithDeptButNoAssignment.forEach(user => { console.info(` - ID: ${user.id}, 이름: ${user.name}, 이메일: ${user.email}, 부서코드: ${user.deptCode}, 부서명: ${user.deptName || 'N/A'}`); }); } const result = { success: true, processedCount: totalNonPartnersCount, assignedCount: totalAssignedCount, skippedCount: totalSkippedCount, pendingWithoutDeptCount: usersWithoutDeptCode.length, pendingWithDeptButNoAssignmentCount: usersWithDeptButNoAssignment.length, message: `${totalAssignedCount}명의 사용자에게 도메인이 자동 할당되었습니다.`, }; console.log(`[DOMAIN-AUTO-ASSIGN] 완료: ${JSON.stringify(result)}`); return result; } catch (error) { console.error('[DOMAIN-AUTO-ASSIGN] pending 사용자 도메인 자동 할당 실패:', error); return { success: false, processedCount: 0, assignedCount: 0, skippedCount: 0, message: getErrorMessage(error), }; } }