summaryrefslogtreecommitdiff
path: root/lib/shi-api/shi-api-utils.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-08-18 09:39:14 +0000
committerjoonhoekim <26rote@gmail.com>2025-08-18 09:39:14 +0000
commitaa71f75ace013b2fe982e5a104e61440458e0fd2 (patch)
tree5270f8f0d4cf8f411c6bc1a9f0e0ca21b3003c8f /lib/shi-api/shi-api-utils.ts
parent13bc512bf26618d5c040fd9b19cc0afd7af7c55b (diff)
(김준회) PCR, PO, 변경PR 거절사유 수신 라우트 구현 (ECC 인터페이스), 세일즈포스 POC 테스트페이지 추가 (경로가 파트너 내부인 이유는 CORS 추가한 경로이기 때문이며, 수정될 수 있음), shi-api 유저 업데이트 로직 개선(분할정복패턴)
Diffstat (limited to 'lib/shi-api/shi-api-utils.ts')
-rw-r--r--lib/shi-api/shi-api-utils.ts157
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){