1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
|
"use server";
import * as cron from 'node-cron';
import { oracleKnex } from '@/lib/oracle-db/db';
import db from '@/db/db';
import { paymentTerms, incoterms, placeOfShipping } from '@/db/schema/procurementRFQ';
import { inArray } from 'drizzle-orm';
// 간단한 로거
const logger = {
info: (message: string, ...args: unknown[]) => console.log(`[PROCUREMENT-SYNC] ${message}`, ...args),
error: (message: string, ...args: unknown[]) => console.error(`[PROCUREMENT-SYNC ERROR] ${message}`, ...args),
warn: (message: string, ...args: unknown[]) => console.warn(`[PROCUREMENT-SYNC WARN] ${message}`, ...args),
success: (message: string, ...args: unknown[]) => console.log(`[PROCUREMENT-SYNC SUCCESS] ${message}`, ...args),
header: (message: string) => {
console.log('\n' + '='.repeat(80));
console.log(`[PROCUREMENT-SYNC] ${message}`);
console.log('='.repeat(80) + '\n');
}
};
/**
* Oracle DB 연결 테스트
*/
async function testOracleConnection(): Promise<boolean> {
try {
const result = await oracleKnex.raw('SELECT 1 FROM DUAL');
return result && result.length > 0;
} catch (error) {
logger.error('Oracle DB 연결 테스트 실패:', error);
return false;
}
}
/**
* 지불조건 동기화
*/
async function syncPaymentTerms(): Promise<void> {
logger.header('지불조건 동기화 시작');
try {
// Oracle에서 지불조건 데이터 조회
const oracleData = await oracleKnex.raw(`
SELECT stc.CD_1 as code, stc.DSC as description
FROM SRMPTB_TYPE_CODE stc
WHERE stc.cd = 'PAYT'
`);
const paymentTermsData = oracleData || [];
logger.info(`Oracle에서 ${paymentTermsData.length}개의 지불조건 데이터 조회`);
if (paymentTermsData.length === 0) {
logger.warn('Oracle에서 지불조건 데이터가 없습니다');
return;
}
// PostgreSQL에서 기존 데이터 조회
const existingData = await db.select().from(paymentTerms);
const existingCodes = new Set(existingData.map(item => item.code));
// Oracle에서 조회한 코드들
const oracleCodes = new Set(paymentTermsData.map((item: { CODE: string; DESCRIPTION: string }) => item.CODE));
// 업데이트할 데이터 준비
const upsertData = paymentTermsData.map((item: { CODE: string; DESCRIPTION: string }) => ({
code: item.CODE,
description: item.DESCRIPTION || 'NONSAP에서 설명 기입 요망',
isActive: true // 기본값 true
}));
// PostgreSQL에 upsert
for (const data of upsertData) {
await db.insert(paymentTerms)
.values(data)
.onConflictDoUpdate({
target: paymentTerms.code,
set: {
description: data.description,
isActive: data.isActive
}
});
}
// PostgreSQL에만 있는 데이터는 isActive = false로 설정
const codesToDeactivate = new Set([...existingCodes].filter(code => !oracleCodes.has(code)));
if (codesToDeactivate.size > 0) {
await db.update(paymentTerms)
.set({ isActive: false })
.where(inArray(paymentTerms.code, Array.from(codesToDeactivate)));
logger.info(`${codesToDeactivate.size}개의 지불조건을 비활성화했습니다`);
}
logger.success(`지불조건 동기화 완료: ${upsertData.length}개 업데이트`);
} catch (error) {
logger.error('지불조건 동기화 중 오류:', error);
throw error;
}
}
/**
* 인코텀즈 동기화
*/
async function syncIncoterms(): Promise<void> {
logger.header('인코텀즈 동기화 시작');
try {
// Oracle에서 인코텀즈 데이터 조회
const oracleData = await oracleKnex.raw(`
SELECT stc.CD_1 as code, stc.DSC as description
FROM SRMPTB_TYPE_CODE stc
WHERE stc.cd = 'INCO'
`);
const incotermsData = oracleData || [];
logger.info(`Oracle에서 ${incotermsData.length}개의 인코텀즈 데이터 조회`);
if (incotermsData.length === 0) {
logger.warn('Oracle에서 인코텀즈 데이터가 없습니다');
return;
}
// PostgreSQL에서 기존 데이터 조회
const existingData = await db.select().from(incoterms);
const existingCodes = new Set(existingData.map(item => item.code));
// Oracle에서 조회한 코드들
const oracleCodes = new Set(incotermsData.map((item: { CODE: string; DESCRIPTION: string }) => item.CODE));
// 업데이트할 데이터 준비
const upsertData = incotermsData.map((item: { CODE: string; DESCRIPTION: string }) => ({
code: item.CODE,
description: item.DESCRIPTION || 'NONSAP에서 설명 기입 요망',
isActive: true // 기본값 true
}));
// PostgreSQL에 upsert
for (const data of upsertData) {
await db.insert(incoterms)
.values(data)
.onConflictDoUpdate({
target: incoterms.code,
set: {
description: data.description,
isActive: data.isActive
}
});
}
// PostgreSQL에만 있는 데이터는 isActive = false로 설정
const codesToDeactivate = new Set([...existingCodes].filter(code => !oracleCodes.has(code)));
if (codesToDeactivate.size > 0) {
await db.update(incoterms)
.set({ isActive: false })
.where(inArray(incoterms.code, Array.from(codesToDeactivate)));
logger.info(`${codesToDeactivate.size}개의 인코텀즈를 비활성화했습니다`);
}
logger.success(`인코텀즈 동기화 완료: ${upsertData.length}개 업데이트`);
} catch (error) {
logger.error('인코텀즈 동기화 중 오류:', error);
throw error;
}
}
/**
* 선적/하역지 동기화
*/
async function syncPlaceOfShipping(): Promise<void> {
logger.header('선적/하역지 동기화 시작');
try {
// Oracle에서 선적/하역지 데이터 조회
const oracleData = await oracleKnex.raw(`
SELECT cd.CD as code, cdnm.CDNM as description, cd.DEL_YN as isActive
FROM CMCTB_CD cd
INNER JOIN CMCTB_CDNM cdnm ON cdnm.cd = cd.cd
AND CDNM.CD_CLF = CD.CD_CLF
WHERE cd.CD_CLF = 'MMM050'
ORDER BY cd.CD asc, cdnm.CDNM asc
`);
const placeOfShippingData = oracleData || [];
logger.info(`Oracle에서 ${placeOfShippingData.length}개의 선적/하역지 데이터 조회`);
if (placeOfShippingData.length === 0) {
logger.warn('Oracle에서 선적/하역지 데이터가 없습니다');
return;
}
// PostgreSQL에서 기존 데이터 조회
const existingData = await db.select().from(placeOfShipping);
const existingCodes = new Set(existingData.map(item => item.code));
// Oracle에서 조회한 코드들
const oracleCodes = new Set(placeOfShippingData.map((item: { CODE: string; DESCRIPTION: string; ISACTIVE: string }) => item.CODE));
// 업데이트할 데이터 준비 (isActive = "Y"인 경우 true, 그 외는 기본값 true)
const upsertData = placeOfShippingData.map((item: { CODE: string; DESCRIPTION: string; ISACTIVE: string }) => ({
code: item.CODE,
description: item.DESCRIPTION || 'NONSAP에서 설명 기입 요망',
isActive: item.ISACTIVE === 'Y' ? true : true // 기본값 true
}));
// PostgreSQL에 upsert
for (const data of upsertData) {
await db.insert(placeOfShipping)
.values(data)
.onConflictDoUpdate({
target: placeOfShipping.code,
set: {
description: data.description,
isActive: data.isActive
}
});
}
// PostgreSQL에만 있는 데이터는 isActive = false로 설정
const codesToDeactivate = new Set([...existingCodes].filter(code => !oracleCodes.has(code)));
if (codesToDeactivate.size > 0) {
await db.update(placeOfShipping)
.set({ isActive: false })
.where(inArray(placeOfShipping.code, Array.from(codesToDeactivate)));
logger.info(`${codesToDeactivate.size}개의 선적/하역지를 비활성화했습니다`);
}
logger.success(`선적/하역지 동기화 완료: ${upsertData.length}개 업데이트`);
} catch (error) {
logger.error('선적/하역지 동기화 중 오류:', error);
throw error;
}
}
/**
* 모든 procurement 관련 테이블 동기화
*/
export async function syncAllProcurementTables(): Promise<void> {
logger.header('Procurement 테이블 동기화 시작');
try {
await syncPaymentTerms();
await syncIncoterms();
await syncPlaceOfShipping();
logger.success('모든 Procurement 테이블 동기화 완료');
} catch (error) {
logger.error('Procurement 테이블 동기화 중 오류:', error);
throw error;
}
}
/**
* 수동 동기화 트리거
*/
export async function triggerProcurementSync(): Promise<void> {
logger.info('수동 Procurement 동기화 시작');
await syncAllProcurementTables();
logger.success('수동 Procurement 동기화 완료');
}
/**
* Procurement 동기화 스케줄러 시작
*/
export async function startProcurementSyncScheduler(): Promise<void> {
logger.info('Initializing Procurement data synchronization scheduler...');
// Oracle DB 연결 테스트 (비동기)
testOracleConnection().then(isConnected => {
if (!isConnected) {
logger.warn('Oracle DB connection failed - procurement sync scheduler will be disabled');
logger.warn('Application will continue to run normally');
return;
}
logger.success('Oracle DB connection test passed');
}).catch(error => {
logger.error('Oracle DB connection test error:', error);
logger.warn('Procurement sync scheduler will be disabled, application continues');
});
try {
// 매일 새벽 2시에 실행 (기존 스케줄러들과 시간 분산)
cron.schedule('0 2 * * *', async () => {
try {
logger.info('Cron job triggered: Starting scheduled procurement sync');
// 동기화 전 Oracle 연결 확인
const isConnected = await testOracleConnection();
if (!isConnected) {
logger.warn('Oracle DB not available, skipping procurement sync');
return;
}
await syncAllProcurementTables();
} catch (error) {
logger.error('Scheduled procurement sync failed:', error);
// 동기화 실패해도 다음 스케줄은 계속 실행
}
}, {
timezone: 'Asia/Seoul'
});
logger.success('Procurement data synchronization cron job registered (daily at 2:00 AM)');
// 애플리케이션 시작 시 한 번 실행 (선택사항)
if (process.env.PROCUREMENT_SYNC_ON_START === 'true') {
logger.info('Initial procurement sync on startup enabled');
setTimeout(async () => {
try {
const isConnected = await testOracleConnection();
if (isConnected) {
await syncAllProcurementTables();
} else {
logger.warn('Initial procurement sync skipped - Oracle DB not available');
}
} catch (error) {
logger.error('Initial procurement sync failed:', error);
}
}, 15000); // 15초 후 실행 (다른 스케줄러들과 시간 분산)
}
} catch (error) {
logger.error('Failed to set up procurement cron scheduler:', error);
logger.warn('Application will continue without procurement sync scheduler');
}
}
|