'use server'; import * as cron from 'node-cron'; import db from '@/db/db'; import { employee as employeeTable } from '@/db/schema/knox/employee'; import { title as titleTable } from '@/db/schema/knox/titles'; import { users, type UserDomainType } from '@/db/schema/users'; import { departmentDomainAssignments } from '@/db/schema/departmentDomainAssignments'; import { searchEmployees, Employee, } from '@/lib/knox-api/employee/employee'; import { sql, eq, and, ne } from 'drizzle-orm'; import { KNOX_COMPANIES, delay, API_CALL_DELAY_MS, checkTimeRestriction, checkApiLimitStatus, initializeApiTracker, logSyncStart, logSyncComplete, logSchedulerInfo, } from './common'; const CRON_STRING = process.env.KNOX_EMPLOYEE_SYNC_CRON || '0 4 * * *'; const DO_FIRST_RUN = process.env.KNOX_EMPLOYEE_SYNC_FIRST_RUN === 'true'; async function insertEmployees(employees: Employee[]) { if (!employees.length) return; const rows = employees.map((e) => ({ epId: e.epId, employeeNumber: e.employeeNumber, userId: e.userId, fullName: e.fullName, givenName: e.givenName, sirName: e.sirName, companyCode: e.companyCode, companyName: e.companyName, departmentCode: e.departmentCode, departmentName: e.departmentName, titleCode: e.titleCode, titleName: e.titleName, emailAddress: e.emailAddress, mobile: e.mobile, employeeStatus: e.employeeStatus, employeeType: e.employeeType, accountStatus: e.accountStatus, securityLevel: e.securityLevel, preferredLanguage: e.preferredLanguage, description: e.description, raw: e as unknown as Record, enCompanyName: e.enCompanyName, enDepartmentName: e.enDepartmentName, enDiscription: e.enDiscription, enFullName: e.enFullName, enGivenName: e.enGivenName, enGradeName: e.enGradeName, enSirName: e.enSirName, enTitleName: e.enTitleName, gradeName: e.gradeName, gradeTitleIndiCode: e.gradeTitleIndiCode, jobName: e.jobName, realNameYn: e.realNameYn, serverLocation: e.serverLocation, titleSortOrder: e.titleSortOrder, })); await db.insert(employeeTable).values(rows); } /** * 회사별 기존 임직원 데이터 삭제 */ async function deleteExistingEmployees(companyCode: string): Promise { const result = await db .delete(employeeTable) .where(eq(employeeTable.companyCode, companyCode)); const deletedCount = result.rowCount || 0; console.log(`[KNOX-SYNC] ${companyCode}: 기존 임직원 데이터 ${deletedCount}건 삭제 완료`); return deletedCount; } /** * 회사별 임직원 동기화 - 직급별 조회 방식 (Delete-Insert) * 기존 데이터를 모두 삭제한 후 새로운 데이터를 삽입하여 삭제된 데이터 문제 해결 */ async function syncEmployeesByCompany(companyCode: string): Promise { let currentApiCalls = 0; let totalEmployees = 0; let currentDelay = API_CALL_DELAY_MS; try { console.log(`[KNOX-SYNC] ${companyCode}: 임직원 동기화 시작 (Delete-Insert 방식)`); // 1단계: 기존 데이터 삭제 await deleteExistingEmployees(companyCode); // 2단계: 내부 DB에서 해당 회사의 직급 목록 조회 (이미 동기화된 데이터 활용) console.log(`[KNOX-SYNC] ${companyCode}: 내부 DB에서 직급 목록 조회 중...`); const titles = await db .select({ titleCode: titleTable.titleCode, titleName: titleTable.titleName, sortOrder: titleTable.sortOrder, }) .from(titleTable) .where(eq(titleTable.companyCode, companyCode)) .orderBy(titleTable.sortOrder); console.log(`[KNOX-SYNC] ${companyCode}: 총 ${titles.length}개 직급 발견`); if (titles.length === 0) { console.warn(`[KNOX-SYNC] ${companyCode}: 직급 정보가 없습니다. 직급 동기화를 먼저 실행해주세요.`); return 0; } // 3단계: 각 직급별로 임직원 조회 for (const title of titles) { let empPage = 1; let empTotalPage = 1; let titleEmployeeCount = 0; do { // 동적 delay 적용 if (currentApiCalls > 0) { await delay(currentDelay); } const empResp = await searchEmployees({ companyCode, titleCode: title.titleCode, page: String(empPage), resultType: 'basic', }); // API 제한 체크 및 delay 조절 const limitStatus = checkApiLimitStatus(1); currentDelay = limitStatus.recommendedDelay; currentApiCalls++; if (empResp.result === 'success') { await insertEmployees(empResp.employees); empTotalPage = empResp.totalPage; totalEmployees += empResp.employees.length; titleEmployeeCount += empResp.employees.length; if (empResp.employees.length > 0) { console.log( `[KNOX-SYNC] ${companyCode}: ${title.titleName}(${title.titleCode}) ${empPage}/${empTotalPage} 페이지 완료 (${empResp.employees.length}명)` ); } } else { console.warn( `[KNOX-SYNC] ${companyCode}: ${title.titleName}(${title.titleCode}) 페이지 ${empPage} 조회 실패` ); break; } empPage += 1; } while (empPage <= empTotalPage); // 직급별 완료 로그 (임직원이 있는 경우만) if (titleEmployeeCount > 0) { console.log( `[KNOX-SYNC] ${companyCode}: ${title.titleName}(${title.titleCode}) 완료 - ${titleEmployeeCount}명` ); } } console.log(`[KNOX-SYNC] ${companyCode}: 완료 - ${totalEmployees}명, API 호출 ${currentApiCalls}회`); } catch (err) { console.error( `[KNOX-SYNC] ${companyCode}: 동기화 오류 (API 호출 ${currentApiCalls}회)`, err ); } return currentApiCalls; } /** * Knox Employee 데이터를 Users 테이블과 동기화 * Knox 임직원 동기화 완료 후 실행되는 함수 */ async function syncEmployeesToUsers(): Promise { try { console.log('[KNOX-SYNC] Users 테이블 동기화 시작'); // Knox employee 테이블에서 이메일이 있는 레코드들 조회 const employees = await db .select({ fullName: employeeTable.fullName, emailAddress: employeeTable.emailAddress, departmentCode: employeeTable.departmentCode, departmentName: employeeTable.departmentName, companyCode: employeeTable.companyCode, epId: employeeTable.epId, }) .from(employeeTable) .where( and( sql`${employeeTable.emailAddress} IS NOT NULL`, sql`${employeeTable.emailAddress} != ''` ) ); console.log(`[KNOX-SYNC] 동기화 대상 Knox 임직원: ${employees.length}명`); if (employees.length === 0) { console.log('[KNOX-SYNC] 동기화할 Knox 임직원이 없습니다.'); return; } // 부서별 도메인 할당 정보 조회 (캐싱을 위해 한 번에 조회) const domainAssignments = await db .select({ companyCode: departmentDomainAssignments.companyCode, departmentCode: departmentDomainAssignments.departmentCode, assignedDomain: departmentDomainAssignments.assignedDomain, }) .from(departmentDomainAssignments) .where(eq(departmentDomainAssignments.isActive, true)); // 부서별 도메인 매핑을 위한 Map 생성 const domainMap = new Map(); domainAssignments.forEach(assignment => { const key = `${assignment.companyCode}-${assignment.departmentCode}`; domainMap.set(key, assignment.assignedDomain); }); let insertCount = 0; let updateCount = 0; let skipCount = 0; // 각 임직원에 대해 동기화 처리 for (const employee of employees) { if (!employee.emailAddress || !employee.fullName) { skipCount++; continue; } try { // 부서별 도메인 할당 정보 조회 const domainKey = `${employee.companyCode}-${employee.departmentCode}`; const assignedDomain = domainMap.get(domainKey) || 'pending'; // 기존 사용자 확인 (partners가 아닌 사용자만 대상) const existingUsers = await db .select({ id: users.id, domain: users.domain }) .from(users) .where( and( eq(users.email, employee.emailAddress), ne(users.domain, 'partners') ) ) .limit(1); if (existingUsers.length > 0) { // 기존 사용자 업데이트 await db .update(users) .set({ name: employee.fullName, deptCode: employee.departmentCode, deptName: employee.departmentName, domain: assignedDomain as UserDomainType, epId: employee.epId, updatedAt: new Date(), }) .where(eq(users.id, existingUsers[0].id)); updateCount++; } else { // 새 사용자 생성 (partners 도메인 사용자가 아닌 경우에만) // 먼저 해당 이메일이 이미 존재하는지 확인 const anyExistingUser = await db .select({ id: users.id }) .from(users) .where(eq(users.email, employee.emailAddress)) .limit(1); if (anyExistingUser.length === 0) { // 완전히 새로운 사용자 생성 await db .insert(users) .values({ name: employee.fullName, email: employee.emailAddress, deptCode: employee.departmentCode, deptName: employee.departmentName, domain: assignedDomain as UserDomainType, epId: employee.epId, }); insertCount++; } else { // partners 도메인 사용자는 스킵 skipCount++; } } } catch (error) { console.error(`[KNOX-SYNC] 사용자 동기화 실패 (${employee.emailAddress}):`, error); skipCount++; } } console.log(`[KNOX-SYNC] Users 테이블 동기화 완료`); console.log(`[KNOX-SYNC] - 신규 생성: ${insertCount}명`); console.log(`[KNOX-SYNC] - 업데이트: ${updateCount}명`); console.log(`[KNOX-SYNC] - 스킵: ${skipCount}명`); } catch (error) { console.error('[KNOX-SYNC] Users 테이블 동기화 오류:', error); throw error; } } export async function syncKnoxEmployees(): Promise { logSyncStart('임직원'); initializeApiTracker(); // API 추적기 초기화 let totalApiCalls = 0; const startTime = Date.now(); try { // 각 회사별 순차 처리 for (const companyCode of KNOX_COMPANIES) { const apiCalls = await syncEmployeesByCompany(companyCode); totalApiCalls += apiCalls; } logSyncComplete('임직원', totalApiCalls, startTime); // Knox Employee 동기화 완료 후 Users 테이블 동기화 실행 await syncEmployeesToUsers(); } catch (error) { console.error('[KNOX-SYNC] 동기화 중 오류 발생:', error); throw error; } } export async function startKnoxEmployeeSyncScheduler() { // 환경 변수에 따라 실행시 즉시 실행 여부 결정 (없으면 false) if (DO_FIRST_RUN) { if (!checkTimeRestriction()) { return; } syncKnoxEmployees().catch(console.error); } // CRON JOB 등록 cron.schedule(CRON_STRING, () => { syncKnoxEmployees().catch(console.error); }); logSchedulerInfo('임직원', CRON_STRING); }