diff options
| -rw-r--r-- | .env.development | 15 | ||||
| -rw-r--r-- | .env.production | 15 | ||||
| -rw-r--r-- | db/schema/index.ts | 7 | ||||
| -rw-r--r-- | db/schema/knox/organization.ts | 59 | ||||
| -rw-r--r-- | db/schema/knox/titles.ts | 30 | ||||
| -rw-r--r-- | instrumentation.ts | 27 | ||||
| -rw-r--r-- | lib/knox-sync/organization-sync-service.ts | 132 | ||||
| -rw-r--r-- | lib/knox-sync/title-sync-service.ts | 70 |
8 files changed, 345 insertions, 10 deletions
diff --git a/.env.development b/.env.development index d6edf7d5..ecf1068e 100644 --- a/.env.development +++ b/.env.development @@ -122,13 +122,22 @@ MDG_SOAP_PASSWORD=SEW2765890 # 품질 SOAP_LOG_MAX_RECORDS=500 # === SOAP 인터페이스 설정 === -# KNOX API 사용을 위한 설정 +# KNOX API 사용을 위한 설정 +# 임직원 API: 임직원, 조직도, 직급 KNOX_COMPANY_CODES="P2" # 삼성중공업 회사코드 = P2 + +KNOX_TITLE_SYNC_CRON="0 3 * * *" # 매일 새벽 3시 +KNOX_TITLE_SYNC_FIRST_RUN=true +KNOX_ORGANIZATION_SYNC_CRON="30 3 * * *" # 매일 새벽 3시 30분 +KNOX_ORGANIZATION_SYNC_FIRST_RUN=true KNOX_EMPLOYEE_SYNC_CRON="0 4 * * *" # 매일 새벽 4시 -KNOX_EMPLOYEE_SYNC_FIRST_RUN=true +KNOX_EMPLOYEE_SYNC_FIRST_RUN=false # 양이 많으므로 시작시 갱신 X +# BASEURL: https://openapi.stage.samsung.net +KNOX_API_BASE_URL="https://openapi.stage.samsung.net" +KNOX_BASE_URL="https://openapi.stage.samsung.net" MESSENGER_ACCESS_TOKEN="" MESSENGER_DEVICE_ID="" -MESSENGER_BASE_URL="" +MESSENGER_BASE_URL="https://openapi.stage.samsung.net" # 임시 환경변수 --- 요구사항 해소되면 삭제 READONLY_DB_URL="postgresql://readonly:tempReadOnly_123@localhost:5432/evcp" diff --git a/.env.production b/.env.production index 005f2fc8..6f8099b4 100644 --- a/.env.production +++ b/.env.production @@ -123,13 +123,22 @@ MDG_SOAP_PASSWORD=SEW2765890 # 품질 SOAP_LOG_MAX_RECORDS=500 # === SOAP 인터페이스 설정 === -# KNOX API 사용을 위한 설정 +# KNOX API 사용을 위한 설정 +# 임직원 API: 임직원, 조직도, 직급 KNOX_COMPANY_CODES="P2" # 삼성중공업 회사코드 = P2 + +KNOX_TITLE_SYNC_CRON="0 3 * * *" # 매일 새벽 3시 +KNOX_TITLE_SYNC_FIRST_RUN=true +KNOX_ORGANIZATION_SYNC_CRON="30 3 * * *" # 매일 새벽 3시 30분 +KNOX_ORGANIZATION_SYNC_FIRST_RUN=true KNOX_EMPLOYEE_SYNC_CRON="0 4 * * *" # 매일 새벽 4시 -KNOX_EMPLOYEE_SYNC_FIRST_RUN=true +KNOX_EMPLOYEE_SYNC_FIRST_RUN=false # 양이 많으므로 시작시 갱신 X +# BASEURL: https://openapi.stage.samsung.net +KNOX_API_BASE_URL="https://openapi.stage.samsung.net" +KNOX_BASE_URL="https://openapi.stage.samsung.net" MESSENGER_ACCESS_TOKEN="" MESSENGER_DEVICE_ID="" -MESSENGER_BASE_URL="" +MESSENGER_BASE_URL="https://openapi.stage.samsung.net" # NAS 경로 설정 NAS_PATH="/evcp_nas" diff --git a/db/schema/index.ts b/db/schema/index.ts index 100084ed..db449616 100644 --- a/db/schema/index.ts +++ b/db/schema/index.ts @@ -40,4 +40,9 @@ export * from './SOAP/soap'; export * from './NONSAP/nonsap'; // ECC SOAP 수신용 (RFQ, PO, PR 데이터) -export * from './ECC/ecc';
\ No newline at end of file +export * from './ECC/ecc'; + +// Knox 임직원, 조직도, 직급 +export * from './knox/employee'; +export * from './knox/organization'; +export * from './knox/titles';
\ No newline at end of file diff --git a/db/schema/knox/organization.ts b/db/schema/knox/organization.ts new file mode 100644 index 00000000..48b13254 --- /dev/null +++ b/db/schema/knox/organization.ts @@ -0,0 +1,59 @@ +import { pgSchema, varchar, jsonb, timestamp, index, primaryKey } from "drizzle-orm/pg-core"; + +// Knox 전용 스키마 네임스페이스 재사용 +export const knoxSchema = pgSchema("knox"); + +export const organization = knoxSchema.table( + "organization", + { + companyCode: varchar("company_code", { length: 10 }).notNull(), + departmentCode: varchar("department_code", { length: 50 }).notNull(), + + companyName: varchar("company_name", { length: 100 }), + departmentLevel: varchar("department_level", { length: 10 }), + departmentName: varchar("department_name", { length: 255 }), + departmentOrder: varchar("department_order", { length: 10 }), + + enCompanyName: varchar("en_company_name", { length: 100 }), + enDepartmentName: varchar("en_department_name", { length: 255 }), + enManagerTitle: varchar("en_manager_title", { length: 255 }), + enSubOrgCode: varchar("en_sub_org_code", { length: 50 }), + + inDepartmentCode: varchar("in_department_code", { length: 50 }), + lowDepartmentYn: varchar("low_department_yn", { length: 2 }), + + managerId: varchar("manager_id", { length: 50 }), + managerName: varchar("manager_name", { length: 100 }), + managerTitle: varchar("manager_title", { length: 255 }), + + preferredLanguage: varchar("preferred_language", { length: 5 }), + + subOrgCode: varchar("sub_org_code", { length: 50 }), + subOrgName: varchar("sub_org_name", { length: 255 }), + + uprDepartmentCode: varchar("upr_department_code", { length: 50 }), + enUprDepartmentName: varchar("en_upr_department_name", { length: 255 }), + uprDepartmentName: varchar("upr_department_name", { length: 255 }), + + hiddenDepartmentYn: varchar("hidden_department_yn", { length: 2 }), + + corpCode: varchar("corp_code", { length: 20 }), + corpName: varchar("corp_name", { length: 100 }), + enCorpName: varchar("en_corp_name", { length: 100 }), + + // 원본 JSON 전체 보관 – 변경 감지 및 추후 확장 대비 + raw: jsonb("raw").notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + }, + (table) => { + return { + companyIdx: index("knox_org_company_idx").on(table.companyCode), + pk: primaryKey(table.companyCode, table.departmentCode), + }; + } +); + +export type KnoxOrganization = typeof organization.$inferSelect; +export type NewKnoxOrganization = typeof organization.$inferInsert; diff --git a/db/schema/knox/titles.ts b/db/schema/knox/titles.ts new file mode 100644 index 00000000..338ba79b --- /dev/null +++ b/db/schema/knox/titles.ts @@ -0,0 +1,30 @@ +import { pgSchema, varchar, jsonb, timestamp, index, primaryKey } from "drizzle-orm/pg-core"; + +export const knoxSchema = pgSchema("knox"); + +export const title = knoxSchema.table( + "title", + { + companyCode: varchar("company_code", { length: 10 }).notNull(), + titleCode: varchar("title_code", { length: 20 }).notNull(), + + titleName: varchar("title_name", { length: 100 }), + enTitleName: varchar("en_title_name", { length: 100 }), + sortOrder: varchar("sort_order", { length: 10 }), + + // 전체 원본 JSON 데이터 저장 + raw: jsonb("raw").notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + }, + (table) => { + return { + companyIdx: index("knox_title_company_idx").on(table.companyCode), + pk: primaryKey(table.companyCode, table.titleCode), + }; + } +); + +export type KnoxTitle = typeof title.$inferSelect; +export type NewKnoxTitle = typeof title.$inferInsert; diff --git a/instrumentation.ts b/instrumentation.ts index 7641c777..415214ce 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -12,11 +12,32 @@ export async function register() { // const { startEnhancedSyncScheduler } = await import('./lib/nonsap-sync/enhanced-sync-service'); // startEnhancedSyncScheduler(); + } catch (error) { + console.error('Failed to start Enhanced NONSAP sync scheduler.'); + // 스케줄러 실패해도 애플리케이션은 계속 실행 + } + + try { + // Knox 직급 동기화 스케줄러 시작 + const { startKnoxTitleSyncScheduler } = await import( + './lib/knox-sync/title-sync-service' + ); + startKnoxTitleSyncScheduler(); + + // Knox 조직 동기화 스케줄러 시작 + const { startKnoxOrganizationSyncScheduler } = await import( + './lib/knox-sync/organization-sync-service' + ); + startKnoxOrganizationSyncScheduler(); + // Knox 임직원 동기화 스케줄러 시작 - const { startKnoxEmployeeSyncScheduler } = await import('./lib/knox-sync/employee-sync-service'); + const { startKnoxEmployeeSyncScheduler } = await import( + './lib/knox-sync/employee-sync-service' + ); startKnoxEmployeeSyncScheduler(); - } catch (error) { - console.error('Failed to start Enhanced NONSAP sync scheduler:', error); + } + catch (error) { + console.error('Failed to start Knox employee/organization/title sync scheduler.'); // 스케줄러 실패해도 애플리케이션은 계속 실행 } } diff --git a/lib/knox-sync/organization-sync-service.ts b/lib/knox-sync/organization-sync-service.ts new file mode 100644 index 00000000..0b77174b --- /dev/null +++ b/lib/knox-sync/organization-sync-service.ts @@ -0,0 +1,132 @@ +'use server'; + +import * as cron from 'node-cron'; +import db from '@/db/db'; +import { organization as organizationTable } from '@/db/schema/knox/organization'; +import { + searchOrganizations, + Organization, +} from '@/lib/knox-api/employee/employee'; +import { sql } from 'drizzle-orm'; + +// 동기화 대상 회사 코드 (쉼표로 구분된 ENV) +const COMPANIES = (process.env.KNOX_COMPANY_CODES || 'P2') + .split(',') + .map((c) => c.trim()) + .filter(Boolean); + +// CRON 스케줄 (기본: 매일 04:15) +const CRON_STRING = process.env.KNOX_ORGANIZATION_SYNC_CRON || '15 4 * * *'; + +// 애플리케이션 기동 시 최초 한 번 실행 여부 +const DO_FIRST_RUN = process.env.KNOX_ORGANIZATION_SYNC_FIRST_RUN === 'true'; + +async function upsertOrganizations(orgs: Organization[]) { + if (!orgs.length) return; + + const rows = orgs.map((o) => ({ + companyCode: o.companyCode, + departmentCode: o.departmentCode, + companyName: o.companyName, + departmentLevel: o.departmentLevel, + departmentName: o.departmentName, + departmentOrder: o.departmentOrder, + enCompanyName: o.enCompanyName, + enDepartmentName: o.enDepartmentName, + enManagerTitle: o.enManagerTitle, + enSubOrgCode: o.enSubOrgCode, + inDepartmentCode: o.inDepartmentCode, + lowDepartmentYn: o.lowDepartmentYn, + managerId: o.managerId, + managerName: o.managerName, + managerTitle: o.managerTitle, + preferredLanguage: o.preferredLanguage, + subOrgCode: o.subOrgCode, + subOrgName: o.subOrgName, + uprDepartmentCode: o.uprDepartmentCode, + enUprDepartmentName: o.enUprDepartmentName, + uprDepartmentName: o.uprDepartmentName, + hiddenDepartmentYn: o.hiddenDepartmentYn, + corpCode: o.corpCode, + corpName: o.corpName, + enCorpName: o.enCorpName, + raw: o as unknown as Record<string, unknown>, + })); + + await db + .insert(organizationTable) + .values(rows) + .onConflictDoUpdate({ + target: [organizationTable.companyCode, organizationTable.departmentCode], + set: { + companyName: sql.raw('excluded.company_name'), + departmentLevel: sql.raw('excluded.department_level'), + departmentName: sql.raw('excluded.department_name'), + departmentOrder: sql.raw('excluded.department_order'), + enCompanyName: sql.raw('excluded.en_company_name'), + enDepartmentName: sql.raw('excluded.en_department_name'), + enManagerTitle: sql.raw('excluded.en_manager_title'), + enSubOrgCode: sql.raw('excluded.en_sub_org_code'), + inDepartmentCode: sql.raw('excluded.in_department_code'), + lowDepartmentYn: sql.raw('excluded.low_department_yn'), + managerId: sql.raw('excluded.manager_id'), + managerName: sql.raw('excluded.manager_name'), + managerTitle: sql.raw('excluded.manager_title'), + preferredLanguage: sql.raw('excluded.preferred_language'), + subOrgCode: sql.raw('excluded.sub_org_code'), + subOrgName: sql.raw('excluded.sub_org_name'), + uprDepartmentCode: sql.raw('excluded.upr_department_code'), + enUprDepartmentName: sql.raw('excluded.en_upr_department_name'), + uprDepartmentName: sql.raw('excluded.upr_department_name'), + hiddenDepartmentYn: sql.raw('excluded.hidden_department_yn'), + corpCode: sql.raw('excluded.corp_code'), + corpName: sql.raw('excluded.corp_name'), + enCorpName: sql.raw('excluded.en_corp_name'), + raw: sql.raw('excluded.raw'), + updatedAt: sql.raw('CURRENT_TIMESTAMP'), + }, + }); +} + +export async function syncKnoxOrganizations(): Promise<void> { + console.log('[KNOX-SYNC] 조직 동기화 시작'); + + for (const companyCode of COMPANIES) { + try { + let page = 1; + let totalPage = 1; + do { + const resp = await searchOrganizations({ companyCode, page: String(page) }); + + if (resp.result === 'success') { + await upsertOrganizations(resp.organizations); + totalPage = resp.totalPage; + console.log( + `[KNOX-SYNC] 조직 동기화 ${companyCode} ${page}/${totalPage} 페이지 처리` + ); + } else { + console.warn(`[KNOX-SYNC] 조직 동기화 실패: ${companyCode} page ${page}`); + break; + } + + page += 1; + } while (page <= totalPage); + } catch (err) { + console.error(`[KNOX-SYNC] 조직 동기화 오류 (company ${companyCode})`, err); + } + } + + console.log('[KNOX-SYNC] 조직 동기화 완료'); +} + +export async function startKnoxOrganizationSyncScheduler() { + if (DO_FIRST_RUN) { + syncKnoxOrganizations().catch(console.error); + } + + cron.schedule(CRON_STRING, () => { + syncKnoxOrganizations().catch(console.error); + }); + + console.log(`[KNOX-SYNC] 조직 동기화 스케줄러 등록 (${CRON_STRING})`); +}
\ No newline at end of file diff --git a/lib/knox-sync/title-sync-service.ts b/lib/knox-sync/title-sync-service.ts new file mode 100644 index 00000000..e7bc13bd --- /dev/null +++ b/lib/knox-sync/title-sync-service.ts @@ -0,0 +1,70 @@ +'use server'; + +import * as cron from 'node-cron'; +import db from '@/db/db'; +import { title as titleTable } from '@/db/schema/knox/titles'; +import { getTitlesByCompany, Title } from '@/lib/knox-api/employee/employee'; +import { sql } from 'drizzle-orm'; + +const COMPANIES = (process.env.KNOX_COMPANY_CODES || 'P2') + .split(',') + .map((c) => c.trim()) + .filter(Boolean); + +const CRON_STRING = process.env.KNOX_TITLE_SYNC_CRON || '30 4 * * *'; +const DO_FIRST_RUN = process.env.KNOX_TITLE_SYNC_FIRST_RUN === 'true'; + +async function upsertTitles(titles: Title[]) { + if (!titles.length) return; + + const rows = titles.map((t) => ({ + companyCode: t.companyCode, + titleCode: t.titleCode, + titleName: t.titleName, + enTitleName: t.enTitleName, + sortOrder: t.sortOrder, + raw: t as unknown as Record<string, unknown>, + })); + + await db + .insert(titleTable) + .values(rows) + .onConflictDoUpdate({ + target: [titleTable.companyCode, titleTable.titleCode], + set: { + titleName: sql.raw('excluded.title_name'), + enTitleName: sql.raw('excluded.en_title_name'), + sortOrder: sql.raw('excluded.sort_order'), + raw: sql.raw('excluded.raw'), + updatedAt: sql.raw('CURRENT_TIMESTAMP'), + }, + }); +} + +export async function syncKnoxTitles(): Promise<void> { + console.log('[KNOX-SYNC] 직급 동기화 시작'); + + for (const companyCode of COMPANIES) { + try { + const titles = await getTitlesByCompany(companyCode); + await upsertTitles(titles); + console.log(`[KNOX-SYNC] 직급 동기화 완료 - ${companyCode}: ${titles.length}건`); + } catch (err) { + console.error(`[KNOX-SYNC] 직급 동기화 오류 (company ${companyCode})`, err); + } + } + + console.log('[KNOX-SYNC] 직급 동기화 전체 완료'); +} + +export async function startKnoxTitleSyncScheduler() { + if (DO_FIRST_RUN) { + syncKnoxTitles().catch(console.error); + } + + cron.schedule(CRON_STRING, () => { + syncKnoxTitles().catch(console.error); + }); + + console.log(`[KNOX-SYNC] 직급 동기화 스케줄러 등록 (${CRON_STRING})`); +}
\ No newline at end of file |
