diff options
Diffstat (limited to 'lib/shi-api')
| -rw-r--r-- | lib/shi-api/shi-api-utils.ts | 157 |
1 files changed, 100 insertions, 57 deletions
diff --git a/lib/shi-api/shi-api-utils.ts b/lib/shi-api/shi-api-utils.ts index b8eeff29..7ea582f6 100644 --- a/lib/shi-api/shi-api-utils.ts +++ b/lib/shi-api/shi-api-utils.ts @@ -41,55 +41,97 @@ export const getAllNonsapUser = async () => { // ** 2. 받은 데이터를 DELETE & INSERT 방식으로 수신 테이블 (nonsap-user) 에 저장 ** await db.delete(nonsapUser); // 전체 정리 if (Array.isArray(data) && data.length > 0) { - await db.insert(nonsapUser).values(data as unknown as NonsapUserInsert[]); // 데이터 저장 (스키마 컬럼 그대로) - debugSuccess(`[STAGE] nonsap_user refreshed with ${data.length} records`); + // 대용량 데이터를 청크 단위로 분할하여 저장 (스택 오버플로 방지) + const STAGING_CHUNK_SIZE = 1000; // 스테이징 테이블용 청크 크기 + let totalStaged = 0; + + for (let i = 0; i < data.length; i += STAGING_CHUNK_SIZE) { + const chunk = data.slice(i, i + STAGING_CHUNK_SIZE); + debugLog(`[STAGING CHUNK ${Math.floor(i/STAGING_CHUNK_SIZE) + 1}] Inserting ${chunk.length} records (${i + 1}-${Math.min(i + chunk.length, data.length)}/${data.length})`); + + try { + await db.insert(nonsapUser).values(chunk as unknown as NonsapUserInsert[]); + totalStaged += chunk.length; + + // 청크 간 잠시 대기하여 시스템 부하 방지 + if (i + STAGING_CHUNK_SIZE < data.length) { + await new Promise(resolve => setTimeout(resolve, 50)); + } + } catch (chunkError) { + debugError(`[STAGING CHUNK ${Math.floor(i/STAGING_CHUNK_SIZE) + 1}] Failed to insert chunk ${i}-${i+chunk.length}`, chunkError); + throw chunkError; + } + } + + debugSuccess(`[STAGE] nonsap_user refreshed with ${totalStaged} records (processed in ${Math.ceil(data.length/STAGING_CHUNK_SIZE)} chunks)`); } // ** 3. 데이터 저장 이후, 비즈니스 테이블인 "public"."users" 에 동기화 시킴 (매핑 필요) ** const now = new Date(); - const mappedRaw: Partial<InsertUser>[] = (Array.isArray(data) ? data : []) - .map((u: NonsapUser): Partial<InsertUser> => { - const isDeleted = ynToBool(u.DEL_YN); // nonsap user 테이블에서 삭제여부 - const isAbsent = ynToBool(u.LOFF_GB); // nonsap user 테이블에서 휴직여부 - const notApproved = (u.AGR_YN || '').toUpperCase() === 'N'; // nonsap user 테이블에서 승인여부 - const isActive = !(isDeleted || isAbsent || notApproved); // eVCP 내에서 활성화 여부 - // S = 정직원 - const isRegularEmployee = (u.REGL_ORORD_GB || '').toUpperCase() === 'S'; - - return { - // mapped fields - nonsapUserId: u.USR_ID || undefined, - employeeNumber: u.EMPNO || undefined, - knoxId: u.MYSNG_ID || undefined, - name: u.USR_NM || undefined, - email: u.EMAIL_ADR || undefined, - epId: u.MYSNG_ID || undefined, - deptCode: u.DEPTCD || undefined, - deptName: u.DEPTNM || undefined, - phone: u.HP_NO || undefined, - isAbsent, - isDeletedOnNonSap: isDeleted, - isActive, - isRegularEmployee, - }; - }); - // users 테이블 제약조건 대응: email, name 은 not null + nonsapUserId 존재 - //.filter((u) => typeof u.email === 'string' && !!u.email && typeof u.name === 'string' && !!u.name && typeof u.nonsapUserId === 'string' && u.nonsapUserId.length > 0); - - const mappedUsers = mappedRaw as InsertUser[]; - - if (mappedUsers.length > 0) { - // 배치 처리: 500개씩 분할하여 처리 (콜스택 크기 문제 대응) - const BATCH_SIZE = 500; - let totalUpserted = 0; + // 매핑과 DB 처리를 청크 단위로 통합 처리하여 메모리 효율성 향상 + const sourceData = Array.isArray(data) ? data : []; + if (sourceData.length === 0) { + debugWarn('[UPSERT] No source data to process'); + return { + fetched: 0, + staged: 0, + upserted: 0, + skippedDueToMissingRequiredFields: 0, + ranAt: now.toISOString(), + }; + } + + const CHUNK_SIZE = 500; // 매핑과 DB 처리 청크 크기 + let totalUpserted = 0; + let totalSkipped = 0; + + // 청크 단위로 매핑과 DB 처리를 동시에 수행 + for (let i = 0; i < sourceData.length; i += CHUNK_SIZE) { + const chunk = sourceData.slice(i, i + CHUNK_SIZE); + debugLog(`[CHUNK ${Math.floor(i/CHUNK_SIZE) + 1}] Processing ${chunk.length} users (${i + 1}-${Math.min(i + chunk.length, sourceData.length)}/${sourceData.length})`); - for (let i = 0; i < mappedUsers.length; i += BATCH_SIZE) { - const batch = mappedUsers.slice(i, i + BATCH_SIZE); + try { + // 청크 단위로 매핑 수행 + const mappedChunk: InsertUser[] = []; - try { + for (const u of chunk) { + const isDeleted = ynToBool(u.DEL_YN); // nonsap user 테이블에서 삭제여부 + const isAbsent = ynToBool(u.LOFF_GB); // nonsap user 테이블에서 휴직여부 + const notApproved = (u.AGR_YN || '').toUpperCase() === 'N'; // nonsap user 테이블에서 승인여부 + const isActive = !(isDeleted || isAbsent || notApproved); // eVCP 내에서 활성화 여부 + // S = 정직원 + const isRegularEmployee = (u.REGL_ORORD_GB || '').toUpperCase() === 'S'; + + const mappedUser: Partial<InsertUser> = { + // mapped fields + nonsapUserId: u.USR_ID || undefined, + employeeNumber: u.EMPNO || undefined, + knoxId: u.MYSNG_ID || undefined, + name: u.USR_NM || undefined, + email: u.EMAIL_ADR || undefined, + epId: u.MYSNG_ID || undefined, + deptCode: u.DEPTCD || undefined, + deptName: u.DEPTNM || undefined, + phone: u.HP_NO || undefined, + isAbsent, + isDeletedOnNonSap: isDeleted, + isActive, + isRegularEmployee, + }; + + // 필수 필드 검증 (기본적인 검증만 수행) + if (mappedUser.nonsapUserId) { + mappedChunk.push(mappedUser as InsertUser); + } else { + totalSkipped++; + } + } + + // 매핑된 청크가 있을 경우에만 DB 처리 + if (mappedChunk.length > 0) { await db.insert(users) - .values(batch) + .values(mappedChunk) .onConflictDoUpdate({ target: users.nonsapUserId, set: { @@ -109,31 +151,32 @@ export const getAllNonsapUser = async () => { }, }); - totalUpserted += batch.length; - debugLog(`[BATCH ${Math.floor(i/BATCH_SIZE) + 1}] Processed ${batch.length} users (${totalUpserted}/${mappedUsers.length})`); - - // 배치 간 잠시 대기하여 시스템 부하 방지 - if (i + BATCH_SIZE < mappedUsers.length) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - } catch (batchError) { - debugError(`[BATCH ${Math.floor(i/BATCH_SIZE) + 1}] Failed to process batch ${i}-${i+batch.length}`, batchError); - throw batchError; + totalUpserted += mappedChunk.length; + debugLog(`[CHUNK ${Math.floor(i/CHUNK_SIZE) + 1}] Successfully processed ${mappedChunk.length} users`); } + + // 청크 간 잠시 대기하여 시스템 부하 방지 + if (i + CHUNK_SIZE < sourceData.length) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + } catch (chunkError) { + debugError(`[CHUNK ${Math.floor(i/CHUNK_SIZE) + 1}] Failed to process chunk ${i}-${i+chunk.length}`, chunkError); + throw chunkError; } - - debugSuccess(`[UPSERT] users upserted=${totalUpserted} using key=nonsapUserId (processed in ${Math.ceil(mappedUsers.length/BATCH_SIZE)} batches)`); - } else { - debugWarn('[UPSERT] No users mapped for upsert (missing name/email or invalid USR_ID)'); } + + debugSuccess(`[UPSERT] users upserted=${totalUpserted}, skipped=${totalSkipped} (processed in ${Math.ceil(sourceData.length/CHUNK_SIZE)} chunks)`); + + // 처리 완료 // 휴직 사용자도 API에서 수신하므로, 기존 사용자와의 비교를 통한 휴직 처리 로직은 더 이상 필요하지 않음 return { fetched: Array.isArray(data) ? data.length : 0, staged: Array.isArray(data) ? data.length : 0, - upserted: mappedUsers.length, - skippedDueToMissingRequiredFields: (Array.isArray(data) ? data.length : 0) - mappedUsers.length, + upserted: totalUpserted, + skippedDueToMissingRequiredFields: totalSkipped, ranAt: now.toISOString(), }; } catch(error){ |
