diff options
Diffstat (limited to 'lib/shi-api')
| -rw-r--r-- | lib/shi-api/shi-api-utils.ts | 125 | ||||
| -rw-r--r-- | lib/shi-api/users-sync-scheduler.ts | 42 |
2 files changed, 167 insertions, 0 deletions
diff --git a/lib/shi-api/shi-api-utils.ts b/lib/shi-api/shi-api-utils.ts new file mode 100644 index 00000000..ddbc186f --- /dev/null +++ b/lib/shi-api/shi-api-utils.ts @@ -0,0 +1,125 @@ +'use server'; + +import { nonsapUser, users } from '@/db/schema'; +import db from '@/db/db'; +import { sql } from 'drizzle-orm'; +import { debugError, debugLog, debugWarn, debugSuccess } from '@/lib/debug-utils'; + +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) { + await db.insert(nonsapUser).values(data as unknown as NonsapUserInsert[]); // 데이터 저장 (스키마 컬럼 그대로) + debugSuccess(`[STAGE] nonsap_user refreshed with ${data.length} records`); + } + + // ** 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 { + // upsert key = USR_ID + nonsapUserId: u.USR_ID || undefined, + + + // mapped fields + 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.CH_DEPTCD || undefined, + deptName: u.CH_DEPTNM || undefined, + phone: u.TELNO || 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) { + await db.insert(users) + .values(mappedUsers) + .onConflictDoUpdate({ + target: users.nonsapUserId, + set: { + name: sql`excluded.name`, + employeeNumber: sql`excluded.employeeNumber`, + knoxId: sql`excluded.knoxId`, + epId: sql`excluded."epId"`, + deptCode: sql`excluded."deptCode"`, + deptName: sql`excluded."deptName"`, + phone: sql`excluded.phone`, + nonsapUserId: sql`excluded."nonsapUserId"`, + isAbsent: sql`excluded."isAbsent"`, + isDeletedOnNonSap: sql`excluded."isDeletedOnNonSap"`, + isActive: sql`excluded."isActive"`, + isRegularEmployee: sql`excluded."isRegularEmployee"`, + updatedAt: sql`now()`, + }, + }); + debugSuccess(`[UPSERT] users upserted=${mappedUsers.length} using key=nonsapUserId`); + } else { + debugWarn('[UPSERT] No users mapped for upsert (missing name/email or invalid USR_ID)'); + } + + // 휴직 사용자도 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, + ranAt: now.toISOString(), + }; + } catch(error){ + debugError('SHI-API 동기화 실패', error); + console.error("SHI-API 를 통한 유저 동기화 프로세스 간 실패 발생: ", error); + throw error; + } +}; diff --git a/lib/shi-api/users-sync-scheduler.ts b/lib/shi-api/users-sync-scheduler.ts new file mode 100644 index 00000000..1cca3441 --- /dev/null +++ b/lib/shi-api/users-sync-scheduler.ts @@ -0,0 +1,42 @@ +'use server'; + +import * as cron from 'node-cron'; +import { getAllNonsapUser } from './shi-api-utils'; + +// 기본: 매일 01:00 KST 실행. 환경변수로 오버라이드 가능 +const CRON_STRING = process.env.SHI_API_USERS_SYNC_CRON || '0 1 * * *'; + +/** + * SHI-API NONSAP 사용자 동기화 - 일일 스케줄러 등록 + */ +export async function startShiApiUsersDailySyncScheduler(): Promise<void> { + try { + cron.schedule( + CRON_STRING, + async () => { + try { + console.log('[SHI-API] CRON 실행: NONSAP 사용자 동기화 시작'); + await getAllNonsapUser(); + console.log('[SHI-API] CRON 완료: NONSAP 사용자 동기화 성공'); + } catch (error) { + console.error('[SHI-API] CRON 실패: NONSAP 사용자 동기화 오류', error); + } + }, + { timezone: 'Asia/Seoul' }, + ); + + console.log('[SHI-API] Daily NONSAP user sync cron registered:', CRON_STRING); + } catch (error) { + console.error('Failed to set up SHI-API users daily cron scheduler.', error); + } + + try { + if(process.env.NONSAP_USERSYNC_FIRST_RUN === 'true') { + await getAllNonsapUser(); + } + } catch (error) { + console.error('Failed to sync NONSAP users in first run mode.', error); + } +} + + |
