summaryrefslogtreecommitdiff
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
parentcb34c5e1a61a20c954e12a8219d82dbdfbe50e13 (diff)
(김준회) knox 동기화 로직 개선
-rw-r--r--.env.development9
-rw-r--r--lib/knox-api/approval/approval.ts122
-rw-r--r--lib/knox-api/common.ts43
-rw-r--r--lib/knox-api/employee/employee.ts34
-rw-r--r--lib/knox-api/realtime-notification/realtime-notification.ts20
-rw-r--r--lib/knox-sync/employee-sync-service.ts130
6 files changed, 202 insertions, 156 deletions
diff --git a/.env.development b/.env.development
index ecf1068e..8583069c 100644
--- a/.env.development
+++ b/.env.development
@@ -122,10 +122,12 @@ 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_TITLE_SYNC_CRON="0 3 * * *" # 매일 새벽 3시
KNOX_TITLE_SYNC_FIRST_RUN=true
KNOX_ORGANIZATION_SYNC_CRON="30 3 * * *" # 매일 새벽 3시 30분
@@ -134,7 +136,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/lib/knox-api/approval/approval.ts b/lib/knox-api/approval/approval.ts
index 6a21e113..75066478 100644
--- a/lib/knox-api/approval/approval.ts
+++ b/lib/knox-api/approval/approval.ts
@@ -1,5 +1,7 @@
"use server"
+import { getKnoxConfig, createJsonHeaders, createFormHeaders } from '../common';
+
// Knox API Approval 서버 액션들
// 가이드: lib/knox-api/approval/guide.html
@@ -63,8 +65,8 @@ export interface ApprovalDetailResponse extends BaseResponse {
status: string; // 암호화실패(-3), 암호화중(-2), 예약상신(-1), 보류(0), 진행중(1), 완결(2), 반려(3), 상신취소(4), 전결(5), 후완결(6)
timeZone: string;
subject: string;
- aplns: any[];
- attachments?: any[];
+ aplns: ApprovalLine[];
+ attachments?: File[];
};
}
@@ -100,27 +102,27 @@ export interface CancelApprovalResponse extends BaseResponse {
// 개인 결재경로 목록 조회 응답 타입
export interface OwnApprovalLineListResponse extends BaseResponse {
- data: any[];
+ data: ApprovalLine[];
}
// 개인 결재경로 상세 조회 응답 타입
export interface OwnApprovalLineDetailResponse extends BaseResponse {
- data: any;
+ data: ApprovalLine;
}
// 상신함 리스트 조회 응답 타입
export interface SubmissionListResponse extends BaseResponse {
- data: any[];
+ data: SubmitApprovalRequest[];
}
// 연계 이력 조회 응답 타입
export interface ApprovalHistoryResponse extends BaseResponse {
- data: any[];
+ data: SubmitApprovalRequest[];
}
// 연계 ID 조회 응답 타입
export interface ApprovalIdsResponse extends BaseResponse {
- data: any[];
+ data: string[];
}
// ========== 서버 액션 함수들 ==========
@@ -130,10 +132,10 @@ export interface ApprovalIdsResponse extends BaseResponse {
* POST /approval/api/v2.0/approvals/submit
*/
export async function submitApproval(
- request: SubmitApprovalRequest,
- systemId: string
+ request: SubmitApprovalRequest
): Promise<SubmitApprovalResponse> {
try {
+ const config = await getKnoxConfig();
const formData = new FormData();
// JSON 데이터 생성
@@ -162,11 +164,9 @@ export async function submitApproval(
});
}
- const response = await fetch(`${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/submit`, {
+ const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/submit`, {
method: 'POST',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createFormHeaders(),
body: formData,
});
@@ -186,10 +186,10 @@ export async function submitApproval(
* POST /approval/api/v2.0/approvals/secu-submit
*/
export async function submitSecurityApproval(
- request: SubmitApprovalRequest,
- systemId: string
+ request: SubmitApprovalRequest
): Promise<SubmitApprovalResponse> {
try {
+ const config = await getKnoxConfig();
const formData = new FormData();
// JSON 데이터 생성
@@ -218,11 +218,9 @@ export async function submitSecurityApproval(
});
}
- const response = await fetch(`${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/secu-submit`, {
+ const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/secu-submit`, {
method: 'POST',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createFormHeaders(),
body: formData,
});
@@ -242,15 +240,13 @@ export async function submitSecurityApproval(
* GET /approval/api/v2.0/approvals/{apInfId}/detail
*/
export async function getApprovalDetail(
- apInfId: string,
- systemId: string
+ apInfId: string
): Promise<ApprovalDetailResponse> {
try {
- const response = await fetch(`${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/${apInfId}/detail`, {
+ const config = await getKnoxConfig();
+ const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/${apInfId}/detail`, {
method: 'GET',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createJsonHeaders(),
});
if (!response.ok) {
@@ -269,15 +265,13 @@ export async function getApprovalDetail(
* GET /approval/api/v2.0/approvals/{apInfId}/content
*/
export async function getApprovalContent(
- apInfId: string,
- systemId: string
+ apInfId: string
): Promise<ApprovalContentResponse> {
try {
- const response = await fetch(`${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/${apInfId}/content`, {
+ const config = await getKnoxConfig();
+ const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/${apInfId}/content`, {
method: 'GET',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createJsonHeaders(),
});
if (!response.ok) {
@@ -296,16 +290,13 @@ export async function getApprovalContent(
* POST /approval/api/v2.0/approvals/status
*/
export async function getApprovalStatus(
- request: ApprovalStatusRequest,
- systemId: string
+ request: ApprovalStatusRequest
): Promise<ApprovalStatusResponse> {
try {
- const response = await fetch(`${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/status`, {
+ const config = await getKnoxConfig();
+ const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/status`, {
method: 'POST',
- headers: {
- 'System-ID': systemId,
- 'Content-Type': 'application/json',
- },
+ headers: await createJsonHeaders(),
body: JSON.stringify(request.apinfids),
});
@@ -325,11 +316,11 @@ export async function getApprovalStatus(
* GET /approval/api/v2.0/approvals/apinfids
*/
export async function getApprovalIds(
- systemId: string,
apIds?: string[]
): Promise<ApprovalIdsResponse> {
try {
- let url = `${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/apinfids`;
+ const config = await getKnoxConfig();
+ let url = `${config.baseUrl}/approval/api/v2.0/approvals/apinfids`;
if (apIds && apIds.length > 0) {
const params = new URLSearchParams();
@@ -339,9 +330,7 @@ export async function getApprovalIds(
const response = await fetch(url, {
method: 'GET',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createJsonHeaders(),
});
if (!response.ok) {
@@ -360,11 +349,11 @@ export async function getApprovalIds(
* GET /approval/api/v2.0/approvals/submission
*/
export async function getSubmissionList(
- systemId: string,
params?: Record<string, string>
): Promise<SubmissionListResponse> {
try {
- let url = `${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/submission`;
+ const config = await getKnoxConfig();
+ let url = `${config.baseUrl}/approval/api/v2.0/approvals/submission`;
if (params) {
const searchParams = new URLSearchParams(params);
@@ -373,9 +362,7 @@ export async function getSubmissionList(
const response = await fetch(url, {
method: 'GET',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createJsonHeaders(),
});
if (!response.ok) {
@@ -394,11 +381,11 @@ export async function getSubmissionList(
* GET /approval/api/v2.0/approvals/apinfidinfos
*/
export async function getApprovalHistory(
- systemId: string,
params?: Record<string, string>
): Promise<ApprovalHistoryResponse> {
try {
- let url = `${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/apinfidinfos`;
+ const config = await getKnoxConfig();
+ let url = `${config.baseUrl}/approval/api/v2.0/approvals/apinfidinfos`;
if (params) {
const searchParams = new URLSearchParams(params);
@@ -407,9 +394,7 @@ export async function getApprovalHistory(
const response = await fetch(url, {
method: 'GET',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createJsonHeaders(),
});
if (!response.ok) {
@@ -428,15 +413,13 @@ export async function getApprovalHistory(
* POST /approval/api/v2.0/approvals/{apInfId}/cancel
*/
export async function cancelApproval(
- apInfId: string,
- systemId: string
+ apInfId: string
): Promise<CancelApprovalResponse> {
try {
- const response = await fetch(`${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/${apInfId}/cancel`, {
+ const config = await getKnoxConfig();
+ const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/${apInfId}/cancel`, {
method: 'POST',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createJsonHeaders(),
});
if (!response.ok) {
@@ -455,11 +438,11 @@ export async function cancelApproval(
* GET /approval/api/v2.0/approvals/ownaplnlist
*/
export async function getOwnApprovalLineList(
- systemId: string,
params?: Record<string, string>
): Promise<OwnApprovalLineListResponse> {
try {
- let url = `${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/ownaplnlist`;
+ const config = await getKnoxConfig();
+ let url = `${config.baseUrl}/approval/api/v2.0/approvals/ownaplnlist`;
if (params) {
const searchParams = new URLSearchParams(params);
@@ -468,9 +451,7 @@ export async function getOwnApprovalLineList(
const response = await fetch(url, {
method: 'GET',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createJsonHeaders(),
});
if (!response.ok) {
@@ -489,15 +470,13 @@ export async function getOwnApprovalLineList(
* GET /approval/api/v2.0/approvals/{pslAplnId}/ownaplndetail
*/
export async function getOwnApprovalLineDetail(
- pslAplnId: string,
- systemId: string
+ pslAplnId: string
): Promise<OwnApprovalLineDetailResponse> {
try {
- const response = await fetch(`${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/approvals/${pslAplnId}/ownaplndetail`, {
+ const config = await getKnoxConfig();
+ const response = await fetch(`${config.baseUrl}/approval/api/v2.0/approvals/${pslAplnId}/ownaplndetail`, {
method: 'GET',
- headers: {
- 'System-ID': systemId,
- },
+ headers: await createJsonHeaders(),
});
if (!response.ok) {
@@ -522,9 +501,10 @@ export async function createSubmitApprovalRequest(
approvalLines: ApprovalLine[],
options: Partial<SubmitApprovalRequest> = {}
): Promise<SubmitApprovalRequest> {
+ const config = await getKnoxConfig();
const now = new Date();
const sbmDt = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
- const apInfId = `${process.env.KNOX_SYSTEM_ID || 'DEFAULT'}${sbmDt}${Math.random().toString(36).substr(2, 9)}`.padEnd(32, '0');
+ const apInfId = `${config.systemId}${sbmDt}${Math.random().toString(36).substr(2, 9)}`.padEnd(32, '0');
return {
contents,
diff --git a/lib/knox-api/common.ts b/lib/knox-api/common.ts
new file mode 100644
index 00000000..4c037e56
--- /dev/null
+++ b/lib/knox-api/common.ts
@@ -0,0 +1,43 @@
+"use server"
+
+// Knox API 공통 설정 및 유틸리티
+
+// 기본 설정 타입
+export interface KnoxConfig {
+ baseUrl: string;
+ systemId: string;
+ bearerToken: string;
+}
+
+// 설정 가져오기 (환경변수 또는 설정에서)
+export const getKnoxConfig = async (): Promise<KnoxConfig> => {
+ return {
+ baseUrl: process.env.KNOX_API_BASE_URL || 'https://openapi.samsung.net',
+ systemId: process.env.KNOX_SYSTEM_ID || 'KCD60REST00046',
+ bearerToken: process.env.KNOX_API_BEARER || '',
+ };
+};
+
+// 공통 헤더 생성
+export const createHeaders = async (contentType: string = 'application/json'): Promise<Record<string, string>> => {
+ const config = await getKnoxConfig();
+ return {
+ 'Content-Type': contentType,
+ 'System-ID': config.systemId,
+ Authorization: `Bearer ${config.bearerToken}`,
+ };
+};
+
+// JSON 전용 헤더
+export const createJsonHeaders = async (): Promise<Record<string, string>> => {
+ return await createHeaders('application/json');
+};
+
+// FormData 전용 헤더 (Content-Type 자동 설정)
+export const createFormHeaders = async (): Promise<Record<string, string>> => {
+ const config = await getKnoxConfig();
+ return {
+ 'System-ID': config.systemId,
+ Authorization: `Bearer ${config.bearerToken}`,
+ };
+}; \ No newline at end of file
diff --git a/lib/knox-api/employee/employee.ts b/lib/knox-api/employee/employee.ts
index 885b5a3f..d9e66555 100644
--- a/lib/knox-api/employee/employee.ts
+++ b/lib/knox-api/employee/employee.ts
@@ -1,12 +1,8 @@
"use server"
-// Knox API 임직원 관련 서버 액션들
+import { getKnoxConfig, createJsonHeaders } from '../common';
-// 기본 설정 타입
-interface KnoxConfig {
- baseUrl: string;
- systemId: string;
-}
+// Knox API 임직원 관련 서버 액션들
// 공통 응답 타입
interface BaseResponse {
@@ -184,28 +180,14 @@ interface TitleResponse extends BaseResponse {
titles: Title[];
}
-// 설정 가져오기 (환경변수 또는 설정에서)
-const getKnoxConfig = (): KnoxConfig => {
- return {
- baseUrl: process.env.KNOX_API_BASE_URL || "https://api.knox.samsung.com",
- systemId: process.env.KNOX_SYSTEM_ID || "C60REST0001"
- };
-};
-// 공통 헤더 생성
-const createHeaders = (systemId: string): Record<string, string> => {
- return {
- "Content-Type": "application/json",
- "System-ID": systemId
- };
-};
/**
* 임직원 조회 API
* POST /employee/api/v2.0/employees
*/
export async function searchEmployees(params: EmployeeSearchParams): Promise<EmployeeResponse> {
- const config = getKnoxConfig();
+ const config = await getKnoxConfig();
try {
// URL 파라미터 구성
@@ -232,7 +214,7 @@ export async function searchEmployees(params: EmployeeSearchParams): Promise<Emp
`${config.baseUrl}/employee/api/v2.0/employees?${searchParams.toString()}`,
{
method: "POST",
- headers: createHeaders(config.systemId),
+ headers: await createJsonHeaders(),
body: JSON.stringify(body)
}
);
@@ -254,7 +236,7 @@ export async function searchEmployees(params: EmployeeSearchParams): Promise<Emp
* GET /employee/api/v2.0/organizations
*/
export async function searchOrganizations(params: OrganizationSearchParams): Promise<OrganizationResponse> {
- const config = getKnoxConfig();
+ const config = await getKnoxConfig();
try {
// URL 파라미터 구성
@@ -270,7 +252,7 @@ export async function searchOrganizations(params: OrganizationSearchParams): Pro
`${config.baseUrl}/employee/api/v2.0/organizations?${searchParams.toString()}`,
{
method: "GET",
- headers: createHeaders(config.systemId)
+ headers: await createJsonHeaders()
}
);
@@ -291,7 +273,7 @@ export async function searchOrganizations(params: OrganizationSearchParams): Pro
* GET /employee/api/v2.0/titles
*/
export async function searchTitles(params: TitleSearchParams): Promise<TitleResponse> {
- const config = getKnoxConfig();
+ const config = await getKnoxConfig();
try {
// URL 파라미터 구성
@@ -302,7 +284,7 @@ export async function searchTitles(params: TitleSearchParams): Promise<TitleResp
`${config.baseUrl}/employee/api/v2.0/titles?${searchParams.toString()}`,
{
method: "GET",
- headers: createHeaders(config.systemId)
+ headers: await createJsonHeaders()
}
);
diff --git a/lib/knox-api/realtime-notification/realtime-notification.ts b/lib/knox-api/realtime-notification/realtime-notification.ts
index a26f5b55..52258010 100644
--- a/lib/knox-api/realtime-notification/realtime-notification.ts
+++ b/lib/knox-api/realtime-notification/realtime-notification.ts
@@ -1,6 +1,7 @@
"use server"
import { z } from "zod"
+import { getKnoxConfig, createJsonHeaders } from '../common';
// 타입 정의
const ColorSchema = z.object({
@@ -73,17 +74,7 @@ export interface NotificationError {
message: string
}
-// 환경 변수 검증
-const getApiConfig = () => {
- const baseUrl = process.env.KNOX_API_BASE_URL
- const systemId = process.env.KNOX_SYSTEM_ID
-
- if (!baseUrl || !systemId) {
- throw new Error("Knox API configuration missing: KNOX_API_BASE_URL and KNOX_SYSTEM_ID are required")
- }
-
- return { baseUrl, systemId }
-}
+
/**
* Knox Suite 실시간 토스트 알림 전송
@@ -95,13 +86,12 @@ export async function sendNotification(
// 요청 데이터 검증
const validatedRequest = NotificationRequestSchema.parse(request)
- const { baseUrl, systemId } = getApiConfig()
+ const config = await getKnoxConfig()
- const response = await fetch(`${baseUrl}/notification/api/v2.0/sendnotification`, {
+ const response = await fetch(`${config.baseUrl}/notification/api/v2.0/sendnotification`, {
method: "POST",
headers: {
- "Content-Type": "application/json",
- "System-ID": systemId,
+ ...(await createJsonHeaders()),
"hint": validatedRequest.exActionsVO.hint || "multibrowser",
},
body: JSON.stringify(validatedRequest),
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`);
}