diff --git a/.gitignore b/.gitignore index 567609b..25c7ff0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ build/ +.settings +.classpath +.project diff --git a/README.md b/README.md index dbb73ea..397e5d2 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,14 @@ eapim-admin 에서 포함하여 테스트 이용시 느린 부팅 속도 떄문 ## 주요 기능 - EAI 배치 연계 -- 고객이메일발송 시스템 연계 \ No newline at end of file +- 고객이메일발송 시스템 연계 + +## 테스트 +```bash +# git pull +GIT_SSH_COMMAND='ssh -i ~/.ssh/id_rsa-jenkins' git pull origin jenkins_with_weblogic + +# test run +KJB_EAPIM_TEST_ENABLED=TRUE kjb-gradle.sh :eapim-admin-kjb:test --tests "com.eactive.ext.kjb.ums.test.UmsTest.testSms" -Psafedb +KJB_EAPIM_TEST_ENABLED=TRUE kjb-gradle.sh :eapim-admin-kjb:test --tests "com.eactive.ext.kjb.ums.test.UmsTest.testEmail" -Psafedb +``` \ No newline at end of file diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..7eed456 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +/main/ +/test/ diff --git a/build.gradle b/build.gradle index a776fa2..d9e6691 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ jar { test { useJUnitPlatform() - systemProperty "kjb_safedb.mode", project.findProperty("kjb_safedb.mode") ?: "" + systemProperty "kjb_safedb.mode", System.getProperty("kjb_safedb.mode") if (project.hasProperty("safedb")) { systemProperty "kjb_safedb.mode", "real" diff --git a/src/main/java/com/eactive/ext/kjb/common/KjbEaiBatchException.java b/src/main/java/com/eactive/ext/kjb/common/KjbEaiBatchException.java new file mode 100644 index 0000000..a28d1c8 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/common/KjbEaiBatchException.java @@ -0,0 +1,11 @@ +package com.eactive.ext.kjb.common; + +public class KjbEaiBatchException extends Exception { + public KjbEaiBatchException(String message) { + super(message); + } + + public KjbEaiBatchException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/common/KjbProperty.java b/src/main/java/com/eactive/ext/kjb/common/KjbProperty.java index 439d526..f848d93 100644 --- a/src/main/java/com/eactive/ext/kjb/common/KjbProperty.java +++ b/src/main/java/com/eactive/ext/kjb/common/KjbProperty.java @@ -3,24 +3,70 @@ 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") + @KjbPropertyValue(key = "kjb.eai.batch.url") + @Pattern(regexp = "^(https?://[^\\s/$.?#][^\\s]*)|\\s*$", message = "kjb.eai_batch.url 는 유효한 URL 형식이거나 공백이어야 합니다. 예: http://example.com/api") private String eaiBatchUrl; + @KjbPropertyValue(key = "kjb.eai.batch.connection_timeout", defaultValue = "5") + @Min(1) @Max(10) + private int eaiBatchConnectionTimeout = 5; + + @KjbPropertyValue(key = "kjb.eai.batch.response_timeout", defaultValue = "5") + @Min(1) @Max(30) + private int eaiBatchResponseTimeout = 5; + + @KjbPropertyValue(key = "kjb.eai.batch.timeout", defaultValue = "10") + @Min(1) @Max(30) + private int eaiBatchTimeout = 10; + + @KjbPropertyValue(key = "kjb.eai.batch.charset", defaultValue = "euc-kr") + @NotEmpty + private String eaiBatchCharset = "euc-kr"; + + @KjbPropertyValue(key = "kjb.ums.eai_batch.send_dir") @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") - private String eaiBatchSendDir; + private String umsEaiBatchSendDir; + @KjbPropertyValue(key = "kjb.ums.eai_batch.backup_dir") @NotEmpty(message = "kjb.ums.eai_batch.backup_dir 는 필수값입니다.") - @Pattern(regexp = "^(?:[a-zA-Z]:\\\\|\\\\|\\/)(?:[\\w\\-\\.]+[\\\\\\/])*[\\w\\-\\.]+$", - message = "kjb.ums.eai_batch.backup_dir 은 절대 경로여야 합니다. 예: /Data/eapim/portal/sendmail/bak 또는 C:\\Data\\eapim\\portal\\sendmail\\bak") - private String eaiBatchBackupDir; + private String umsEaiBatchBackupDir; + @KjbPropertyValue(key = "kjb.ums.eai_batch.error_dir") + @NotEmpty(message = "kjb.ums.eai_batch.error_dir 는 필수값입니다.") + private String umsEaiBatchErrorDir; + @KjbPropertyValue(key = "kjb.ums.eai_batch.interface_id") @Pattern(regexp = "^[a-zA-Z0-9]{13}$", message = "kjb.ums.eai_batch.interface_id 는 12자리 영문대소문자 및 숫자 조합이어야 합니다.") - private String eaiBatchInterfaceId; + private String umsEaiBatchInterfaceId; + + @KjbPropertyValue(key = "kjb.ums.eai_batch.retention_date", defaultValue = "60") + @Min(1) @Max(365) + private int umsEaiBatchRetentionDate = 60; + + + @KjbPropertyValue(key = "kjb.ums.host") + @Pattern(regexp = "^(https?://[^\\s/$.?#][^\\s]*)|\\s*$", message = "kjb.ums.host_url 는 유효한 URL 형식이거나 공백이어야 합니다. 예: http://example.com/api") + private String umsHostUrl; + + @KjbPropertyValue(key = "kjb.ums.api_key") + @Pattern(regexp = "^[a-zA-Z0-9_-]{36}$", message = "kjb.ums.api_key 입력 형식이 맞지 않습니다.") + private String umsApiKey; + + @KjbPropertyValue(key = "kjb.ums.connection_timeout", defaultValue = "5") + @Min(1) @Max(10) + private int umsConnectionTimeout = 5; + + @KjbPropertyValue(key = "kjb.ums.response_timeout", defaultValue = "5") + @Min(1) @Max(30) + private int umsResponseTimeout = 5; + + @KjbPropertyValue(key = "kjb.ums.timeout", defaultValue = "10") + @Min(1) @Max(30) + private int umsTimeout = 10; } diff --git a/src/main/java/com/eactive/ext/kjb/common/KjbPropertyValue.java b/src/main/java/com/eactive/ext/kjb/common/KjbPropertyValue.java new file mode 100644 index 0000000..0522f7d --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/common/KjbPropertyValue.java @@ -0,0 +1,13 @@ +package com.eactive.ext.kjb.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface KjbPropertyValue { + String key(); + String defaultValue() default ""; +} diff --git a/src/main/java/com/eactive/ext/kjb/common/KjbUmsException.java b/src/main/java/com/eactive/ext/kjb/common/KjbUmsException.java new file mode 100644 index 0000000..07a1082 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/common/KjbUmsException.java @@ -0,0 +1,11 @@ +package com.eactive.ext.kjb.common; + +public class KjbUmsException extends Exception { + public KjbUmsException(String message) { + super(message); + } + + public KjbUmsException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/eai/EaiBatchInterface.java b/src/main/java/com/eactive/ext/kjb/eai/EaiBatchInterface.java new file mode 100644 index 0000000..6d9832e --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/eai/EaiBatchInterface.java @@ -0,0 +1,64 @@ +package com.eactive.ext.kjb.eai; + +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.Pattern; +import java.io.File; +import java.nio.file.Paths; + +@Data +@Builder +@Slf4j +public class EaiBatchInterface { + @Pattern(regexp = "^[a-zA-Z0-9]{13}$", message = "id는 12자리 영문대소문자 및 숫자 조합이어야 합니다.") + private String id; + + @NotEmpty + @Pattern(regexp = "^(?:[a-zA-Z]:\\\\|\\\\|\\/)(?:[\\w\\-\\.]+[\\\\\\/])*[\\w\\-\\.]+$", message = "sendDir은 절대경로로 지정되어야 합니다.") + private String sendDir; + + @NotEmpty + @Pattern(regexp = "^(?:[a-zA-Z]:\\\\|\\\\|\\/)(?:[\\w\\-\\.]+[\\\\\\/])*[\\w\\-\\.]+$", message = "backupDir은 절대경로로 지정되어야 합니다.") + private String backupDir; + + @NotEmpty + @Pattern(regexp = "^(?:[a-zA-Z]:\\\\|\\\\|\\/)(?:[\\w\\-\\.]+[\\\\\\/])*[\\w\\-\\.]+$", message = "errorDir은 절대경로로 지정되어야 합니다.") + private String errorDir; + + + public void moveError(String filename) { + move(filename, errorDir); + } + + public void moveBackup(String filename) { + move(filename, backupDir); + } + + private void move(String filename, String targetDir) { + File file = Paths.get(sendDir, filename).toFile(); + File targetDirFile = new File(targetDir); + File targetFile = new File(targetDir, file.getName()); + + log.debug("송신파일 이동 시작 - {} -> {}", filename, targetDir); + + if (!file.exists()) { + log.warn("송신 파일이 존재 하지 않습니다. ({})", file.getAbsolutePath()); + return; + } + + if (!targetDirFile.exists()) { + if (!targetDirFile.mkdirs()) { + log.warn("대상 디렉토리 생성 실패 ({})", file.getAbsolutePath()); + return; + } + } + + if (!file.renameTo(targetFile)) { + log.warn("송신 파일 이동 실패 - {}", targetFile.getAbsolutePath()); + return; + } + } +} diff --git a/src/main/java/com/eactive/ext/kjb/eai/EaiBatchMessage.java b/src/main/java/com/eactive/ext/kjb/eai/EaiBatchMessage.java index 7ce84d2..db6685a 100644 --- a/src/main/java/com/eactive/ext/kjb/eai/EaiBatchMessage.java +++ b/src/main/java/com/eactive/ext/kjb/eai/EaiBatchMessage.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; @Slf4j @@ -17,6 +18,9 @@ public class EaiBatchMessage { @Getter private String count = ""; + /** + * 요청시 사용 안함, 응답시 1: 성공, 2: 실패 + */ @Getter private String resultCode = ""; @@ -28,14 +32,17 @@ public class EaiBatchMessage { private boolean isReadOnly = false; + private String charset; + private EaiBatchMessage() {} - public static EaiBatchMessage createRequest(String interfaceId, String filename) { + public static EaiBatchMessage createRequest(String interfaceId, String filename, String charset) { EaiBatchMessage message = new EaiBatchMessage(); message.interfaceId = interfaceId; message.filename = filename; + message.charset = charset; return message; } @@ -46,16 +53,16 @@ public class EaiBatchMessage { message.isReadOnly = true; if (rawMessage.length() >= 40) { - message.interfaceId = rawMessage.substring(0, 20).trim(); - message.baseDate = rawMessage.substring(20, 28).trim(); - message.count = rawMessage.substring(28, 38).trim(); - message.resultCode = rawMessage.substring(38, 39).trim(); + message.interfaceId = rawMessage.substring(0, 19).trim(); + message.baseDate = rawMessage.substring(19, 27).trim(); + message.count = rawMessage.substring(27, 37).trim(); + message.resultCode = rawMessage.substring(37, 38).trim(); // filename 은 @@가 발견되면 이전 까지 trim / 발견되지 않으면 나머지 전체 그리고 trim - int endIdx = rawMessage.indexOf("@@", 39); + int endIdx = rawMessage.indexOf("@@", 38); if (endIdx == -1) { - message.filename = rawMessage.substring(39).trim(); + message.filename = rawMessage.substring(38).trim(); } else { - message.filename = rawMessage.substring(39, endIdx).trim(); + message.filename = rawMessage.substring(38, endIdx).trim(); } } @@ -74,16 +81,20 @@ public class EaiBatchMessage { return rawMessage; } else { ByteBuffer buffer = ByteBuffer.allocate(1024); - // 0~19 : 인터페이스ID (20) - buffer.put(fixedString(interfaceId, 20)); - // 19-8: 기준일자 (8) - buffer.put(fixedString(baseDate, 8)); - // 27-10 : 건수 (10) - buffer.put(fixedString(count, 10)); - // 37-1 : 결과코드 (1) - buffer.put(fixedString(resultCode, 1)); - // 38-100 : 파일명 (100) - buffer.put(fixedString(filename, 100)); + try { + // 인터페이스ID (19) + buffer.put(fixedString(interfaceId, 19)); + // 기준일자 (8) + buffer.put(fixedString(baseDate, 8)); + // 건수 (10) + buffer.put(fixedString(count, 10)); + // 결과코드 (1) + buffer.put(fixedString(resultCode, 1)); + // 파일명 (100) + buffer.put(fixedString(filename, 100)); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } // 종료 구분자 (2) buffer.put("@@".getBytes()); @@ -93,11 +104,11 @@ public class EaiBatchMessage { } } - private byte[] fixedString(String str, int length) { + private byte[] fixedString(String str, int length) throws UnsupportedEncodingException { if (str == null) { str = ""; } - byte[] strBytes = str.getBytes(); + byte[] strBytes = str.getBytes(this.charset); byte[] result = new byte[length]; int copyLength = Math.min(strBytes.length, length); diff --git a/src/main/java/com/eactive/ext/kjb/eai/KjbEaiBatchModule.java b/src/main/java/com/eactive/ext/kjb/eai/KjbEaiBatchModule.java index ccd6774..72c57f3 100644 --- a/src/main/java/com/eactive/ext/kjb/eai/KjbEaiBatchModule.java +++ b/src/main/java/com/eactive/ext/kjb/eai/KjbEaiBatchModule.java @@ -1,20 +1,41 @@ package com.eactive.ext.kjb.eai; +import com.eactive.ext.kjb.common.KjbEaiBatchException; 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.Valid; import javax.validation.constraints.NotNull; +import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Comparator; +import java.util.concurrent.*; +import java.util.stream.Stream; @Slf4j public class KjbEaiBatchModule { private final KjbProperty property; private final CloseableHttpClient httpClient; + private final RequestConfig requestConfig; + private LocalDateTime lastFileRetentionCheckTime; + public KjbEaiBatchModule(@NotNull KjbProperty property) { @@ -23,6 +44,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(); @@ -32,26 +57,110 @@ public class KjbEaiBatchModule { } } - public EaiBatchMessage call(String interfaceId, String filename) throws IOException { - EaiBatchMessage reqMessage = EaiBatchMessage.createRequest(interfaceId, filename); + public EaiBatchMessage call(@Valid EaiBatchInterface intf, String filename) throws KjbEaiBatchException { + EaiBatchMessage reqMessage = EaiBatchMessage.createRequest(intf.getId(), filename, property.getEaiBatchCharset()); log.debug("EAI 배치 호출: {}", reqMessage); + // 송신 파일 체크 + File sendFile = Paths.get(intf.getSendDir(), filename).toFile(); + if (!sendFile.exists() || !sendFile.isFile()) { + throw new KjbEaiBatchException("Not found send file - " + sendFile.toPath()); + } + 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()); - String message = httpClient.execute(httpGet, response -> EntityUtils.toString(response.getEntity(), "EUC-KR")); + final HttpGet httpGet = new HttpGet(uri); + httpGet.setConfig(requestConfig); - EaiBatchMessage resMessage = EaiBatchMessage.fromResponse(message); - log.debug("EAI 배치 응답: {}", resMessage); - return resMessage; + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> + httpClient.execute(httpGet, response -> EntityUtils.toString(response.getEntity(), "EUC-KR"))); + + EaiBatchMessage resMessage = null; + try { + String message = future.get(property.getEaiBatchTimeout(), TimeUnit.SECONDS); + resMessage = EaiBatchMessage.fromResponse(message); + log.debug("EAI 배치 응답: {}", resMessage); + + return resMessage; + } catch (InterruptedException | ExecutionException | TimeoutException e) { + log.error("EAI Batch call exception", e); + throw new KjbEaiBatchException("EAI Batch call failed", e); + } finally { + executor.shutdown(); + + // EAI 송신 실패일 경우 에러 경로로 이동 + if (resMessage != null && "1".equals(resMessage.getResultCode())) { + intf.moveBackup(filename); + } else { + intf.moveError(filename); + } + } } else { log.info("EAI_BATCH_URL 이 설정되지 않아 실제 호출하지 않음. 요청 메시지: {}", reqMessage); // 정상응답 리턴 - EaiBatchMessage resMessage = EaiBatchMessage.fromResponse(String.format("%-20s%-8s%-10s%-1s%s@@", interfaceId, "", "0000000000", "1", "S:" + filename)); + EaiBatchMessage resMessage = EaiBatchMessage.fromResponse(String.format("%-19s%-8s%-10s%-1s%s@@", intf.getId(), "", "0000000000", "1", "S:" + filename)); log.debug("EAI 배치 응답(모의): {}", resMessage); + intf.moveBackup(filename); return resMessage; } } + + /** + * 1시간 주기로 파일 유지 및 삭제 처리 + */ + public void processFileRetentionDays() { + if (lastFileRetentionCheckTime == null) { + lastFileRetentionCheckTime = LocalDateTime.now(); + } + + // 현재 시간과 1시간 이상 차이 날때 동작 + LocalDateTime now = LocalDateTime.now(); + boolean isOver = Duration.between(lastFileRetentionCheckTime, now).abs().toHours() == 1; + + if (!isOver) return; + + this.walkDeletePath(property.getUmsEaiBatchSendDir()); + this.walkDeletePath(property.getUmsEaiBatchBackupDir()); + this.walkDeletePath(property.getUmsEaiBatchErrorDir()); + + lastFileRetentionCheckTime = now; + } + + private void walkDeletePath(String path) { + final int retentionDays = property.getUmsEaiBatchRetentionDate(); + log.info("WalkDelete - {} / Retention days : {}", path, retentionDays); + + Path dir = Paths.get(path); + LocalDateTime now = LocalDateTime.now(); + + try (Stream paths = Files.walk(dir)) { + paths.sorted(Comparator.reverseOrder()).forEach(p -> { + try { + BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class); + LocalDateTime created = attrs.creationTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + + long over = Math.abs(Duration.between(created, now).toDays()); + if (over >= retentionDays) { + Files.deleteIfExists(p); + log.info("Delete over {} days - {}", retentionDays, p); + } + } catch (Exception e) { + log.warn("Delete fail - {}", p.getFileName().toString(), e); + } + }); + } catch (IOException e) { + log.warn("Not found path : {}", path); + } + } } diff --git a/src/main/java/com/eactive/ext/kjb/ums/EmailJob.java b/src/main/java/com/eactive/ext/kjb/ums/EmailJob.java index 9a92b1c..7a38cde 100644 --- a/src/main/java/com/eactive/ext/kjb/ums/EmailJob.java +++ b/src/main/java/com/eactive/ext/kjb/ums/EmailJob.java @@ -3,9 +3,13 @@ package com.eactive.ext.kjb.ums; import com.eactive.apim.portal.portaluser.entity.PortalUser; import com.eactive.apim.portal.template.entity.MessageRequest; import com.eactive.apim.portal.template.repository.MessageRequestRepository; +import com.eactive.ext.kjb.common.KjbEaiBatchException; import com.eactive.ext.kjb.common.KjbProperty; +import com.eactive.ext.kjb.eai.EaiBatchInterface; import com.eactive.ext.kjb.eai.EaiBatchMessage; import com.eactive.ext.kjb.eai.KjbEaiBatchModule; +import com.eactive.ext.kjb.safedb.KjbSafedbWrapper; +import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; @@ -28,24 +32,28 @@ public class EmailJob { private final KjbProperty prop; private final MessageRequest messageRequest; - private final Path tempFilePath; + private static final int READY = 1; // Temp 파일 생성 단계 + private static final int BEFORE_SEND = 2; + private static final int SEND = 3; + private final Path eaiFilePath; private final Path backupFilePath; private final Path failedFilePath; private final MessageRequestRepository messageRequestRepository; private final LocalDateTime now = LocalDateTime.now(); + private int eaiPhase = 0; + private EmailJob(KjbProperty prop, MessageRequest messageRequest, MessageRequestRepository messageRequestRepository) { this.messageRequest = messageRequest; this.prop = prop; - String filename = messageRequest.getServiceId() + "_" + DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").format(now); - this.tempFilePath = Paths.get(prop.getEaiBatchSendDir(), filename + ".tmp"); - this.eaiFilePath = Paths.get(prop.getEaiBatchSendDir(), filename); - this.backupFilePath = Paths.get(prop.getEaiBatchBackupDir(), filename); - this.failedFilePath = Paths.get(prop.getEaiBatchBackupDir(), filename + ".failed"); + String filename = messageRequest.getServiceId() + "_" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(now) + ".dat"; + this.eaiFilePath = Paths.get(prop.getUmsEaiBatchSendDir(), filename); + this.backupFilePath = Paths.get(prop.getUmsEaiBatchBackupDir(), filename); + this.failedFilePath = Paths.get(prop.getUmsEaiBatchBackupDir(), filename + ".failed"); this.messageRequestRepository = messageRequestRepository; @@ -65,7 +73,8 @@ public class EmailJob { if (messageRequest.getUser() != null) { PortalUser user = messageRequest.getUser(); - messageRequest.setUmsUid(UUIDBase62Util.uuidStringToBase62(user.getId())); +// messageRequest.setUmsUid(UUIDBase62Util.uuidStringToBase62(user.getId())); + messageRequest.setUmsUid(user.getId()); if (repository != null) { repository.save(messageRequest); @@ -83,17 +92,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"); } @@ -114,11 +121,12 @@ public class EmailJob { @Transactional - public void sendEmail() { + public void sendEmail() throws KjbEaiBatchException { if (messageRequestRepository != null) { PortalUser user = messageRequest.getUser(); if (user != null) { - messageRequest.setUmsUid(UUIDBase62Util.uuidStringToBase62(user.getId())); + // messageRequest.setUmsUid(UUIDBase62Util.uuidStringToBase62(user.getId())); + messageRequest.setUmsUid(user.getId()); } messageRequestRepository.save(messageRequest); @@ -132,9 +140,11 @@ public class EmailJob { log.debug("발송 성공 처리"); doSuccess(); + } catch (KjbEaiBatchException e) { + doFail(e); } catch (Exception e) { - log.error("이메일 발송 실패", e); - doFail(); + log.error(e.getMessage(), e); + doFail(e); } finally { doDeleteEaiFile(); } @@ -144,13 +154,24 @@ public class EmailJob { // TODO: EAI 발송용 파일 삭제 } - private void doFail() { + private void doFail(Exception e) throws KjbEaiBatchException { // TODO: EAI 발송 실패 처리 + if (e instanceof KjbEaiBatchException) { + throw (KjbEaiBatchException) e; + } else { + throw new KjbEaiBatchException("이메일 발송 실패", e); + } } - private void eaiBatchCall() throws IOException { + private void eaiBatchCall() throws KjbEaiBatchException { KjbEaiBatchModule eaiBatchModule = new KjbEaiBatchModule(prop); - EaiBatchMessage result = eaiBatchModule.call(prop.getEaiBatchInterfaceId(), eaiFilePath.getFileName().toString()); + EaiBatchInterface eaiBatchInterface = EaiBatchInterface.builder() + .id(prop.getUmsEaiBatchInterfaceId()) + .sendDir(prop.getUmsEaiBatchSendDir()) + .backupDir(prop.getUmsEaiBatchBackupDir()) + .errorDir(prop.getUmsEaiBatchErrorDir()) + .build(); + EaiBatchMessage result = eaiBatchModule.call(eaiBatchInterface, eaiFilePath.getFileName().toString()); if (!"1".equals(result.getResultCode())) { log.error("EAI Batch call failed: {}", result); @@ -161,25 +182,17 @@ public class EmailJob { } private void doSuccess() { - // 백업 디렉토리로 복사 - try { - Files.copy(eaiFilePath, backupFilePath); - log.debug("Copied EAI file to backup: {} -> {}", eaiFilePath.toString(), backupFilePath.toString()); - } catch (IOException e) { - log.error("Cannot copy EAI file to backup: {} -> {}", eaiFilePath.toString(), backupFilePath.toString(), e); - } + // 딱히 뭐 할 거 없음. } public void doCreateNewFile() { - final String filename = "EMAIL_" + messageRequest.getId() + ".json"; - if (eaiFilePath.toFile().exists()) { log.error("Mail File exists for ID: {}", messageRequest.getId()); throw new IllegalStateException("Mail File exists for ID: " + messageRequest.getId()); } - File mailFile = tempFilePath.toFile(); + File mailFile = eaiFilePath.toFile(); try { boolean created = mailFile.createNewFile(); if (!created) { @@ -205,9 +218,7 @@ public class EmailJob { throw new RuntimeException(e); } try { - Files.write(tempFilePath, data); - Files.copy(tempFilePath, eaiFilePath); - Files.delete(tempFilePath); + Files.write(eaiFilePath, data); log.debug("Wrote JSON to file: {}", eaiFilePath.toString()); } catch (IOException e) { log.error("Cannot write JSON to file: {}", eaiFilePath.toString(), e); @@ -217,24 +228,34 @@ public class EmailJob { public String generateJSON(boolean isPretty) { validateMessageRequest(messageRequest, true, true); + KjbSafedbWrapper safedb = KjbSafedbWrapper.getInstance(); JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("MAKE_DATE", DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDateTime.now())); - jsonObject.addProperty("MAKE_SEQNO", "0001"); + jsonObject.addProperty("MAKE_SEQNO", DateTimeFormatter.ofPattern("HHmmssSSS").format(LocalDateTime.now())); jsonObject.addProperty("CAMP_ID", messageRequest.getServiceId()); // 전자금융 고객이메일 발송 업무ID (SERVICE_ID) jsonObject.addProperty("ID", messageRequest.getUmsUid()); // 고객 ID - jsonObject.addProperty("EMAIL", messageRequest.getEmail()); + jsonObject.addProperty("EMAIL", safedb.encryptNotRnnoString(messageRequest.getEmail())); jsonObject.addProperty("NAME", messageRequest.getUsername()); jsonObject.addProperty("ENCKEY", ""); // 보안메일용으로 기입 불필요 + Gson gson = new Gson(); + log.debug("Template string to json: {}", messageRequest.getMessage()); + JsonObject contentJSon = gson.fromJson(messageRequest.getMessage(), JsonObject.class); + contentJSon.entrySet().forEach(entry -> { + jsonObject.add(entry.getKey(), entry.getValue()); + }); - jsonObject.addProperty("SUBJECT", messageRequest.getSubject()); - jsonObject.addProperty("MESSAGE", messageRequest.getMessage()); +// jsonObject.addProperty("SUBJECT", messageRequest.getSubject()); +// jsonObject.addProperty("MESSAGE", messageRequest.getMessage()); + String jsonString; if (isPretty) { - return new GsonBuilder().setPrettyPrinting().create().toJson(jsonObject); + jsonString = new GsonBuilder().setPrettyPrinting().create().toJson(jsonObject); } else { - return jsonObject.toString(); + jsonString = jsonObject.toString(); } + + return jsonString; } } diff --git a/src/main/java/com/eactive/ext/kjb/ums/EmailSendModule.java b/src/main/java/com/eactive/ext/kjb/ums/EmailSendModule.java deleted file mode 100644 index fbfbb54..0000000 --- a/src/main/java/com/eactive/ext/kjb/ums/EmailSendModule.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.eactive.ext.kjb.ums; - -import com.eactive.apim.portal.template.entity.MessageRequest; -import com.eactive.apim.portal.template.repository.MessageRequestRepository; -import com.eactive.ext.kjb.common.KjbProperty; -import com.eactive.ext.kjb.utils.ValidationUtil; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.CompletableFuture; -import java.util.function.BiConsumer; - -@Slf4j -public class EmailSendModule { - private static EmailSendModule instance = null; - private static KjbProperty prop; - - private final MessageRequestRepository repo; - - - public static synchronized EmailSendModule getInstance(KjbProperty property, - MessageRequestRepository messageRequestRepository) { - - ValidationUtil.validateOrThrow(property); - - if (instance == null) { - instance = new EmailSendModule(property, messageRequestRepository); - } - - return instance; - } - - - public static void environmentCheck() { - if (StringUtils.isEmpty(prop.getEaiBatchUrl())) { - log.warn("EAI_BATCH_URL 가 설정되지 않음. 실제 EAI 배치 연동 없이 동작함"); - } - - log.info("EAI Batch URL: {}", prop.getEaiBatchUrl()); - log.info("EAI Batch Send Dir: {}", prop.getEaiBatchSendDir()); - - - // 디렉토리 검사(쓰기 권한 포함) - Path sendDirPath = Paths.get(prop.getEaiBatchSendDir()); - if (!sendDirPath.toFile().exists()) { - // 생성 시도 - boolean created = sendDirPath.toFile().mkdirs(); - if (!created) { - log.error("EAI_BATCH_SEND_DIR 디렉토리를 생성할 수 없음: {}", prop.getEaiBatchSendDir()); - throw new IllegalStateException("EAI_BATCH_SEND_DIR 디렉토리를 생성할 수 없음: " + prop.getEaiBatchSendDir()); - } - } - - if (!sendDirPath.toFile().isDirectory()) { - log.error("EAI_BATCH_SEND_DIR 가 디렉토리가 아님: {}", prop.getEaiBatchSendDir()); - throw new IllegalStateException("EAI_BATCH_SEND_DIR 가 디렉토리가 아님: " + prop.getEaiBatchSendDir()); - } - - if (!sendDirPath.toFile().canWrite()) { - log.error("EAI_BATCH_SEND_DIR 에 쓰기 권한이 없음: {}", prop.getEaiBatchSendDir()); - throw new IllegalStateException("EAI_BATCH_SEND_DIR 에 쓰기 권한이 없음: " + prop.getEaiBatchSendDir()); - } - - - // 백업 디렉토리 - Path backupDirPath = Paths.get(prop.getEaiBatchBackupDir()); - if (!backupDirPath.toFile().exists()) { - // 생성 시도 - boolean created = backupDirPath.toFile().mkdirs(); - if (!created) { - log.error("EAI_BATCH_BACKUP_DIR 디렉토리를 생성할 수 없음: {}", prop.getEaiBatchBackupDir()); - throw new IllegalStateException("EAI_BATCH_BACKUP_DIR 디렉토리를 생성할 수 없음: " + prop.getEaiBatchBackupDir()); - } - } - - if (!backupDirPath.toFile().isDirectory()) { - log.error("EAI_BATCH_BACKUP_DIR 가 디렉토리가 아님: {}", prop.getEaiBatchBackupDir()); - throw new IllegalStateException("EAI_BATCH_BACKUP_DIR 가 디렉토리가 아님: " + prop.getEaiBatchBackupDir()); - } - - if (!backupDirPath.toFile().canWrite()) { - log.error("EAI_BATCH_BACKUP_DIR 에 쓰기 권한이 없음: {}", prop.getEaiBatchBackupDir()); - throw new IllegalStateException("EAI_BATCH_BACKUP_DIR 에 쓰기 권한이 없음: " + prop.getEaiBatchBackupDir()); - } - - log.info("KjbEmailSendModule - Environment check passed."); - } - - - - private EmailSendModule(KjbProperty property, - MessageRequestRepository repo) { - this.repo = repo; - - if (property != null && prop == null) { - prop = property; - } - - environmentCheck(); - } - - public CompletableFuture sendEmail(MessageRequest messageRequest, BiConsumer callback) { - return CompletableFuture - .supplyAsync(() -> { - String serviceId = "UNKNOWN"; - - serviceId = messageRequest.getMessageCode().getServiceId(); - messageRequest.setServiceId(serviceId); - - if (StringUtils.isEmpty(serviceId)) { - throw new IllegalArgumentException("MessageRequest MessageCode serviceId is required"); - } - - if (repo != null) { - repo.save(messageRequest); - } - - EmailJob job = EmailJob.create(prop, messageRequest, repo); - job.sendEmail(); - - log.debug("JSON : {}", job.generateJSON(true)); - return true; - }) - .whenComplete(callback); - } -} diff --git a/src/main/java/com/eactive/ext/kjb/ums/KjbEmailSendModule.java b/src/main/java/com/eactive/ext/kjb/ums/KjbEmailSendModule.java new file mode 100644 index 0000000..159f554 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/ums/KjbEmailSendModule.java @@ -0,0 +1,141 @@ +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.KjbEaiBatchException; +import com.eactive.ext.kjb.common.KjbProperty; +import com.eactive.ext.kjb.utils.ValidationUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; + +@Slf4j +public class KjbEmailSendModule { + public final static int[] ALLOW_CHANNELS = { MessageCode.Channels.EMAIL.getValue() }; + + private static KjbEmailSendModule instance = null; + private static KjbProperty prop; + + private final MessageRequestRepository repo; + + + public static synchronized KjbEmailSendModule getInstance(KjbProperty property, + MessageRequestRepository messageRequestRepository) { + + ValidationUtil.validateOrThrow(property); + + if (instance == null) { + instance = new KjbEmailSendModule(property, messageRequestRepository); + } + + return instance; + } + + public static boolean isAllowChannel(MessageRequest message) { + for (int channel : ALLOW_CHANNELS) { + if ((message.getMessageCode().getChannels() & channel) == channel) { + return true; + } + } + return false; + } + + + public static void environmentCheck() { + if (StringUtils.isEmpty(prop.getEaiBatchUrl())) { + log.warn("EAI_BATCH_URL 가 설정되지 않음. 실제 EAI 배치 연동 없이 동작함"); + } + + log.info("EAI Batch URL: {}", prop.getEaiBatchUrl()); + log.info("EAI Batch Send Dir: {}", prop.getUmsEaiBatchSendDir()); + + + // 디렉토리 검사(쓰기 권한 포함) + Path sendDirPath = Paths.get(prop.getUmsEaiBatchSendDir()); + if (!sendDirPath.toFile().exists()) { + // 생성 시도 + boolean created = sendDirPath.toFile().mkdirs(); + if (!created) { + log.error("EAI_BATCH_SEND_DIR 디렉토리를 생성할 수 없음: {}", prop.getUmsEaiBatchSendDir()); + throw new IllegalStateException("EAI_BATCH_SEND_DIR 디렉토리를 생성할 수 없음: " + prop.getUmsEaiBatchSendDir()); + } + } + + if (!sendDirPath.toFile().isDirectory()) { + log.error("EAI_BATCH_SEND_DIR 가 디렉토리가 아님: {}", prop.getUmsEaiBatchSendDir()); + throw new IllegalStateException("EAI_BATCH_SEND_DIR 가 디렉토리가 아님: " + prop.getUmsEaiBatchSendDir()); + } + + if (!sendDirPath.toFile().canWrite()) { + log.error("EAI_BATCH_SEND_DIR 에 쓰기 권한이 없음: {}", prop.getUmsEaiBatchSendDir()); + throw new IllegalStateException("EAI_BATCH_SEND_DIR 에 쓰기 권한이 없음: " + prop.getUmsEaiBatchSendDir()); + } + + + // 백업 디렉토리 + Path backupDirPath = Paths.get(prop.getUmsEaiBatchBackupDir()); + if (!backupDirPath.toFile().exists()) { + // 생성 시도 + boolean created = backupDirPath.toFile().mkdirs(); + if (!created) { + log.error("EAI_BATCH_BACKUP_DIR 디렉토리를 생성할 수 없음: {}", prop.getUmsEaiBatchBackupDir()); + throw new IllegalStateException("EAI_BATCH_BACKUP_DIR 디렉토리를 생성할 수 없음: " + prop.getUmsEaiBatchBackupDir()); + } + } + + if (!backupDirPath.toFile().isDirectory()) { + log.error("EAI_BATCH_BACKUP_DIR 가 디렉토리가 아님: {}", prop.getUmsEaiBatchBackupDir()); + throw new IllegalStateException("EAI_BATCH_BACKUP_DIR 가 디렉토리가 아님: " + prop.getUmsEaiBatchBackupDir()); + } + + if (!backupDirPath.toFile().canWrite()) { + log.error("EAI_BATCH_BACKUP_DIR 에 쓰기 권한이 없음: {}", prop.getUmsEaiBatchBackupDir()); + throw new IllegalStateException("EAI_BATCH_BACKUP_DIR 에 쓰기 권한이 없음: " + prop.getUmsEaiBatchBackupDir()); + } + + log.info("KjbEmailSendModule - Environment check passed."); + } + + + + private KjbEmailSendModule(KjbProperty property, + MessageRequestRepository repo) { + this.repo = repo; + + if (property != null && prop == null) { + prop = property; + } + + environmentCheck(); + } + + public void sendEmail(MessageRequest messageRequest) throws KjbEaiBatchException { + String serviceId = "UNKNOWN"; + + serviceId = messageRequest.getMessageCode().getServiceId(); + messageRequest.setServiceId(serviceId); + + if (StringUtils.isEmpty(serviceId)) { + 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); + } + + EmailJob job = EmailJob.create(prop, messageRequest, repo); + job.sendEmail(); + + log.debug("JSON : {}", job.generateJSON(true)); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/ums/KjbUmsService.java b/src/main/java/com/eactive/ext/kjb/ums/KjbUmsService.java new file mode 100644 index 0000000..c849023 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/ums/KjbUmsService.java @@ -0,0 +1,211 @@ +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.common.KjbUmsException; +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.JsonUtil; +import com.eactive.ext.kjb.utils.ValidationUtil; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +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.ProtocolException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.util.Timeout; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.concurrent.*; + +@Slf4j +public class KjbUmsService { + public final static int[] ALLOW_CHANNELS = { MessageCode.Channels.KAKAO_ALIMTALK.getValue() }; + + 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.getUmsHostUrl() != null && StringUtils.isNotEmpty(property.getUmsHostUrl().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 static boolean isAllowChannel(MessageRequest message) { + for (int channel : ALLOW_CHANNELS) { + if ((message.getMessageCode().getChannels() & channel) == channel) { + return true; + } + } + return false; + } + + + public boolean send(MessageRequest messageRequest) throws KjbUmsException { + 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(), + UmsBizWorkCode.VERIFY_PHONE, + messageRequest.getPhone(), + content + ); + + log.info("MessageRequest sent - {}", umsMessageId); + + return true; + } + + public String sendKakaoAlimTalk(String guid, UmsBizWorkCode bizWorkCode, String cpno, IUmsGsonObject content) throws KjbUmsException { + if (content == null) { + throw new IllegalArgumentException("content is null"); + } + + if (StringUtils.isEmpty(guid)) { + guid = String.format("APM-%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(); + + try { + ValidationUtil.validateOrThrow(payload); + } catch (Exception ex) { + log.debug("UMS Request payload validation failed - {}", JsonUtil.toPrettyString(payload)); + throw ex; + } + + UmsCallResult result = call(SEND_URL, payload); + if (!result.isSuccess()) { + throw new KjbUmsException(result.getThrowable().getMessage()); + } + + return result.getUmsMessageId(); + } + + private UmsCallResult call(String url, UmsRequestPayload requestPayload) throws KjbUmsException { + ExecutorService executor = Executors.newSingleThreadExecutor(); + Gson gson = new Gson(); + String json = gson.toJson(requestPayload); + + HttpPost httpPost = new HttpPost(property.getUmsHostUrl() + 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()); + + if (log.isDebugEnabled()) { + try { + log.debug("Header - {}", httpPost.getHeader("Content-Type")); + log.debug("Header - {}", httpPost.getHeader("Authorization")); + log.debug("Request Body : {}", StreamUtils.copyToString(httpPost.getEntity().getContent(), Charset.defaultCharset())); + } catch (ProtocolException | IOException e) { + throw new RuntimeException(e); + } + } + + httpPost.setConfig(requestConfig); + + if (isRealMode) { + Future future = executor.submit(() -> { + return httpClient.execute(httpPost, res -> { + try { + UmsCallResult result; + String body = EntityUtils.toString(res.getEntity()); + if (log.isDebugEnabled()) { + log.debug("Response - {}", body); + } + result = gson.fromJson(body, UmsCallResult.class); + result.setUmsMessageId(requestPayload.getMessageIdentityNo()); + result.setSuccess(true); + return result; + } catch (JsonSyntaxException e) { + log.error("UMS Call error(JSON Syntax)", e); + return UmsCallResult.builder() + .umsMessageId(requestPayload.getMessageIdentityNo()) + .isSuccess(false) + .throwable(e) + .build(); + } catch (Exception e) { + log.error("UMS Call error", e); + return UmsCallResult.builder() + .umsMessageId(requestPayload.getMessageIdentityNo()) + .isSuccess(false) + .throwable(e) + .build(); + } finally { + log.debug("Response status - {}", res.getCode()); + + if (res.getCode() != 200) { + Arrays.asList(res.getHeaders()).forEach(o -> { + log.warn("[Header] {}: {}", o.getName(), o.getValue()); + }); + } + } + }); + }); + + try { + return future.get(property.getUmsTimeout(), TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + log.error("EAI Batch call exception", e); + throw new KjbUmsException("EAI Ums call failed", e); + } + } else { + log.warn("UMS URL 미지정으로 실제 호출을 진행하지 않음."); + return UmsCallResult.builder().isSuccess(true).build(); + } + } +} diff --git a/src/main/java/com/eactive/ext/kjb/ums/UmsBizWorkCode.java b/src/main/java/com/eactive/ext/kjb/ums/UmsBizWorkCode.java new file mode 100644 index 0000000..b56d628 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/ums/UmsBizWorkCode.java @@ -0,0 +1,19 @@ +package com.eactive.ext.kjb.ums; + +import lombok.Getter; + +public enum UmsBizWorkCode { + MONITORING("B_ELFN_09510", "OBP 모니터링"), + VERIFY_PHONE("S55_ICT_0006", "API Portal 인증번호"), + ; + + @Getter + private String code; + @Getter + private String displayName; + + UmsBizWorkCode(String code, String displayName) { + this.code = code; + this.displayName = displayName; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/ums/UmsCallResult.java b/src/main/java/com/eactive/ext/kjb/ums/UmsCallResult.java new file mode 100644 index 0000000..f510345 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/ums/UmsCallResult.java @@ -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; +} diff --git a/src/main/java/com/eactive/ext/kjb/ums/gson/IUmsGsonObject.java b/src/main/java/com/eactive/ext/kjb/ums/gson/IUmsGsonObject.java new file mode 100644 index 0000000..bee5d2b --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/ums/gson/IUmsGsonObject.java @@ -0,0 +1,7 @@ +package com.eactive.ext.kjb.ums.gson; + +/** + * GSON Serialize 용 Object + */ +public interface IUmsGsonObject { +} diff --git a/src/main/java/com/eactive/ext/kjb/ums/gson/ObpMonitoringUmsTemplate.java b/src/main/java/com/eactive/ext/kjb/ums/gson/ObpMonitoringUmsTemplate.java new file mode 100644 index 0000000..b384155 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/ums/gson/ObpMonitoringUmsTemplate.java @@ -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; +} diff --git a/src/main/java/com/eactive/ext/kjb/ums/gson/UmsRequestPayload.java b/src/main/java/com/eactive/ext/kjb/ums/gson/UmsRequestPayload.java new file mode 100644 index 0000000..9dade0e --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/ums/gson/UmsRequestPayload.java @@ -0,0 +1,103 @@ +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") + @Builder.Default + 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") + @Builder.Default + private String sendBranchCode = "0990"; + + // 발송직원번호 / 7 / ECEB057(텔러번호?) + // SND_EMPNO string 발송직원번호 7 O ? 넣긴해야함 + @NotEmpty + @SerializedName("SND_EMPNO") + @Builder.Default + private String sendEmployeeNo = "ECEB057"; + + // SRVC_ID string 서비스ID 11 요청단 측 서비스ID + @NotEmpty + @SerializedName("SRVC_ID") + @Builder.Default + private String serviceId = "eAPIM"; + + // MSG_SND_CHNL_CD string 메시지발송채널코드 2 + @SerializedName("MSG_SND_CHNL_CD") + @NotNull + @Builder.Default + private ChannelCode msgSndChnlCd = ChannelCode.KM; + + // VALDTN_RSLT_CD string 검증결과코드 2 00으로 고정 + @SerializedName("VALDTN_RSLT_CD") + @NotEmpty + @Builder.Default + 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 + } +} + diff --git a/src/main/java/com/eactive/ext/kjb/ums/gson/VerifyPhoneUmsTemplate.java b/src/main/java/com/eactive/ext/kjb/ums/gson/VerifyPhoneUmsTemplate.java new file mode 100644 index 0000000..f6ecca8 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/ums/gson/VerifyPhoneUmsTemplate.java @@ -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; +} diff --git a/src/main/java/com/eactive/ext/kjb/utils/JsonUtil.java b/src/main/java/com/eactive/ext/kjb/utils/JsonUtil.java new file mode 100644 index 0000000..bf2033e --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/utils/JsonUtil.java @@ -0,0 +1,10 @@ +package com.eactive.ext.kjb.utils; + +import com.google.gson.GsonBuilder; + +public class JsonUtil { + public static String toPrettyString(Object obj) { + if (obj == null) { return "null"; } + return new GsonBuilder().setPrettyPrinting().create().toJson(obj); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/utils/KjbPropertyUtil.java b/src/main/java/com/eactive/ext/kjb/utils/KjbPropertyUtil.java new file mode 100644 index 0000000..39f836a --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/utils/KjbPropertyUtil.java @@ -0,0 +1,35 @@ +package com.eactive.ext.kjb.utils; + +import com.eactive.ext.kjb.common.KjbProperty; +import com.eactive.ext.kjb.common.KjbPropertyValue; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Field; + +public class KjbPropertyUtil { + public static void main(String[] args) { + Class clazz = KjbProperty.class; + + StringBuilder sb = new StringBuilder(); + + for (Field field : clazz.getDeclaredFields()) { + KjbPropertyValue anno = field.getAnnotation(KjbPropertyValue.class); + + if (anno != null) { + sb.append("insert into EMSADM.TSEAIRM24 (PRPTYGROUPNAME, PRPTYNAME, PRPTY2VAL) values ('Monitoring', "); + + String key = anno.key(); + String defValue = anno.defaultValue(); + if (StringUtils.isEmpty(defValue)) defValue = ""; + + sb.append("'").append(key).append("', '").append(addQuote(defValue)).append("');\n"); + } + } + + System.out.println(sb); + } + + private static String addQuote(String str) { + return str.replace("'", "''"); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/utils/OsUtil.java b/src/main/java/com/eactive/ext/kjb/utils/OsUtil.java new file mode 100644 index 0000000..de9679a --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/utils/OsUtil.java @@ -0,0 +1,24 @@ +package com.eactive.ext.kjb.utils; + +import java.net.InetAddress; + +public class OsUtil { + public static boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("win"); + } + + public static boolean isLinux() { + String os = System.getProperty("os.name").toLowerCase(); + return os.contains("nux") || os.contains("nix") || os.contains("aix") ; + } + + public static String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (Exception e) { + String name = System.getProperty("COMPUTERNAME"); + if (name == null) name = System.getenv("HOSTNAME"); + return name != null ? name : "unknown"; + } + } +} diff --git a/src/test/java/com/eactive/ext/kjb/ums/test/KjbEmailSendModuleTests.java b/src/test/java/com/eactive/ext/kjb/ums/test/KjbEmailSendModuleTests.java index 10e4035..aa024f6 100644 --- a/src/test/java/com/eactive/ext/kjb/ums/test/KjbEmailSendModuleTests.java +++ b/src/test/java/com/eactive/ext/kjb/ums/test/KjbEmailSendModuleTests.java @@ -1,16 +1,7 @@ package com.eactive.ext.kjb.ums.test; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -import org.junit.jupiter.api.Test; - -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.EmailSendModule; - import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; @Slf4j @@ -18,31 +9,32 @@ public class KjbEmailSendModuleTests { @Test public void test() { - KjbProperty prop = new KjbProperty(); - prop.setEaiBatchUrl(null); - prop.setEaiBatchSendDir("D:\\kjb-logs\\sendmail\\snd"); - prop.setEaiBatchBackupDir("D:\\kjb-logs\\sendmail\\bak"); - prop.setEaiBatchInterfaceId("UAGFF00001UIE"); - - EmailSendModule service = EmailSendModule.getInstance(prop, null); - MessageRequest messageRequest = new MessageRequest(); - messageRequest.setId(UUID.randomUUID().toString()); - messageRequest.setMessageCode(MessageCode.USER_VERIFICATION_EMAIL); - messageRequest.setEmail("dearmai@dearmai.net"); - messageRequest.setUsername("김광은"); - messageRequest.setSubject("제목 - test"); - messageRequest.setMessage("내용 - test"); - - - CompletableFuture future = service.sendEmail(messageRequest, (result, ex) -> { - if (ex != null) { - log.error("failed", ex); - } else { - log.info("result: {}", result); - } - }); - - future.join(); +// KjbProperty prop = new KjbProperty(); +// prop.setEaiBatchUrl(null); +// prop.setEaiBatchSendDir("D:\\kjb-logs\\sendmail\\snd"); +// prop.setEaiBatchBackupDir("D:\\kjb-logs\\sendmail\\bak"); +// prop.setEaiBatchInterfaceId("UAGFF00001UIE"); +// +// EmailSendModule service = EmailSendModule.getInstance(prop, null); +// MessageRequest messageRequest = new MessageRequest(); +// messageRequest.setId(UUID.randomUUID().toString()); +// messageRequest.setEmail("dearmai@dearmai.net"); +// messageRequest.setUsername("김광은"); +// messageRequest.setSubject("제목 - test"); +// +// messageRequest.setMessageCode(MessageCode.USER_ACCOUNT_LOCKED); +//// messageRequest.setMessage("내용 - test"); +// +// +// CompletableFuture future = service.sendEmail(messageRequest, (result, ex) -> { +// if (ex != null) { +// log.error("failed", ex); +// } else { +// log.info("result: {}", result); +// } +// }); +// +// future.join(); log.info("done"); } diff --git a/src/test/java/com/eactive/ext/kjb/ums/test/UmsTest.java b/src/test/java/com/eactive/ext/kjb/ums/test/UmsTest.java new file mode 100644 index 0000000..5015ef9 --- /dev/null +++ b/src/test/java/com/eactive/ext/kjb/ums/test/UmsTest.java @@ -0,0 +1,106 @@ +package com.eactive.ext.kjb.ums.test; + +import com.eactive.apim.portal.template.entity.MessageCode; +import com.eactive.apim.portal.template.entity.MessageRequest; +import com.eactive.ext.kjb.common.KjbEaiBatchException; +import com.eactive.ext.kjb.common.KjbProperty; +import com.eactive.ext.kjb.common.KjbUmsException; +import com.eactive.ext.kjb.ums.KjbEmailSendModule; +import com.eactive.ext.kjb.ums.KjbUmsService; +import com.eactive.ext.kjb.utils.OsUtil; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + + +@Slf4j +public class UmsTest { + private static KjbProperty prop = null; + + @BeforeAll + public static void init() { + prop = new KjbProperty(); + prop.setEaiBatchUrl("http://172.31.32.111:10230/BATAppWeb/EaiBatCall"); + prop.setUmsEaiBatchInterfaceId("UAGFF00001UIE"); + + prop.setUmsHostUrl("http://172.31.35.144:9021"); + prop.setUmsApiKey("7cca6d58-e4f7-474d-9edd-525806bc99ff"); + + if (OsUtil.isWindows()) { + prop.setUmsEaiBatchSendDir("c:\\kjb-logs\\sendmail\\snd"); + prop.setUmsEaiBatchBackupDir("c:\\kjb-logs\\sendmail\\bak"); + prop.setUmsEaiBatchErrorDir("c:\\kjb-logs\\sendmail\\error"); + } + + if (OsUtil.isLinux()) { + prop.setUmsEaiBatchSendDir("/Data/eapim/portal/sendmail/snd"); + prop.setUmsEaiBatchBackupDir("/Data/eapim/portal/sendmail/bak"); + prop.setUmsEaiBatchErrorDir("/Data/eapim/portal/sendmail/error"); + } + } + + @Test + public void testSms() { + Assumptions.assumeTrue( + "true".equalsIgnoreCase(System.getenv("KJB_EAPIM_TEST_ENABLED")), + "OS 환경변수 내 KJB_EAPIM_TEST_ENABLED=TRUE 설정되어 있어야 동작함"); + + MessageRequest messageRequest = new MessageRequest(); + messageRequest.setId(UUID.randomUUID().toString()); + messageRequest.setMessageCode(MessageCode.USER_VERIFICATION_MOBILEPHONE); + messageRequest.setPhone("01099750188"); + messageRequest.setEmail("dearmai@dearmai.net"); + messageRequest.setUsername("김광은"); + messageRequest.setSubject("제목 - test"); + messageRequest.setMessage("{\"ATHN_NO\"; \"369369\"}"); + + KjbUmsService umsService = new KjbUmsService(prop); + try { + boolean result = umsService.send(messageRequest); + + } catch (KjbUmsException e) { + log.error(e.getMessage(), e); + Assertions.fail(e); + return; + } + + log.info("done"); + } + + @Test + public void testEmail() { + Assumptions.assumeTrue( + "true".equalsIgnoreCase(System.getenv("KJB_EAPIM_TEST_ENABLED")), + "OS 환경변수 내 KJB_EAPIM_TEST_ENABLED=TRUE 설정되어 있어야 동작함"); + + + System.getProperties().keySet().forEach(key -> { + log.info("{}: {}", key, System.getProperty((String) key)); + }); + + KjbEmailSendModule service = KjbEmailSendModule.getInstance(prop, null); + MessageRequest messageRequest = new MessageRequest(); + messageRequest.setId(UUID.randomUUID().toString()); + messageRequest.setMessageCode(MessageCode.USER_VERIFICATION_EMAIL); + messageRequest.setEmail("dearmai@dearmai.net"); + messageRequest.setUmsUid(UUID.randomUUID().toString()); + messageRequest.setUsername("김광은"); + messageRequest.setSubject("제목 - test"); + messageRequest.setMessage("{\"ATHN_NO\": \"543216\"}"); + messageRequest.setServiceId("FM_APIM001"); + + log.info("MessageRequest - {}", messageRequest); + + try { + service.sendEmail(messageRequest); + } catch (KjbEaiBatchException e) { + throw new RuntimeException(e); + } + + log.info("done"); + } +} diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index c7a3a0d..b4222d8 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -1,9 +1,10 @@ - + - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + %d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger - %msg %n @@ -13,6 +14,7 @@ +