diff --git a/Jenkinsfile b/Jenkinsfile index d9f98f0..e4245b6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,7 +11,7 @@ pipeline { // 앱 정보 APP_NAME = 'gcone_batch' - TOMCAT_HOME = '/usr/local/tomcat' + TOMCAT_HOME = '/usr/local/tomcat_dev' DEPLOY_PATH = "${TOMCAT_HOME}/webapps" BACKUP_PATH = "${TOMCAT_HOME}/backup" DEPLOY_FILE = "${TOMCAT_HOME}/webapps/${APP_NAME}.war" @@ -24,7 +24,7 @@ pipeline { stages { stage('Checkout') { steps { - git branch: 'master', + git branch: 'dev', credentialsId: 'itfirst', url: 'http://115.68.207.90:3000/itfirst/sake_batch.git' } @@ -90,7 +90,7 @@ pipeline { post { success { - echo '✅ sake_batch 배포 완료' + echo '✅ sake_batch 개발 배포 완료' } failure { script { @@ -112,7 +112,7 @@ pipeline { fi """ } - echo '❌ 배포 실패 - 롤백 처리됨' + echo '❌ 게빌 배포 실패 - 롤백 처리됨' } } } diff --git a/shell/aciDlivyJob_run.sh b/shell/aciDlivyJob_run.sh new file mode 100755 index 0000000..de6d7d1 --- /dev/null +++ b/shell/aciDlivyJob_run.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +JOB_NAME="aciDlivyJob" +HOST="10.2.1.76" +PORT="18211" + +LOG_DIR="/home/batch_shell/logs" +LOG_FILE="${LOG_DIR}/${JOB_NAME}_$(date +%Y%m%d).log" + +mkdir -p ${LOG_DIR} + +TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') +echo "[${TIMESTAMP}] ===== ${JOB_NAME} 실행 시작 =====" >> ${LOG_FILE} + +HTTP_CODE=$(curl -s -o /tmp/${JOB_NAME}_resp.tmp -w "%{http_code}" \ + -X POST "http://${HOST}:${PORT}/gcone_batch/batch/run/${JOB_NAME}" \ + -H "Content-Type: application/json") + +BODY=$(cat /tmp/${JOB_NAME}_resp.tmp 2>/dev/null) +rm -f /tmp/${JOB_NAME}_resp.tmp + +TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') +echo "[${TIMESTAMP}] HTTP Status: ${HTTP_CODE}" >> ${LOG_FILE} +echo "[${TIMESTAMP}] Response : ${BODY}" >> ${LOG_FILE} + +if [ "${HTTP_CODE}" = "200" ]; then + echo "[${TIMESTAMP}] ${JOB_NAME} 실행 성공" >> ${LOG_FILE} + echo "Batch job ${JOB_NAME} executed successfully." +else + echo "[${TIMESTAMP}] ${JOB_NAME} 실행 실패 (HTTP ${HTTP_CODE})" >> ${LOG_FILE} + echo "Failed to execute batch job ${JOB_NAME}. HTTP Status: ${HTTP_CODE}" +fi + +echo "[${TIMESTAMP}] ===== ${JOB_NAME} 실행 완료 =====" >> ${LOG_FILE} diff --git a/src/main/java/com/wb/domainserver/acidlivy/AciDlivyJobConfig.java b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyJobConfig.java new file mode 100644 index 0000000..66d897a --- /dev/null +++ b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyJobConfig.java @@ -0,0 +1,35 @@ +package com.wb.domainserver.acidlivy; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import lombok.RequiredArgsConstructor; + +// ACI ???? ?? ? ???? ?? Job/Step ?? +@Configuration +@RequiredArgsConstructor +public class AciDlivyJobConfig { + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + private final AciDlivyTasklet aciDlivyTasklet; + + @Bean + public Job aciDlivyJob() { + return new JobBuilder("aciDlivyJob", jobRepository) + .start(aciDlivyStep()) + .build(); + } + + @Bean + public Step aciDlivyStep() { + return new StepBuilder("aciDlivyStep", jobRepository) + .tasklet(aciDlivyTasklet, transactionManager) + .build(); + } +} diff --git a/src/main/java/com/wb/domainserver/acidlivy/AciDlivyMapper.java b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyMapper.java new file mode 100644 index 0000000..d486f25 --- /dev/null +++ b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyMapper.java @@ -0,0 +1,54 @@ +package com.wb.domainserver.acidlivy; + +import com.wb.domainserver.util.dataaccess.DataMap; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Mapper +@Repository +public interface AciDlivyMapper { + + /** 출고 대상 주문 목록 조회 (gc_order_shipping.ORDR_STTUS_CD = '35') */ + List selectAciDlivyTargetList(); + + /** 출고 대상 주문상품 옵션 조회 (gc_order_item_option) */ + List selectAciDlivyTargetItemList(AciDlivyOrderVO vo); + + /** gc_goods_export 존재 여부 */ + int selectGoodsExportCount(AciDlivyOrderVO vo); + + /** gc_goods_export INSERT */ + int insertGoodsExport(AciDlivyOrderVO vo); + + /** gc_goods_export UPDATE (기존 레코드 있음) */ + int updateGoodsExport(AciDlivyOrderVO vo); + + /** gc_goods_export DLIVY_CD_NB UPDATE (INSERT 경로에서 dlivyCdNb 생성 후 호출) */ + int updateGoodsExportDlivyCdNb(AciDlivyOrderVO vo); + + /** gc_goods_export_item 존재 여부 */ + int selectGoodsExportItemCount(DataMap itemMap); + + /** gc_goods_export_item INSERT */ + int insertGoodsExportItem(DataMap itemMap); + + /** gc_goods_export_item UPDATE (기존 레코드 있음) */ + int updateGoodsExportItem(DataMap itemMap); + + /** gc_order_item_option 상태 UPDATE */ + int updateOrderItemOptionStatus(DataMap itemMap); + + /** gc_order_shipping 상태 UPDATE (하위 item_option 상태 기반으로 동적 결정) */ + int updateOrderShippingStatus(AciDlivyOrderVO vo); + + /** gc_order 상태 UPDATE (하위 shipping 상태 기반으로 동적 결정) */ + int updateOrderStatus(AciDlivyOrderVO vo); + + /** GC_GOODS_EXPORT_LOG INSERT */ + int insertGoodsExportLog(DataMap logMap); + + /** GC_ORDER_LOG INSERT */ + int insertOrderLog(DataMap logMap); +} diff --git a/src/main/java/com/wb/domainserver/acidlivy/AciDlivyOrderVO.java b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyOrderVO.java new file mode 100644 index 0000000..787622e --- /dev/null +++ b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyOrderVO.java @@ -0,0 +1,26 @@ +package com.wb.domainserver.acidlivy; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +// DB ?? ?? ?? ???? ?? + ??? ?? +@Getter +@Setter +@ToString +public class AciDlivyOrderVO { + + // DB ?? ?? (gc_order_shipping) + private String ordrDlvyInnb; // ???????? (ACI OverseaSellerNo ???) + private String ordrInnb; // ???? (API orderNumber ????) + private String sopmalCd; // ????? + + // ACI API ?? ? ?? + private String blno; // ACI ????? (BLNO) + + // ???? ? ?? + private String hdrycpnCd; // ????? (???: code22) + private String dlivySttusCd; // ?????? (???: 55) + private String dlivyInnb; // ?????? (insertGoodsExport ? selectKey? ??) + private String dlivyCdNb; // ?????? (dlivyInnb ?? ? Java?? ??) +} diff --git a/src/main/java/com/wb/domainserver/acidlivy/AciDlivyTasklet.java b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyTasklet.java new file mode 100644 index 0000000..0cfbedd --- /dev/null +++ b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyTasklet.java @@ -0,0 +1,324 @@ +package com.wb.domainserver.acidlivy; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wb.domainserver.util.dataaccess.DataMap; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.client.RestTemplate; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; + +// ACI 출고자료 조회 및 출고처리 배치 Tasklet +// 흐름: gc_order_shipping(ORDR_STTUS_CD=35) 조회 → 토큰 발급 → ACI API 호출 → BLNO 추출 → 출고처리 +@Slf4j +@Component +@RequiredArgsConstructor +public class AciDlivyTasklet implements Tasklet { + + private final AciDlivyMapper aciDlivyMapper; + private final PlatformTransactionManager transactionManager; + + @Value("${acikey.url}") + private String aciKeyUrl; + + @Value("${aci.dlivy.api-url}") + private String aciDlivyApiUrl; + + @Value("${acikey.userid}") + private String userId; + + @Value("${acikey.apikey}") + private String apiKey; + + private static final String HDRYCPN_CD = "code22"; // ACI 택배사 코드 + private static final String DLIVY_STTUS_CD = "55"; // 출고완료 코드 + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + log.info("[AciDlivyBatch] 배치 시작"); + + // 토큰 발급 (yaml 고정 apikey → ACIKey 토큰 발급, 메모리에만 유지) + String aciKeyToken = issueAciKeyToken(); + + // 대상 주문 목록 조회 (ORDR_STTUS_CD = '35') + List targetList = aciDlivyMapper.selectAciDlivyTargetList(); + log.info("[AciDlivyBatch] 대상 주문 건수: {}", targetList.size()); + + if (targetList.isEmpty()) { + log.info("[AciDlivyBatch] 처리할 대상 주문이 없습니다."); + return RepeatStatus.FINISHED; + } + + int successCnt = 0; + int failCnt = 0; + int skipCnt = 0; + + // 건별로 독립 트랜잭션 (REQUIRES_NEW) 으로 처리 - 한 건 실패가 다른 건에 영향 없도록 + TransactionTemplate txTemplate = new TransactionTemplate(transactionManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + for (AciDlivyOrderVO orderVO : targetList) { + try { + // 건별 ACI 출고자료 API 호출 → BLNO 추출 + String blno = callAciDlivyApi(orderVO, aciKeyToken); + + if (blno == null) { + log.info("[AciDlivyBatch] 스킵 - ORDR_DLVY_INNB={}, ORDR_INNB={} (BLNO 없음 또는 조건 미충족)", + orderVO.getOrdrDlvyInnb(), orderVO.getOrdrInnb()); + skipCnt++; + } else { + orderVO.setBlno(blno); + orderVO.setHdrycpnCd(HDRYCPN_CD); + orderVO.setDlivySttusCd(DLIVY_STTUS_CD); + + // 건별 처리 (독립 트랜잭션) + txTemplate.execute(status -> { + processDelivery(orderVO); + return null; + }); + + successCnt++; + log.info("[AciDlivyBatch] 출고처리 완료 - ORDR_DLVY_INNB={}, BLNO={}", + orderVO.getOrdrDlvyInnb(), blno); + } + + } catch (Exception e) { + log.error("[AciDlivyBatch] 출고처리 실패 - ORDR_DLVY_INNB={}, 오류={}", + orderVO.getOrdrDlvyInnb(), e.getMessage(), e); + failCnt++; + } + + // 건별 API 호출 간격 유지 (과부하 방지 - 1초 대기) + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + + log.info("[AciDlivyBatch] 배치 완료 - 성공: {}, 실패: {}, 스킵: {}", successCnt, failCnt, skipCnt); + return RepeatStatus.FINISHED; + } + + /** + * ACIKey 토큰 발급 + * yaml 고정 apikey를 APIKEY Header로 사용하여 POST 호출 → 발급받은 토큰은 메모리에만 유지 + */ + private String issueAciKeyToken() { + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("USERID", userId); + headers.set("APIKEY", apiKey); + + HttpEntity requestEntity = new HttpEntity<>("{}", headers); + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange( + aciKeyUrl, + HttpMethod.POST, + requestEntity, + String.class + ); + + ObjectMapper objectMapper = new ObjectMapper(); + Map body = objectMapper.readValue(response.getBody(), Map.class); + + String status = (String) body.get("Status"); + String aciKey = (String) body.get("ACIKey"); + String errMsg = (String) body.get("ErrMsg"); + + if ("success".equals(status) && aciKey != null) { + log.info("[AciDlivyBatch] ACIKey 토큰 발급 성공"); + return aciKey; + } else { + throw new RuntimeException("ACIKey 토큰 발급 실패. Status: " + status + ", ErrMsg: " + errMsg); + } + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("ACIKey 토큰 발급 오류", e); + } + } + + /** + * ACI 출고자료 API 호출 + * Content-Type이 text/html로 오는 경우를 대비해 JSON을 String으로 받아 ObjectMapper 파싱 + * @param aciKeyToken 발급받은 ACIKey 토큰 + * @return BLNO(운송장번호), 없으면 null + */ + private String callAciDlivyApi(AciDlivyOrderVO orderVO, String aciKeyToken) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("UserID", userId); + headers.set("ACIKey", aciKeyToken); + + String url = aciDlivyApiUrl + "?searchType=1&orderNumber=" + orderVO.getOrdrInnb(); + HttpEntity entity = new HttpEntity<>(headers); + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + log.debug("[AciDlivyBatch] API 응답 원문 (ORDR_INNB={}): {}", orderVO.getOrdrInnb(), response.getBody()); + + ObjectMapper objectMapper = new ObjectMapper(); + Map body = objectMapper.readValue(response.getBody(), Map.class); + + String status = (String) body.get("Status"); + log.debug("[AciDlivyBatch] API Status={} (ORDR_INNB={})", status, orderVO.getOrdrInnb()); + + // 출고/발송완료 상태 검증 (정확한 Status값은 실제 응답 확인 후 조정 필요) + if (!"A10".equals(status)) { + log.info("[AciDlivyBatch] API 출고 미완료 - ORDR_INNB={}, Status={}", orderVO.getOrdrInnb(), status); + return null; + } + + // Data 파싱 (단일 Map 또는 List 모두 대응) + Object dataObj = body.get("Data"); + if (dataObj == null) { + return null; + } + + List> dataList; + if (dataObj instanceof List) { + dataList = (List>) dataObj; + } else if (dataObj instanceof Map) { + dataList = List.of((Map) dataObj); + } else { + return null; + } + + // OverseaSellerNo == ORDR_DLVY_INNB 매칭 → BLNO 추출 + for (Map item : dataList) { + Object sellerNo = item.get("OverseaSellerNo"); + if (sellerNo != null && orderVO.getOrdrDlvyInnb().equals(String.valueOf(sellerNo).trim())) { + Object blno = item.get("BLNO"); + if (blno != null && !String.valueOf(blno).isBlank()) { + return String.valueOf(blno).trim(); + } + } + } + + log.info("[AciDlivyBatch] OverseaSellerNo 매칭 없음 - ORDR_DLVY_INNB={}", orderVO.getOrdrDlvyInnb()); + return null; + + } catch (Exception e) { + log.error("[AciDlivyBatch] API 호출 오류 - ORDR_INNB={}, 오류={}", orderVO.getOrdrInnb(), e.getMessage(), e); + return null; + } + } + + /** + * 출고처리 (순서: gc_goods_export → gc_goods_export_item → gc_order_item_option + * → gc_order_shipping → gc_order → 출고로그 → 주문로그) + * gc_order_shipping/gc_order 상태변경은 모든 아이템 처리 완료 후 일괄 수행 + */ + private void processDelivery(AciDlivyOrderVO orderVO) { + + // ① 주문 상품 옵션 목록 먼저 조회 (0건이면 출고처리 전체 스킵) + List itemList = aciDlivyMapper.selectAciDlivyTargetItemList(orderVO); + if (itemList.isEmpty()) { + log.warn("[AciDlivyBatch] 주문 상품 옵션 없음 - 출고처리 스킵 (ORDR_DLVY_INNB={})", orderVO.getOrdrDlvyInnb()); + return; + } + log.debug("[AciDlivyBatch] 주문 상품 옵션 수={} (ORDR_DLVY_INNB={})", itemList.size(), orderVO.getOrdrDlvyInnb()); + + // ② gc_goods_export 처리 (selectKey로 dlivyInnb 채번) + int exportCnt = aciDlivyMapper.selectGoodsExportCount(orderVO); + if (exportCnt > 0) { + aciDlivyMapper.updateGoodsExport(orderVO); + } else { + aciDlivyMapper.insertGoodsExport(orderVO); + } + + // ③ dlivyCdNb 생성 (insertGoodsExport 실행 후, dlivyInnb 채번 완료 후) + String dlivyCdNb = "D" + new SimpleDateFormat("yyMMddHH").format(new Date()) + orderVO.getDlivyInnb(); + orderVO.setDlivyCdNb(dlivyCdNb); + + // ④ INSERT 경로: DLIVY_CD_NB 업데이트 (INSERT 시 #{dlivyCdNb}가 null이었으므로) + if (exportCnt == 0) { + aciDlivyMapper.updateGoodsExportDlivyCdNb(orderVO); + } + + // ⑤ 주문 상품 옵션별 처리 (gc_goods_export_item, gc_order_item_option) + for (DataMap itemMap : itemList) { + Object qyObj = itemMap.get("qy"); + if (qyObj == null) continue; + int qy; + try { + qy = Integer.parseInt(String.valueOf(qyObj)); + } catch (NumberFormatException e) { + continue; + } + if (qy <= 0) continue; + + itemMap.put("sopmalCd", orderVO.getSopmalCd()); + itemMap.put("ordrDlvyInnb", orderVO.getOrdrDlvyInnb()); + itemMap.put("dlivyInnb", orderVO.getDlivyInnb()); + itemMap.put("ordrInnb", orderVO.getOrdrInnb()); + itemMap.put("hdrycpnCd", HDRYCPN_CD); + itemMap.put("invcnb", orderVO.getBlno()); + itemMap.put("dlivySttusCd", DLIVY_STTUS_CD); + + // gc_goods_export_item 처리 + int exportItemCnt = aciDlivyMapper.selectGoodsExportItemCount(itemMap); + if (exportItemCnt > 0) { + aciDlivyMapper.updateGoodsExportItem(itemMap); + } else { + aciDlivyMapper.insertGoodsExportItem(itemMap); + } + + // gc_order_item_option 상태 UPDATE + aciDlivyMapper.updateOrderItemOptionStatus(itemMap); + + // 출고 로그 INSERT (건별) + DataMap exportLogMap = new DataMap(); + exportLogMap.put("ordrInnb", orderVO.getOrdrInnb()); + exportLogMap.put("ordrDlvyInnb", orderVO.getOrdrDlvyInnb()); + exportLogMap.put("sopmalCd", orderVO.getSopmalCd()); + exportLogMap.put("dlivyInnb", orderVO.getDlivyInnb()); + exportLogMap.put("dlivyGoodsCdNb", itemMap.get("ordrGoodsOptnInnb")); + exportLogMap.put("reqstProcsSttusCd", DLIVY_STTUS_CD); + exportLogMap.put("mssageCn", "[ACI 배치] ORDR_INNB:" + orderVO.getOrdrInnb() + " 출고처리"); + exportLogMap.put("regusr", 0); + aciDlivyMapper.insertGoodsExportLog(exportLogMap); + } + + // ⑥ gc_order_shipping 상태 UPDATE (모든 item_option 처리 완료 후 일괄) + aciDlivyMapper.updateOrderShippingStatus(orderVO); + + // ⑦ gc_order 상태 UPDATE (모든 shipping 처리 완료 후 일괄) + aciDlivyMapper.updateOrderStatus(orderVO); + + // ⑧ 주문 로그 INSERT + DataMap orderLogMap = new DataMap(); + orderLogMap.put("ordrInnb", orderVO.getOrdrInnb()); + orderLogMap.put("sopmalCd", orderVO.getSopmalCd()); + orderLogMap.put("logSj", "출고처리(" + dlivyCdNb + ")"); + orderLogMap.put("logDtlCn", "[ACI 배치] 배치 출고처리 완료"); + orderLogMap.put("aditInfoCn", null); + orderLogMap.put("dlivyCdNb", dlivyCdNb); + orderLogMap.put("refundCdNb", null); + orderLogMap.put("logDvCd", "01"); + orderLogMap.put("dvCd", "BATCH"); + orderLogMap.put("opetrNm", "ACI 배치"); + orderLogMap.put("qy", 0); + orderLogMap.put("amt", 0); + aciDlivyMapper.insertOrderLog(orderLogMap); + } +} diff --git a/src/main/java/com/wb/domainserver/acidlivy/AciDlivyVO.java b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyVO.java new file mode 100644 index 0000000..3c5e030 --- /dev/null +++ b/src/main/java/com/wb/domainserver/acidlivy/AciDlivyVO.java @@ -0,0 +1,14 @@ +package com.wb.domainserver.acidlivy; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +// ACI API ???? ??? Data ?? (BLNO, OverseaSellerNo) +@Getter +@Setter +@ToString +public class AciDlivyVO { + private String blno; // ACI ????? + private String overseaSellerNo; // ????? ???? (= ORDR_DLVY_INNB) +} diff --git a/src/main/java/com/wb/domainserver/acikey/AciKeyTokenJobConfig.java b/src/main/java/com/wb/domainserver/acikey/AciKeyTokenJobConfig.java new file mode 100644 index 0000000..e7e2da3 --- /dev/null +++ b/src/main/java/com/wb/domainserver/acikey/AciKeyTokenJobConfig.java @@ -0,0 +1,35 @@ +package com.wb.domainserver.acikey; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; +import lombok.RequiredArgsConstructor; + +// ACIKey 토큰 발급 테스트 배치 - Job/Step 설정 +@Configuration +@RequiredArgsConstructor +public class AciKeyTokenJobConfig { + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + private final AciKeyTokenTasklet aciKeyTokenTasklet; + + @Bean + public Job aciKeyTokenJob() { + return new JobBuilder("aciKeyTokenJob", jobRepository) + .start(aciKeyTokenStep()) + .build(); + } + + @Bean + public Step aciKeyTokenStep() { + return new StepBuilder("aciKeyTokenStep", jobRepository) + .tasklet(aciKeyTokenTasklet, transactionManager) + .build(); + } +} diff --git a/src/main/java/com/wb/domainserver/acikey/AciKeyTokenTasklet.java b/src/main/java/com/wb/domainserver/acikey/AciKeyTokenTasklet.java new file mode 100644 index 0000000..df24b66 --- /dev/null +++ b/src/main/java/com/wb/domainserver/acikey/AciKeyTokenTasklet.java @@ -0,0 +1,82 @@ +package com.wb.domainserver.acikey; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; + +// ACIKey 토큰 발급 테스트 배치 - API 호출 및 토큰 확인 로직 +@Slf4j +@Component +public class AciKeyTokenTasklet implements Tasklet { + + @Value("${acikey.url}") + private String aciKeyUrl; + + @Value("${acikey.userid}") + private String userId; + + @Value("${acikey.apikey}") + private String apiKey; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + + log.info("[AciKeyBatch] 배치 시작"); + log.info("[AciKeyBatch] API URL: {}", aciKeyUrl); + + try { + // Header 설정 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("USERID", userId); + headers.set("APIKEY", apiKey); + + HttpEntity requestEntity = new HttpEntity<>("{}", headers); + + // API 호출 + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange( + aciKeyUrl, + HttpMethod.POST, + requestEntity, + String.class + ); + + ObjectMapper objectMapper = new ObjectMapper(); + Map body = objectMapper.readValue(response.getBody(), Map.class); + + log.info("[AciKeyBatch] 응답 전체: {}", body); + + String status = (String) body.get("Status"); + String aciKey = (String) body.get("ACIKey"); + String errMsg = (String) body.get("ErrMsg"); + + if ("success".equals(status)) { + log.info("[AciKeyBatch] ✅ 토큰 발급 성공!"); + log.info("[AciKeyBatch] ACIKey: {}", aciKey); + contribution.incrementWriteCount(1); + } else { + log.error("[AciKeyBatch] ❌ 토큰 발급 실패. Status: {}, ErrMsg: {}", status, errMsg); + throw new RuntimeException("ACIKey 토큰 발급 실패. Status: " + status + ", ErrMsg: " + errMsg); + } + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + log.error("[AciKeyBatch] ❌ API 호출 중 오류 발생: {}", e.getMessage(), e); + throw new RuntimeException("AciKey API 호출 오류", e); + } + + log.info("[AciKeyBatch] 배치 종료"); + return RepeatStatus.FINISHED; + } +} diff --git a/src/main/java/com/wb/domainserver/common/config/GconeSourceConfig.java b/src/main/java/com/wb/domainserver/common/config/GconeSourceConfig.java index 0b52ebd..9ec79f9 100644 --- a/src/main/java/com/wb/domainserver/common/config/GconeSourceConfig.java +++ b/src/main/java/com/wb/domainserver/common/config/GconeSourceConfig.java @@ -10,18 +10,29 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Primary; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; @Configuration @EnableTransactionManagement @MapperScan( - basePackages = "com.wb.domainserver.mapper.gcone", + basePackages = { + "com.wb.domainserver.mapper.gcone", + "com.wb.domainserver.acidlivy" + }, sqlSessionFactoryRef = "gconeSqlSessionFactory" ) public class GconeSourceConfig { @@ -45,7 +56,13 @@ public class GconeSourceConfig { // SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); SqlSessionFactoryBean sessionFactory = new RefreshableSqlSessionFactoryBean(); sessionFactory.setDataSource(gconeDataSource); - sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mybatis/xml/mapper/gcone/**/*.xml")); + + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + List mapperLocations = new ArrayList<>(); + mapperLocations.addAll(Arrays.asList(resolver.getResources("classpath*:mybatis/xml/mapper/gcone/**/*.xml"))); + mapperLocations.addAll(Arrays.asList(resolver.getResources("classpath*:mybatis/xml/mapper/acidlivy/**/*.xml"))); + sessionFactory.setMapperLocations(mapperLocations.toArray(new Resource[0])); + sessionFactory.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis/xml/config/mapper-config.xml")); return sessionFactory.getObject(); } @@ -61,14 +78,27 @@ public class GconeSourceConfig { return new DataSourceTransactionManager(gconeDataSource); } + // Spring Batch schema init (BATCH_JOB_SEQ etc) + @Bean(name = "batchSchemaInitializer") + public DataSourceInitializer batchSchemaInitializer(@Qualifier("gconeDataSource") DataSource gconeDataSource) { + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); + populator.addScript(new ClassPathResource("org/springframework/batch/core/schema-drop-mysql.sql")); + populator.addScript(new ClassPathResource("org/springframework/batch/core/schema-mysql.sql")); + populator.setContinueOnError(true); + DataSourceInitializer initializer = new DataSourceInitializer(); + initializer.setDataSource(gconeDataSource); + initializer.setDatabasePopulator(populator); + return initializer; + } + @Bean - public JobRepository jobRepository( @Qualifier("gconeTransactionManager") PlatformTransactionManager transactionManager, @Qualifier("gconeDataSource") DataSource gconeDataSource) throws Exception { + @DependsOn("batchSchemaInitializer") + public JobRepository jobRepository(@Qualifier("gconeTransactionManager") PlatformTransactionManager transactionManager, @Qualifier("gconeDataSource") DataSource gconeDataSource) throws Exception { JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); factory.setDataSource(gconeDataSource); factory.setTransactionManager(transactionManager); + factory.setDatabaseType("MYSQL"); factory.afterPropertiesSet(); return factory.getObject(); } -} - - +} \ No newline at end of file diff --git a/src/main/java/com/wb/domainserver/security/config/BatchServerConfig.java b/src/main/java/com/wb/domainserver/security/config/BatchServerConfig.java index 201ef3e..f7d77a7 100644 --- a/src/main/java/com/wb/domainserver/security/config/BatchServerConfig.java +++ b/src/main/java/com/wb/domainserver/security/config/BatchServerConfig.java @@ -1,56 +1,15 @@ package com.wb.domainserver.security.config; -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTParser; -import com.wb.domainserver.security.authentication.CustomAuthenticationEntryPoint; -import com.wb.domainserver.security.authentication.MemberJwtAuthenticationConverter; -import com.wb.domainserver.security.authorization.WnbtAuthorizationManager; -import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.aop.Advisor; -import org.springframework.aop.support.JdkRegexpMethodPointcut; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtDecoders; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.util.StringUtils; - -import java.text.ParseException; - -import static org.springframework.beans.factory.config.BeanDefinition.ROLE_INFRASTRUCTURE; @EnableWebSecurity @Configuration -@EnableMethodSecurity public class BatchServerConfig { - private static final Logger logger = LoggerFactory.getLogger(BatchServerConfig.class); - - @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") - String issuerUri; - - @Autowired - private CustomAuthenticationEntryPoint authenticationEntryPoint; - - @Bean - @Role(ROLE_INFRASTRUCTURE) - public Advisor authorizationManagerBeforeMethodInterception(WnbtAuthorizationManager authorizationManager) { - JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut(); - pattern.setPattern("com.wb.domainserver.*.controller.*"); - return new AuthorizationManagerBeforeMethodInterceptor(pattern, authorizationManager); - } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -58,53 +17,7 @@ public class BatchServerConfig { .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize .anyRequest().permitAll() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt - .decoder(jwtDecoder()) - .jwtAuthenticationConverter(new MemberJwtAuthenticationConverter()) - ) - .bearerTokenResolver(this::customBearerTokenResolver) - - ) - .exceptionHandling(exceptions -> exceptions - .authenticationEntryPoint(authenticationEntryPoint) ); - return http.build(); } - - private String customBearerTokenResolver(HttpServletRequest request) { - String authorization = request.getHeader("Authorization"); - if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) { - return authorization.substring(7); - } - return null; - } - - @Bean - public JwtDecoder jwtDecoder() { - return token -> { - String issuerUri = determineIssuerUri(); - logger.debug("Determined issuerUri: {}", issuerUri); - JWT jwt = null; - try { - jwt = JWTParser.parse(token); - logger.debug("Token issuer: {}", jwt.getJWTClaimsSet().getIssuer()); - } catch (ParseException e) { - throw new RuntimeException(e); - } - NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuerUri); - return jwtDecoder.decode(token); - }; - } - - private String determineIssuerUri() { - return String.format(issuerUri); - } - - @Bean - public BCryptPasswordEncoder bCryptPasswordEncoder() { - return new BCryptPasswordEncoder(); - } } diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 2af5716..dc29a54 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -9,7 +9,14 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver username: sakeone password: dnlfxjtm13@$ + gconedatasource: + jdbc-url: jdbc:mysql://10.2.3.97:3306/sake?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&autoReconnect=true&allowMultiQueries=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=false&verifyServerCertificate=false + driver-class-name: com.mysql.cj.jdbc.Driver + username: sakeone + password: dnlfxjtm13@$ batch: + jdbc: + initialize-schema: always job: enabled: false mybatis: @@ -25,8 +32,17 @@ url: resource: server: gconebo: http://10.2.0.129:18500 + reviewbo: http://127.0.0.1:18220 secret: key: 1df5be2f7e90ae36 mail: host: 10.2.1.76 - port: 25 \ No newline at end of file + port: 25 + +acikey: + url: https://acieshop.com/api/kr/token/ + userid: UNIHAN + apikey: DC94F29AFD5399F54AAE7FAA46045B65 +aci: + dlivy: + api-url: https://acieshop.com/api/kr/delivery/v1.5/detail/index.php diff --git a/src/main/resources/mybatis/xml/mapper/acidlivy/AciDlivyMapper.xml b/src/main/resources/mybatis/xml/mapper/acidlivy/AciDlivyMapper.xml new file mode 100644 index 0000000..58d342b --- /dev/null +++ b/src/main/resources/mybatis/xml/mapper/acidlivy/AciDlivyMapper.xml @@ -0,0 +1,370 @@ + + + + + + + + + + + + + + + + + + + + + + + + SELECT IFNULL(MAX(DLIVY_INNB), 0) + 1 FROM GC_GOODS_EXPORT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT IFNULL(MAX(DLIVY_GOODS_INNB), 0) + 1 FROM GC_GOODS_EXPORT_ITEM + + + + + + + + + + + + + + + + #{qy} THEN '50' + WHEN (ORDR_QY - STEP85) = #{qy} THEN '55' + ELSE ORDR_STTUS_CD + END, + STEP55 = #{qy}, + STEP45 = 0, + UPDT_DT = SYSDATE() + WHERE SOPMAL_CD = #{sopmalCd} + AND OPTN_TY = #{optnTy} + AND ORDR_GOODS_OPTN_INNB = #{ordrGoodsOptnInnb} + ]]> + + + + + + + OIOP.STEP85 + WHERE SH.SOPMAL_CD = #{sopmalCd} + AND SH.ORDR_INNB = #{ordrInnb} + AND SH.ORDR_DLVY_INNB = #{ordrDlvyInnb} + AND SH.USE_AT = 'Y' + GROUP BY SH.SOPMAL_CD, SH.ORDR_INNB, SH.ORDR_DLVY_INNB + ) STEP + SET + SH.ORDR_STTUS_CD = STEP.ORDR_STTUS_CD, + SH.UPDT_DT = SYSDATE() + WHERE SH.SOPMAL_CD = STEP.SOPMAL_CD + AND SH.ORDR_INNB = STEP.ORDR_INNB + AND SH.ORDR_DLVY_INNB = STEP.ORDR_DLVY_INNB + ]]> + + + + + + + '85' + WHERE O.SOPMAL_CD = #{sopmalCd} + AND O.ORDR_INNB = #{ordrInnb} + GROUP BY O.SOPMAL_CD, O.ORDR_INNB + ) STEP + SET + O.ORDR_STTUS_CD = STEP.ORDR_STTUS_CD, + O.UPDT_DT = SYSDATE() + WHERE O.SOPMAL_CD = STEP.SOPMAL_CD + AND O.ORDR_INNB = STEP.ORDR_INNB + AND O.USE_AT = 'Y' + ]]> + + + + + + + + SELECT IFNULL(MAX(DLIVY_LOG_INNB), 0) + 1 FROM GC_GOODS_EXPORT_LOG + + + + + + + + + + + +