summaryrefslogtreecommitdiff
path: root/lib/knox-sync/organization-sync-service.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-07-24 10:32:34 +0000
committerjoonhoekim <26rote@gmail.com>2025-07-24 10:32:34 +0000
commited0d6fcc98f671280c2ccde797b50693da88152e (patch)
tree6ea4bc8b13546fbd9de949a378fd2efb22c9cbdd /lib/knox-sync/organization-sync-service.ts
parenta50bc9baea332f996e6bc3a5d70c69f6d2d0f194 (diff)
(김준회) knox 임직원, 조직도, 직급 동기화 로직 수정 (delete & insert) 및 시간당 호출제한 반영, SSO 운영 변경 대응 (버튼 텍스트에 Stage 인 경우에만 Stage 표시), 부서 트리뷰에서 '하위' 뱃지 제거
Diffstat (limited to 'lib/knox-sync/organization-sync-service.ts')
-rw-r--r--lib/knox-sync/organization-sync-service.ts167
1 files changed, 63 insertions, 104 deletions
diff --git a/lib/knox-sync/organization-sync-service.ts b/lib/knox-sync/organization-sync-service.ts
index 8ce9c0ca..6ee72b13 100644
--- a/lib/knox-sync/organization-sync-service.ts
+++ b/lib/knox-sync/organization-sync-service.ts
@@ -7,13 +7,18 @@ import {
searchOrganizations,
Organization,
} from '@/lib/knox-api/employee/employee';
-import { sql } from 'drizzle-orm';
-
-// 동기화 대상 회사 코드 (쉼표로 구분된 ENV)
-const COMPANIES = (process.env.KNOX_COMPANY_CODES || 'D60')
- .split(',')
- .map((c) => c.trim())
- .filter(Boolean);
+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 * * *';
@@ -21,24 +26,10 @@ const CRON_STRING = process.env.KNOX_ORGANIZATION_SYNC_CRON || '15 4 * * *';
// 애플리케이션 기동 시 최초 한 번 실행 여부
const DO_FIRST_RUN = process.env.KNOX_ORGANIZATION_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) // 분당 제한에서 자동 계산
-);
-
-// Knox API 시간대 제한 강제 적용 여부 (테스트용)
-const FORCE_TIME_LIMIT = process.env.KNOX_API_FORCE_LIMIT === 'true';
-
-// API 호출 제한을 위한 지연 함수 (분당 1000건 제한 준수)
-const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-
-async function upsertOrganizations(orgs: Organization[]) {
+async function insertOrganizations(orgs: Organization[]) {
if (!orgs.length) return;
- console.log(`[KNOX-SYNC] ${orgs.length}개 조직 정보 업서트 시작`);
+ console.log(`[KNOX-SYNC] ${orgs.length}개 조직 정보 삽입 시작`);
const startTime = Date.now();
const rows = orgs.map((o) => ({
@@ -70,62 +61,50 @@ async function upsertOrganizations(orgs: Organization[]) {
raw: o as unknown as Record<string, unknown>,
}));
- await db
- .insert(organizationTable)
- .values(rows)
- .onConflictDoUpdate({
- target: [organizationTable.companyCode, organizationTable.departmentCode],
- set: {
- companyName: sql.raw('excluded.company_name'),
- departmentLevel: sql.raw('excluded.department_level'),
- departmentName: sql.raw('excluded.department_name'),
- departmentOrder: sql.raw('excluded.department_order'),
- enCompanyName: sql.raw('excluded.en_company_name'),
- enDepartmentName: sql.raw('excluded.en_department_name'),
- enManagerTitle: sql.raw('excluded.en_manager_title'),
- enSubOrgCode: sql.raw('excluded.en_sub_org_code'),
- inDepartmentCode: sql.raw('excluded.in_department_code'),
- lowDepartmentYn: sql.raw('excluded.low_department_yn'),
- managerId: sql.raw('excluded.manager_id'),
- managerName: sql.raw('excluded.manager_name'),
- managerTitle: sql.raw('excluded.manager_title'),
- preferredLanguage: sql.raw('excluded.preferred_language'),
- subOrgCode: sql.raw('excluded.sub_org_code'),
- subOrgName: sql.raw('excluded.sub_org_name'),
- uprDepartmentCode: sql.raw('excluded.upr_department_code'),
- enUprDepartmentName: sql.raw('excluded.en_upr_department_name'),
- uprDepartmentName: sql.raw('excluded.upr_department_name'),
- hiddenDepartmentYn: sql.raw('excluded.hidden_department_yn'),
- corpCode: sql.raw('excluded.corp_code'),
- corpName: sql.raw('excluded.corp_name'),
- enCorpName: sql.raw('excluded.en_corp_name'),
- raw: sql.raw('excluded.raw'),
- updatedAt: sql.raw('CURRENT_TIMESTAMP'),
- },
- });
+ await db.insert(organizationTable).values(rows);
const endTime = Date.now();
- console.log(`[KNOX-SYNC] ${orgs.length}개 조직 정보 업서트 완료 (소요시간: ${endTime - startTime}ms)`);
+ console.log(`[KNOX-SYNC] ${orgs.length}개 조직 정보 삽입 완료 (소요시간: ${endTime - startTime}ms)`);
+}
+
+/**
+ * 회사별 기존 조직 데이터 삭제
+ */
+async function deleteExistingOrganizations(companyCode: string): Promise<number> {
+ const result = await db
+ .delete(organizationTable)
+ .where(eq(organizationTable.companyCode, companyCode));
+
+ const deletedCount = result.rowCount || 0;
+ console.log(`[KNOX-SYNC] ${companyCode}: 기존 조직 데이터 ${deletedCount}건 삭제 완료`);
+ return deletedCount;
}
/**
- * 회사별 조직 동기화 - 최적화된 버전
- * Knox API 가이드에 따라 첫 번째 호출은 페이지 없이 최대 2,000건 조회
- * 그보다 많은 경우에만 페이징 처리하여 API 호출 수 최소화
+ * 회사별 조직 동기화 - Delete-Insert 방식
+ * 기존 데이터를 모두 삭제한 후 새로운 데이터를 삽입하여 삭제된 데이터 문제 해결
*/
async function syncOrganizationsByCompany(companyCode: string): Promise<number> {
- let totalApiCalls = 0;
+ let currentApiCalls = 0;
let totalOrganizations = 0;
+ let currentDelay = API_CALL_DELAY_MS;
try {
- console.log(`[KNOX-SYNC] ${companyCode}: 조직 동기화 시작`);
+ console.log(`[KNOX-SYNC] ${companyCode}: 조직 동기화 시작 (Delete-Insert 방식)`);
+
+ // 1단계: 기존 데이터 삭제
+ await deleteExistingOrganizations(companyCode);
- // 첫 번째 호출: 페이지 없이 최대 2,000건까지 조회 (Knox API 가이드 기준)
+ // 2단계: 첫 번째 호출 - 페이지 없이 최대 2,000건까지 조회 (Knox API 가이드 기준)
const firstResp = await searchOrganizations({ companyCode });
- totalApiCalls++;
+
+ // API 제한 체크 및 delay 조절
+ const limitStatus = checkApiLimitStatus(1);
+ currentDelay = limitStatus.recommendedDelay;
+ currentApiCalls++;
if (firstResp.result === 'success') {
- await upsertOrganizations(firstResp.organizations);
+ await insertOrganizations(firstResp.organizations);
totalOrganizations += firstResp.organizations.length;
console.log(
@@ -137,14 +116,18 @@ async function syncOrganizationsByCompany(companyCode: string): Promise<number>
console.log(`[KNOX-SYNC] ${companyCode}: ${firstResp.totalPage - 1}개 추가 페이지 처리 시작`);
for (let page = 2; page <= firstResp.totalPage; page++) {
- // Rate limiting을 위한 지연
- await delay(API_CALL_DELAY_MS);
+ // 동적 delay 적용
+ await delay(currentDelay);
const resp = await searchOrganizations({ companyCode, page: String(page) });
- totalApiCalls++;
+
+ // API 제한 체크 및 delay 조절
+ const pageLimit = checkApiLimitStatus(1);
+ currentDelay = pageLimit.recommendedDelay;
+ currentApiCalls++;
if (resp.result === 'success') {
- await upsertOrganizations(resp.organizations);
+ await insertOrganizations(resp.organizations);
totalOrganizations += resp.organizations.length;
console.log(
@@ -157,61 +140,41 @@ async function syncOrganizationsByCompany(companyCode: string): Promise<number>
}
}
- console.log(`[KNOX-SYNC] ${companyCode}: 완료 - ${totalOrganizations}개 조직, API 호출 ${totalApiCalls}회`);
+ console.log(`[KNOX-SYNC] ${companyCode}: 완료 - ${totalOrganizations}개 조직, API 호출 ${currentApiCalls}회`);
} else {
console.warn(`[KNOX-SYNC] ${companyCode}: 첫 번째 조회 실패`);
}
} catch (err) {
console.error(
- `[KNOX-SYNC] ${companyCode}: 동기화 오류 (API 호출 ${totalApiCalls}회)`,
+ `[KNOX-SYNC] ${companyCode}: 동기화 오류 (API 호출 ${currentApiCalls}회)`,
err
);
}
- return totalApiCalls;
+ return currentApiCalls;
}
export async function syncKnoxOrganizations(): Promise<void> {
- console.log('[KNOX-SYNC] Knox 조직 동기화 시작');
- console.log(`[KNOX-SYNC] 대상 회사: ${COMPANIES.join(', ')}`);
+ logSyncStart('조직');
+ initializeApiTracker(); // API 추적기 초기화
let totalApiCalls = 0;
const startTime = Date.now();
- // 각 회사별 순차 처리로 API 호출 제한 준수
- for (const companyCode of COMPANIES) {
+ // 각 회사별 순차 처리
+ for (const companyCode of KNOX_COMPANIES) {
const apiCalls = await syncOrganizationsByCompany(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;
- }
}
- const duration = Math.round((Date.now() - startTime) / 1000);
- console.log(`[KNOX-SYNC] 조직 동기화 완료 - 총 ${totalApiCalls}회 API 호출, ${duration}초 소요`);
+ logSyncComplete('조직', totalApiCalls, startTime);
}
export async function startKnoxOrganizationSyncScheduler() {
// 환경 변수에 따라 실행시 즉시 실행 여부 결정 (없으면 false)
if (DO_FIRST_RUN) {
- // 현재 시간이 주간이면 경고 메시지 또는 실행 중단 (Knox API 가이드 권장사항)
- const currentHour = new Date().getHours();
- if (currentHour >= 6 && currentHour < 22) {
- if (FORCE_TIME_LIMIT) {
- console.error('[KNOX-SYNC] 🚨 주간 시간대(06:00-22:00) 대량 호출 금지 - 야간에 실행하세요');
- console.log('[KNOX-SYNC] 💡 테스트용 실행을 원하면 KNOX_API_FORCE_LIMIT 환경변수를 제거하세요');
- return;
- } else {
- console.warn('[KNOX-SYNC] ⚠️ 주간 시간대 대량 호출 - Knox Portal과 사전 협의 권장 (테스트 모드)');
- }
+ if (!checkTimeRestriction()) {
+ return;
}
syncKnoxOrganizations().catch(console.error);
@@ -222,9 +185,5 @@ export async function startKnoxOrganizationSyncScheduler() {
syncKnoxOrganizations().catch(console.error);
});
- 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`);
- console.log(`[KNOX-SYNC] Knox API 권장: 대량 호출 시 야간(22시 이후) 실행`);
- console.log(`[KNOX-SYNC] 시간대 제한 강제 적용: ${FORCE_TIME_LIMIT ? '활성화' : '비활성화 (테스트 모드)'}`);
+ logSchedulerInfo('조직', CRON_STRING);
} \ No newline at end of file