diff options
| author | joonhoekim <26rote@gmail.com> | 2025-08-14 13:15:21 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-08-14 13:15:21 +0000 |
| commit | 49d236df3bd2bd976ebc424644f34f5affa1074f (patch) | |
| tree | 7b0f60c399e724847894061fae74876aa1bf5c7e /lib | |
| parent | 969c25b56f6d29d7ffa4bc2ce04c5fb4e5846b34 (diff) | |
(김준회) 결재 테스트 모듈 수정, 환경병수 eVCP 운영 대응, SGIPS JWT TOKEN 수정, SHI-API 기반 유저 관리 추가, 유저목록 테이블 변경
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/knox-sync/master-sync-service.ts | 7 | ||||
| -rw-r--r-- | lib/sedp/sedp-token.ts | 4 | ||||
| -rw-r--r-- | lib/shi-api/shi-api-utils.ts | 125 | ||||
| -rw-r--r-- | lib/shi-api/users-sync-scheduler.ts | 42 | ||||
| -rw-r--r-- | lib/users/auth/verifyCredentails.ts | 2 | ||||
| -rw-r--r-- | lib/users/table/users-table-columns.tsx | 25 |
6 files changed, 195 insertions, 10 deletions
diff --git a/lib/knox-sync/master-sync-service.ts b/lib/knox-sync/master-sync-service.ts index ed77a3fd..8950f514 100644 --- a/lib/knox-sync/master-sync-service.ts +++ b/lib/knox-sync/master-sync-service.ts @@ -34,9 +34,10 @@ export async function syncAllKnoxData(): Promise<void> { console.log('[KNOX-SYNC] 2/3: 조직 동기화 완료 ✅'); // 3단계: 임직원 동기화 (조직 완료 후) - console.log('[KNOX-SYNC] 3/3: 임직원 동기화 시작'); - await syncKnoxEmployees(); - console.log('[KNOX-SYNC] 3/3: 임직원 동기화 완료 ✅'); + console.log('[KNOX-SYNC] 3/3: 임직원 동기화는 생략 (SHI-API를 통한 nonsap 사용자로 동기화함'); + // console.log('[KNOX-SYNC] 3/3: 임직원 동기화 시작'); + // await syncKnoxEmployees(); + // console.log('[KNOX-SYNC] 3/3: 임직원 동기화 완료 ✅'); const overallDuration = Math.round((Date.now() - overallStartTime) / 1000); console.log(`[KNOX-SYNC] 🎉 Knox 통합 동기화 완료 - 총 ${overallDuration}초 소요`); diff --git a/lib/sedp/sedp-token.ts b/lib/sedp/sedp-token.ts index 9335a74e..0aa3b185 100644 --- a/lib/sedp/sedp-token.ts +++ b/lib/sedp/sedp-token.ts @@ -36,8 +36,8 @@ export async function getSEDPToken(): Promise<string> { const jsonData = JSON.parse(tokenData); if (typeof jsonData === 'string') { return jsonData; // JSON 문자열이지만 내용물이 토큰 문자열인 경우 - } else if (jsonData.token) { - return jsonData.token; // { token: "..." } 형태인 경우 + } else if (jsonData.Token) { + return jsonData.Token; // { Token: "..." } 형태인 경우 } else { console.warn('예상치 못한 토큰 응답 형식:', jsonData); // 가장 가능성 있는 필드를 찾아봄 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); + } +} + + diff --git a/lib/users/auth/verifyCredentails.ts b/lib/users/auth/verifyCredentails.ts index a5dbab41..5cb9c24f 100644 --- a/lib/users/auth/verifyCredentails.ts +++ b/lib/users/auth/verifyCredentails.ts @@ -510,7 +510,7 @@ export async function verifySGipsCredentials( method: 'GET', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.S_GIPS_TOKEN}`, + 'Authorization': `Bearer ${process.env.SHI_API_JWT_TOKEN}`, }, }); diff --git a/lib/users/table/users-table-columns.tsx b/lib/users/table/users-table-columns.tsx index 217fefcf..d4c5c78a 100644 --- a/lib/users/table/users-table-columns.tsx +++ b/lib/users/table/users-table-columns.tsx @@ -6,7 +6,6 @@ import { type ColumnDef } from "@tanstack/react-table" import { Ellipsis } from "lucide-react" import { userRoles, type UserView } from "@/db/schema/users" -import { formatDate } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" @@ -96,10 +95,28 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<UserVie type: cfg.type, }, cell: ({ row, cell }) => { + // 날짜 컬럼: YYYY-MM-DD HH:mm + if (cfg.id === "created_at" || cfg.id === "updated_at" || cfg.id === "deactivated_at") { + const v = cell.getValue() as Date | string | null | undefined + if (!v) return "" + const d = new Date(v as any) + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, "0") + const day = String(d.getDate()).padStart(2, "0") + const hh = String(d.getHours()).padStart(2, "0") + const mm = String(d.getMinutes()).padStart(2, "0") + return `${y}-${m}-${day} ${hh}:${mm}` + } - if (cfg.id === "created_at") { - const dateVal = cell.getValue() as Date - return formatDate(dateVal, "KR") + // 불리언 컬럼: Y/N + if ( + cfg.id === "is_locked" || + cfg.id === "is_absent" || + cfg.id === "is_deleted_on_non_sap" || + cfg.id === "is_regular_employee" + ) { + const v = row.getValue(cfg.id) as boolean | null | undefined + return v === true ? "Y" : v === false ? "N" : "" } if (cfg.id === "roles") { |
