summaryrefslogtreecommitdiff
path: root/lib/knox-sync
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-07-22 03:45:58 +0000
committerjoonhoekim <26rote@gmail.com>2025-07-22 03:45:58 +0000
commite1b1b57b6bfcd18ba4daa44230e8a915b4e93a15 (patch)
tree434ead1baf9aba787316f7cf129e7a447e9c98e7 /lib/knox-sync
parentcb34c5e1a61a20c954e12a8219d82dbdfbe50e13 (diff)
(김준회) knox 동기화 로직 개선
Diffstat (limited to 'lib/knox-sync')
-rw-r--r--lib/knox-sync/employee-sync-service.ts130
1 files changed, 90 insertions, 40 deletions
diff --git a/lib/knox-sync/employee-sync-service.ts b/lib/knox-sync/employee-sync-service.ts
index c517be14..e9d422c7 100644
--- a/lib/knox-sync/employee-sync-service.ts
+++ b/lib/knox-sync/employee-sync-service.ts
@@ -5,13 +5,12 @@ import db from '@/db/db';
import { employee as employeeTable } from '@/db/schema/knox/employee';
import {
searchEmployees,
- getDepartmentsByCompany,
Employee,
} from '@/lib/knox-api/employee/employee';
import { sql } from 'drizzle-orm';
// 동기화 대상 회사 코드 (쉼표로 구분된 ENV)
-const COMPANIES = (process.env.KNOX_COMPANY_CODES || 'P2')
+const COMPANIES = (process.env.KNOX_COMPANY_CODES || 'D60')
.split(',')
.map((c) => c.trim())
.filter(Boolean);
@@ -20,6 +19,17 @@ const CRON_STRING = process.env.KNOX_EMPLOYEE_SYNC_CRON || '0 4 * * *';
const DO_FIRST_RUN = process.env.KNOX_EMPLOYEE_SYNC_FIRST_RUN === 'true';
+// API 호출 제한 설정 (환경변수로 제어 가능)
+const DAILY_API_LIMIT = parseInt(process.env.KNOX_API_DAILY_LIMIT || '100');
+const RATE_LIMIT_PER_MINUTE = parseInt(process.env.KNOX_API_RATE_LIMIT || '1000');
+const API_CALL_DELAY_MS = Math.max(
+ parseInt(process.env.KNOX_API_CALL_DELAY_MS || '100'),
+ Math.ceil(60000 / RATE_LIMIT_PER_MINUTE) // 분당 제한에서 자동 계산
+);
+
+// API 호출 제한을 위한 지연 함수 (분당 1000건 제한 준수)
+const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
async function upsertEmployees(employees: Employee[]) {
if (!employees.length) return;
@@ -90,50 +100,88 @@ async function upsertEmployees(employees: Employee[]) {
});
}
+/**
+ * 회사별 임직원 동기화 - 최적화된 버전
+ * 부서별 개별 호출 대신 회사 코드만으로 전체 조회하여 API 호출 수 최소화
+ */
+async function syncEmployeesByCompany(companyCode: string): Promise<number> {
+ let totalApiCalls = 0;
+ let totalEmployees = 0;
+
+ try {
+ let page = 1;
+ let totalPage = 1;
+
+ console.log(`[KNOX-SYNC] ${companyCode}: 임직원 동기화 시작`);
+
+ do {
+ // Rate limiting을 위한 지연
+ if (totalApiCalls > 0) {
+ await delay(API_CALL_DELAY_MS);
+ }
+
+ const resp = await searchEmployees({
+ companyCode,
+ page: String(page),
+ resultType: 'basic',
+ });
+ totalApiCalls++;
+
+ if (resp.result === 'success') {
+ await upsertEmployees(resp.employees);
+ totalPage = resp.totalPage;
+ totalEmployees += resp.employees.length;
+
+ console.log(
+ `[KNOX-SYNC] ${companyCode}: ${page}/${totalPage} 페이지 처리 완료 (${resp.employees.length}명, 누적: ${totalEmployees}명)`
+ );
+ } else {
+ console.warn(
+ `[KNOX-SYNC] ${companyCode}: 페이지 ${page} 조회 실패`
+ );
+ break;
+ }
+
+ page += 1;
+ } while (page <= totalPage);
+
+ console.log(`[KNOX-SYNC] ${companyCode}: 완료 - ${totalEmployees}명, API 호출 ${totalApiCalls}회`);
+ } catch (err) {
+ console.error(
+ `[KNOX-SYNC] ${companyCode}: 동기화 오류 (API 호출 ${totalApiCalls}회)`,
+ err
+ );
+ }
+
+ return totalApiCalls;
+}
+
export async function syncKnoxEmployees(): Promise<void> {
- console.log('[KNOX-SYNC] 임직원 동기화 시작');
+ console.log('[KNOX-SYNC] Knox 임직원 동기화 시작');
+ console.log(`[KNOX-SYNC] 대상 회사: ${COMPANIES.join(', ')}`);
+
+ let totalApiCalls = 0;
+ const startTime = Date.now();
+ // 각 회사별 순차 처리로 API 호출 제한 준수
for (const companyCode of COMPANIES) {
- try {
- const departments = await getDepartmentsByCompany(companyCode);
- console.log(`[KNOX-SYNC] ${companyCode}: 부서 ${departments.length}개`);
-
- for (const dept of departments) {
- let page = 1;
- let totalPage = 1;
- do {
- const resp = await searchEmployees({
- companyCode,
- departmentCode: dept.departmentCode,
- page: String(page),
- resultType: 'basic',
- });
-
- if (resp.result === 'success') {
- await upsertEmployees(resp.employees);
- totalPage = resp.totalPage;
- console.log(
- `[KNOX-SYNC] 임직원 동기화 ${companyCode}/${dept.departmentCode} ${page}/${totalPage} 페이지 처리`
- );
- } else {
- console.warn(
- `[KNOX-SYNC] 임직원 동기화 실패: ${companyCode}/${dept.departmentCode} page ${page}`
- );
- break;
- }
-
- page += 1;
- } while (page <= totalPage);
- }
- } catch (err) {
- console.error(
- `[KNOX-SYNC] 임직원 동기화 오류 (company ${companyCode})`,
- err
- );
+ const apiCalls = await syncEmployeesByCompany(companyCode);
+ totalApiCalls += apiCalls;
+
+ // 일일 호출 제한(100건) 근접 시 경고
+ if (totalApiCalls >= DAILY_API_LIMIT - 10) { // 일일 제한에 가까워지면 경고
+ console.warn(`[KNOX-SYNC] ⚠️ API 호출 ${totalApiCalls}회 - 일일 제한(${DAILY_API_LIMIT}건) 근접!`);
+ }
+
+ // 일일 제한 초과 방지
+ if (totalApiCalls >= DAILY_API_LIMIT) {
+ console.error(`[KNOX-SYNC] 🚨 일일 API 호출 제한(${DAILY_API_LIMIT}건) 초과로 중단`);
+ break;
}
}
- console.log('[KNOX-SYNC] 임직원 동기화 완료');
+ const duration = Math.round((Date.now() - startTime) / 1000);
+ console.log(`[KNOX-SYNC] 임직원 동기화 완료 - 총 ${totalApiCalls}회 API 호출, ${duration}초 소요`);
}
export async function startKnoxEmployeeSyncScheduler() {
@@ -148,4 +196,6 @@ export async function startKnoxEmployeeSyncScheduler() {
});
console.log(`[KNOX-SYNC] 임직원 동기화 스케줄러 등록 (${CRON_STRING})`);
+ console.log(`[KNOX-SYNC] API 제한사항: 분당 ${RATE_LIMIT_PER_MINUTE}건, 일일 ${DAILY_API_LIMIT}건`);
+ console.log(`[KNOX-SYNC] 호출 간격: ${API_CALL_DELAY_MS}ms`);
}