diff options
| author | joonhoekim <26rote@gmail.com> | 2025-07-22 03:45:58 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-07-22 03:45:58 +0000 |
| commit | e1b1b57b6bfcd18ba4daa44230e8a915b4e93a15 (patch) | |
| tree | 434ead1baf9aba787316f7cf129e7a447e9c98e7 | |
| parent | cb34c5e1a61a20c954e12a8219d82dbdfbe50e13 (diff) | |
(김준회) knox 동기화 로직 개선
| -rw-r--r-- | .env.development | 9 | ||||
| -rw-r--r-- | lib/knox-api/approval/approval.ts | 122 | ||||
| -rw-r--r-- | lib/knox-api/common.ts | 43 | ||||
| -rw-r--r-- | lib/knox-api/employee/employee.ts | 34 | ||||
| -rw-r--r-- | lib/knox-api/realtime-notification/realtime-notification.ts | 20 | ||||
| -rw-r--r-- | lib/knox-sync/employee-sync-service.ts | 130 |
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`); } |
