diff options
| -rw-r--r-- | .env.development | 5 | ||||
| -rw-r--r-- | .env.production | 5 | ||||
| -rw-r--r-- | components/knox/approval/ApprovalSubmit.tsx | 2 | ||||
| -rw-r--r-- | db/schema/users.ts | 3 | ||||
| -rw-r--r-- | instrumentation.ts | 9 | ||||
| -rw-r--r-- | lib/nonsap-sync/purchase-group-code/purchase-group-code-sync.ts | 143 | ||||
| -rw-r--r-- | lib/nonsap-sync/purchase-group-code/user-code-sync-scheduler.ts | 46 |
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; + } +} |
