summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.development5
-rw-r--r--.env.production5
-rw-r--r--components/knox/approval/ApprovalSubmit.tsx2
-rw-r--r--db/schema/users.ts3
-rw-r--r--instrumentation.ts9
-rw-r--r--lib/nonsap-sync/purchase-group-code/purchase-group-code-sync.ts143
-rw-r--r--lib/nonsap-sync/purchase-group-code/user-code-sync-scheduler.ts46
7 files changed, 210 insertions, 3 deletions
diff --git a/.env.development b/.env.development
index d2e34235..50902e0e 100644
--- a/.env.development
+++ b/.env.development
@@ -61,6 +61,9 @@ ORACLE_CONNECTION_STRING=60.100.89.191:7971/SEVMQ # 품질
# NON-SAP 인코텀즈, 지불조건, 선적지, 하역지 동기화 관련
PROCUREMENT_SYNC_ON_START=false
+# 구매그룹코드를 users 테이블의 userCode 컬럼에 동기화
+USER_CODE_SYNC_CRON="0 3 * * *" # 매일 새벽 3시 (기본값)
+USER_CODE_SYNC_FIRST_RUN="false"
# 기본 DOLCE 동기화 값 (60.100.99.217=dolce 개발, 60.100.98.68=dolce 운영)
SYNC_DOLCE_BATCH=150 # 없으면 100으로 fallback
@@ -101,7 +104,7 @@ DOLCE_DOWNLOAD_URL="http://60.100.99.217:1111/Download.aspx"
# SHI_API_BASE_URL="http://qa.shi-api.com"
SHI_API_BASE_URL="http://shi-api.com"
SHI_NONSAP_USER_SEGMENT="/evcp/Common/CMCTB_USR"
-NONSAP_USERSYNC_FIRST_RUN="true"
+NONSAP_USERSYNC_FIRST_RUN="false"
# Bearer Token 운영
# SHI_API_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJrZXlhdXRoLWV2Y3AiLCJuYmYiOjE3NTM5NzQwMDAsImV4cCI6MzI1MzUxNjE5NDB9.Ec_xP5lrhGQBRP_7rfZCtQXQQ8X1wzzPrEubhCe9fXg"
diff --git a/.env.production b/.env.production
index 917a2c5e..77c06e58 100644
--- a/.env.production
+++ b/.env.production
@@ -62,6 +62,9 @@ ORACLE_CONNECTION_STRING=60.100.89.191:7971/SEVMQ # 품질
# NON-SAP 인코텀즈, 지불조건, 선적지, 하역지 동기화 관련
PROCUREMENT_SYNC_ON_START=false
+# 구매그룹코드를 users 테이블의 userCode 컬럼에 동기화
+USER_CODE_SYNC_CRON="0 3 * * *" # 매일 새벽 3시 (기본값)
+USER_CODE_SYNC_FIRST_RUN="false"
# 기본 DOLCE 동기화 값 (60.100.99.217=dolce 개발, 60.100.98.68=dolce 운영)
SYNC_DOLCE_BATCH=150 # 없으면 100으로 fallback
@@ -102,7 +105,7 @@ DOLCE_DOWNLOAD_URL="http://60.100.99.217:1111/Download.aspx"
# SHI_API_BASE_URL="http://qa.shi-api.com"
SHI_API_BASE_URL="http://shi-api.com"
SHI_NONSAP_USER_SEGMENT="/evcp/Common/CMCTB_USR"
-NONSAP_USERSYNC_FIRST_RUN="true"
+NONSAP_USERSYNC_FIRST_RUN="false"
# Bearer Token 운영
# SHI_API_JWT_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJrZXlhdXRoLWV2Y3AiLCJuYmYiOjE3NTM5NzQwMDAsImV4cCI6MzI1MzUxNjE5NDB9.Ec_xP5lrhGQBRP_7rfZCtQXQQ8X1wzzPrEubhCe9fXg"
diff --git a/components/knox/approval/ApprovalSubmit.tsx b/components/knox/approval/ApprovalSubmit.tsx
index d9ccc785..e4854888 100644
--- a/components/knox/approval/ApprovalSubmit.tsx
+++ b/components/knox/approval/ApprovalSubmit.tsx
@@ -926,7 +926,7 @@ export default function ApprovalSubmit({
debugLog("Submit Response", response);
- if (response.result === "SUCCESS") {
+ if (response.result === "success") {
setSubmitResult({ apInfId: response.data.apInfId });
toast.success("결재가 성공적으로 상신되었습니다.");
onSubmitSuccess?.(response.data.apInfId);
diff --git a/db/schema/users.ts b/db/schema/users.ts
index 0e8809b7..f01db8db 100644
--- a/db/schema/users.ts
+++ b/db/schema/users.ts
@@ -67,6 +67,9 @@ export const users = pgTable("users", {
isDeletedOnNonSap: boolean("is_deleted_on_non_sap"), // 퇴직여부 (SHI-API DEL_YN (Y/N))
isRegularEmployee: boolean("is_regular_employee"), // 정직원여부 (SHI-API REGL_ORORD_GB (S/N))
+ // 사용자 코드 (구매그룹코드)
+ userCode: varchar("user_code", { length: 50 }),
+
}, (table) => {
return {
emailIdx: uniqueIndex("users_email_idx").on(table.email),
diff --git a/instrumentation.ts b/instrumentation.ts
index a98cda12..3a2cc9bb 100644
--- a/instrumentation.ts
+++ b/instrumentation.ts
@@ -49,5 +49,14 @@ export async function register() {
console.error('Failed to start SHI-API users daily cron scheduler.');
// 스케줄러 실패해도 애플리케이션은 계속 실행
}
+
+ try {
+ // 사용자 코드 동기화 스케줄러 시작 (구매그룹코드)
+ const { startUserCodeSyncScheduler } = await import('./lib/nonsap-sync/purchase-group-code/user-code-sync-scheduler');
+ await startUserCodeSyncScheduler();
+ } catch {
+ console.error('Failed to start User Code sync scheduler.');
+ // 스케줄러 실패해도 애플리케이션은 계속 실행
+ }
}
}
diff --git a/lib/nonsap-sync/purchase-group-code/purchase-group-code-sync.ts b/lib/nonsap-sync/purchase-group-code/purchase-group-code-sync.ts
new file mode 100644
index 00000000..903a164c
--- /dev/null
+++ b/lib/nonsap-sync/purchase-group-code/purchase-group-code-sync.ts
@@ -0,0 +1,143 @@
+import { oracleKnex } from '@/lib/oracle-db/db';
+import db from '@/db/db';
+import { users } from '@/db/schema/users';
+import { eq } from 'drizzle-orm';
+
+/**
+ * 오라클 NonSAP 데이터베이스에서 구매그룹 정보 조회
+ * CMCTB_CD 테이블에서 CD_CLF='MMA070' 조건으로 구매그룹 데이터를 가져옴
+ */
+async function fetchPurchasingGroupsFromOracle(): Promise<Array<{ code: string; employeeNumber: string }>> {
+ try {
+ console.log('🔍 오라클에서 구매그룹 데이터 조회 시작...');
+
+ const result = await oracleKnex
+ .select('CD as code', 'USR_DF_CHAR_9 as employeeNumber')
+ .from('CMCTB_CD')
+ .where('CD_CLF', 'MMA070')
+ .whereNotNull('USR_DF_CHAR_9')
+
+ console.log(`✅ 구매그룹 데이터 ${result.length}건 조회 완료`);
+ return result;
+ } catch (error) {
+ console.error('❌ 오라클에서 구매그룹 데이터 조회 중 오류:', error);
+ throw error;
+ }
+}
+
+/**
+ * employeeNumber 기준으로 users 테이블에 사용자 코드 upsert
+ */
+async function updateUsersCode(purchasingGroups: Array<{ code: string; employeeNumber: string }>) {
+ try {
+ console.log('🔄 사용자 테이블 사용자 코드 업데이트 시작...');
+
+ let updateCount = 0;
+ let notFoundCount = 0;
+ const errors: Array<{ employeeNumber: string; error: string }> = [];
+
+ // 개별 트랜잭션으로 안전하게 처리
+ for (const group of purchasingGroups) {
+ try {
+ await db.transaction(async (tx) => {
+ const result = await tx
+ .update(users)
+ .set({
+ userCode: group.code,
+ updatedAt: new Date()
+ })
+ .where(eq(users.employeeNumber, group.employeeNumber))
+ .returning({ id: users.id });
+
+ if (result.length > 0) {
+ updateCount++;
+ } else {
+ notFoundCount++;
+ console.warn(`⚠️ 사번 ${group.employeeNumber}에 해당하는 사용자를 찾을 수 없음 (사용자 코드: ${group.code})`);
+ }
+ });
+ } catch (error) {
+ errors.push({
+ employeeNumber: group.employeeNumber,
+ error: error instanceof Error ? error.message : '알 수 없는 오류'
+ });
+ console.error(`❌ 사번 ${group.employeeNumber} 업데이트 중 오류:`, error);
+ }
+ }
+
+ // 에러가 있었다면 로그 출력
+ if (errors.length > 0) {
+ console.error(`❌ ${errors.length}건의 업데이트 오류 발생:`);
+ errors.forEach(({ employeeNumber, error }) => {
+ console.error(` - 사번 ${employeeNumber}: ${error}`);
+ });
+ }
+
+ console.log(`✅ 사용자 코드 업데이트 완료: 성공 ${updateCount}건, 미발견 ${notFoundCount}건`);
+ return { updateCount, notFoundCount };
+ } catch (error) {
+ console.error('❌ 사용자 테이블 업데이트 중 오류:', error);
+ throw error;
+ }
+}
+
+/**
+ * 사용자 코드 동기화 메인 함수
+ * 오라클에서 구매그룹 데이터를 조회하여 users 테이블을 업데이트
+ */
+export async function syncUserCodes(): Promise<{ success: boolean; updateCount: number; notFoundCount: number; error?: string }> {
+ try {
+ console.log('🚀 사용자 코드 동기화 작업 시작...');
+ const startTime = Date.now();
+
+ // 1. 오라클에서 구매그룹 데이터 조회
+ const purchasingGroups = await fetchPurchasingGroupsFromOracle();
+
+ if (purchasingGroups.length === 0) {
+ console.log('⚠️ 조회된 구매그룹 데이터가 없습니다.');
+ return { success: true, updateCount: 0, notFoundCount: 0 };
+ }
+
+ // 2. users 테이블 업데이트
+ const { updateCount, notFoundCount } = await updateUsersCode(purchasingGroups);
+
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+
+ console.log(`🎉 사용자 코드 동기화 완료! 소요시간: ${duration}ms`);
+ console.log(`📊 처리 결과: 총 ${purchasingGroups.length}건 중 업데이트 ${updateCount}건, 미발견 ${notFoundCount}건`);
+
+ return {
+ success: true,
+ updateCount,
+ notFoundCount
+ };
+ } catch (error) {
+ console.error('💥 사용자 코드 동기화 작업 중 오류:', error);
+ return {
+ success: false,
+ updateCount: 0,
+ notFoundCount: 0,
+ error: error instanceof Error ? error.message : '알 수 없는 오류'
+ };
+ }
+}
+
+/**
+ * 특정 사번의 사용자 코드를 조회하는 함수 (개별 조회용)
+ */
+export async function getUserCodeByEmployeeNumber(employeeNumber: string): Promise<string | null> {
+ try {
+ const result = await oracleKnex
+ .select('CD')
+ .from('CMCTB_CD')
+ .where('CD_CLF', 'MMA070')
+ .andWhere('USR_DF_CHAR_9', employeeNumber)
+ .limit(1);
+
+ return result.length > 0 ? result[0].CD : null;
+ } catch (error) {
+ console.error(`사용자 코드 조회 중 오류 (사번: ${employeeNumber}):`, error);
+ return null;
+ }
+}
diff --git a/lib/nonsap-sync/purchase-group-code/user-code-sync-scheduler.ts b/lib/nonsap-sync/purchase-group-code/user-code-sync-scheduler.ts
new file mode 100644
index 00000000..23d0b568
--- /dev/null
+++ b/lib/nonsap-sync/purchase-group-code/user-code-sync-scheduler.ts
@@ -0,0 +1,46 @@
+'use server';
+
+import * as cron from 'node-cron';
+import { syncUserCodes } from './purchase-group-code-sync';
+
+// CRON 스케줄 (기본: 매일 새벽 3시)
+const CRON_STRING = process.env.USER_CODE_SYNC_CRON || '0 3 * * *';
+
+// 애플리케이션 기동 시 최초 한 번 실행 여부
+const DO_FIRST_RUN = process.env.USER_CODE_SYNC_FIRST_RUN === 'true';
+
+/**
+ * 사용자 코드 동기화 스케줄러 시작
+ */
+export async function startUserCodeSyncScheduler(): Promise<void> {
+ console.log('[USER-CODE-SYNC] 사용자 코드 동기화 스케줄러 초기화 중...');
+
+ try {
+ // 환경 변수에 따라 실행시 즉시 실행 여부 결정
+ if (DO_FIRST_RUN) {
+ console.log('[USER-CODE-SYNC] 첫 실행: 사용자 코드 동기화 즉시 시작');
+ syncUserCodes().catch((error) => {
+ console.error('[USER-CODE-SYNC] 첫 실행 중 오류:', error);
+ });
+ }
+
+ // CRON JOB 등록
+ cron.schedule(CRON_STRING, async () => {
+ try {
+ console.log('[USER-CODE-SYNC] CRON 실행: 사용자 코드 동기화 시작');
+ await syncUserCodes();
+ } catch (error) {
+ console.error('[USER-CODE-SYNC] 예약된 동기화 실패:', error);
+ // 동기화 실패해도 다음 스케줄은 계속 실행
+ }
+ }, {
+ timezone: 'Asia/Seoul'
+ });
+
+ console.log(`[USER-CODE-SYNC] ✅ 사용자 코드 동기화 CRON 작업 등록 완료 (${CRON_STRING})`);
+
+ } catch (error) {
+ console.error('[USER-CODE-SYNC] 스케줄러 초기화 실패:', error);
+ throw error;
+ }
+}