출고 상태 변경

This commit is contained in:
이대환
2026-06-10 11:36:59 +09:00
parent 28fb6bca61
commit f2049bc49d
8 changed files with 365 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
package com.wb.domainserver.acidlivy.jobs;
import com.wb.domainserver.acidlivy.tasklet.AciDlivyTasklet;
import com.wb.domainserver.acidlivy.tasklet.AciPodTasklet;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
@@ -19,6 +20,7 @@ public class AciDlivyJobConfig {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final AciDlivyTasklet aciDlivyTasklet;
private final AciPodTasklet aciPodTasklet;
@Bean
public Job aciDlivyJob() {
@@ -33,4 +35,18 @@ public class AciDlivyJobConfig {
.tasklet(aciDlivyTasklet, transactionManager)
.build();
}
@Bean
public Job aciPodJob() {
return new JobBuilder("aciPodJob", jobRepository)
.start(aciDlivyStep())
.build();
}
@Bean
public Step aciPodStep() {
return new StepBuilder("aciPodStep", jobRepository)
.tasklet(aciPodTasklet, transactionManager)
.build();
}
}

View File

@@ -2,6 +2,7 @@ package com.wb.domainserver.acidlivy.tasklet;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wb.domainserver.acidlivy.vo.AciDlivyOrderVO;
import com.wb.domainserver.mapper.gcone.AciDlivyMapper;
import com.wb.domainserver.util.dataaccess.DataMap;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -0,0 +1,301 @@
package com.wb.domainserver.acidlivy.tasklet;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wb.domainserver.acidlivy.vo.AciDlivyOrderVO;
import com.wb.domainserver.mapper.gcone.AciDlivyMapper;
import com.wb.domainserver.response.WnbtWebClientResponseHandler;
import com.wb.domainserver.util.communicate.RESRC;
import com.wb.domainserver.util.communicate.SendToResrc;
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.Autowired;
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.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;
import java.text.SimpleDateFormat;
import java.util.*;
// ACI 배송자료 조회 및 배송처리 배치 Tasklet
// 흐름: gc_order_shipping(DLIVY_STTUS_CD=55) 조회 → 토큰 발급 → ACI API 호출 → BLNO 추출 → 배송처리
@Slf4j
@Component
@RequiredArgsConstructor
public class AciPodTasklet implements Tasklet {
private final AciDlivyMapper AciDlivyMapper;
private final PlatformTransactionManager transactionManager;
@Autowired
WnbtWebClientResponseHandler responseHandler;
@Autowired
private SendToResrc sendToResrc;
@Value("${acikey.url}")
private String aciKeyUrl;
@Value("${aci.pod.api-url}")
private String aciPodApiUrl;
@Value("${acikey.userid}")
private String userId;
@Value("${acikey.apikey}")
private String apiKey;
private static final String DLIVY_STTUS_CD = "55"; // 출고완료 코드
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[AciPodBatch] 배치 시작");
// 토큰 발급 (yaml 고정 apikey → ACIKey 토큰 발급, 메모리에만 유지)
String aciKeyToken = issueAciKeyToken();
// 대상 주문 목록 조회 (DLIVY_STTUS_CD = '55')
List<AciDlivyOrderVO> goodsExportList = AciDlivyMapper.selectGoodsExportList(DLIVY_STTUS_CD);
log.info("[AciPodBatch] 대상 출고 건수: {}", goodsExportList.size());
if (goodsExportList.isEmpty()) {
log.info("[AciPodBatch] 처리할 대상 출고건이 없습니다.");
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 dlivyOrderVO : goodsExportList) {
try {
// 건별 ACI 출고자료 API 호출
List<DataMap> podInfoList = callAciPodApi(dlivyOrderVO, aciKeyToken);
if (podInfoList.isEmpty()) {
skipCnt++;
} else {
boolean dlvyInPrg = podInfoList.stream()
.anyMatch(podInfo -> !isBlank(podInfo, "dateLocalwhin"));
boolean dlvyCompt = podInfoList.stream()
.allMatch(podInfo -> !isBlank(podInfo, "dateDelivery"));
// 건별 배송처리
DataMap dataMap = new DataMap();
dataMap.put("dlivyInnb", dlivyOrderVO.getDlivyInnb());
dataMap.put("dlivySttusCd", dlivyOrderVO.getDlivySttusCd());
dataMap.put("sopmalCd", dlivyOrderVO.getSopmalCd());
if(dlvyInPrg) {
dataMap.put("dlivySttusCd", "65");
processDeliveryPod(dataMap);
}
if(dlvyCompt) {
dataMap.put("dlivySttusCd", "75");
processDeliveryPod(dataMap);
}
successCnt++;
}
} catch (Exception e) {
log.error("[AciPodBatch] 배송처리 실패 - DLIVY_INNB={}, 오류={}",
dlivyOrderVO.getDlivyInnb(), e.getMessage(), e);
failCnt++;
}
// 건별 API 호출 간격 유지 (과부하 방지 - 1초 대기)
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
log.info("[AciPodBatch] 배치 완료 - 성공: {}, 실패: {}, 스킵: {}", 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<String> requestEntity = new HttpEntity<>("{}", headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
aciKeyUrl,
HttpMethod.POST,
requestEntity,
String.class
);
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> 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("[AciPodBatch] 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 호출
* @param aciKeyToken 발급받은 ACIKey 토큰
* @return
*/
private List<DataMap> callAciPodApi(AciDlivyOrderVO orderVO, String aciKeyToken) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("UserID", userId);
headers.set("ACIKey", aciKeyToken);
Map<String, Object> body = new HashMap<>();
body.put("BLNO", orderVO.getInvcnb());
HttpEntity<Map<String, Object>> apiRequest =
new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
aciPodApiUrl,
HttpMethod.POST,
apiRequest,
String.class
);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(response.getBody());
log.debug("[AciPodBatch] API 응답 Status: {}", jsonNode.get("Status").asText());
JsonNode podInfoNode = jsonNode.get("PODInfo");
if(!podInfoNode.isNull()) {
List<DataMap> podInfoList = new ArrayList<>();
for(JsonNode podInfoItem : podInfoNode) {
DataMap PODInfo = new DataMap();
PODInfo.put("blno", podInfoItem.get("BLNO").asText());
PODInfo.put("dateLocalwhin", podInfoItem.get("DATE_LOCALWHIN").asText());
PODInfo.put("dateLocaldeparture", podInfoItem.get("DATE_LOCALDEPARTURE").asText());
PODInfo.put("dateIcnarrival", podInfoItem.get("DATE_ICNARRIVAL").asText());
PODInfo.put("dateCustomes", podInfoItem.get("DATE_CUSTOMES").asText());
PODInfo.put("dateDelivery", podInfoItem.get("DATE_DELIVERY").asText());
PODInfo.put("recipient", podInfoItem.get("RECIPIENT").asText());
PODInfo.put("wta", podInfoItem.get("WTA").asText());
PODInfo.put("wtdan", podInfoItem.get("WTDAN").asText());
List<DataMap> volumeList = new ArrayList<>();
for(JsonNode volumeItem : podInfoItem.get("VOLUME")) {
DataMap volume = new DataMap();
volume.put("width", volumeItem.get("WIDTH").asText());
volume.put("length", volumeItem.get("LENGTH").asText());
volume.put("height", volumeItem.get("HEIGHT").asText());
volumeList.add(volume);
}
PODInfo.put("volume", volumeList);
List<List> customsList = new ArrayList<>();
for(JsonNode customsItem : podInfoItem.get("CUSTOMS")) {
List<DataMap> customDetail = new ArrayList<>();
for(JsonNode customItem : customsItem) {
DataMap Custom = new DataMap();
Custom.put("dateEvent", customItem.get("DATE_EVENT").asText());
Custom.put("traceStatus", customItem.get("TRACE_STATUS").asText());
customDetail.add(Custom);
}
customsList.add(customDetail);
}
PODInfo.put("customs", customsList);
List<DataMap> localPodList = new ArrayList<>();
for(JsonNode localPodItem : podInfoItem.get("LOCAL_POD")) {
DataMap localPod = new DataMap();
localPod.put("traceStatus", localPodItem.get("TRACE_STATUS").asText());
localPod.put("dateEvent", localPodItem.get("DATE_EVENT").asText());
localPod.put("position", localPodItem.get("POSITION").asText());
localPodList.add(localPod);
}
PODInfo.put("localPod", localPodList);
podInfoList.add(PODInfo);
}
log.debug("[AciPodBatch] API 응답 PODInfo: {}", podInfoList);
return podInfoList;
} else {
log.debug("[AciPodBatch] API 응답 ErrMsg: {}", jsonNode.get("ErrMsg").asText());
return null;
}
} catch (Exception e) {
log.error("[AciPodBatch] API 호출 오류 - DLIVY_INNB={}, 오류={}", orderVO.getDlivyInnb(), e.getMessage(), e);
return null;
}
}
/**
* 배송처리
*/
private void processDeliveryPod(DataMap dataMap) {
dataMap.put("opetrNm", "시스템");
dataMap.put("regusr", 0);
dataMap.put("updusr", 0);
Map<String, Object> result = responseHandler.handleResponseAsMap(
sendToResrc.ofPost(RESRC.GCONEBO, "/mngr/ordr/dlivy/updateDlivySttusGetbackBatchUpdt", dataMap)
);
log.debug("result = {}", result);
// 메시지전송 ==================================================================
/*List<DataMap> smsList = mapper.selectDlivySmsListInqire(tempVO);
log.debug("메시지 발송 파라미터 확인 : {}", smsList);
if (!CollectionUtils.isEmpty(smsList)) {
System.out.println("smsList = " + smsList);
try {
DataMap result = mssageService.insertBOAtmcsndng(smsList);
} catch (Exception e) {
log.debug("메시지 발송 실패");
}
}*/
}
/**
* 오브젝트 값 공백 확인
*/
private Boolean isBlank(DataMap podInfo, String key) {
return podInfo.get(key) == null
|| podInfo.get(key).toString().trim().isEmpty();
}
}

View File

@@ -14,6 +14,7 @@ public class AciDlivyOrderVO {
private String ordrDlvyInnb; // 주문배송고유번호 (ACI OverseaSellerNo 매칭기준)
private String ordrInnb; // 주문번호 (API orderNumber 파라미터)
private String sopmalCd; // 쇼핑몰코드
private String invcnb; // 운송장번호
// ACI API 조회 및 처리
private String blno; // ACI 운송장번호 (BLNO)

View File

@@ -1,4 +1,4 @@
package com.wb.domainserver.acidlivy.tasklet;
package com.wb.domainserver.mapper.gcone;
import com.wb.domainserver.acidlivy.vo.AciDlivyOrderVO;
import com.wb.domainserver.util.dataaccess.DataMap;
@@ -52,4 +52,7 @@ public interface AciDlivyMapper {
/** GC_ORDER_LOG INSERT */
int insertOrderLog(DataMap logMap);
/** 배송처리 대상 주문 목록 조회 (gc_goods_export.DLIVY_STTUS_CD = '55') */
List<AciDlivyOrderVO> selectGoodsExportList(String dlivySttusCd);
}

View File

@@ -46,3 +46,5 @@ acikey:
aci:
dlivy:
api-url: https://acieshop.com/api/kr/delivery/v1.5/detail/index.php
pod:
api-url: https://acieshop.com/api/kr/pod/v1.5/

View File

@@ -46,3 +46,5 @@ acikey:
aci:
dlivy:
api-url: https://acieshop.com/api/kr/delivery/v1.5/detail/index.php
pod:
api-url: https://acieshop.com/api/kr/pod/v1.5/

View File

@@ -353,4 +353,41 @@
]]>
</insert>
<!-- 배송처리 대상 주문 목록 조회 (DLIVY_STTUS_CD = '55' 출고완료) -->
<select id="selectGoodsExportList" parameterType="java.lang.String"
resultType="com.wb.domainserver.acidlivy.AciDlivyOrderVO">
<![CDATA[
SELECT
DLIVY_INNB,
ORDR_INNB,
ORDR_DLIVY_INNB,
SOPMAL_CD,
INVCNB
FROM GC_GOODS_EXPORT
WHERE DLIVY_STTUS_CD = #{param}
AND USE_AT = 'Y'
ORDER BY DLVY_INNB ASC
]]>
</select>
<!-- 출고 상태 일괄 변경 다건 -->
<update id="updateBODlivySttusGetbackUpdtArr" parameterType="dataMap">
<![CDATA[
UPDATE GC_GOODS_EXPORT SET
DLIVY_STTUS_CD = #{dlivySttusCd},
]]>
<if test="dlivySttusCd == '75'">
<![CDATA[
DLVY_COMPT_DT = SYSDATE(),
]]>
</if>
<![CDATA[
STTUS_UPDT_DT = SYSDATE(),
MNGR_INNB = #{mngrInnb},
UPDT_DT = SYSDATE() -- TODO : 상태 변경일시, 구매확정일시를 수정하는데 수정일시도 수정?
WHERE SOPMAL_CD = #{sopmalCd}
AND DLIVY_INNB = #{dlivyInnb}
]]>
</update>
</mapper>