로컬 테스트 완료

This commit is contained in:
Rinjae
2025-10-21 17:53:50 +09:00
parent f5e689fe51
commit 798f4e7ace
14 changed files with 421 additions and 22 deletions
@@ -3,13 +3,21 @@ package com.eactive.ext.kjb.common;
import lombok.Data;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Pattern;
@Data
public class KjbProperty {
@Pattern(regexp = "^(https?://[^\\s/$.?#][^\\s]*)|\\s*$", message = "kjb.ums.eai_batch.url 는 유효한 URL 형식이거나 공백이어야 합니다. 예: http://example.com/api")
@Pattern(regexp = "^(https?://[^\\s/$.?#][^\\s]*)|\\s*$", message = "kjb.eai_batch.url 는 유효한 URL 형식이거나 공백이어야 합니다. 예: http://example.com/api")
private String eaiBatchUrl;
@Min(1) @Max(10)
private int eaiBatchConnectionTimeout = 5;
@Min(1) @Max(30)
private int eaiBatchResponseTimeout = 10;
@NotEmpty(message = "kjb.ums.eai_batch.send_dir 는 필수값입니다.")
@Pattern(regexp = "^(?:[a-zA-Z]:\\\\|\\\\|\\/)(?:[\\w\\-\\.]+[\\\\\\/])*[\\w\\-\\.]+$",
message = "kjb.ums.eai_batch.send_dir 은 절대 경로여야 합니다. 예: /Data/eapim/portal/sendmail/snd 또는 C:\\Data\\eapim\\portal\\sendmail\\snd")
@@ -20,7 +28,19 @@ public class KjbProperty {
message = "kjb.ums.eai_batch.backup_dir 은 절대 경로여야 합니다. 예: /Data/eapim/portal/sendmail/bak 또는 C:\\Data\\eapim\\portal\\sendmail\\bak")
private String eaiBatchBackupDir;
@Pattern(regexp = "^[a-zA-Z0-9]{13}$", message = "kjb.ums.eai_batch.interface_id 는 12자리 영문대소문자 및 숫자 조합이어야 합니다.")
private String eaiBatchInterfaceId;
@Pattern(regexp = "^(https?://[^\\s/$.?#][^\\s]*)|\\s*$", message = "kjb.ums.url 는 유효한 URL 형식이거나 공백이어야 합니다. 예: http://example.com/api")
private String umsUrl;
@Pattern(regexp = "^[a-zA-Z0-9_-]{36}$", message = "kjb.ums.api_key 입력 형식이 맞지 않습니다.")
private String umsApiKey;
@Min(1) @Max(10)
private int umsConnectionTimeout = 5;
@Min(1) @Max(30)
private int umsResponseTimeout = 10;
}
@@ -4,17 +4,23 @@ import com.eactive.ext.kjb.common.KjbProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.net.URIBuilder;
import org.apache.hc.core5.util.Timeout;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
@Slf4j
public class KjbEaiBatchModule {
private final KjbProperty property;
private final CloseableHttpClient httpClient;
private final RequestConfig requestConfig;
public KjbEaiBatchModule(@NotNull KjbProperty property) {
@@ -23,6 +29,10 @@ public class KjbEaiBatchModule {
}
this.property = property;
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(property.getEaiBatchConnectionTimeout()))
.setResponseTimeout(Timeout.ofSeconds(property.getEaiBatchResponseTimeout()))
.build();
if (StringUtils.isNotEmpty(property.getEaiBatchUrl())) {
this.httpClient = HttpClients.createDefault();
@@ -37,8 +47,18 @@ public class KjbEaiBatchModule {
log.debug("EAI 배치 호출: {}", reqMessage);
if (this.httpClient != null) {
String url = property.getEaiBatchUrl() + "?eaibatMsg=" + reqMessage.toString();
final HttpGet httpGet = new HttpGet(url);
URI uri = null;
try {
uri = new URIBuilder(property.getEaiBatchUrl())
.addParameter("eaibatMsg", reqMessage.toString())
.build();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
log.debug("EAI Batch call - {}", uri.toASCIIString());
final HttpGet httpGet = new HttpGet(uri);
httpGet.setConfig(requestConfig);
String message = httpClient.execute(httpGet, response -> EntityUtils.toString(response.getEntity(), "EUC-KR"));
@@ -83,17 +83,15 @@ public class EmailJob {
errors.put("id", "ID is required");
}
if (StringUtils.isEmpty(messageRequest.getEmail())) {
errors.put("email", "Email is required");
}
if (StringUtils.isEmpty(messageRequest.getUsername())) {
errors.put("username", "Username is required");
}
if (forSend) {
// TODO: 발송용 체크
if (StringUtils.isEmpty(messageRequest.getEmail())) {
errors.put("email", "Email is required");
}
if (StringUtils.isEmpty(messageRequest.getUsername())) {
errors.put("username", "Username is required");
}
if (StringUtils.isEmpty(messageRequest.getServiceId())) {
errors.put("serviceId", "Service ID is required");
}
@@ -1,5 +1,6 @@
package com.eactive.ext.kjb.ums;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.apim.portal.template.repository.MessageRequestRepository;
import com.eactive.ext.kjb.common.KjbProperty;
@@ -113,6 +114,11 @@ public class EmailSendModule {
throw new IllegalArgumentException("MessageRequest MessageCode serviceId is required");
}
if (MessageCode.Channels.EMAIL.getValue() != (messageRequest.getMessageCode().getChannels() & MessageCode.Channels.EMAIL.getValue())) {
log.error("지원하지 않는 발송 채널 - {}", messageRequest.getMessageCode());
throw new IllegalArgumentException("지원하지 않는 발송 채널 - " + messageRequest.getMessageCode());
}
if (repo != null) {
repo.save(messageRequest);
}
@@ -0,0 +1,167 @@
package com.eactive.ext.kjb.ums;
import com.eactive.apim.portal.template.entity.MessageCode;
import com.eactive.apim.portal.template.entity.MessageRequest;
import com.eactive.ext.kjb.common.KjbProperty;
import com.eactive.ext.kjb.ums.gson.IUmsGsonObject;
import com.eactive.ext.kjb.ums.gson.UmsRequestPayload;
import com.eactive.ext.kjb.ums.gson.VerifyPhoneUmsTemplate;
import com.eactive.ext.kjb.utils.ValidationUtil;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
@Slf4j
public class KjbUmsService {
private final static String SEND_URL = "/ums/openapi/send";
private final static String CONTENT_TYPE = "application/json; charset=utf-8";
private final static String AUTHORIZATION_FMT = "Bearer %s";
private final KjbProperty property;
private final boolean isRealMode;
private final CloseableHttpClient httpClient;
private final RequestConfig requestConfig;
public KjbUmsService(KjbProperty property) {
if (property == null) {
log.error("[KJB-UMS] 파라미터 설정 되지 않음");
throw new IllegalArgumentException("property is null");
}
isRealMode = property.getUmsUrl() != null && StringUtils.isNotEmpty(property.getUmsUrl().trim());
if (isRealMode) {
ValidationUtil.validateOrThrow(property);
httpClient = HttpClients.createDefault();
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(property.getEaiBatchConnectionTimeout()))
.setResponseTimeout(Timeout.ofSeconds(property.getEaiBatchResponseTimeout()))
.build();
} else {
httpClient = HttpClients.createDefault();
requestConfig = null;
}
this.property = property;
}
public boolean send(MessageRequest messageRequest) {
if (messageRequest == null) {
throw new IllegalArgumentException("messageRequest is null");
}
if (MessageCode.Channels.KAKAO_ALIMTALK.getValue() != (messageRequest.getMessageCode().getChannels() & MessageCode.Channels.KAKAO_ALIMTALK.getValue())) {
log.error("지원하지 않는 발송 채널 - {}", messageRequest.getMessageCode().toString());
throw new IllegalArgumentException("지원하지 않는 발송 채널 - " + messageRequest.getMessageCode().toString());
}
IUmsGsonObject content = null;
content = VerifyPhoneUmsTemplate.builder().authNumber(messageRequest.getMessage()).build();
String umsMessageId = this.sendKakaoAlimTalk(messageRequest.getId(),
UmzBizWorkCode.VERIFY_PHONE,
messageRequest.getPhone(),
content
);
log.info("MessageRequest sent - {}", umsMessageId);
return true;
}
public String sendKakaoAlimTalk(String guid, UmzBizWorkCode bizWorkCode, String cpno, IUmsGsonObject content) {
if (content == null) {
throw new IllegalArgumentException("content is null");
}
if (StringUtils.isEmpty(guid)) {
guid = String.format("OBP-%s", DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(LocalDateTime.now()));
}
UmsRequestPayload payload = UmsRequestPayload.builder()
.messageIdentityNo(guid)
.msgBizWorkCode(bizWorkCode.getCode())
.content(content)
.cellphone(cpno)
.msgSndChnlCd(UmsRequestPayload.ChannelCode.KM)
.build();
return callUms(SEND_URL, payload, (messageId, ex) -> {
if (ex != null) {
log.error("UMS Call error", ex);
throw new RuntimeException(ex);
}
log.info("UMS Call success. ({})", messageId);
}).join().getUmsMessageId();
}
private CompletableFuture<UmsCallResult> callUms(@SuppressWarnings("SameParameterValue") String url, UmsRequestPayload requestPayload,
BiConsumer<UmsCallResult, Throwable> callback) {
return CompletableFuture
.supplyAsync(() -> {
Gson gson = new Gson();
String json = gson.toJson(requestPayload);
HttpPost httpPost = new HttpPost(property.getUmsUrl() + url);
httpPost.setHeader("Content-Type", CONTENT_TYPE);
httpPost.setHeader("Authorization", String.format(AUTHORIZATION_FMT, property.getUmsApiKey()));
httpPost.setEntity(EntityBuilder.create().setText(json).setContentType(ContentType.APPLICATION_JSON).build());
httpPost.setConfig(requestConfig);
AtomicReference<UmsCallResult> result = new AtomicReference<>();
if (isRealMode) {
try {
httpClient.execute(httpPost, res -> {
String body = EntityUtils.toString(res.getEntity());
result.set(gson.fromJson(body, UmsCallResult.class));
result.get().setUmsMessageId(requestPayload.getMessageIdentityNo());
result.get().setSuccess(true);
return result;
});
} catch (IOException e) {
log.error("UMS Call error", e);
result.set(
UmsCallResult.builder()
.umsMessageId(requestPayload.getMessageIdentityNo())
.isSuccess(false)
.throwable(e)
.build()
);
}
} else {
log.warn("UMS URL 미지정으로 실제 호출을 진행하지 않음.");
result.set(UmsCallResult.builder().isSuccess(true).build());
}
try {
log.debug("UMS Call URI : {}", httpPost.getUri().toASCIIString());
log.debug("UMS Call Request payload : {}", json);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
return result.get();
})
.whenComplete(callback);
}
}
@@ -0,0 +1,18 @@
package com.eactive.ext.kjb.ums;
import com.google.gson.annotations.SerializedName;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class UmsCallResult {
private String umsMessageId;
private boolean isSuccess;
private String returnCode;
private String returnMessage;
private String returnPayload;
private Throwable throwable;
}
@@ -0,0 +1,19 @@
package com.eactive.ext.kjb.ums;
import lombok.Getter;
public enum UmzBizWorkCode {
MONITORING("B_ELFN_09510", "OBP 모니터링"),
VERIFY_PHONE("S55_ICT_0006", "API Portal 인증번호"),
;
@Getter
private String code;
@Getter
private String displayName;
UmzBizWorkCode(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
}
@@ -0,0 +1,7 @@
package com.eactive.ext.kjb.ums.gson;
/**
* GSON Serialize 용 Object
*/
public interface IUmsGsonObject {
}
@@ -0,0 +1,8 @@
package com.eactive.ext.kjb.ums.gson;
import com.google.gson.annotations.SerializedName;
public class ObpMonitoringUmsTemplate implements IUmsGsonObject {
@SerializedName("AUTH_NO")
private String authNumber;
}
@@ -0,0 +1,97 @@
package com.eactive.ext.kjb.ums.gson;
import com.google.gson.annotations.SerializedName;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Data
@Builder
@Getter
public class UmsRequestPayload {
// MSG_IDNT_NO string 메시지식별번호 50 O "발송 구분을 위한 거래 번호 (GUID 권고)
//GUID로 입력이 어려울시
//어플리케이션코드(3자리)+난수 로 유니크한 ID로 채번 권고"
@Length(min = 1, max = 50)
@NotEmpty
@SerializedName("MSG_IDNT_NO")
private String messageIdentityNo;
// MSG_BZWK_CD string 메시지업무코드 30 O UMS에 등록된 메시지업무코드(AS-IS 템플릿ID)
@NotEmpty
@Length(min = 1, max = 30)
@SerializedName("MSG_BZWK_CD")
private String msgBizWorkCode;
// 메시지업무파라미터내용 / 4000 / JSONObject
// MSG_BZWK_PARAM_CTNT object 메시지업무파라미터내용 4000 메시지업무코드의 템플릿에서 사용하는 변수와 변수값
@NotNull
@SerializedName("MSG_BZWK_PARAM_CTNT")
private IUmsGsonObject content;
// 발송예약일시 / 14 / 실 발송 일자(yyyyMMddHHmmss)
// SND_RESR_DTTM string 발송예약일시 14 20240418111500 O 실 발송 일자(현재시간)
@Pattern(regexp = "^[0-9]{14}$")
@NotEmpty
@SerializedName("SND_RESR_DTTM")
private String sendReservationDatetime = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
// 휴대전화번호 / 44 / 휴대폰번호(평문, 암호화 둘다가능)
// CPNO string 휴대전화번호 44 "암호화 또는 평문 (safedb not rrno)
//문자일 시 암호문
//숫자일 시 전화번호
//고객번호 또는 휴대전화번호 필수"
@NotEmpty
@Pattern(regexp = "^[0-9]{10,11}$")
@SerializedName("CPNO")
private String cellphone;
// 발송부점코드 / 4 /
// SND_BRCD string 발송부점코드 4 O 0333
@NotEmpty
@SerializedName("SND_BRCD")
private String sendBranchCode = "0990";
// 발송직원번호 / 7 / ECEB057(텔러번호?)
// SND_EMPNO string 발송직원번호 7 O ? 넣긴해야함
@NotEmpty
@SerializedName("SND_EMPNO")
private String sendEmployeeNo = "ECEB057";
// SRVC_ID string 서비스ID 11 요청단 측 서비스ID
@NotEmpty
@SerializedName("SRVC_ID")
private String serviceId = "eAPIM";
// MSG_SND_CHNL_CD string 메시지발송채널코드 2
@SerializedName("MSG_SND_CHNL_CD")
@NotEmpty
private ChannelCode msgSndChnlCd = ChannelCode.KM;
// VALDTN_RSLT_CD string 검증결과코드 2 00으로 고정
@SerializedName("VALDTN_RSLT_CD")
@NotEmpty
private String validationResultCode = "00";
public enum ChannelCode {
SM, // SMS
LM, // LMS
MM, // MMS
KM, // 카카오-알림톡
FM, // 카카오-친구톡
SR, // RCS-SMS
LR, // RCS-LMS
MR, // RCS-MMS
TR, // RCS-TEMPLATE
IR, // RCS-IMAGE Template
PM // Push
}
}
@@ -0,0 +1,12 @@
package com.eactive.ext.kjb.ums.gson;
import com.google.gson.annotations.SerializedName;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class VerifyPhoneUmsTemplate implements IUmsGsonObject {
@SerializedName("ATHN_NO")
private String authNumber;
}