summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.development15
-rw-r--r--.env.production15
-rw-r--r--db/schema/index.ts7
-rw-r--r--db/schema/knox/organization.ts59
-rw-r--r--db/schema/knox/titles.ts30
-rw-r--r--instrumentation.ts27
-rw-r--r--lib/knox-sync/organization-sync-service.ts132
-rw-r--r--lib/knox-sync/title-sync-service.ts70
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