'use server'; import * as cron from 'node-cron'; import db from '@/db/db'; import { organization as organizationTable } from '@/db/schema/knox/organization'; import { searchOrganizations, Organization, } from '@/lib/knox-api/employee/employee'; import { eq } from 'drizzle-orm'; import { KNOX_COMPANIES, delay, API_CALL_DELAY_MS, checkTimeRestriction, checkApiLimitStatus, initializeApiTracker, logSyncStart, logSyncComplete, logSchedulerInfo, } from './common'; // CRON 스케줄 (기본: 매일 04:15) const CRON_STRING = process.env.KNOX_ORGANIZATION_SYNC_CRON || '15 4 * * *'; // 애플리케이션 기동 시 최초 한 번 실행 여부 const DO_FIRST_RUN = process.env.KNOX_ORGANIZATION_SYNC_FIRST_RUN === 'true'; async function insertOrganizations(orgs: Organization[]) { if (!orgs.length) return; console.log(`[KNOX-SYNC] ${orgs.length}개 조직 정보 삽입 시작`); const startTime = Date.now(); const rows = orgs.map((o) => ({ companyCode: o.companyCode, departmentCode: o.departmentCode, companyName: o.companyName, departmentLevel: o.departmentLevel, departmentName: o.departmentName, departmentOrder: o.departmentOrder, enCompanyName: o.enCompanyName, enDepartmentName: o.enDepartmentName, enManagerTitle: o.enManagerTitle, enSubOrgCode: o.enSubOrgCode, inDepartmentCode: o.inDepartmentCode, lowDepartmentYn: o.lowDepartmentYn, managerId: o.managerId, managerName: o.managerName, managerTitle: o.managerTitle, preferredLanguage: o.preferredLanguage, subOrgCode: o.subOrgCode, subOrgName: o.subOrgName, uprDepartmentCode: o.uprDepartmentCode, enUprDepartmentName: o.enUprDepartmentName, uprDepartmentName: o.uprDepartmentName, hiddenDepartmentYn: o.hiddenDepartmentYn, corpCode: o.corpCode, corpName: o.corpName, enCorpName: o.enCorpName, raw: o as unknown as Record, })); await db.insert(organizationTable).values(rows); const endTime = Date.now(); console.log(`[KNOX-SYNC] ${orgs.length}개 조직 정보 삽입 완료 (소요시간: ${endTime - startTime}ms)`); } /** * 회사별 기존 조직 데이터 삭제 */ async function deleteExistingOrganizations(companyCode: string): Promise { const result = await db .delete(organizationTable) .where(eq(organizationTable.companyCode, companyCode)); const deletedCount = result.rowCount || 0; console.log(`[KNOX-SYNC] ${companyCode}: 기존 조직 데이터 ${deletedCount}건 삭제 완료`); return deletedCount; } /** * 회사별 조직 동기화 - Delete-Insert 방식 * 기존 데이터를 모두 삭제한 후 새로운 데이터를 삽입하여 삭제된 데이터 문제 해결 */ async function syncOrganizationsByCompany(companyCode: string): Promise { let currentApiCalls = 0; let totalOrganizations = 0; let currentDelay = API_CALL_DELAY_MS; try { console.log(`[KNOX-SYNC] ${companyCode}: 조직 동기화 시작 (Delete-Insert 방식)`); // 1단계: 기존 데이터 삭제 await deleteExistingOrganizations(companyCode); // 2단계: 첫 번째 호출 - 페이지 없이 최대 2,000건까지 조회 (Knox API 가이드 기준) const firstResp = await searchOrganizations({ companyCode }); // API 제한 체크 및 delay 조절 const limitStatus = checkApiLimitStatus(1); currentDelay = limitStatus.recommendedDelay; currentApiCalls++; if (firstResp.result === 'success') { await insertOrganizations(firstResp.organizations); totalOrganizations += firstResp.organizations.length; console.log( `[KNOX-SYNC] ${companyCode}: 첫 번째 조회 완료 - 총 ${firstResp.totalCount}개 중 ${firstResp.organizations.length}개 처리 (${firstResp.totalPage} 페이지)` ); // 2,000개를 초과하는 경우 나머지 페이지 처리 if (firstResp.totalPage > 1) { console.log(`[KNOX-SYNC] ${companyCode}: ${firstResp.totalPage - 1}개 추가 페이지 처리 시작`); for (let page = 2; page <= firstResp.totalPage; page++) { // 동적 delay 적용 await delay(currentDelay); const resp = await searchOrganizations({ companyCode, page: String(page) }); // API 제한 체크 및 delay 조절 const pageLimit = checkApiLimitStatus(1); currentDelay = pageLimit.recommendedDelay; currentApiCalls++; if (resp.result === 'success') { await insertOrganizations(resp.organizations); totalOrganizations += resp.organizations.length; console.log( `[KNOX-SYNC] ${companyCode}: ${page}/${resp.totalPage} 페이지 처리 완료 (${resp.organizations.length}개, 누적: ${totalOrganizations}개)` ); } else { console.warn(`[KNOX-SYNC] ${companyCode}: 페이지 ${page} 조회 실패`); break; } } } console.log(`[KNOX-SYNC] ${companyCode}: 완료 - ${totalOrganizations}개 조직, API 호출 ${currentApiCalls}회`); } else { console.warn(`[KNOX-SYNC] ${companyCode}: 첫 번째 조회 실패`); } } catch (err) { console.error( `[KNOX-SYNC] ${companyCode}: 동기화 오류 (API 호출 ${currentApiCalls}회)`, err ); } return currentApiCalls; } export async function syncKnoxOrganizations(): Promise { logSyncStart('조직'); initializeApiTracker(); // API 추적기 초기화 let totalApiCalls = 0; const startTime = Date.now(); // 각 회사별 순차 처리 for (const companyCode of KNOX_COMPANIES) { const apiCalls = await syncOrganizationsByCompany(companyCode); totalApiCalls += apiCalls; } logSyncComplete('조직', totalApiCalls, startTime); } export async function startKnoxOrganizationSyncScheduler() { // 환경 변수에 따라 실행시 즉시 실행 여부 결정 (없으면 false) if (DO_FIRST_RUN) { if (!checkTimeRestriction()) { return; } syncKnoxOrganizations().catch(console.error); } // CRON JOB 등록 cron.schedule(CRON_STRING, () => { syncKnoxOrganizations().catch(console.error); }); logSchedulerInfo('조직', CRON_STRING); }