'use server'; import { nonsapUser, users } from '@/db/schema'; import db from '@/db/db'; import { debugError, debugLog, debugWarn, debugSuccess } from '@/lib/debug-utils'; import { bulkUpsert } from '@/lib/soap/batch-utils'; import { autoAssignPendingUsersDomains } from '@/lib/users/department-domain/service'; const shiApiBaseUrl = process.env.SHI_API_BASE_URL; const shiNonsapUserSegment = process.env.SHI_NONSAP_USER_SEGMENT; const shiApiJwtToken = process.env.SHI_API_JWT_TOKEN; type NonsapUser = typeof nonsapUser.$inferSelect; type NonsapUserInsert = typeof nonsapUser.$inferInsert; type InsertUser = typeof users.$inferInsert; export const getAllNonsapUser = async () => { try{ debugLog('Starting NONSAP user sync via SHI-API'); if (!shiApiBaseUrl || !shiNonsapUserSegment || !shiApiJwtToken) { throw new Error('SHI API 환경변수가 설정되지 않았습니다. (SHI_API_BASE_URL, SHI_NONSAP_USER_SEGMENT, SHI_API_JWT_TOKEN)'); } const ynToBool = (value: string | null | undefined) => (value || '').toUpperCase() === 'Y'; // ** 1. 전체 데이터 조회해 응답 받음 (js 배열) ** const response = await fetch(`${shiApiBaseUrl}${shiNonsapUserSegment}`, { headers: { Authorization: `Bearer ${shiApiJwtToken}`, }, cache: 'no-store', }); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`SHI-API 요청 실패: ${response.status} ${response.statusText} ${text}`); } const data: NonsapUser[] = await response.json(); debugSuccess(`[SHI-API] fetched ${Array.isArray(data) ? data.length : 0} users`); // ** 2. 받은 데이터를 DELETE & INSERT 방식으로 수신 테이블 (nonsap-user) 에 저장 ** await db.delete(nonsapUser); // 전체 정리 if (Array.isArray(data) && data.length > 0) { // 대용량 데이터를 청크 단위로 분할하여 저장 (스택 오버플로 방지) 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(); // 매핑과 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})`); try { // 청크 단위로 매핑 수행 const mappedChunk: InsertUser[] = []; 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 = { // 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_USR_ID || undefined, deptCode: u.DEPTCD || undefined, deptName: u.DEPTNM || undefined, phone: u.HP_NO || undefined, domain: 'pending', // SHI-API를 통해 동기화되는 사용자는 pending 도메인으로 설정 isAbsent, isDeletedOnNonSap: isDeleted, isActive, isRegularEmployee, }; // 필수 필드 검증 (기본적인 검증만 수행) if (mappedUser.nonsapUserId) { mappedChunk.push(mappedUser as InsertUser); } else { totalSkipped++; } } // 매핑된 청크가 있을 경우에만 DB 처리 (트랜잭션으로 처리) if (mappedChunk.length > 0) { await db.transaction(async (tx) => { await bulkUpsert(tx, users, mappedChunk, 'email', 200); // 청크 내에서도 200개씩 세분화 }); 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}, skipped=${totalSkipped} (processed in ${Math.ceil(sourceData.length/CHUNK_SIZE)} chunks)`); // 처리 완료 // 휴직 사용자도 API에서 수신하므로, 기존 사용자와의 비교를 통한 휴직 처리 로직은 더 이상 필요하지 않음 // ** 4. 사용자 동기화 완료 후 부서별 도메인 자동 할당 처리 ** debugLog('[DOMAIN-AUTO-ASSIGN] SHI-API 동기화 완료 후 부서별 도메인 자동 할당 시작'); try { const domainAssignResult = await autoAssignPendingUsersDomains(); debugSuccess(`[DOMAIN-AUTO-ASSIGN] 부서별 도메인 자동 할당 완료: ${domainAssignResult.message}`); } catch (domainError) { debugError('[DOMAIN-AUTO-ASSIGN] 부서별 도메인 자동 할당 실패', domainError); // 도메인 할당 실패해도 메인 동기화 결과는 반환 } return { fetched: Array.isArray(data) ? data.length : 0, staged: Array.isArray(data) ? data.length : 0, upserted: totalUpserted, skippedDueToMissingRequiredFields: totalSkipped, ranAt: now.toISOString(), }; } catch(error){ debugError('SHI-API 동기화 실패', error); console.error("SHI-API 를 통한 유저 동기화 프로세스 간 실패 발생: ", error); throw error; } };