summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-07-23 05:55:31 +0000
committerjoonhoekim <26rote@gmail.com>2025-07-23 05:55:31 +0000
commitedc0eabc8f5fc44408c28023ca155bd73ddf8183 (patch)
tree8469a2d932008f2fb5aae9be854cba436b9f807b
parente1b1b57b6bfcd18ba4daa44230e8a915b4e93a15 (diff)
(김준회) Knox 임직원 기준으로 users 테이블 upsert 처리 (cron-job과 동기식 처리)
-rw-r--r--.env.development1
-rw-r--r--.env.production10
-rw-r--r--db/schema/index.ts3
-rw-r--r--db/schema/knox/organization.ts6
-rw-r--r--db/schema/knox/titles.ts5
-rw-r--r--db/schema/users.ts1
-rw-r--r--lib/knox-sync/employee-sync-service.ts200
-rw-r--r--lib/knox-sync/organization-sync-service.ts144
8 files changed, 319 insertions, 51 deletions
diff --git a/.env.development b/.env.development
index 8583069c..f8167f05 100644
--- a/.env.development
+++ b/.env.development
@@ -128,6 +128,7 @@ KNOX_COMPANY_CODES="D60" # 삼성중공업 회사코드 = D60
KNOX_SYSTEM_ID="KCD60REST00046"
KNOX_API_BEARER="3c7ac68c-b262-3f5b-a5c1-2c208add2964"
# 동기화 설정
+KNOX_API_FORCE_LIMIT=false # 주간대량호출 강제 제한 여부
KNOX_TITLE_SYNC_CRON="0 3 * * *" # 매일 새벽 3시
KNOX_TITLE_SYNC_FIRST_RUN=true
KNOX_ORGANIZATION_SYNC_CRON="30 3 * * *" # 매일 새벽 3시 30분
diff --git a/.env.production b/.env.production
index 6f8099b4..9ea7c1b6 100644
--- a/.env.production
+++ b/.env.production
@@ -123,10 +123,13 @@ MDG_SOAP_PASSWORD=SEW2765890 # 품질
SOAP_LOG_MAX_RECORDS=500
# === SOAP 인터페이스 설정 ===
-# KNOX API 사용을 위한 설정
+# === KNOX API 사용을 위한 설정 ===
# 임직원 API: 임직원, 조직도, 직급
-KNOX_COMPANY_CODES="P2" # 삼성중공업 회사코드 = P2
-
+KNOX_COMPANY_CODES="D60" # 삼성중공업 회사코드 = D60
+KNOX_SYSTEM_ID="KCD60REST00046"
+KNOX_API_BEARER="3c7ac68c-b262-3f5b-a5c1-2c208add2964"
+# 동기화 설정
+KNOX_API_FORCE_LIMIT=false # 주간대량호출 강제 제한 여부
KNOX_TITLE_SYNC_CRON="0 3 * * *" # 매일 새벽 3시
KNOX_TITLE_SYNC_FIRST_RUN=true
KNOX_ORGANIZATION_SYNC_CRON="30 3 * * *" # 매일 새벽 3시 30분
@@ -135,7 +138,6 @@ KNOX_EMPLOYEE_SYNC_CRON="0 4 * * *" # 매일 새벽 4시
KNOX_EMPLOYEE_SYNC_FIRST_RUN=false # 양이 많으므로 시작시 갱신 X
# BASEURL: https://openapi.stage.samsung.net
KNOX_API_BASE_URL="https://openapi.stage.samsung.net"
-KNOX_BASE_URL="https://openapi.stage.samsung.net"
MESSENGER_ACCESS_TOKEN=""
MESSENGER_DEVICE_ID=""
MESSENGER_BASE_URL="https://openapi.stage.samsung.net"
diff --git a/db/schema/index.ts b/db/schema/index.ts
index db449616..19470fca 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -30,6 +30,9 @@ export * from './history';
export * from './notification';
export * from './templates';
+// 부서별 도메인 할당 관리
+export * from './departmentDomainAssignments';
+
// MDG SOAP 수신용
export * from './MDG/mdg'
diff --git a/db/schema/knox/organization.ts b/db/schema/knox/organization.ts
index 48b13254..8d540562 100644
--- a/db/schema/knox/organization.ts
+++ b/db/schema/knox/organization.ts
@@ -1,7 +1,5 @@
-import { pgSchema, varchar, jsonb, timestamp, index, primaryKey } from "drizzle-orm/pg-core";
-
-// Knox 전용 스키마 네임스페이스 재사용
-export const knoxSchema = pgSchema("knox");
+import { varchar, jsonb, timestamp, index, primaryKey } from "drizzle-orm/pg-core";
+import { knoxSchema } from "./employee";
export const organization = knoxSchema.table(
"organization",
diff --git a/db/schema/knox/titles.ts b/db/schema/knox/titles.ts
index 338ba79b..6fdf7329 100644
--- a/db/schema/knox/titles.ts
+++ b/db/schema/knox/titles.ts
@@ -1,6 +1,5 @@
-import { pgSchema, varchar, jsonb, timestamp, index, primaryKey } from "drizzle-orm/pg-core";
-
-export const knoxSchema = pgSchema("knox");
+import { varchar, jsonb, timestamp, index, primaryKey } from "drizzle-orm/pg-core";
+import { knoxSchema } from "./employee";
export const title = knoxSchema.table(
"title",
diff --git a/db/schema/users.ts b/db/schema/users.ts
index ae97da6f..bf5d41de 100644
--- a/db/schema/users.ts
+++ b/db/schema/users.ts
@@ -13,6 +13,7 @@ export const users = pgTable("users", {
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
name: varchar("name", { length: 255 }).notNull(),
email: varchar("email", { length: 255 }).notNull().unique(),
+ deptCode: varchar("deptCode", { length: 50 }),
deptName: varchar("deptName", { length: 255 }),
companyId: integer("company_id")
diff --git a/lib/knox-sync/employee-sync-service.ts b/lib/knox-sync/employee-sync-service.ts
index e9d422c7..282c493b 100644
--- a/lib/knox-sync/employee-sync-service.ts
+++ b/lib/knox-sync/employee-sync-service.ts
@@ -3,11 +3,13 @@
import * as cron from 'node-cron';
import db from '@/db/db';
import { employee as employeeTable } from '@/db/schema/knox/employee';
+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 } from 'drizzle-orm';
+import { sql, eq, and, ne } from 'drizzle-orm';
// 동기화 대상 회사 코드 (쉼표로 구분된 ENV)
const COMPANIES = (process.env.KNOX_COMPANY_CODES || 'D60')
@@ -27,6 +29,9 @@ const API_CALL_DELAY_MS = Math.max(
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));
@@ -156,6 +161,144 @@ async function syncEmployeesByCompany(companyCode: string): Promise<number> {
return totalApiCalls;
}
+/**
+ * Knox Employee 데이터를 Users 테이블과 동기화
+ * Knox 임직원 동기화 완료 후 실행되는 함수
+ */
+async function syncEmployeesToUsers(): Promise<void> {
+ 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,
+ })
+ .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<string, string>();
+ 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,
+ 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,
+ });
+
+ 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<void> {
console.log('[KNOX-SYNC] Knox 임직원 동기화 시작');
console.log(`[KNOX-SYNC] 대상 회사: ${COMPANIES.join(', ')}`);
@@ -163,30 +306,51 @@ export async function syncKnoxEmployees(): Promise<void> {
let totalApiCalls = 0;
const startTime = Date.now();
- // 각 회사별 순차 처리로 API 호출 제한 준수
- for (const companyCode of COMPANIES) {
- const apiCalls = await syncEmployeesByCompany(companyCode);
- totalApiCalls += apiCalls;
+ try {
+ // 각 회사별 순차 처리로 API 호출 제한 준수
+ for (const companyCode of COMPANIES) {
+ 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;
+ // 일일 호출 제한(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}초 소요`);
+ const duration = Math.round((Date.now() - startTime) / 1000);
+ console.log(`[KNOX-SYNC] 임직원 동기화 완료 - 총 ${totalApiCalls}회 API 호출, ${duration}초 소요`);
+
+ // Knox Employee 동기화 완료 후 Users 테이블 동기화 실행
+ await syncEmployeesToUsers();
+
+ } catch (error) {
+ console.error('[KNOX-SYNC] 동기화 중 오류 발생:', error);
+ throw error;
+ }
}
export async function startKnoxEmployeeSyncScheduler() {
// 환경 변수에 따라 실행시 즉시 실행 여부 결정 (없으면 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과 사전 협의 권장 (테스트 모드)');
+ }
+ }
+
syncKnoxEmployees().catch(console.error);
}
@@ -198,4 +362,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`);
+ console.log(`[KNOX-SYNC] Knox API 권장: 대량 호출 시 야간(22시 이후) 실행`);
+ console.log(`[KNOX-SYNC] 시간대 제한 강제 적용: ${FORCE_TIME_LIMIT ? '활성화' : '비활성화 (테스트 모드)'}`);
}
diff --git a/lib/knox-sync/organization-sync-service.ts b/lib/knox-sync/organization-sync-service.ts
index 0b77174b..8ce9c0ca 100644
--- a/lib/knox-sync/organization-sync-service.ts
+++ b/lib/knox-sync/organization-sync-service.ts
@@ -10,7 +10,7 @@ import {
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);
@@ -21,9 +21,26 @@ 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[]) {
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,
@@ -86,47 +103,128 @@ async function upsertOrganizations(orgs: Organization[]) {
updatedAt: sql.raw('CURRENT_TIMESTAMP'),
},
});
+
+ const endTime = Date.now();
+ console.log(`[KNOX-SYNC] ${orgs.length}개 조직 정보 업서트 완료 (소요시간: ${endTime - startTime}ms)`);
+}
+
+/**
+ * 회사별 조직 동기화 - 최적화된 버전
+ * Knox API 가이드에 따라 첫 번째 호출은 페이지 없이 최대 2,000건 조회
+ * 그보다 많은 경우에만 페이징 처리하여 API 호출 수 최소화
+ */
+async function syncOrganizationsByCompany(companyCode: string): Promise<number> {
+ let totalApiCalls = 0;
+ let totalOrganizations = 0;
+
+ try {
+ console.log(`[KNOX-SYNC] ${companyCode}: 조직 동기화 시작`);
+
+ // 첫 번째 호출: 페이지 없이 최대 2,000건까지 조회 (Knox API 가이드 기준)
+ const firstResp = await searchOrganizations({ companyCode });
+ totalApiCalls++;
+
+ if (firstResp.result === 'success') {
+ await upsertOrganizations(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++) {
+ // Rate limiting을 위한 지연
+ await delay(API_CALL_DELAY_MS);
+
+ const resp = await searchOrganizations({ companyCode, page: String(page) });
+ totalApiCalls++;
+
+ if (resp.result === 'success') {
+ await upsertOrganizations(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 호출 ${totalApiCalls}회`);
+ } else {
+ console.warn(`[KNOX-SYNC] ${companyCode}: 첫 번째 조회 실패`);
+ }
+ } catch (err) {
+ console.error(
+ `[KNOX-SYNC] ${companyCode}: 동기화 오류 (API 호출 ${totalApiCalls}회)`,
+ err
+ );
+ }
+
+ return totalApiCalls;
}
export async function syncKnoxOrganizations(): 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 {
- let page = 1;
- let totalPage = 1;
- do {
- const resp = await searchOrganizations({ companyCode, page: String(page) });
-
- if (resp.result === 'success') {
- await upsertOrganizations(resp.organizations);
- totalPage = resp.totalPage;
- console.log(
- `[KNOX-SYNC] 조직 동기화 ${companyCode} ${page}/${totalPage} 페이지 처리`
- );
- } else {
- console.warn(`[KNOX-SYNC] 조직 동기화 실패: ${companyCode} page ${page}`);
- break;
- }
+ const apiCalls = await syncOrganizationsByCompany(companyCode);
+ totalApiCalls += apiCalls;
- page += 1;
- } while (page <= totalPage);
- } catch (err) {
- console.error(`[KNOX-SYNC] 조직 동기화 오류 (company ${companyCode})`, err);
+ // 일일 호출 제한(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 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과 사전 협의 권장 (테스트 모드)');
+ }
+ }
+
syncKnoxOrganizations().catch(console.error);
}
+ // CRON JOB 등록
cron.schedule(CRON_STRING, () => {
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 ? '활성화' : '비활성화 (테스트 모드)'}`);
} \ No newline at end of file