summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/knox-sync/master-sync-service.ts7
-rw-r--r--lib/sedp/sedp-token.ts4
-rw-r--r--lib/shi-api/shi-api-utils.ts125
-rw-r--r--lib/shi-api/users-sync-scheduler.ts42
-rw-r--r--lib/users/auth/verifyCredentails.ts2
-rw-r--r--lib/users/table/users-table-columns.tsx25
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") {