Compare commits

...

43 Commits

Author SHA1 Message Date
kjh 1396c8113e fix: 쿼리 오타 수정 1 year ago
kjh 7ec0901a9d fix: 춘천 /ens contextPath 사용 1 year ago
kjh 9b468c27a2 fix: 카카오톡 상태 조회 null 조회 쿼리 변경 1 year ago
kjh c74318e62b conf: kt bulk 건수 100건으로 변경 1 year ago
Jonguk. Lim b4a45d36e6 fix: 카카오톡 상태 조회 대상 조회 조건 변경
-> 상태가 'READ' 인 대상 제외
1 year ago
Jonguk. Lim 067ca1ccc6 fix: KT sendbulk bulk단위 전송 반영 1 year ago
Jonguk. Lim b961e0c6d8 fix: accept시 주민번호 자리수 체크 추가
-> 카카오, KT 모두 적용
1 year ago
kjh 26faccab0b fix: 카카오톡 전자고지 발송 ApiHttpClient 방식으로 변경해도 오류가 발생하여 기존ApiWebClient 방식으로 변경 1 year ago
kjh dbcfe13007 conf: db 유효성 검사 최대 대기 시간 변경 1 year ago
Jonguk. Lim 54f33af776 fix: DTO fix 1 year ago
Jonguk. Lim a2b43a9b09 fix: 카카오페이/톡 API, batch dummy 테스트 추가 1 year ago
Jonguk. Lim 0a11d21123 fix: 카카오톡 send시 묶음 단위 처리로 변경 1 year ago
Jonguk. Lim a093dfa0c8 fix: swagger, DTO 정리
WebClientConfig proxy 설정 fix
1 year ago
Jonguk. Lim a4b41fb45d fix: 카카오톡 send시 묶음 단위 처리로 변경 1 year ago
Jonguk. Lim e70d17a2e1 feat: Json method 추가 1 year ago
Jonguk. Lim 19229fd911 feat: API 테스트 클래스 추가 1 year ago
Jonguk. Lim cc89604fa4 fix: reqs JsonProperty 설정 1 year ago
Jonguk. Lim 0548eb1f91 doc: swagger 설정 fix 1 year ago
kjh 7ec1643b1b fix: 카카오페이 전자고지 열람만 ApiHttpClient 방식 오류가 발생하여 ApiWebClient 방식 사용함 1 year ago
Jonguk. Lim 75fdaee812 fix: ToString 추가 1 year ago
Jonguk. Lim abbec3e722 fix: API 호출 모듈 적용 comment 추가 1 year ago
Jonguk. Lim 0a021307ec fix: 모바일 페이지 조회 추적을 위한 로그 추가 - error로 적용 1 year ago
Jonguk. Lim fe25594cca fix: 모바일 페이지 적용시 서버 에러 처리를 위한 방어 코드 적용 1 year ago
Jonguk. Lim b94bae960f fix: 모바일 페이지 적용시 서버 에러 처리를 위한 방어 코드 적용 1 year ago
Jonguk. Lim 757778caec fix: Exception 메세지 처리 세분화 1 year ago
kjh 621fa6a54d conf: rest 타임아웃 시간 변경 1 year ago
Jonguk. Lim 382552fa14 Merge branch 'main' into main-restful-apiutil 1 year ago
kjh 348f673fdb docs: tb_cmm_api_log 상태 조회 결과 response 컬럼 제외 1 year ago
Jonguk. Lim 71a55ff0a5 config: hikari 설정 적용 1 year ago
Jonguk. Lim d55f500195 config: server log 설정 조정 1 year ago
Jonguk. Lim 16a3326926 feat: API trace log 제외 추가 1 year ago
Jonguk. Lim cbe7a38b26 feat: accept 자료 생성시 - 카카오톡 CI 획득시 주민 번호 유효성 체크 추가 1 year ago
Jonguk. Lim 25e67e384c feat: Restful API util 반영
batch sendBulk, api statusBulk 적용
1 year ago
Jonguk. Lim 6dec45075f feat: Restful API RetryTemplate 미사용 1 year ago
Jonguk. Lim 5ba2593c53 feat: Restful API util 반영
batch sendBulk, api statusBulk 적용
1 year ago
Jonguk. Lim 3664cd109a fix: HttpClient 모듈 사용 1 year ago
Jonguk. Lim 7b48e9cf45 fix: API 호출 결과 List 수신시 아래 에러 fix
-> TypeReference<T>(){} 적용 : Generic 사용 List 등에서 발생
     java.util.LinkedHashMap is in module java.base of loader 'bootstrap'
1 year ago
Jonguk. Lim 41fe70bcc9 fix: WebClient API -> RestTemplate API 변경 적용
-> 카카오 페이, 톡 sendbulk, bulk status
     RestTemplateConfig 연결 관련 설정 properties로 분리
1 year ago
Jonguk. Lim d716ddf0f8 Merge branch 'main' into main-restful-apiutil 1 year ago
Jonguk. Lim 7c378d512f feat: log-back error log 추가 1 year ago
Jonguk. Lim 2d4fc744b8 feat: Restful API util 추가 반영
batch sendbulk, api requestBulk 카카오톡에 적용
1 year ago
Jonguk. Lim 0ff8d68199 feat: Restful API util 추가 반영
batch sendbulk, api requestBulk 카카오톡에 적용
1 year ago
Jonguk. Lim f79ee50bd1 feat: Restful API util 추가 반영
batch sendbulk, api 카카오톡에 적용
1 year ago

@ -14,7 +14,9 @@ import kr.xit.biz.mbl.model.*;
import kr.xit.biz.mbl.service.*;
import kr.xit.core.consts.*;
import kr.xit.core.exception.*;
import kr.xit.core.support.utils.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
@ -31,9 +33,10 @@ import lombok.*;
*
* </pre>
*/
@Tag(name = "MobilePageAsIsController", description = "전자문서 중개자 모바일 페이지 API Controller")
@Tag(name = "MobilePageAsIsController", description = "전자문서 중개자 모바일 페이지 API Controller - 천안 can")
@RequiredArgsConstructor
@Controller
@Slf4j
public class MobilePageAsIsController {
private final IMobilePageService service;
@ -95,12 +98,19 @@ public class MobilePageAsIsController {
throw BizRuntimeException.create("정상적인 요청이 아닙니다. 재인증 후 시도하시기 바랍니다.");
}
String mblPage = "";
try {
if (StringUtils.isNotEmpty(reqDTO.getDocument_binder_uuid())) {
model.addAttribute("details", service.findKkopayReadyAndMblPage(reqDTO));
mblPage = service.findKkopayReadyAndMblPage(reqDTO);
} else {
model.addAttribute("details", service.findKkotalkReadyAndMblPage(reqDTO));
mblPage = service.findKkotalkReadyAndMblPage(reqDTO);
}
if(!JsonUtils.isJson(mblPage)){
log.error("모바일 페이지 에러 :: {}", mblPage);
throw BizRuntimeException.create("서버 에러 입니다(모바일 페이지 조회)");
}
model.addAttribute("details", mblPage);
model.addAttribute("payButtonLinks", "{}");
} catch (BizRuntimeException e) {
@ -113,7 +123,7 @@ public class MobilePageAsIsController {
public void findKtMblPage(final String token, final String srcKey, Model model) {
try {
final List<MobilePageDTO.MobilePageManage> sigunguList = service.findKtGbsSignguCode(srcKey);
if(sigunguList.size() == 0 || sigunguList.size() > 1) {
if(sigunguList.size() != 1) {
throw BizRuntimeException.create(
String.format("정상적인 요청이 아닙니다. src_key를 확인해 주세요.(요청 src_key: %s)", srcKey));
}
@ -139,17 +149,22 @@ public class MobilePageAsIsController {
.build()
);
*/
model.addAttribute("details",
service.findKtGbsMblPage(
KtGbsDTO.TokenConfirmRequest.builder()
.signguCode(sigunguList.get(0).getSignguCode())
.ffnlgCode(sigunguList.get(0).getFfnlgCode())
.token(token)
.srcKey(srcKey)
.build()
)
final String mblPage = service.findKtGbsMblPage(
KtGbsDTO.TokenConfirmRequest.builder()
.signguCode(sigunguList.get(0).getSignguCode())
.ffnlgCode(sigunguList.get(0).getFfnlgCode())
.token(token)
.srcKey(srcKey)
.build()
);
if(!JsonUtils.isJson(mblPage)){
log.error("모바일 페이지 에러 :: {}", mblPage);
throw BizRuntimeException.create("서버 에러 입니다(모바일 페이지 조회)");
}
model.addAttribute("details", mblPage);
model.addAttribute("details",mblPage);
} catch (BizRuntimeException e) {
throw e;

@ -1,45 +1,29 @@
package kr.xit.core.aop;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.json.simple.JSONObject;
import org.slf4j.MDC;
import java.util.*;
import java.util.concurrent.*;
import javax.servlet.http.*;
import org.apache.commons.lang3.*;
import org.aspectj.lang.*;
import org.aspectj.lang.annotation.*;
import org.json.simple.*;
import org.slf4j.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import kr.xit.core.biz.model.LoggingDTO;
import kr.xit.core.biz.service.IApiLoggingService;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.exception.ErrorParse;
import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.support.slack.SlackWebhookPush;
import kr.xit.core.support.utils.Checks;
import kr.xit.core.support.utils.JsonUtils;
import kr.xit.core.support.utils.LogUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.*;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import org.springframework.web.context.request.*;
import kr.xit.core.biz.model.*;
import kr.xit.core.biz.service.*;
import kr.xit.core.exception.*;
import kr.xit.core.model.*;
import kr.xit.core.support.slack.*;
import kr.xit.core.support.utils.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
@ -87,8 +71,11 @@ public class TraceLoggerAspect {
private boolean isMdcLogEnabled;
@Value("#{'${app.log.mdc.exclude-patterns}'.split(',')}")
private String[] excludes;
private String[] logExcludePatterns;
@Value("#{'${app.log.api.exclude-patterns}'.split(',')}")
private String[] traceAppExcludes;
private final IApiLoggingService apiLoggingService;
private final SlackWebhookPush slackWebhookPush;
private static final String REQUEST_TRACE_ID = "request_trace_id";
@ -135,9 +122,13 @@ public class TraceLoggerAspect {
while(true) {
if (future.isDone()) break;
}
traceApiLoggingResult(future.get());
if (!Arrays.stream(traceAppExcludes).anyMatch(request.getRequestURI()::matches)){
traceApiLoggingResult(future.get());
}
}else{
traceApiLoggingResult(result);
if (!Arrays.stream(traceAppExcludes).anyMatch(request.getRequestURI()::matches)){
traceApiLoggingResult(result);
}
}
return result;
}
@ -155,7 +146,7 @@ public class TraceLoggerAspect {
@AfterThrowing(value = "errorPointCut()", throwing="error")
public void afterThrowingProceed(final JoinPoint jp, final Throwable error) {
traceApiLoggingError(jp, error);
traceApiLoggingError(error);
}
/**
@ -167,12 +158,12 @@ public class TraceLoggerAspect {
String uri = "";
if(request != null) {
uri = request.getRequestURI();
if(Arrays.stream(excludes).anyMatch(uri::matches)) return;
if(Arrays.stream(logExcludePatterns).anyMatch(uri::matches)) return;
MDC.put(REQUEST_TRACE_ID,
StringUtils.defaultString(MDC.get("request_trace_batch_id"), UUID.randomUUID().toString().replaceAll("/-/g", "")));
MDC.put("method", request.getMethod());
MDC.put("uri", uri);
MDC.put("uri", request.getRequestURI());
MDC.put("ip", request.getRemoteAddr());
MDC.put("sessionId", request.getSession().getId());
@ -240,15 +231,10 @@ log.info("@@@@@@@@@@@@@@@@@로깅 start : [\n{}\n]",MDC.getCopyOfContextMap());
.response(getResult(result))
.message(HttpStatus.OK.name())
.build();
//}
apiLoggingService.modifyApiLogging(reqDTO);
//loggingService.saveLogging(reqDTO);
apiLoggingService.modifyApiLogging(reqDTO);
log.info("@@@@@@@@@@@@@@로깅 end[\n{}\n]", MDC.getCopyOfContextMap());
//if(RequestContextHolder.getRequestAttributes() != null)
MDC.clear();
//}
}
private String getResult(final Object o){
@ -269,7 +255,7 @@ log.info("@@@@@@@@@@@@@@@@@로깅 start : [\n{}\n]",MDC.getCopyOfContextMap());
}
}
protected void traceApiLoggingError(final JoinPoint jp, final Throwable e) {
protected void traceApiLoggingError(final Throwable e) {
log.info("MDC request_trace_id :: {}", MDC.get(REQUEST_TRACE_ID));
if(Checks.isEmpty(MDC.get(REQUEST_TRACE_ID))) return;

@ -37,7 +37,7 @@ public class SpringDocsApiConfig {
@Bean
public GroupedOpenApi kakaopayEltrcDoc() {
return GroupedOpenApi.builder()
.group("2. 문서중계자 API")
.group("2. 문서중계자 API 테스트 WEB")
.pathsToMatch(
"/api/ens/**"
)
@ -47,10 +47,21 @@ public class SpringDocsApiConfig {
@Bean
public GroupedOpenApi inboudnApiDoc() {
return GroupedOpenApi.builder()
.group("3. 문서중계자 API(Inbound)")
.group("3. 문서중계자 Inbound 테스트 WEB")
.pathsToMatch(
"/api/msg/**",
"/api/ag/**"
"/api/ag/**",
"/goji/**"
)
.build();
}
@Bean
public GroupedOpenApi apiDocTest() {
return GroupedOpenApi.builder()
.group("4. 문서중계자 API dummy 테스트")
.pathsToMatch(
"/api/**/test/**"
)
.build();
}
@ -58,7 +69,7 @@ public class SpringDocsApiConfig {
@Bean
public GroupedOpenApi bizDoc() {
return GroupedOpenApi.builder()
.group("6. 전자고지 업무 API")
.group("6. 서비스(전자고지 업무) 테스트 WEB")
.pathsToMatch(
"/api/biz/**"
)

@ -0,0 +1,258 @@
package kr.xit.ens.kakao.pay.service;
import kr.xit.biz.common.*;
import kr.xit.biz.ens.model.kakao.pay.*;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.spring.annotation.TraceLogging;
import kr.xit.core.spring.util.*;
import kr.xit.core.support.utils.Checks;
import kr.xit.core.support.utils.JsonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.*;
import java.util.stream.Collectors;
/**
* <pre>
* description : API dummy
* packageName : kr.xit.ens.kakao.pay.service
* fileName : KkopayApiDummyTestService
* author : julim
* date : 2023-04-28
* ======================================================================
*
* ----------------------------------------------------------------------
* 2023-04-28 julim
*
* </pre>
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class KkopayApiDummyTestService extends EgovAbstractServiceImpl {
// FIXME:: API 호출 모듈 선택 - ApiWebClientUtil | ApiRestTemplateUtil | ApiHttpClientUtil
private final ApiHttpClientUtil webClient;
public KkopayDocAttrDTO.DocumentBinderUuid requestSend(final KkopayDocDTO.SendRequest reqDTO) {
List<String> errors = new ArrayList<>();
final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
// 실제 데이타만 검증
Set<ConstraintViolation<KkopayDocDTO.RequestSend>> list = validator.validate(reqDTO.getDocument());
if (list.size() > 0) {
errors = list.stream()
.map(row -> String.format("%s=%s", row.getPropertyPath(), row.getMessageTemplate()))
.collect(Collectors.toList());
}
KkopayDocDTO.RequestSend reqSendDTO = reqDTO.getDocument();
if(reqSendDTO.getRead_expired_at() != null && reqSendDTO.getRead_expired_sec() != null){
errors.add("처리마감시간(절대시간 또는 상대시간)은 하나만 지정해야 합니다.");
}
if(reqSendDTO.getRead_expired_at() == null && reqSendDTO.getRead_expired_sec() == null){
errors.add("처리마감시간(절대시간 또는 상대시간)을 지정해야 합니다.");
}
KkopayDocAttrDTO.Receiver receiver = reqSendDTO.getReceiver();
if(Checks.isEmpty(receiver.getCi())){
if(Checks.isEmpty(receiver.getName())) errors.add("받는이 이름은 필수입니다.");
if(Checks.isEmpty(receiver.getPhone_number())) errors.add("받는이 전화번호는 필수입니다.");
if(Checks.isEmpty(receiver.getBirthday())) errors.add("받는이 생년월일은 필수입니다.");
}
if(errors.size() > 0){
throw BizRuntimeException.create(errors.toString());
}
return KkopayDocAttrDTO.DocumentBinderUuid
.builder()
.document_binder_uuid("BIN-eue7e73uw737377eeeee")
.build();
}
/**
* <pre>
* (Redirect URL /)
* </pre>
* @param reqDTO KkoPayEltrDocDTO.RequestSendReq
* @return ResponseEntity<KkopayDocDTO.ValidTokenRes></KkopayDocDTO.ValidTokenRes>
*/
public KkopayDocDTO.ValidTokenResponse validToken(final KkopayDocDTO.ValidTokenRequest reqDTO) {
final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
// 실제 데이타만 검증
Set<ConstraintViolation<KkopayDocDTO.ValidTokenRequest>> list = validator.validate(reqDTO);
if (list.size() > 0) {
List<String> errors = list.stream()
.map(row -> String.format("%s=%s", row.getPropertyPath(), row.getMessageTemplate()))
.collect(Collectors.toList());
throw BizRuntimeException.create(errors.toString());
}
return KkopayDocDTO.ValidTokenResponse
.builder()
.token_status("USED")
.token_expires_at(1624344762L)
.payload("payload 파라미터 입니다.")
.build();
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkoPayEltrDocDTO.RequestSendReq
* @return
*/
public KkopayDocBulkDTO.BulkSendResponses requestSendBulk(final KkopayDocBulkDTO.BulkSendRequests reqDTO) {
List<String> errors = new ArrayList<>();
final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
//TODO :: Collection validation
List<KkopayDocBulkDTO.BulkSendReq> dtos = reqDTO.getDocuments();
for(int idx = 0; idx < dtos.size(); idx++) {
Set<ConstraintViolation<KkopayDocBulkDTO.BulkSendReq>> list = validator.validate(dtos.get(idx));
if (list.size() > 0) {
int finalIdx = idx;
errors.addAll(list.stream()
.map(row -> String.format("%s[%d]=%s", row.getPropertyPath(), finalIdx +1, row.getMessageTemplate()))
.collect(Collectors.toList())
);
}
}
for(int idx = 0; idx < dtos.size(); idx++) {
if(dtos.get(idx).getRead_expired_at() != null && dtos.get(idx).getRead_expired_sec() != null){
errors.add("처리마감시간(절대시간 또는 상대시간)은 하나만 지정해야 합니다.");
}
if(dtos.get(idx).getRead_expired_at() == null && dtos.get(idx).getRead_expired_sec() == null){
errors.add("처리마감시간(절대시간 또는 상대시간)을 지정해야 합니다.");
}
KkopayDocAttrDTO.Receiver receiver = dtos.get(idx).getReceiver();
if (Checks.isEmpty(receiver.getCi())) {
if (Checks.isEmpty(receiver.getName())) errors.add(String.format("받는이 이름은 필수입니다([%d] 번째 오류)", idx+1));
if (Checks.isEmpty(receiver.getPhone_number())) errors.add(String.format("받는이 전화번호는 필수입니다([%d] 번째 오류)", idx+1));
if (Checks.isEmpty(receiver.getBirthday())) errors.add(String.format("받는이 생년월일은 필수입니다([%d] 번째 오류)", idx+1));
} else {
StringBuilder sb = new StringBuilder()
.append(StringUtils.defaultString(receiver.getName(), StringUtils.EMPTY))
.append(StringUtils.defaultString(receiver.getPhone_number(), StringUtils.EMPTY))
.append(StringUtils.defaultString(receiver.getBirthday(), StringUtils.EMPTY));
if(Checks.isNotEmpty(sb.toString())){
errors.add(String.format("CI가 지정 되었습니다(받는이 정보 불필요:[%d] 번째 오류) .", idx+1));
}
}
}
if(errors.size() > 0){
throw BizRuntimeException.create(errors.toString());
}
List<KkopayDocBulkDTO.BulkSendRes> resDTO = new ArrayList<>();
resDTO.add(KkopayDocBulkDTO.BulkSendRes.builder()
.external_document_uuid(reqDTO.getDocuments().get(0).getProperty().getExternal_document_uuid())
.document_binder_uuid("BIN-127de7hdcyeuudfeuuewe8e8e")
.build()
);
resDTO.add(KkopayDocBulkDTO.BulkSendRes.builder()
.external_document_uuid(reqDTO.getDocuments().get(0).getProperty().getExternal_document_uuid())
.error_code(ApiConstants.Error.NOT_FOUND.getCode())
.error_message("요청 정보를 찾을 수 없습니다. documentBinder를 찾을수 없습니다.")
.build()
);
KkopayDocBulkDTO.BulkSendResponses res = KkopayDocBulkDTO.BulkSendResponses.builder()
.documents(resDTO)
.build();
KkopayDocBulkDTO.BulkSendResponses object = JsonUtils.toObject(JsonUtils.toJson(res),
KkopayDocBulkDTO.BulkSendResponses.class);
return res;
}
/**
* <pre>
* (bulk) API
* -. .
* : , flow
* : polling , 5 .
* -.doc_box_status
* : SENT() > RECEIVED() > READ()/EXPIRED( )
* </pre>
* @param reqDTO
* @return KkopayDocBulkDTO.BulkStatusResponse
*/
public KkopayDocBulkDTO.BulkStatusResponses findBulkStatus(final KkopayDocBulkDTO.BulkStatusRequests reqDTO) {
List<String> errors = new ArrayList<>();
List<String> dtos = reqDTO.getDocument_binder_uuids();
for(int idx = 0; idx < dtos.size(); idx++) {
String binderUuid = dtos.get(idx);
if (Checks.isEmpty(binderUuid) || binderUuid.length() > 40) {
errors.add(String.format("문서 식별 번호는 40자를 넘을 수 없습니다[%d번째]", idx+1));
}
}
if(errors.size() > 0) {
throw BizRuntimeException.create(errors.toString());
}
List<KkopayDocBulkDTO.BulkStatus> resDTO = new ArrayList<>();
resDTO.add(KkopayDocBulkDTO.BulkStatus.builder()
.document_binder_uuid(reqDTO.getDocument_binder_uuids().get(0))
.error_code(ApiConstants.Error.NOT_FOUND.getCode())
.error_message("요청 정보를 찾을 수 없습니다. documentBinder를 찾을수 없습니다.")
.build()
);
resDTO.add(KkopayDocBulkDTO.BulkStatus.builder()
.document_binder_uuid(reqDTO.getDocument_binder_uuids().get(1))
.status_data(KkopayDocAttrDTO.DocStatus.builder()
.doc_box_status(ApiConstants.KkopayDocStatus.RECEIVED)
.doc_box_sent_at(1443456743L)
.doc_box_received_at(1443456743L)
.user_notified_at(1443456743L)
.build())
.build()
);
resDTO.add(KkopayDocBulkDTO.BulkStatus.builder()
.document_binder_uuid(reqDTO.getDocument_binder_uuids().get(2))
.status_data(KkopayDocAttrDTO.DocStatus.builder()
.doc_box_status(ApiConstants.KkopayDocStatus.READ)
.doc_box_sent_at(1443456743L)
.doc_box_received_at(1443456743L)
.doc_box_read_at(1443456743L)
.authenticated_at(1443456743L)
.token_used_at(1443456743L)
.user_notified_at(1443456743L)
.build())
.build()
);
KkopayDocBulkDTO.BulkStatusResponses res = KkopayDocBulkDTO.BulkStatusResponses.builder()
.documents(resDTO)
.build();
KkopayDocBulkDTO.BulkStatusResponses object = JsonUtils.toObject(JsonUtils.toJson(res),
KkopayDocBulkDTO.BulkStatusResponses.class);
return res;
}
}

@ -65,7 +65,12 @@ public class KkopayEltrcDocService extends AbstractService implements
@Value("#{'${app.contract.kakao.api.pay.findStatus}'.split(';')}")
private String[] API_STATUS;
private final ApiWebClientUtil webClient;
// FIXME:: API 호출 모듈 선택 - ApiWebClientUtil | ApiRestTemplateUtil | ApiHttpClientUtil
// FIXME:: 천안 ApiWebClient 방식 사용 시 원인 모를 500에러 발생하여 ApiHttpClient 방식으로 변경 처리했지만
// 카카오페이 전자고지 열람만 오류가 발생하여 카카오페이 전자고지만 ApiWebClient 방식 사용함
//private final ApiWebClientUtil webClient;
private final ApiWebClientUtil webClient;
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private static final CharSequence DOCUMENT_BINDER_UUID = "{document_binder_uuid}";
@ -255,9 +260,7 @@ public class KkopayEltrcDocService extends AbstractService implements
//FIXME:: 테스트후 로그 제거
log.debug(">>>>>>>>findKkopayReadyAndMblPage reqDTO={}", reqDTO);
CmmEnsRlaybsnmDTO rlaybsnmInfo = getRlaybsnmInfo(reqDTO);
// 춘천
if(ApiConstants.IS_CCN) {
final String url = HOST + API_VALID_TOKEN[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid())
@ -267,6 +270,8 @@ public class KkopayEltrcDocService extends AbstractService implements
final KkopayDocDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkopay(url,
HttpMethod.valueOf(API_VALID_TOKEN[1]), null,
KkopayDocDTO.ValidTokenResponse.class, rlaybsnmInfo);
// FIXME:: 서버 결과 추적을 위해 임시로 error level 적용 - 서버안정화 후 debug로 변경 해야함
log.error("validTokenRes={}", validTokenRes);
if (!"USED".equals(validTokenRes.getToken_status())) {
return ApiResponseDTO.error(validTokenRes.getError_code(), validTokenRes.getError_message());
@ -281,6 +286,9 @@ public class KkopayEltrcDocService extends AbstractService implements
// error : body에 error_code, error_message return
final KkopayErrorDTO errorDTO = webClient.exchangeKkopay(url2, HttpMethod.valueOf(API_MODIFY_STATUS[1]),
body, KkopayErrorDTO.class, rlaybsnmInfo);
// FIXME:: 서버 결과 추적을 위해 임시로 error level 적용 - 서버안정화 후 debug로 변경 해야함
log.error("errorDTO={}", errorDTO);
if (errorDTO != null) {
return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage());
}
@ -291,23 +299,20 @@ public class KkopayEltrcDocService extends AbstractService implements
} else {
final String url = HOST + API_VALID_TOKEN[0].replace("{documentBinderUuid}", reqDTO.getDocument_binder_uuid())
.replace("{tokens}", reqDTO.getToken());
//FIXME:: 테스트후 로그 제거
log.debug(">>>>>>>>천안");
Map<String, String> headerMap = new HashMap<>();
headerMap.put(HttpHeaders.AUTHORIZATION,
String.format("%s %s", Constants.JwtToken.GRANT_TYPE.getCode(), rlaybsnmInfo.getKakaoAccessToken()));
headerMap.put("X-Xit-DBUuid", reqDTO.getDocument_binder_uuid());
headerMap.put("X-Xit-Ott", reqDTO.getToken());
//FIXME:: 테스트후 로그 제거
log.debug(">>>>>>>>천안:: headerMap={}", headerMap);
log.debug(">>>>>>>>천안:: url={}", url);
// 유효성 검증
final KkopayDocDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkopayAsIsCan(url,
HttpMethod.valueOf(API_VALID_TOKEN[1]), null,
KkopayDocDTO.ValidTokenResponse.class, headerMap);
// FIXME:: 서버 결과 추적을 위해 임시로 error level 적용 - 서버안정화 후 debug로 변경 해야함
log.error("validTokenRes={}", validTokenRes);
if (!"USED".equals(validTokenRes.getToken_status())) {
return ApiResponseDTO.error(validTokenRes.getError_code(), validTokenRes.getError_message());
}
@ -316,10 +321,6 @@ public class KkopayEltrcDocService extends AbstractService implements
final String body = "{\"document\": {\"is_detail_read\": true} }";
final String url2 =
HOST + API_MODIFY_STATUS[0].replace("{documentBinderUuid}", reqDTO.getDocument_binder_uuid());
//FIXME:: 테스트후 로그 제거
log.debug(">>>>>>>>천안:: headerMap={}", headerMap);
log.debug(">>>>>>>>천안:: url2={}", url2);
// 정상 : HttpStatus.NO_CONTENT(204) return
// error : body에 error_code, error_message return
@ -327,6 +328,9 @@ public class KkopayEltrcDocService extends AbstractService implements
final KkopayErrorDTO errorDTO = webClient.exchangeKkopayAsIsCan(url2,
HttpMethod.valueOf(API_MODIFY_STATUS[1]),
body, KkopayErrorDTO.class, headerMap);
// FIXME:: 서버 결과 추적을 위해 임시로 error level 적용 - 서버안정화 후 debug로 변경 해야함
log.error("errorDTO={}", errorDTO);
if (errorDTO != null) {
return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage());
}

@ -0,0 +1,151 @@
package kr.xit.ens.kakao.pay.web;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.xit.biz.common.*;
import kr.xit.biz.ens.model.kakao.pay.*;
import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.support.utils.Checks;
import kr.xit.ens.kakao.pay.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.SecureRandom;
/**
* <pre>
* description : API dummy controller
* packageName : kr.xit.ens.kakao.pay.controller
* fileName : KkopayApiDummyTestController
* author : julim
* date : 2023-04-28
* ======================================================================
*
* ----------------------------------------------------------------------
* 2023-04-28 julim
*
* </pre>
*/
@Tag(name = "KkopayApiDummyTestController", description = "카카오페이 전자문서 API dummy 테스트")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/api/kakao/pay/test")
public class KkopayApiDummyTestController {
private final KkopayApiDummyTestService service;
@Operation(summary = "문서발송 요청", description = "카카오페이 전자문서 서버로 문서발송 처리를 요청")
@PostMapping(value = "/documents", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> requestSend(
@RequestBody final KkopayDocDTO.SendRequest reqDTO
) {
return ApiResponseDTO.success(service.requestSend(reqDTO));
}
@Operation(summary = "토큰 유효성 검증", description = "Redirect URL 접속 허용/불허")
@PostMapping(value = "/validToken", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> validToken(
@RequestBody final KkopayDocDTO.ValidTokenRequest reqDTO
) {
return ApiResponseDTO.success(service.validToken(reqDTO));
}
/**
* <pre>
* API
* -. . (OTT ) API .
* -.
* 1) API .
* 2) API(/v1/documents/{document_binder_uuid}/status) read_at ) .
* </pre>
* @param reqDTO KkopayDocAttrDTO.DocumentBinderUuid
* @return ApiResponseDTO
*/
@Operation(summary = "문서 상태 변경", description = "문서 상태 변경")
@PostMapping(value = "/modifyStatus", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> modifyStatus(
@RequestBody final KkopayDocAttrDTO.DocumentBinderUuid reqDTO
) {
return ApiResponseDTO.empty();
}
/**
* <pre>
* API
* -. .
* : , flow
* : polling , 5 .
* -.doc_box_status
* : SENT() > RECEIVED() > READ()/EXPIRED( )
* </pre>
* @param reqDTO KkopayDocAttrDTO.DocumentBinderUuid
* @return ApiResponseDTO
*/
@Operation(summary = "문서 상태 조회", description = "문서 상태 조회")
@PostMapping(value = "/findStatus", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> findStatus(
@RequestBody final KkopayDocAttrDTO.DocumentBinderUuid reqDTO
) {
SecureRandom random = new SecureRandom(); // Compliant for security-sensitive use cases
int bound = 1000000000;
return ApiResponseDTO.success(KkopayDocDTO.DocStatusResponse
.builder()
.payload(reqDTO.getDocument_binder_uuid())
.doc_box_status(ApiConstants.KkopayDocStatus.SENT)
.doc_box_sent_at((long)(random.nextInt(bound)) + 1000000000)
.doc_box_received_at((long)(random.nextInt(bound)) + 1000000000)
//.payload("payload 파라미터 입니다.")
.build());
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkopayDocBulkDTO.BulkSendRequests
* @return ApiResponseDTO
*/
@Operation(summary = "대량 문서 발송 요청", description = "카카오페이 전자문서 서버로 대량 문서 발송 요청")
@PostMapping(value = "/documents/bulk", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> requestSendBulk(
@RequestBody final KkopayDocBulkDTO.BulkSendRequests reqDTO
) {
return ApiResponseDTO.success(service.requestSendBulk(reqDTO));
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkopayDocBulkDTO.BulkStatusRequests
* @return ApiResponseDTO
*/
@Operation(summary = "대량 문서 상태 조회 요청", description = "카카오페이 전자문서 서버로 대량 문서 상태 조회 요청")
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(mediaType = "application/json", examples = {
@ExampleObject(name = "Example"
, summary = "부과정보 전송", description = "개별시스템 -> 세외수입 시스템 호출하여 응답 Response"
, value = "{\"document_binder_uuids\":[\"BIN-ff806328863311ebb61432ac599d6151\",\"BIN-ff806328863311ebb61432ac599d6152\",\"BIN-ff806328863311ebb61432ac599d6153\"]}")
})
})
@PostMapping(value = "/documents/bulk/status", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> findBulkStatus(
@RequestBody final KkopayDocBulkDTO.BulkStatusRequests reqDTO
) {
return ApiResponseDTO.success(service.findBulkStatus(reqDTO));
}
}

@ -28,7 +28,7 @@ public interface IKkotalkEltrcDocService {
* @param reqDTO KkotalkApiDTO.SendRequest
* @return KkotalkApiDTO.SendResponse
*/
KkotalkDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO);
KkotalkApiDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO);
/**
* <pre>
@ -49,7 +49,7 @@ public interface IKkotalkEltrcDocService {
* </pre>
* @param reqDTO KkotalkDTO.EnvelopeId
*/
void modifyStatus(final KkotalkDTO.EnvelopeId reqDTO);
void modifyStatus(final KkotalkApiDTO.EnvelopeId reqDTO);
/**

@ -0,0 +1,335 @@
package kr.xit.ens.kakao.talk.service;
import java.util.*;
import java.util.stream.*;
import javax.validation.*;
import org.apache.commons.lang3.*;
import org.egovframe.rte.fdl.cmmn.*;
import org.springframework.stereotype.*;
import kr.xit.biz.common.*;
import kr.xit.biz.ens.model.kakao.pay.*;
import kr.xit.biz.ens.model.kakao.talk.*;
import kr.xit.core.exception.*;
import kr.xit.core.model.*;
import kr.xit.core.spring.annotation.*;
import kr.xit.core.spring.util.*;
import kr.xit.core.support.utils.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
* description : API dummy
* packageName : kr.xit.ens.kakao.talk.service
* fileName : KkotalkApiDummyTestService
* author : julim
* date : 2024-12-17
* ======================================================================
*
* ----------------------------------------------------------------------
* 2024-12-17 julim
*
* </pre>
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class KkotalkApiDummyTestService extends EgovAbstractServiceImpl {
// FIXME:: API 호출 모듈 선택 - ApiWebClientUtil | ApiRestTemplateUtil | ApiHttpClientUtil
private final ApiHttpClientUtil webClient;
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private static final CharSequence ENVELOPE_ID = "{ENVELOPE_ID}";
public KkotalkApiDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO) {
if(Checks.isEmpty(reqDTO.getProductCode())){
throw BizRuntimeException.create("상품 코드는 필수 입니다.");
}
List<String> errors = new ArrayList<>();
errors = validate(reqDTO.getEnvelope(), errors);
final KkotalkApiDTO.Envelope envelope = reqDTO.getEnvelope();
if(envelope.getReviewExpiresAt() != null){
if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){
errors.add("reviewExpiresAt=재열람 만료일시를 최조 열람 만료일시 보다 큰 날짜로 입력해주세요.");
}
}
if(Checks.isEmpty(envelope.getCi())){
if(Checks.isEmpty(envelope.getName())) Objects.requireNonNull(errors).add("name=받는이 이름은 필수입니다.");
if(Checks.isEmpty(envelope.getPhoneNumber())) Objects.requireNonNull(errors).add("phoneNumber=받는이 전화번호는 필수입니다.");
if(Checks.isEmpty(envelope.getBirthday())) Objects.requireNonNull(errors).add("birthday=받는이 생년월일은 필수입니다.");
}
if(!Objects.requireNonNull(errors).isEmpty()) throw BizRuntimeException.create(errors.toString());
return KkotalkApiDTO.SendResponse
.builder()
.envelopeId("EVLP-01JAF27KSMDYSZ6JP1R66SV2ZC-06")
.externalId("41220202410170021")
.build();
}
/**
* <pre>
* (Redirect URL /)
* </pre>
* @param reqDTO KkoPayEltrDocDTO.RequestSendReq
* @return KkotalkApiDTO.ValidTokenResponse
*/
public KkotalkApiDTO.ValidTokenResponse validToken(final KkotalkApiDTO.ValidTokenRequest reqDTO) {
validate(reqDTO, null);
return KkotalkApiDTO.ValidTokenResponse
.builder()
.status(ApiConstants.KkotalkDocStatus.READ) //"USED"
//.token_expires_at(1624344762L)
.payload("payload 파라미터 입니다.")
.build();
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkotalkDTO.BulkSendRequest
* @return KkotalkDTO.BulkSendResponse
*/
public KkotalkDTO.BulkSendResponse requestSendBulk(final KkotalkDTO.BulkSendRequest reqDTO) {
if(Checks.isEmpty(reqDTO.getProductCode())){
throw BizRuntimeException.create("상품 코드는 필수 입니다.");
}
List<String> errors = new ArrayList<>();
List<KkotalkApiDTO.Envelope> envelopes = reqDTO.getEnvelopes();
for(int idx = 0; idx < envelopes.size(); idx++) {
final Set<ConstraintViolation<KkotalkApiDTO.Envelope>> list = validator.validate(envelopes.get(idx));
if (!list.isEmpty()) {
int finalIdx = idx;
errors.addAll(list.stream()
.map(row -> String.format("%s[%d]=%s", row.getPropertyPath(), finalIdx +1, row.getMessageTemplate()))
.toList()
);
}
}
for(int idx = 0; idx < envelopes.size(); idx++) {
final KkotalkApiDTO.Envelope envelope = envelopes.get(idx);
if(envelope.getReviewExpiresAt() != null){
if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){
errors.add("reviewExpiresAt=재열람 만료일시를 최조 열람 만료일시 보다 큰 날짜로 입력해주세요.");
}
}
if (Checks.isEmpty(envelope.getCi())) {
if (Checks.isEmpty(envelope.getName())) errors.add(String.format("받는이 이름은 필수입니다(name[%d] 번째 오류)", idx+1));
if (Checks.isEmpty(envelope.getPhoneNumber())) errors.add(String.format("받는이 전화번호는 필수입니다(phoneNumber[%d] 번째 오류)", idx+1));
if (Checks.isEmpty(envelope.getBirthday())) errors.add(String.format("받는이 생년월일은 필수입니다(birthday[%d] 번째 오류)", idx+1));
} else {
final StringBuilder sb = new StringBuilder()
.append(StringUtils.defaultString(envelope.getName(), StringUtils.EMPTY))
.append(StringUtils.defaultString(envelope.getPhoneNumber(), StringUtils.EMPTY))
.append(StringUtils.defaultString(envelope.getBirthday(), StringUtils.EMPTY));
if(Checks.isNotEmpty(sb.toString())){
errors.add(String.format("CI가 지정 되었습니다(받는이 정보 불필요:[%d] 번째 오류) .", idx+1));
}
}
}
if(!errors.isEmpty()){
throw BizRuntimeException.create(errors.toString());
}
String param = "{\"envelopes\":" + JsonUtils.toJson(envelopes) + "}";
List<KkotalkApiDTO.EnvelopeRes> resDTO = new ArrayList<>();
resDTO.add(KkotalkApiDTO.EnvelopeRes.builder()
.externalId(envelopes.get(0).getExternalId())
.errorCode(ApiConstants.Error.NOT_FOUND.getCode())
.errorMessage("요청 정보를 찾을 수 없습니다. documentBinder를 찾을수 없습니다.")
.build()
);
resDTO.add(KkotalkApiDTO.EnvelopeRes.builder()
.externalId(envelopes.get(1).getExternalId())
.envelopeId("EVLP-01JAF27KSMDYSZ6JP1R66SV2ZC-06")
.edocGtid(null)
.build()
);
resDTO.add(KkotalkApiDTO.EnvelopeRes.builder()
.externalId(envelopes.get(2).getExternalId())
.envelopeId("EVLP-01J5YX9QNMS3Y8D3QZTB51NMMN-00")
.edocGtid(null)
.build()
);
KkotalkDTO.BulkSendResponse res = KkotalkDTO.BulkSendResponse.builder()
.envelopes(resDTO)
.build();
KkopayDocBulkDTO.BulkSendResponses object = JsonUtils.toObject(JsonUtils.toJson(res),
KkopayDocBulkDTO.BulkSendResponses.class);
return res;
}
public ApiResponseDTO<?> requestSendBulkError(final KkotalkDTO.BulkSendRequest reqDTO) {
if(Checks.isEmpty(reqDTO.getProductCode())){
throw BizRuntimeException.create("상품 코드는 필수 입니다.");
}
List<String> errors = new ArrayList<>();
List<KkotalkApiDTO.Envelope> envelopes = reqDTO.getEnvelopes();
for(int idx = 0; idx < envelopes.size(); idx++) {
final Set<ConstraintViolation<KkotalkApiDTO.Envelope>> list = validator.validate(envelopes.get(idx));
if (!list.isEmpty()) {
int finalIdx = idx;
errors.addAll(list.stream()
.map(row -> String.format("%s[%d]=%s", row.getPropertyPath(), finalIdx +1, row.getMessageTemplate()))
.toList()
);
}
}
for(int idx = 0; idx < envelopes.size(); idx++) {
final KkotalkApiDTO.Envelope envelope = envelopes.get(idx);
if(envelope.getReviewExpiresAt() != null){
if(envelope.getReviewExpiresAt().compareTo(envelope.getReadExpiresAt()) < 0){
errors.add("reviewExpiresAt=재열람 만료일시를 최조 열람 만료일시 보다 큰 날짜로 입력해주세요.");
}
}
if (Checks.isEmpty(envelope.getCi())) {
if (Checks.isEmpty(envelope.getName())) errors.add(String.format("받는이 이름은 필수입니다(name[%d] 번째 오류)", idx+1));
if (Checks.isEmpty(envelope.getPhoneNumber())) errors.add(String.format("받는이 전화번호는 필수입니다(phoneNumber[%d] 번째 오류)", idx+1));
if (Checks.isEmpty(envelope.getBirthday())) errors.add(String.format("받는이 생년월일은 필수입니다(birthday[%d] 번째 오류)", idx+1));
} else {
final StringBuilder sb = new StringBuilder()
.append(StringUtils.defaultString(envelope.getName(), StringUtils.EMPTY))
.append(StringUtils.defaultString(envelope.getPhoneNumber(), StringUtils.EMPTY))
.append(StringUtils.defaultString(envelope.getBirthday(), StringUtils.EMPTY));
if(Checks.isNotEmpty(sb.toString())){
errors.add(String.format("CI가 지정 되었습니다(받는이 정보 불필요:[%d] 번째 오류) .", idx+1));
}
}
}
if(!errors.isEmpty()){
throw BizRuntimeException.create(errors.toString());
}
String param = "{\"envelopes\":" + JsonUtils.toJson(envelopes) + "}";
String errorRtn = """
{"timestamp":"2024-12-16T01:19:57.040+00:00","path":"/pxy/kkoNew/v1/bulk/envelopes/D10_2","status":500,"error":"Internal Server Error","requestId":"541faefe-45126"}
""";
return ApiResponseDTO.success(errorRtn);
}
/**
* <pre>
* (bulk) API
* -. .
* : , flow
* : polling , 5 .
* -.doc_box_status
* : SENT() > RECEIVED() > READ()/EXPIRED( )
* </pre>
* @param reqDTO KkotalkDTO.BulkStatusRequest
* @return KkotalkDTO.BulkStatusResponse
*/
public KkotalkDTO.BulkStatusResponse findBulkStatus(final KkotalkDTO.BulkStatusRequest reqDTO) {
List<String> errors = new ArrayList<>();
List<String> envelopes = reqDTO.getEnvelopes();
for(int idx = 0; idx < envelopes.size(); idx++) {
final String binderUuid = envelopes.get(idx);
if (Checks.isEmpty(binderUuid) || binderUuid.length() > 40) {
errors.add(String.format("문서 식별 번호는 40자를 넘을 수 없습니다[%d번째]", idx+1));
}
}
if(!errors.isEmpty()) {
throw BizRuntimeException.create(errors.toString());
}
String param = "{\"envelopeIds\":" + JsonUtils.toJson(envelopes) + "}";
List<KkotalkApiDTO.EnvelopeStatusResponse> resDTO = new ArrayList<>();
resDTO.add(KkotalkApiDTO.EnvelopeStatusResponse.builder()
.envelopeId(reqDTO.getEnvelopes().get(0))
.errorCode(ApiConstants.Error.NOT_FOUND.getCode())
.errorMessage("요청 정보를 찾을 수 없습니다. documentBinder를 찾을수 없습니다.")
.build()
);
resDTO.add(KkotalkApiDTO.EnvelopeStatusResponse.builder()
.envelopeId(reqDTO.getEnvelopes().get(1))
.externalId("ddhhdhdhdhdhdhd")
.status(ApiConstants.KkotalkDocStatus.RECEIVED)
.sentAt("2024-08-16T15:24:39")
.receivedAt("2024-08-16T15:24:39")
.authenticatedAt("2024-08-30T11:14:33")
.isNotificationUnavailable(false)
.userNotifiedAt("2024-08-16T15:24:47")
.receivedAt("2024-09-30T23:59:59")
.build()
);
resDTO.add(KkotalkApiDTO.EnvelopeStatusResponse.builder()
.envelopeId(reqDTO.getEnvelopes().get(2))
.externalId("ddhhdhdhdhdhdhd")
.status(ApiConstants.KkotalkDocStatus.RECEIVED)
.sentAt("2024-08-16T15:24:39")
.receivedAt("2024-08-16T15:24:39")
.authenticatedAt("2024-08-30T11:14:33")
.isNotificationUnavailable(false)
.userNotifiedAt("2024-08-16T15:24:47")
.receivedAt("2024-09-30T23:59:59")
.build()
);
KkotalkDTO.BulkStatusResponse res = KkotalkDTO.BulkStatusResponse.builder()
.envelopeStatus(resDTO)
.build();
KkopayDocBulkDTO.BulkStatusResponses object = JsonUtils.toObject(JsonUtils.toJson(res),
KkopayDocBulkDTO.BulkStatusResponses.class);
return res;
}
//-------------------------------------------------------------------------------------------------------------------
private static <T> List<String> validate(T t, List<String> errList) {
final Set<ConstraintViolation<T>> list = validator.validate(t);
if(!list.isEmpty()) {
final List<String> errors = list.stream()
.map(row -> String.format("%s=%s", row.getPropertyPath(), row.getMessageTemplate()))
.toList();
// 추가적인 유효성 검증이 필요 없는 경우
if(errList == null){
if(!errors.isEmpty()) throw BizRuntimeException.create(errors.toString());
return null;
}
errList.addAll(errors);
}
return errList;
}
}

@ -1,34 +1,26 @@
package kr.xit.ens.kakao.talk.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import kr.xit.biz.common.ApiConstants.SndngSeCode;
import kr.xit.biz.ens.model.cmm.CmmEnsRequestDTO;
import kr.xit.biz.ens.model.cmm.CmmEnsRlaybsnmDTO;
import kr.xit.biz.ens.model.kakao.talk.KkotalkApiDTO;
import kr.xit.biz.ens.model.kakao.talk.KkotalkDTO;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.service.AbstractService;
import kr.xit.core.spring.annotation.TraceLogging;
import kr.xit.core.spring.util.ApiWebClientUtil;
import kr.xit.core.support.utils.Checks;
import kr.xit.core.support.utils.JsonUtils;
import kr.xit.ens.cmm.CmmEnsUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import kr.xit.biz.common.ApiConstants.*;
import kr.xit.biz.ens.model.cmm.*;
import kr.xit.biz.ens.model.kakao.talk.*;
import kr.xit.core.exception.*;
import kr.xit.core.model.*;
import kr.xit.core.service.*;
import kr.xit.core.spring.annotation.*;
import kr.xit.core.spring.util.*;
import kr.xit.core.support.utils.*;
import kr.xit.ens.cmm.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
@ -69,6 +61,8 @@ public class KkotalkEltrcDocService extends AbstractService implements
@Value("#{'${app.contract.kakao.api.talk.bulkstatus}'.split(';')}")
private String[] API_BULKSTATUS;
// FIXME:: API 호출 모듈 선택 - ApiWebClientUtil | ApiRestTemplateUtil | ApiHttpClientUtil
//private final ApiWebClientUtil webClient;
private final ApiWebClientUtil webClient;
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private static final CharSequence ENVELOPE_ID = "{ENVELOPE_ID}";
@ -79,12 +73,12 @@ public class KkotalkEltrcDocService extends AbstractService implements
* : POST
* -.
* </pre>
* @param reqDTO KkoPayEltrDocDTO.RequestSendReq
* @return ApiResponseDTO<KkopayDocDTO.SendResponse>
* @param reqDTO KkotalkDTO.SendRequest
* @return KkotalkApiDTO.SendResponse
*/
@Override
@TraceLogging
public KkotalkDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO) {
public KkotalkApiDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO) {
if(Checks.isEmpty(reqDTO.getProductCode())){
throw BizRuntimeException.create("상품 코드는 필수 입니다.");
}
@ -108,7 +102,7 @@ public class KkotalkEltrcDocService extends AbstractService implements
HOST + API_SEND[0].replace("{PRODUCT_CODE}", reqDTO.getProductCode()),
HttpMethod.valueOf(API_SEND[1]),
JsonUtils.toJson(envelope),
KkotalkDTO.SendResponse.class,
KkotalkApiDTO.SendResponse.class,
getRlaybsnmInfo(reqDTO));
}
@ -116,8 +110,8 @@ public class KkotalkEltrcDocService extends AbstractService implements
* <pre>
* (Redirect URL /) : GET
* </pre>
* @param reqDTO KkopayDocDTO.ValidTokenRequest
* @return ApiResponseDTO<KkopayDocDTO.ValidTokenResponse>
* @param reqDTO KkotalkApiDTO.ValidTokenRequest
* @return KkotalkApiDTO.ValidTokenResponse
*/
@Override
@TraceLogging
@ -146,7 +140,7 @@ public class KkotalkEltrcDocService extends AbstractService implements
*/
@Override
@TraceLogging
public void modifyStatus(final KkotalkDTO.EnvelopeId reqDTO){
public void modifyStatus(final KkotalkApiDTO.EnvelopeId reqDTO){
validate(reqDTO.getEnvelopeId(), null);
final String url = HOST + API_MODIFY_STATUS[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId());
@ -290,13 +284,18 @@ public class KkotalkEltrcDocService extends AbstractService implements
// 유효성 검증
final KkotalkApiDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkotalk(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null,
KkotalkApiDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO));
// FIXME:: 서버 결과 추적을 위해 임시로 error level 적용 - 서버안정화 후 debug로 변경 해야함
log.error("validTokenRes={}", validTokenRes);
// 문서상태 변경
final String url2 = HOST + API_MODIFY_STATUS[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId());
// 정상 : HttpStatus.NO_CONTENT(204) return
// error : body에 error_code, error_message return
final KkotalkApiDTO.KkotalkErrorDTO errorDTO = webClient.exchangeKkotalk(url2, HttpMethod.valueOf(API_MODIFY_STATUS[1]), null, KkotalkApiDTO.KkotalkErrorDTO.class, getRlaybsnmInfo(reqDTO));
// FIXME:: 서버 결과 추적을 위해 임시로 error level 적용 - 서버안정화 후 debug로 변경 해야함
log.error("errorDTO={}", errorDTO);
if(errorDTO != null){
return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage());
}

@ -0,0 +1,133 @@
package kr.xit.ens.kakao.talk.web;
import java.security.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.tags.*;
import kr.xit.biz.ens.model.kakao.talk.*;
import kr.xit.core.model.*;
import kr.xit.ens.kakao.talk.service.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
* description : API dummy controller
* packageName : kr.xit.ens.kakao.talk.controller
* fileName : KkopayApiDummyTestController
* author : julim
* date : 2024-12-17
* ======================================================================
*
* ----------------------------------------------------------------------
* 2024-12-17 julim
*
* </pre>
*/
@Tag(name = "KkotalkApiDummyTestController", description = "카카오톡 전자문서 API dummy 테스트")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/api/kakao/talk/test")
public class KkotalkApiDummyTestController {
private final KkotalkApiDummyTestService service;
@Operation(summary = "문서발송 요청", description = "카카오톡 전자문서 서버로 문서발송 처리를 요청")
@PostMapping(value = "/documents", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> requestSend(
@RequestBody final KkotalkDTO.SendRequest reqDTO
) {
return ApiResponseDTO.success(service.requestSend(reqDTO));
}
@Operation(summary = "토큰 유효성 검증", description = "Redirect URL 접속 허용/불허")
@PostMapping(value = "/validToken", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> validToken(
@RequestBody final KkotalkApiDTO.ValidTokenRequest reqDTO
) {
return ApiResponseDTO.success(service.validToken(reqDTO));
}
/**
* <pre>
* API
* -. .
* : , flow
* : polling , 5 .
* -.doc_box_status
* : SENT() > RECEIVED() > READ()/EXPIRED( )
* </pre>
* @param reqDTO KkopayDocAttrDTO.DocumentBinderUuid
* @return ApiResponseDTO
*/
@Operation(summary = "문서 상태 조회", description = "문서 상태 조회")
@PostMapping(value = "/findStatus", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> findStatus(
@RequestBody final KkotalkApiDTO.EnvelopeId reqDTO
) {
SecureRandom random = new SecureRandom(); // Compliant for security-sensitive use cases
int bound = 1000000000;
return ApiResponseDTO.success(KkotalkApiDTO.EnvelopeStatusResponse
.builder()
.payload(reqDTO.getEnvelopeId())
//.doc_box_status(ApiConstants.KkopayDocStatus.SENT)
//.doc_box_sent_at((long)(random.nextInt(bound)) + 1000000000)
//.doc_box_received_at((long)(random.nextInt(bound)) + 1000000000)
//.payload("payload 파라미터 입니다.")
.build());
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkotalkDTO.BulkSendRequest
* @return ApiResponseDTO
*/
@Operation(summary = "대량 문서 발송 요청", description = "카카오톡 전자문서 서버로 대량 문서 발송 요청")
@PostMapping(value = "/documents/bulk", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> requestSendBulk(
@RequestBody final KkotalkDTO.BulkSendRequest reqDTO
) {
return ApiResponseDTO.success(service.requestSendBulk(reqDTO));
}
@Operation(summary = "대량 문서 발송 요청", description = "카카오톡 전자문서 서버로 대량 문서 발송 요청 에러")
@PostMapping(value = "/documents/bulk/error", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> requestSendBulkError(
@RequestBody final KkotalkDTO.BulkSendRequest reqDTO
) {
return service.requestSendBulkError(reqDTO);
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkotalkDTO.BulkStatusRequest
* @return ApiResponseDTO
*/
@Operation(summary = "대량 문서 상태 조회 요청", description = "카카오톡 전자문서 서버로 대량 문서 상태 조회 요청")
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(mediaType = "application/json", examples = {
@ExampleObject(name = "Example"
, summary = "부과정보 전송", description = "개별시스템 -> 세외수입 시스템 호출하여 응답 Response"
, value = "{\"envelopes\":[\"BIN-ff806328863311ebb61432ac599d6151\",\"BIN-ff806328863311ebb61432ac599d6152\",\"BIN-ff806328863311ebb61432ac599d6153\"]}")
})
})
@PostMapping(value = "/documents/bulk/status", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponseDTO<?> findBulkStatus(
@RequestBody final KkotalkDTO.BulkStatusRequest reqDTO
) {
return ApiResponseDTO.success(service.findBulkStatus(reqDTO));
}
}

@ -113,7 +113,7 @@ public class KkotalkEltrcDocController {
@Operation(summary = "토큰 유효성 검증", description = "Redirect URL 접속 허용/불허")
@PostMapping(value = "/validToken", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse validToken(
@RequestBody final KkotalkDTO.ValidTokenRequest reqDTO
@RequestBody final KkotalkApiDTO.ValidTokenRequest reqDTO
) {
return ApiResponseDTO.success(service.validToken(reqDTO));
}

@ -52,49 +52,46 @@ public class KtBcInboundController {
@ExampleObject(
value = """
{
"service_cd" : "CHUMO",
"req_msg_type_dvcd" : "1",
"reqs" : [
{
"src_key" : "MEDKT202311030000011",
"mms_sndg_rslt_sqno" : 1,
"prcs_dt" : "20231103",
"mms_bsns_dvcd" : "ME112",
"mbl_bzowr_dvcd" : "02",
"rl_mms_sndg_telno" : "4345",
"mms_sndg_rslt_dvcd" : "40",
"mms_sndg_tmst" : "20231103123912",
"mms_rcv_tmst" : "20231103123916",
"mms_rdg_tmst" : "",
"prev_approve_yn" : "N",
"msg_type" : "2",
"rcv_npost" : "",
"rcv_plfm_id": "",
"click_dt": "",
"approve_dt": "",
"part_nm": null,
"rcv_yn": "N"
},
{
"src_key" : "MEDKT202311030000012",
"mms_sndg_rslt_sqno" : 1,
"prcs_dt" : "20231103",
"mms_bsns_dvcd" : "ME112",
"mbl_bzowr_dvcd" : "02",
"rl_mms_sndg_telno" : null,
"mms_sndg_rslt_dvcd" : "4V",
"mms_sndg_tmst" : "20231103123917",
"mms_rcv_tmst" : "20231103123917",
"mms_rdg_tmst" : "",
"prev_approve_yn" : "",
"msg_type" : "2",
"rcv_npost" : "",
"rcv_plfm_id": "",
"click_dt": "",
"approve_dt": "",
"part_nm": null,
"rcv_yn": "N"
}
"service_cd":"CHUMO",
"req_msg_type_dvcd":"1",
"reqs":[
{
"src_key":"MEDKT202311030000011",
"mms_sndg_rslt_sqno":1,
"prcs_dt":"20231103",
"mms_bsns_dvcd":"ME112",
"mbl_bzowr_dvcd":"02",
"mms_sndg_rslt_dvcd":"40",
"mms_sndg_tmst":"20231103123912",
"msg_type":2,
"rl_mms_sndg_telno":"4345",
"mms_rcv_tmst":"20231103123916",
"mms_rdg_tmst":"",
"prev_approve_yn":"N",
"rcv_npost":"",
"rcv_plfm_id":"",
"click_dt":"",
"approve_dt":"",
"ffnlgCode":"11"
},
{
"src_key":"MEDKT202311030000012",
"mms_sndg_rslt_sqno":1,
"prcs_dt":"20231103",
"mms_bsns_dvcd":"ME112",
"mbl_bzowr_dvcd":"02",
"mms_sndg_rslt_dvcd":"4V",
"mms_sndg_tmst":"20231103123917",
"msg_type":2,
"mms_rcv_tmst":"20231103123917",
"mms_rdg_tmst":"",
"prev_approve_yn":"",
"rcv_npost":"",
"rcv_plfm_id":"",
"click_dt":"",
"approve_dt":"",
"ffnlgCode":"11"
}
]
}
"""

@ -1,24 +1,27 @@
package kr.xit.ens.ktgbs.web;
import org.springframework.http.MediaType;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.xit.biz.ens.model.kt.KtCommonDTO;
import kr.xit.biz.ens.model.ktgbs.KtGbsDTO;
import kr.xit.biz.ktgbs.service.IBizKtGbsService;
import lombok.RequiredArgsConstructor;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import com.fasterxml.jackson.core.type.*;
import com.fasterxml.jackson.databind.*;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.tags.*;
import kr.xit.biz.ens.model.kt.*;
import kr.xit.biz.ens.model.ktgbs.*;
import kr.xit.biz.ktgbs.service.*;
import kr.xit.core.exception.*;
import kr.xit.core.support.utils.*;
import lombok.*;
/**
* <pre>
@ -34,7 +37,7 @@ import java.net.URL;
*
* </pre>
*/
@Tag(name = "KtGbsInboundController", description = "KT GIBIS Inbound API - KT GIBIS 에서 사용하는 API")
@Tag(name = "KtGbsInboundController", description = "KT GIBIS Inbound API - KT GIBIS 에서 사용하는 API- 천안 can")
@RequiredArgsConstructor
@RestController
public class KtGbsInboundController {
@ -49,6 +52,7 @@ public class KtGbsInboundController {
* @return KtCommonResponse
* </pre>
*/
/*
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(
mediaType = "application/json",
@ -56,30 +60,31 @@ public class KtGbsInboundController {
@ExampleObject(
value = """
{
"service_cd" : "CD001",
"reqs" : [
{
"src_key" : "OvSsuljd1K-67c1fc38-1a59-4fbe-930e-802db6609140",
"mms_sndg_rslt_sqno" : 2,
"prcs_dt" : "20240819",
"mms_bsns_dvcd" : "01001",
"mbl_bzowr_dvcd" : "02",
"rl_mms_sndg_telno" : "4230",
"mms_rslt_dvcd" : "40",
"mms_sndg_tmst" : "20240819090237",
"mms_rcv_tmst" : "20240819090239",
"mms_rdg_tmst" : "20240819114405",
"prev_approve_yn" : "Y",
"msg_type" : "2"
}
]
"reqs":[
{
"src_key":"44133110005571110KT",
"mms_sndg_rslt_sqno":2,
"prcs_dt":"20241213",
"mms_bsns_dvcd":"01001",
"mbl_bzowr_dvcd":"02",
"mms_rslt_dvcd":"40",
"mms_sndg_tmst":"20241213091014",
"msg_type":2,
"rl_mms_sndg_telno":"6017",
"mms_rcv_tmst":"20241213091016",
"mms_rdg_tmst":"20241213105733",
"prev_approve_yn":"Y",
"ffnlgCode":"11"
}
],
"service_cd":"CSO01"
}
"""
),
})
})
*/
@Operation(summary = "메세지 발송/수신 결과 전송 -> KT GIBIS에서 호출", description = "메세지 발송/수신 결과 전송 -> KT GIBIS에서 호출")
//@PostMapping(value = "/goji/kt/gibis/stat/bulk", produces = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(value = "/goji/api/msg/result", produces = MediaType.APPLICATION_JSON_VALUE)
public KtCommonDTO.KtCommonResponse messageResult(@RequestBody final KtGbsDTO.MsgRsltRequest reqDTO) {
return bizService.messageResult(reqDTO);
@ -124,4 +129,4 @@ public class KtGbsInboundController {
response.getWriter().println("I/O Error Occurred!!. " + e.getMessage());
}
}
}
}

@ -25,7 +25,10 @@ app:
mdc:
enabled: true
exclude-patterns: '/api/kakao/(.*), /api/v1/ens/sendBulks(.*)'
# API log trace - DB
api:
exclude-patterns: '/api/ens/kakao/v1/documents/bulk/status'
# slack
slack-webhook:
enabled: false

@ -76,8 +76,7 @@ app:
#---------------------------------------------------------------
log:
request:
common-enabled: true
response-enabled: false
custom-enabled: true
# MDC logging trace 활성
mdc:
enabled: true

@ -77,8 +77,7 @@ app:
#---------------------------------------------------------------
log:
request:
common-enabled: true
response-enabled: false
custom-enabled: true
# MDC logging trace 활성
mdc:
enabled: true

@ -60,10 +60,18 @@ spring:
primary:
read-only: false
auto-commit: false
# 인프라의 적용된 connection time limit보다 작아야함
# 연결 대기 최대 시간(30~60초) - 인프라의 적용된 connection time limit보다 작아야함
connection-timeout: 60000
# 커넥션풀에서 가져온 연결 유효성 검사 최대 대기 시간(5 ~ 10초)
validation-timeout: 300000
# 커넥션 풀에서 유지되는 연결의 최대 생명주기(30분) --
# DB 서버나 네트워크 방화벽 connection time limit보다 작아야함
max-lifetime: 1800000
# 유휴 풀 제거 최대 시간(10분)
idle-timeout: 600000
# 동시에 처리되는 trasanction 크기보다 크게 설정
maximum-pool-size: 15
# 최소 유휴 pool 갯수
minimum-idle: 5
#transaction-isolation: TRANSACTION_READ_UNCOMMITTED
data-source-properties:

@ -82,7 +82,7 @@
</filter>
</appender>
<appender name="Error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>ACCEPT</onMatch>
@ -91,7 +91,7 @@
<file>${LOG_PATH}/${LOG_FILE}-error.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
@ -196,11 +196,13 @@
<springProfile name="prod-ccn, prod-can">
<appender-ref ref="ASYNC_ROLLING"/>
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ERROR"/>
</springProfile>
<springProfile name="local, local-ccn, local-can, dev-ccn, dev-can">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ROLLING"/>
<appender-ref ref="ERROR"/>
</springProfile>
</root>

@ -10,7 +10,8 @@
<html>
<head>
<meta charset="UTF-8">
<c:set var="ctx" value="${pageContext.request.contextPath}"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="/resource/css/style.css"/>
<link rel="icon" href="/resource/images/favicon.ico" type="image/x-icon"/>
@ -263,8 +264,16 @@
* 데이터 출력
=============== */
var draw = (id) => {
const jsonStr = '${details}'.replace(/\n/gi, '\\n').replace(/\r/gi, '\\r');
var Dataset = JSON.parse(jsonStr);
// FIXME:: html(서버에러) 페이지 return 처리
try{
const jsonStr = '${details}'.replace(/\n/gi, '\\n').replace(/\r/gi, '\\r');
var Dataset = JSON.parse(jsonStr);
}catch(e){
console.log(e);
alert('서버 에러(네트웍장애) 입니다\n잠시후 다시 시도해 주세요');
return;
}
Dataset?.details?.forEach((row) => {
switch (row.item_type) {
case "TEXT":

@ -99,8 +99,7 @@
alert('[' + errCd + ']' + errMsg);
return false;
}
draw('contents');
// $('.img-slider').bxSlider({
// auto: false, // 자동으로 애니메이션 시작
@ -264,8 +263,16 @@
* 데이터 출력
=============== */
var draw = (id) => {
const jsonStr = '${details}'.replace(/\n/gi, '\\n').replace(/\r/gi, '\\r');
var Dataset = JSON.parse(jsonStr);
// FIXME:: html(서버에러) 페이지 return 처리
try{
const jsonStr = '${details}'.replace(/\n/gi, '\\n').replace(/\r/gi, '\\r');
var Dataset = JSON.parse(jsonStr);
}catch(e){
console.log(e);
alert('서버 에러(네트웍장애) 입니다\n잠시후 다시 시도해 주세요');
return;
}
Dataset?.details?.forEach((row) => {
switch (row.item_type) {
case "TEXT":

@ -0,0 +1,166 @@
package kr.xit.biz.nice.service;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.*;
import org.springframework.transaction.annotation.Transactional;
import kr.xit.biz.cmm.service.*;
import kr.xit.biz.common.ApiConstants;
import kr.xit.biz.ens.model.nice.*;
import kr.xit.biz.ens.model.nice.NiceCiDTO.NiceCiInfo;
import kr.xit.biz.ens.model.nice.NiceCiDTO.NiceCiRequest;
import kr.xit.biz.ens.model.nice.NiceCiDTO.NiceTokenResponse;
import kr.xit.biz.ens.model.nice.NiceCiDTO.ResponseDataHeader;
import kr.xit.biz.ens.model.nice.NiceCiDTO.TokenRevokeResponse;
import kr.xit.biz.ens.model.nice.NiceCiDTO.TokenResDataBody;
import kr.xit.biz.nice.mapper.IBizNiceCiMapper;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.ens.nice.service.*;
import static kr.xit.core.support.utils.JsonUtils.toJson;
@SpringBootTest
@ActiveProfiles("local-ccn")
class BizNiceCiServiceTest {
@Mock
private INiceCiService niceCiService;
@Mock
private CmmEnsCacheService cacheService;
@Mock
private IBizNiceCiMapper niceCiMapper;
@InjectMocks
private BizNiceCiService bizNiceCiService;
public BizNiceCiServiceTest() {
MockitoAnnotations.openMocks(this);
}
@Test
@Transactional
void testGenerateToken_whenTokenGeneratedSuccessfully_DoesNotRevokeTokens() {
NiceCiRequest reqDTO = NiceCiRequest.builder()
.signguCode("88328")
.ffnlgCode("11")
.build();
ResponseDataHeader responseHeader = new ResponseDataHeader();
responseHeader.setGwRsltCd("1200");
TokenResDataBody responseBody = new TokenResDataBody();
responseBody.setAccessToken("ThisIsToken");
responseBody.setExpiresIn(3600);
responseBody.setTokenType("Bearer");
responseBody.setScope("read");
NiceTokenResponse initialResponse = new NiceTokenResponse();
initialResponse.setDataHeader(responseHeader);
initialResponse.setDataBody(responseBody);
when(niceCiService.generateToken(reqDTO)).thenReturn(initialResponse);
NiceTokenResponse result = bizNiceCiService.generateToken(reqDTO);
assertNotNull(result);
assertEquals("1200", result.getDataHeader().getGwRsltCd());
verify(niceCiService, times(1)).generateToken(reqDTO);
verify(niceCiMapper, times(1)).updateNiceCrtfToken(any(NiceCiInfo.class));
verify(cacheService, times(1)).removeNiceCiInfoCache(reqDTO.getSignguCode(), reqDTO.getFfnlgCode());
}
@Test
@Transactional
void testGenerateToken_whenPreviousTokenRevokedAndNewTokenGenerated() {
NiceCiRequest reqDTO = NiceCiRequest.builder()
.signguCode("88328")
.ffnlgCode("11")
.build();
ResponseDataHeader initialHeader = new ResponseDataHeader();
initialHeader.setGwRsltCd("1800");
NiceTokenResponse initialResponse = new NiceTokenResponse();
initialResponse.setDataHeader(initialHeader);
ResponseDataHeader revokeHeader = new ResponseDataHeader();
revokeHeader.setGwRsltCd("1200");
TokenRevokeResponse revokedResponse = new TokenRevokeResponse();
revokedResponse.setDataHeader(revokeHeader);
revokedResponse.setDataBody(new NiceCiDTO.TokenRevokeResDataBody(true));
ResponseDataHeader finalHeader = new ResponseDataHeader();
finalHeader.setGwRsltCd("1200");
TokenResDataBody finalBody = new TokenResDataBody();
finalBody.setAccessToken("NewAccessToken");
finalBody.setExpiresIn(7200);
finalBody.setTokenType("Bearer");
finalBody.setScope("write");
NiceTokenResponse finalResponse = new NiceTokenResponse();
finalResponse.setDataHeader(finalHeader);
finalResponse.setDataBody(finalBody);
when(niceCiService.generateToken(reqDTO))
.thenReturn(initialResponse)
.thenReturn(finalResponse);
when(niceCiService.revokeToken(reqDTO)).thenReturn(revokedResponse);
NiceTokenResponse result = bizNiceCiService.generateToken(reqDTO);
assertNotNull(result);
assertEquals("1200", result.getDataHeader().getGwRsltCd());
assertEquals("NewAccessToken", result.getDataBody().getAccessToken());
verify(niceCiService, times(2)).generateToken(reqDTO);
verify(niceCiService, times(1)).revokeToken(reqDTO);
verify(niceCiMapper, times(1)).updateNiceCrtfToken(any(NiceCiInfo.class));
verify(cacheService, times(1)).removeNiceCiInfoCache(reqDTO.getSignguCode(), reqDTO.getFfnlgCode());
}
@Test
@Transactional
void testGenerateToken_whenTokenRevocationFails_ThrowsException() {
NiceCiRequest reqDTO = NiceCiRequest.builder()
.signguCode("88328")
.ffnlgCode("11")
.build();
ResponseDataHeader initialHeader = new ResponseDataHeader();
initialHeader.setGwRsltCd("1800");
NiceTokenResponse initialResponse = new NiceTokenResponse();
initialResponse.setDataHeader(initialHeader);
ResponseDataHeader revokeHeader = new ResponseDataHeader();
revokeHeader.setGwRsltCd("1200");
TokenRevokeResponse failedRevokeResponse = new TokenRevokeResponse();
failedRevokeResponse.setDataHeader(revokeHeader);
failedRevokeResponse.setDataBody(new NiceCiDTO.TokenRevokeResDataBody(false));
when(niceCiService.generateToken(reqDTO)).thenReturn(initialResponse);
when(niceCiService.revokeToken(reqDTO)).thenReturn(failedRevokeResponse);
BizRuntimeException exception = assertThrows(BizRuntimeException.class, () -> {
bizNiceCiService.generateToken(reqDTO);
});
assertNotNull(exception);
verify(niceCiService, times(1)).generateToken(reqDTO);
verify(niceCiService, times(1)).revokeToken(reqDTO);
verify(niceCiMapper, never()).updateNiceCrtfToken(any(NiceCiInfo.class));
verify(cacheService, never()).removeNiceCiInfoCache(eq(reqDTO.getSignguCode()), eq(reqDTO.getFfnlgCode()));
}
}

@ -1,41 +1,24 @@
package kr.xit.batch.ens.web;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import java.util.*;
import org.springframework.batch.core.*;
import org.springframework.batch.core.launch.*;
import org.springframework.batch.core.repository.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.xit.batch.ens.job.KtGbsAccessTokenUpdateJobConfig;
import kr.xit.batch.ens.job.SndngAcceptJobConfig;
import kr.xit.batch.ens.job.SndngCloseJobConfig;
import kr.xit.batch.ens.job.SndngMakeJobConfig;
import kr.xit.batch.ens.job.SndngSnedBulksJobConfig;
import kr.xit.batch.ens.job.SndngStatusBulksJobConfig;
import kr.xit.biz.common.ApiConstants;
import kr.xit.biz.ens.model.cmm.CmmEnsRequestDTO;
import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.model.IApiResponse;
import kr.xit.core.support.utils.Checks;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.tags.*;
import kr.xit.batch.ens.job.*;
import kr.xit.biz.common.*;
import kr.xit.biz.ens.model.cmm.*;
import kr.xit.core.model.*;
import kr.xit.core.support.utils.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
@ -52,7 +35,7 @@ import lombok.extern.slf4j.Slf4j;
*
* </pre>
*/
@Tag(name = "BatchJobWebController", description = "전자고지 통합발송 배치 WEB")
@Tag(name = "BatchJobWebController", description = "배치(전자고지) 실행 WEB")
@Slf4j
@RequiredArgsConstructor
@RestController

@ -393,7 +393,7 @@ public interface IEnsBatchMapper {
* @return int
* </pre>
*/
int updateKakaotalkStatusInfo(final KkotalkDTO.EnvelopeStatusResponse dto);
int updateKakaotalkStatusInfo(final KkotalkApiDTO.EnvelopeStatusResponse dto);
//----------------------------------------------------------------------
// status
//----------------------------------------------------------------------

@ -1,45 +1,30 @@
package kr.xit.biz.ens.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import java.util.*;
import javax.validation.*;
import org.apache.commons.lang3.*;
import org.slf4j.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import KISINFO.VNO.VNOInterop;
import egovframework.com.cmm.util.EgovDateUtil;
import egovframework.com.cmm.util.EgovStringUtil;
import kr.xit.biz.common.ApiConstants;
import kr.xit.biz.common.ApiConstants.MappingCanSndngProcessStatus;
import kr.xit.biz.common.ApiConstants.MappingCcnSndngProcessStatus;
import kr.xit.biz.common.ApiConstants.SndngProcessStatus;
import kr.xit.biz.common.ApiConstants.SndngSeCode;
import kr.xit.biz.ens.cmm.CmmEnsBizUtils;
import kr.xit.biz.ens.mapper.IEnsBatchMapper;
import kr.xit.biz.ens.model.EnsDTO;
import kr.xit.biz.ens.model.cmm.SndngMssageParam;
import kr.xit.biz.ens.model.nice.NiceCiDTO;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.service.AbstractService;
import kr.xit.core.spring.util.ApiWebClientUtil;
import kr.xit.core.spring.util.MessageUtil;
import kr.xit.core.support.utils.Checks;
import kr.xit.core.support.utils.DateUtils;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import org.springframework.transaction.annotation.*;
import KISINFO.VNO.*;
import egovframework.com.cmm.util.*;
import kr.xit.biz.common.*;
import kr.xit.biz.common.ApiConstants.*;
import kr.xit.biz.ens.cmm.*;
import kr.xit.biz.ens.mapper.*;
import kr.xit.biz.ens.model.*;
import kr.xit.biz.ens.model.cmm.*;
import kr.xit.biz.ens.model.nice.*;
import kr.xit.core.exception.*;
import kr.xit.core.service.*;
import kr.xit.core.spring.util.*;
import kr.xit.core.support.utils.*;
import lombok.*;
/**
* <pre>
@ -245,14 +230,15 @@ public class EnsBatchAcceptService extends AbstractService implements IEnsBatchA
boolean isCiCreate = false;
boolean isError = false;
switch(sndngSeCode) {
case KAKAO -> {
//FIXME : 카카오, KT 데이타 적합성 체크
case KAKAO, KAKAO_NEW, KT_BC, KT_GIBIS -> {
if (Checks.isEmpty(dto.getCi())) {
isCiCreate = true;
if (Checks.isEmpty(dto.getIhidnum())) {
if (Checks.isEmpty(dto.getIhidnum()) || dto.getIhidnum().length() != 13) {
isError = true;
errors.add(
String.format("주민등록번호는 필수입니다(dto.getIhidnum[%d] 번째 오류)",
String.format("주민번호는 필수[13자리]입니다(dto.getIhidnum[%d] 번째 오류)",
idx + 1));
}
/*if (Checks.isEmpty(dto.getMoblphonNo())) {
@ -270,23 +256,6 @@ public class EnsBatchAcceptService extends AbstractService implements IEnsBatchA
}
}
//FIXME : KT 데이타 적합성 체크
case KT_BC, KT_GIBIS -> {
isCiCreate = true;
}
case KAKAO_NEW -> {
if (Checks.isEmpty(dto.getCi())) {
isCiCreate = true;
if (Checks.isEmpty(dto.getIhidnum())) {
isError = true;
errors.add(
String.format("주민등록번호는 필수입니다(dto.getIhidnum[%d] 번째 오류)",
idx + 1));
}
}
}
default -> {}
}

@ -26,6 +26,7 @@ import kr.xit.biz.ens.model.EnsDTO.*;
import kr.xit.biz.ens.model.cmm.*;
import kr.xit.biz.ens.model.kakao.pay.*;
import kr.xit.biz.ens.model.kakao.talk.*;
import kr.xit.biz.ens.model.kt.*;
import kr.xit.biz.ens.model.kt.KtCommonDTO.*;
import kr.xit.biz.ens.model.kt.KtMmsSendDTO.*;
import kr.xit.biz.ens.model.ktgbs.*;
@ -85,6 +86,8 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
private static final String UNITY_SNDNG_MST_ID = "unitySndngMastrId";
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private final EnsBatchExtractService extractService;
// FIXME:: API 호출 모듈 선택 - ApiWebClientUtil | ApiRestTemplateUtil | ApiHttpClientUtil
private final ApiWebClientUtil apiWebClient;
private final IEnsBatchMapper mapper;
@ -192,16 +195,11 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
final List<List<KkopayDocBulkDTO.BulkSendReq>> partitions = ListUtils.partition(bulkList, bulkKkoMaxCnt);
//noinspection rawtypes
final List<ApiResponseDTO> apiResults = partitions.stream()
.map(bulkSendList -> apiWebClient.exchange(
url,
HttpMethod.POST,
KkopayDocBulkDTO.BulkSendRequests.builder()
.map(bulkSendList -> callApiServer(url, KkopayDocBulkDTO.BulkSendRequests.builder()
.signguCode(dto.getSignguCode())
.ffnlgCode(dto.getFfnlgCode())
.documents(bulkSendList)
.build(),
ApiResponseDTO.class,
CmmEnsBizUtils.getHeadeMap())
.build())
)
.toList();
@ -262,36 +260,60 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
}
final List<List<KkotalkApiDTO.Envelope>> partitions = ListUtils.partition(bulkList, bulkKkoMaxCnt);
//noinspection rawtypes
final List<ApiResponseDTO> apiResults = partitions.stream()
.map(bulkSendList -> apiWebClient.exchange(
url,
HttpMethod.POST,
KkotalkDTO.BulkSendRequest.builder()
.signguCode(dto.getSignguCode())
.ffnlgCode(dto.getFfnlgCode())
.envelopes(bulkSendList)
.build(),
ApiResponseDTO.class,
CmmEnsBizUtils.getHeadeMap())
)
.toList();
final List<KkotalkDTO.BulkSendResponse> resList = new ArrayList<>();
boolean isSuccess = false;
String errMsg = null;
//noinspection rawtypes
for(ApiResponseDTO apiResult : apiResults) {
if(apiResult.getData() != null) {
resList.add(toObjByObj(apiResult.getData(), KkotalkDTO.BulkSendResponse.class));
// FIXME:: 카카오톡 bulk send별 결과 처리
// FIXME:: 카카오톡 bulk send별 결과 처리
for (List<KkotalkApiDTO.Envelope> envelopes : partitions) {
ApiResponseDTO<?> response = null;
try {
response = callApiServer(url, KkotalkDTO.BulkSendRequest.builder()
.signguCode(dto.getSignguCode())
.ffnlgCode(dto.getFfnlgCode())
.envelopes(envelopes)
.build());
}catch(Exception e){
log.error("카카오톡 API 서버 호출 에러::{}", e.getMessage());
setErrorSendKkotalkDto(envelopes, resList, "카카오톡 API 서버 호출 에러");
extractService.saveKkotalkSendResult(resList);
resList.clear();
isSuccess = true;
continue;
}
errMsg = apiResult.getMessage();
if(ObjectUtils.isNotEmpty(response) && response.getData() != null) {
try {
resList.add(toObjByObj(response.getData(), KkotalkDTO.BulkSendResponse.class));
// FIXME::카카오톡 API 수신 결과가 아닌 경우 - 이렇게 수신 되는 경우가 있는지 테스트 불가 하여 방어 코드 추가 함
// {"timestamp":"2024-12-16T01:19:57.040+00:00","path":"/pxy/kkoNew/v1/bulk/envelopes/D10_2","status":500,"error":"Internal Server Error","requestId":"541faefe-45126"}
if(ObjectUtils.isNotEmpty(resList)) {
KkotalkDTO.BulkSendResponse bulkSendResponse = resList.get(0);
List<KkotalkApiDTO.EnvelopeRes> envelopeRes = bulkSendResponse.getEnvelopes();
// FIXME::카카오톡 API 수신 결과가 아닌 경우 - 이렇게 수신 되는 경우가 있는지 테스트 불가 하여 방어 코드 추가 함
if (ObjectUtils.isEmpty(envelopeRes) || ObjectUtils.isEmpty(envelopeRes.get(0).getExternalId())) {
resList.clear();
setErrorSendKkotalkDto(envelopes, resList, "Internal Server Error");
} else {
isSuccess = true;
}
}
}catch (Exception e){
log.error("API 호출 결과 비정상 데이타 응답::response.getData() - {}", response.getData());
setErrorSendKkotalkDto(envelopes, resList, "Internal Server Error");
}
extractService.saveKkotalkSendResult(resList);
resList.clear();
continue;
}else{
log.error(">>>> API 호출 결과 에러[비정상 return - proxy 에서 error return]::ApiResponseDTO - {}", response);
setErrorSendKkotalkDto(envelopes, resList, "Internal Server Error");
extractService.saveKkotalkSendResult(resList);
resList.clear();
}
errMsg = response.getMessage();
}
// 카카오 send 결과 반영
if(!isSuccess){
extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KAKAO_NEW, "", errMsg, errMsg);
@ -530,47 +552,86 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
}
final List<List<KtMainSendReqData>> partitions = ListUtils.partition(sendReqs, bulkKtMaxCnt);
@SuppressWarnings("rawtypes")
List<ApiResponseDTO> apiResults = partitions.stream()
.map(bulkSendList -> {
mstDTO.setReqs(bulkSendList);
return apiWebClient.exchange(
url,
HttpMethod.POST,
mstDTO,
ApiResponseDTO.class,
CmmEnsBizUtils.getHeadeMap());
}
)
.toList();
boolean isSuccess = false;
List<ErrorMsg> errList = new ArrayList<>();
//noinspection rawtypes
for(ApiResponseDTO apiResult : apiResults) {
// KT-BC API 정상 호출
if(ObjectUtils.isNotEmpty(apiResult.getData())) {
KtCommonResponse resDTO = toObjByObj(apiResult.getData(), KtCommonResponse.class);
assert resDTO != null;
dto.setResultDt(resDTO.getResultDt());
if(ObjectUtils.isNotEmpty(resDTO) && "00".equals(resDTO.getResultCd())){
isSuccess = true;
dto.setErrorMssage("정상");
}else{
//TODO:: 모바일 콘텐츠 삭제??
errList.addAll(resDTO.getErrors());
}
// FIXME:: KT-BC bulk send별 결과 처리
for (List<KtMainSendReqData> reqsDatas : partitions) {
ApiResponseDTO<?> response = null;
try {
mstDTO.setReqs(reqsDatas);
response = callApiServer(url, mstDTO);
} catch (Exception e) {
log.error("KT-BC API 서버 호출 에러::{}", e.getMessage());
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
errList.add(new ErrorMsg("KT-BC API 서버 호출 에러"));
continue;
}
}else{
// KT-BC API 호출전 에러 발생
if(ObjectUtils.isEmpty(dto.getResultDt())) {
if (ObjectUtils.isNotEmpty(response) && response.getData() != null) {
try {
KtCommonResponse resDTO = toObjByObj(response.getData(), KtCommonResponse.class);
if (ObjectUtils.isNotEmpty(resDTO)) {
dto.setResultDt(resDTO.getResultDt());
if ("00".equals(resDTO.getResultCd())) {
isSuccess = true;
} else {
//TODO:: 모바일 콘텐츠 삭제??
errList.addAll(resDTO.getErrors());
}
} else {
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
errList.add(new ErrorMsg(response.getMessage()));
}
} catch (Exception e) {
log.error("API 호출 결과 비정상 데이타 응답::response.getData() - {}", response.getData());
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
errList.add(new ErrorMsg("API 호출 결과 비정상 데이타 응답"));
}
dto.setErrorMssage(apiResult.getMessage());
errList.add(new ErrorMsg(apiResult.getMessage()));
} else {
log.error(">>>> API 호출 결과 에러[비정상 return - proxy 에서 error return]::ApiResponseDTO - {}", response);
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
errList.add(new ErrorMsg("API 호출 결과 에러[비정상 return]"));
}
}
// @SuppressWarnings("rawtypes")
// List<ApiResponseDTO> apiResults = partitions.stream()
// .map(bulkSendList -> {
// mstDTO.setReqs(bulkSendList);
// return callApiServer(url, mstDTO);
// })
// .toList();
//
// //noinspection rawtypes
// for(ApiResponseDTO apiResult : apiResults) {
// // KT-BC API 정상 호출
// if(ObjectUtils.isNotEmpty(apiResult.getData())) {
// KtCommonResponse resDTO = toObjByObj(apiResult.getData(), KtCommonResponse.class);
// assert resDTO != null;
// dto.setResultDt(resDTO.getResultDt());
//
// if(ObjectUtils.isNotEmpty(resDTO) && "00".equals(resDTO.getResultCd())){
// isSuccess = true;
// dto.setErrorMssage("정상");
// }else{
// errList.addAll(resDTO.getErrors());
// }
//
// }else{
// // KT-BC API 호출전 에러 발생
// if(ObjectUtils.isEmpty(dto.getResultDt())) {
// dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
// }
// dto.setErrorMssage(apiResult.getMessage());
// errList.add(new ErrorMsg(apiResult.getMessage()));
// }
// }
// 모두 실패한 경우
if(!isSuccess) {
@ -611,47 +672,84 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
}
final List<List<KtGbsDTO.MsgSendReqsData>> partitions = ListUtils.partition(sendReqs, bulkKtMaxCnt);
@SuppressWarnings("rawtypes")
List<ApiResponseDTO> apiResults = partitions.stream()
.map(bulkSendList -> {
mstDTO.setReqs(bulkSendList);
return apiWebClient.exchange(
url,
HttpMethod.POST,
mstDTO,
ApiResponseDTO.class,
CmmEnsBizUtils.getHeadeMap());
}
)
.toList();
boolean isSuccess = false;
// FIXME:: KT-GIBIS bulk send별 결과 처리
List<ErrorMsg> errList = new ArrayList<>();
//noinspection rawtypes
for(ApiResponseDTO apiResult : apiResults) {
// KT-BC API 정상 호출
if(ObjectUtils.isNotEmpty(apiResult.getData())) {
KtCommonResponse resDTO = toObjByObj(apiResult.getData(), KtCommonResponse.class);
assert resDTO != null;
dto.setResultDt(resDTO.getResultDt());
if(ObjectUtils.isNotEmpty(resDTO) && "00".equals(resDTO.getResultCd())){
isSuccess = true;
dto.setErrorMssage("정상");
}else{
//TODO:: 모바일 콘텐츠 삭제??
errList.addAll(resDTO.getErrors());
for (List<KtGbsDTO.MsgSendReqsData> reqsDatas : partitions) {
ApiResponseDTO<?> response = null;
try {
mstDTO.setReqs(reqsDatas);
response = callApiServer(url, mstDTO);
} catch (Exception e) {
log.error("KT-GIBIS API 서버 호출 에러::{}", e.getMessage());
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
errList.add(new ErrorMsg("KT-GIBIS API 서버 호출 에러"));
continue;
}
if(ObjectUtils.isNotEmpty(response) && response.getData() != null) {
try {
KtCommonResponse resDTO = toObjByObj(response.getData(), KtCommonResponse.class);
if(ObjectUtils.isNotEmpty(resDTO)) {
dto.setResultDt(resDTO.getResultDt());
if("00".equals(resDTO.getResultCd())){
isSuccess = true;
}else{
//TODO:: 모바일 콘텐츠 삭제??
errList.addAll(resDTO.getErrors());
}
}else{
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
errList.add(new ErrorMsg(response.getMessage()));
}
}catch (Exception e){
log.error("API 호출 결과 비정상 데이타 응답::response.getData() - {}", response.getData());
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
errList.add(new ErrorMsg("API 호출 결과 비정상 데이타 응답"));
}
}else{
// KT-BC API 호출전 에러 발생
if(ObjectUtils.isEmpty(dto.getResultDt())) {
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
}
dto.setErrorMssage(apiResult.getMessage());
errList.add(new ErrorMsg(apiResult.getMessage()));
log.error(">>>> API 호출 결과 에러[비정상 return - proxy 에서 error return]::ApiResponseDTO - {}", response);
dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
errList.add(new ErrorMsg("API 호출 결과 에러[비정상 return]"));
}
}
// @SuppressWarnings("rawtypes")
// List<ApiResponseDTO> apiResults = partitions.stream()
// .map(bulkSendList -> {
// mstDTO.setReqs(bulkSendList);
// return callApiServer(url, mstDTO);
// })
// .toList();
//
// //noinspection rawtypes
// for(ApiResponseDTO apiResult : apiResults) {
// // KT-BC API 정상 호출
// if(ObjectUtils.isNotEmpty(apiResult.getData())) {
// KtCommonResponse resDTO = toObjByObj(apiResult.getData(), KtCommonResponse.class);
// assert resDTO != null;
// dto.setResultDt(resDTO.getResultDt());
//
// if(ObjectUtils.isNotEmpty(resDTO) && "00".equals(resDTO.getResultCd())){
// isSuccess = true;
// }else{
// errList.addAll(resDTO.getErrors());
// }
//
// }else{
// // KT-BC API 호출전 에러 발생
// if(ObjectUtils.isEmpty(dto.getResultDt())) {
// dto.setResultDt(DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_EMPTY_DLT));
// }
// dto.setErrorMssage(apiResult.getMessage());
// errList.add(new ErrorMsg(apiResult.getMessage()));
// }
// }
// 모두 실패한 경우
if(!isSuccess) {
@ -662,14 +760,14 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
.collect(Collectors.joining(","))
);
extractService.saveKtGbsResult(dto, null);
extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KT_BC, dto.getErrorCode(), dto.getErrorMssage(), "[send] KT-BC 발송(bulks)요청 실패");
extractService.updateSndngMstFailStatus(mstId, SndngSeCode.KT_GIBIS, dto.getErrorCode(), dto.getErrorMssage(), "[send] KT-GIBIS 발송(bulks)요청 실패");
return;
}
// 성공 건수 존재시 성공 처리
dto.setResultCd("00");
dto.setErrorMssage("정상");
extractService.saveKtGbsResult(dto, sendReqs);
extractService.updateSendSndngMstStatus(mstId, unitySndMstId, SndngSeCode.KT_BC, "KT-BC 실패(발송마스터 데이타 오류)");
extractService.updateSendSndngMstStatus(mstId, unitySndMstId, SndngSeCode.KT_GIBIS, "KT-GIBIS 실패(발송마스터 데이타 오류)");
}
private void validatedKtBcSendBulks(List<KtMainSendReqData> sendReqs) {
@ -719,5 +817,36 @@ public class EnsBatchSendService extends AbstractService implements IEnsBatchSen
throw BizRuntimeException.create(errors.toString());
}
}
private void setErrorSendKkotalkDto(List<KkotalkApiDTO.Envelope> envelopes, List<KkotalkDTO.BulkSendResponse> resList, String errMsg){
List<KkotalkApiDTO.EnvelopeRes> envelopeResList = new ArrayList<>();
for(KkotalkApiDTO.Envelope envelope :envelopes){
envelopeResList.add(
KkotalkApiDTO.EnvelopeRes.builder()
.externalId(envelope.getExternalId())
.errorCode("E400_00")
.errorMessage(errMsg)
.build()
);
}
resList.add(
KkotalkDTO.BulkSendResponse.builder()
.envelopes(envelopeResList)
.build()
);
}
@SuppressWarnings("rawtypes")
private <T> ApiResponseDTO callApiServer(final String url, final T param){
final Map<String, String> map = new HashMap<>();
map.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return apiWebClient.exchange(
url,
HttpMethod.POST,
param,
ApiResponseDTO.class,
map);
}
//-----------------------------------------------------------------------------------------------------------------
}

@ -62,7 +62,9 @@ public class EnsBatchStatusService extends AbstractService implements IEnsBatchS
private static final String UNITY_SNDNG_MST_ID = "unitySndngMastrId";
private final ApiWebClientUtil apiWebClient;
// FIXME:: API 호출 모듈 선택 - ApiWebClientUtil | ApiRestTemplateUtil | ApiHttpClientUtil
//private final ApiWebClientUtil apiWebClient;
private final ApiRestTemplateUtil apiWebClient;
private final IEnsBatchMapper mapper;
private final EnsBatchExtractService extractService;

@ -26,7 +26,7 @@ import lombok.*;
*
* </pre>
*/
@Tag(name = "EnsBatchController", description = "전자고지 배치 테스트")
@Tag(name = "EnsBatchController", description = "서비스(전자 고지 배치) 테스트 controller")
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/batch/ens/v1")

@ -1,6 +1,6 @@
package kr.xit.biz.ens.web;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.*;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
@ -26,7 +26,7 @@ import lombok.RequiredArgsConstructor;
* description :
*
* packageName : kr.xit.biz.ens.web
* fileName : SendMessageLinkController
* fileName : KkopayApiDummyTestController
* author : limju
* date : 2023-08-31
* ======================================================================
@ -36,20 +36,21 @@ import lombok.RequiredArgsConstructor;
*
* </pre>
*/
@Tag(name = "ApiCallTestController", description = "전자고지 통합발송 연계 서비스(배치) 테스트")
@Tag(name = "KkopayApiDummyTestController", description = "카카오페이 전자고지 API Dummy 테스트")
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/batch/ens/test/v1")
public class ApiCallTestController {
@RequestMapping(value = "/batch/ens/test/kakao/pay")
public class KkopayApiDummyTestController {
@Value("${app.contract.host}")
private String apiHost;
@Value("${app.contract.kakao.api.pay.bulksend}")
private String apiBulkSend;
@Value("${app.contract.kakao.api.pay.bulkstatus}")
private String apiBulkStatus;
private String requestSend = "/api/kakao/pay/test/documents";
private String apiBulkSend = "/api/kakao/pay/test/documents/bulk";
private String apiBulkStatus = "/api/kakao/pay/test/documents/bulk/status";
private final ApiWebClientUtil apiWebClient;
private static final String SNDNG_PROCESS_STTUS = "sndngProcessSttus";
/**
* <pre>
@ -94,8 +95,8 @@ public class ApiCallTestController {
@RequestBody final KkopayDocDTO.SendRequest reqDTO
) {
StringBuilder url = new StringBuilder()
.append("http://localhost:18090")
.append("/api/ens/kakao/v1/documents");
.append(apiHost)
.append(requestSend);
return apiWebClient.exchangeKkopay(
url.toString(),
@ -177,7 +178,9 @@ public class ApiCallTestController {
@ExampleObject(value = """
{
"document_binder_uuids": [
"BIN-883246dbff7b11edb3bb7affed8a016d"
"BIN-883246dbff7b11edb3bb7affed8a016d",
"BIN-883246dbff7b11edb3bb7affed8a0161",
"BIN-883246dbff7b11edb3bb7affed8a0162"
]
}
""")

@ -0,0 +1,510 @@
package kr.xit.biz.ens.web;
import static kr.xit.core.support.utils.JsonUtils.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import org.apache.commons.collections4.*;
import org.apache.commons.lang3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.*;
import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.tags.*;
import kr.xit.biz.ens.cmm.*;
import kr.xit.biz.ens.model.cmm.*;
import kr.xit.biz.ens.model.kakao.pay.*;
import kr.xit.biz.ens.model.kakao.talk.*;
import kr.xit.core.model.*;
import kr.xit.core.spring.util.*;
import kr.xit.core.support.utils.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
* description :
*
* packageName : kr.xit.biz.ens.web
* fileName : KkotalkApiDummyTestController
* author : limju
* date : 2023-08-31
* ======================================================================
*
* ----------------------------------------------------------------------
* 2023-08-31 limju
*
* </pre>
*/
@Tag(name = "KkotalkApiDummyTestController", description = "카카오톡 전자고지 API Dummy 테스트")
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/batch/ens/test/kakao/talk")
@Slf4j
public class KkotalkApiDummyTestController {
@Value("${app.contract.host}")
private String apiHost;
private String requestSend = "/api/kakao/talk/test/documents";
private String apiBulkSend = "/api/kakao/talk/test/documents/bulk";
private String apiBulkSendError = "/api/kakao/talk/test/documents/bulk/error";
private String apiBulkStatus = "/api/kakao/talk/test/documents/bulk/status";
private final ApiWebClientUtil apiWebClient;
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO
* @return ResponseEntity
*/
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(mediaType = "application/json", examples = {
@ExampleObject(value = """
{
"productCode": "D10_2",
"envelope": {
"title": "문서 제목",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "73deae8287321e7237ed080f515d064fffe9dec380c3d3cb57a9d436007ad9e2",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이",
"birthday": "19801101",
"externalId": ""
},
"signguCode": "51110",
"ffnlgCode": "11",
"profile": "local"
}
""")
})
})
@Operation(summary = "문서발송 요청", description = "카카오페이 전자문서 서버로 문서발송 처리를 요청")
@PostMapping(value = "/documents", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse requestSend(
@RequestBody final KkopayDocDTO.SendRequest reqDTO
) {
StringBuilder url = new StringBuilder()
.append(apiHost)
.append(requestSend);
return apiWebClient.exchangeKkotalk(
url.toString(),
HttpMethod.POST,
JsonUtils.toJson(reqDTO),
ApiResponseDTO.class,
getRlaybsnmDTO()
);
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkopayDocBulkDTO.BulkSendRequests
* @return ApiResponseDTO
*/
@Operation(summary = "대량 문서발송 요청", description = "카카오페이 전자문서 서버로 대량 문서발송 처리를 요청")
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(mediaType = "application/json", examples = {
@ExampleObject(value = """
{
"productCode": "D10_1",
"envelopes": [
{
"title": "문서 제목",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "1375b68517c3055a2d5b425d8c6d32f6dbc9c74fb7f5fc796eb16c11a7a183b7",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이",
"birthday": "19801101",
"externalId": "41220202410170021"
},
{
"title": "문서 제목1",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "ad23397eea6951e4a967e4561b9b0459d59239d5f1e92f3b3fe5a8a511e99e83",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다1.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이1",
"birthday": "19801101",
"externalId": "4413311202408200033"
},
{
"title": "문서 제목2",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "73deae8287321e7237ed080f515d064fffe9dec380c3d3cb57a9d436007ad9e2",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다2.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이2",
"birthday": "19801101",
"externalId": "41220202410170011"
}
],
"signguCode": "51110",
"ffnlgCode": "11",
"profile": "local"
}
""")
})
})
@PostMapping(value = "/documents/bulk", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse requestSendBulk(
@RequestBody final KkotalkDTO.BulkSendRequest reqDTO
) {
final String url = apiHost + apiBulkSend;
return apiWebClient.exchangeKkotalk(
url,
HttpMethod.POST,
JsonUtils.toJson(reqDTO),
ApiResponseDTO.class,
getRlaybsnmDTO()
);
}
@Operation(summary = "대량 문서발송 요청", description = "카카오페이 전자문서 서버로 대량 문서발송 처리를 요청")
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(mediaType = "application/json", examples = {
@ExampleObject(value = """
{
"productCode": "D10_1",
"envelopes": [
{
"title": "문서 제목",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "1375b68517c3055a2d5b425d8c6d32f6dbc9c74fb7f5fc796eb16c11a7a183b7",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이",
"birthday": "19801101",
"externalId": "41220202410170021"
},
{
"title": "문서 제목1",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "ad23397eea6951e4a967e4561b9b0459d59239d5f1e92f3b3fe5a8a511e99e83",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다1.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이1",
"birthday": "19801101",
"externalId": "4413311202408200033"
},
{
"title": "문서 제목2",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "73deae8287321e7237ed080f515d064fffe9dec380c3d3cb57a9d436007ad9e2",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다2.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이2",
"birthday": "19801101",
"externalId": "41220202410170011"
}
],
"signguCode": "51110",
"ffnlgCode": "11",
"profile": "local"
}
""")
})
})
@PostMapping(value = "/documents/bulk/error", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse requestSendBulkAndError(
@RequestBody final KkotalkDTO.BulkSendRequest reqDTO
) {
final String url = apiHost + apiBulkSend;
final List<KkotalkDTO.BulkSendResponse> resList = new ArrayList<>();
boolean isSuccess = false;
String errMsg = null;
final List<KkotalkApiDTO.Envelope> envelopes = reqDTO.getEnvelopes();
for(int idx = 0; idx<3; idx++) {
ApiResponseDTO<?> response = apiWebClient.exchangeKkotalk(
apiHost + (idx==0? apiBulkSendError : apiBulkSend),
HttpMethod.POST,
JsonUtils.toJson(reqDTO),
ApiResponseDTO.class,
getRlaybsnmDTO()
);
if(response.getData() != null) {
try {
resList.add(toObjByObj(response.getData(), KkotalkDTO.BulkSendResponse.class));
// FIXME::카카오톡 API 수신 결과가 아닌 경우 - 이렇게 수신 되는 경우가 있는지 테스트 불가 하여 방어 코드 추가 함
// {"timestamp":"2024-12-16T01:19:57.040+00:00","path":"/pxy/kkoNew/v1/bulk/envelopes/D10_2","status":500,"error":"Internal Server Error","requestId":"541faefe-45126"}
if(ObjectUtils.isNotEmpty(resList)){
KkotalkDTO.BulkSendResponse bulkSendResponse = resList.get(0);
List<KkotalkApiDTO.EnvelopeRes> envelopeRes = bulkSendResponse.getEnvelopes();
// FIXME::카카오톡 API 수신 결과가 아닌 경우 - 이렇게 수신 되는 경우가 있는지 테스트 불가 하여 방어 코드 추가 함
if(ObjectUtils.isEmpty(envelopeRes) || ObjectUtils.isEmpty(envelopeRes.get(0).getExternalId())){
resList.clear();
setErrorSendDto(envelopes, resList, "Internal Server Error");
}else{
isSuccess = true;
};
}
}catch (Exception e){
log.error("API 호출 결과 비정상 데이타::apiResult.getData() - {}", response.getMessage());
setErrorSendDto(envelopes, resList, "Internal Server Error");
}
//extractService.saveKkotalkSendResult(resList);
resList.clear();
continue;
}else{
log.error(">>>> API 호출 결과 에러[비정상 return - proxy 에서 error return]::ApiResponseDTO - {}", response);
setErrorSendDto(envelopes, resList, "Internal Server Error");
//extractService.saveKkotalkSendResult(resList);
resList.clear();
}
errMsg = response.getMessage();
}
return ApiResponseDTO.success(isSuccess);
}
@Operation(summary = "대량 문서발송 요청", description = "카카오페이 전자문서 서버로 대량 문서발송 처리를 요청")
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(mediaType = "application/json", examples = {
@ExampleObject(value = """
{
"productCode": "D10_1",
"envelopes": [
{
"title": "문서 제목",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "1375b68517c3055a2d5b425d8c6d32f6dbc9c74fb7f5fc796eb16c11a7a183b7",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이",
"birthday": "19801101",
"externalId": "41220202410170021"
},
{
"title": "문서 제목1",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "ad23397eea6951e4a967e4561b9b0459d59239d5f1e92f3b3fe5a8a511e99e83",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다1.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이1",
"birthday": "19801101",
"externalId": "4413311202408200033"
},
{
"title": "문서 제목2",
"content": {
"link": "http://ipAddress/api/kakaopay/v1/ott",
"html": ""
},
"hash": "73deae8287321e7237ed080f515d064fffe9dec380c3d3cb57a9d436007ad9e2",
"guide": "문서 정보 안내 화면에 노출할 문구",
"payload": "payload 파라미터 입니다2.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": false,
"ci": "",
"phoneNumber": "01012345678",
"name": "김페이2",
"birthday": "19801101",
"externalId": "41220202410170011"
}
],
"signguCode": "51110",
"ffnlgCode": "11",
"profile": "local"
}
""")
})
})
@PostMapping(value = "/documents/bulk/error2", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse requestSendBulkAndError2(
@RequestBody final KkotalkDTO.BulkSendRequest reqDTO
) {
final List<KkotalkApiDTO.Envelope> envelopes = reqDTO.getEnvelopes();
final List<List<KkotalkApiDTO.Envelope>> partitions = ListUtils.partition(envelopes, 1);
Map<String, String> headerMap = CmmEnsBizUtils.getHeadeMap();
AtomicInteger idx = new AtomicInteger(-1);
//noinspection rawtypes
final List<ApiResponseDTO> apiResults = partitions.stream()
.map(bulkSendList -> {
idx.getAndIncrement();
return apiWebClient.exchangeKkotalk(
apiHost + (idx.get() ==0? apiBulkSendError : apiBulkSend),
HttpMethod.POST,
JsonUtils.toJson(reqDTO),
ApiResponseDTO.class,
getRlaybsnmDTO());
}
)
.toList();
final List<KkotalkDTO.BulkSendResponse> resList = new ArrayList<>();
boolean isSuccess = false;
String errMsg = null;
//noinspection rawtypes
for(ApiResponseDTO apiResult : apiResults) {
if(apiResult.getData() != null) {
resList.add(toObjByObj(apiResult.getData(), KkotalkDTO.BulkSendResponse.class));
// FIXME:: 카카오톡 bulk send별 결과 처리
//extractService.saveKkotalkSendResult(resList);
resList.clear();
isSuccess = true;
continue;
}
errMsg = apiResult.getMessage();
}
return ApiResponseDTO.success(reqDTO);
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkopayDocBulkDTO.BulkStatusRequests
* @return ApiResponseDTO<BulkStatusResponses.BulkStatusResponses>
*/
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(mediaType = "application/json", examples = {
@ExampleObject(value = """
{
"document_binder_uuids": [
"BIN-883246dbff7b11edb3bb7affed8a016d",
"BIN-883246dbff7b11edb3bb7affed8a0161",
"BIN-883246dbff7b11edb3bb7affed8a0162"
]
}
""")
})
})
@Operation(summary = "대량 문서 상태 조회 요청", description = "카카오페이 전자문서 서버로 대량 문서 상태 조회 요청")
@PostMapping(value = "/documents/bulk/status", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse findBulkStatus(
@RequestBody final KkopayDocBulkDTO.BulkStatusRequests reqDTO
) {
final String url = apiHost + apiBulkStatus;
return apiWebClient.exchangeKkotalk(
url,
HttpMethod.POST,
JsonUtils.toJson(reqDTO),
ApiResponseDTO.class,
getRlaybsnmDTO()
);
}
private CmmEnsRlaybsnmDTO getRlaybsnmDTO(){
return CmmEnsRlaybsnmDTO.builder()
.kakaoClientId("")
.kakaoContractUuid("")
.build();
}
private void setErrorSendDto(List<KkotalkApiDTO.Envelope> envelopes, List<KkotalkDTO.BulkSendResponse> resList, String errMsg){
List<KkotalkApiDTO.EnvelopeRes> envelopeResList = new ArrayList<>();
for(KkotalkApiDTO.Envelope envelope :envelopes){
envelopeResList.add(
KkotalkApiDTO.EnvelopeRes.builder()
.externalId(envelope.getExternalId())
.errorCode("E400_00")
.errorMessage(errMsg)
.build()
);
}
resList.add(
KkotalkDTO.BulkSendResponse.builder()
.envelopes(envelopeResList)
.build()
);
}
}

@ -27,7 +27,7 @@ public class SpringDocsApiConfig {
@Bean
public GroupedOpenApi kakaopayEltrcDocBatch() {
return GroupedOpenApi.builder()
.group("1. 전자고지 통합발송 연계 Batch WEB")
.group("1. 전자고지 Batch 실행 WEB")
.pathsToMatch(
"/batch/v1/**"
)
@ -37,13 +37,23 @@ public class SpringDocsApiConfig {
@Bean
public GroupedOpenApi bizDoc() {
return GroupedOpenApi.builder()
.group("2. 전자고지 발송 연계 Batch API")
.group("2. 전자고지 배치 서비스 실행 controller WEB")
.pathsToMatch(
"/batch/ens/**"
"/batch/ens/v1/**"
)
.build();
}
@Bean
public GroupedOpenApi testDoc() {
return GroupedOpenApi.builder()
.group("3. 전자고지 API 테스트 controller WEB")
.pathsToMatch(
"/batch/ens/test/**"
)
.build();
}
@Bean
public GroupedOpenApi cmmDoc() {
return GroupedOpenApi.builder()

@ -31,7 +31,7 @@ app:
bulksend: /api/ens/kakao/v2/envelopes/bulk
bulkstatus: /api/ens/kakao/v2/envelopes/bulk/status
kt:
bulk-max-cnt: 10
bulk-max-cnt: 100
bc:
api:
bulksend: /api/ens/kt/v1/mainSend

@ -69,10 +69,18 @@ spring:
primary:
pool-name: xit-maria-pool
auto-commit: false
# 인프라의 적용된 connection time limit보다 작아야함
# 연결 대기 최대 시간(30~60초) - 인프라의 적용된 connection time limit보다 작아야함
connection-timeout: 60000
# 커넥션풀에서 가져온 연결 유효성 검사 최대 대기 시간(5 ~ 10초)
validation-timeout: 300000
# 커넥션 풀에서 유지되는 연결의 최대 생명주기(30분) --
# DB 서버나 네트워크 방화벽 connection time limit보다 작아야함
max-lifetime: 1800000
maximum-pool-size: 10
# 유휴 풀 제거 최대 시간(10분)
idle-timeout: 600000
# 동시에 처리되는 trasanction 크기보다 크게 설정
maximum-pool-size: 15
# 최소 유휴 pool 갯수
minimum-idle: 5
#transaction-isolation: TRANSACTION_READ_UNCOMMITTED
data-source-properties:

@ -877,6 +877,7 @@
WHERE tesm.sndng_process_sttus IN ('send-ok', 'sending1', 'sending2')
AND tesm.unity_sndng_mastr_id = #{unitySndngMastrId}
AND IFNULL(tekd.envelope_id, '') != ''
AND (IFNULL(tekd.status, '') = '' OR tekd.status != 'READ')
AND tesm.signgu_code = #{signguCode}
AND tesm.ffnlg_code = #{ffnlgCode}
</select>

@ -1039,6 +1039,7 @@
WHERE tesm.sndng_process_sttus IN ('send-ok', 'sending1', 'sending2')
AND tesm.unity_sndng_mastr_id = #{unitySndngMastrId}
AND tekd10.envelope_id IS NOT NULL
AND (tekd10.status != 'READ' OR tekd10.status IS NULL OR tekd10.status= '')
AND tesm.signgu_code = #{signguCode}
AND tesm.ffnlg_code = #{ffnlgCode}
</select>

@ -82,7 +82,7 @@
</filter>
</appender>
<appender name="Error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>ACCEPT</onMatch>
@ -91,7 +91,7 @@
<file>${LOG_PATH}/${LOG_FILE}-error.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
@ -195,11 +195,13 @@
<root level="DEBUG">
<springProfile name="prod-ccn, prod-can">
<appender-ref ref="ASYNC_ROLLING"/>
<appender-ref ref="ERROR"/>
</springProfile>
<springProfile name="local, local-ccn, local-can, dev-ccn, dev-can">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_ROLLING"/>
<appender-ref ref="ERROR"/>
</springProfile>
</root>

@ -474,6 +474,17 @@
</dependency>
<!-- MacOS DNS resolver error -->
<!-- //FIXME:: RetryTemplate 미사용 -->
<!--dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

@ -7,9 +7,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import kr.xit.core.model.IApiResponse;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.*;
import lombok.experimental.SuperBuilder;
/**
@ -33,6 +31,7 @@ import lombok.experimental.SuperBuilder;
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@ToString
public class KkopayErrorDTO implements IApiResponse {
/**
* (max:40)

@ -12,11 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import kr.xit.biz.common.ApiConstants;
import kr.xit.biz.ens.model.cmm.CmmEnsRequestDTO;
import kr.xit.core.model.IApiResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.*;
import lombok.experimental.SuperBuilder;
/**
@ -62,7 +58,7 @@ public class KkotalkApiDTO {
* D10_2
* </pre>
*/
@Schema(title = "문서 원문(열람정보)에 대한 hash 값", example = "6EFE827AC88914DE471C621AE")
@Schema(title = "문서 원문(열람정보)에 대한 hash 값", example = "ad23397eea6951e4a967e4561b9b0459d59239d5f1e92f3b3fe5a8a511e99e83")
@Pattern(regexp = "^$|^[a-fA-F0-9]{44}$|^[a-fA-F0-9]{64}$", message = "문서 해시값은 44자 또는 64자의 16진수여야 합니다")
private String hash;
@ -359,6 +355,7 @@ public class KkotalkApiDTO {
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@ToString
public static class KkotalkErrorDTO {
/**
*

@ -29,7 +29,7 @@ import lombok.experimental.SuperBuilder;
*
* </pre>
*/
public class KkotalkDTO extends KkotalkApiDTO {
public class KkotalkDTO {
//------------------ envelop ----------------------------------------------------------------------
@Schema(name = "SendRequest DTO", description = "문서발송 request DTO")
@ -51,7 +51,7 @@ public class KkotalkDTO extends KkotalkApiDTO {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
private Envelope envelope;
private KkotalkApiDTO.Envelope envelope;
}
//------------------ envelop ----------------------------------------------------------------------
@ -75,7 +75,7 @@ public class KkotalkDTO extends KkotalkApiDTO {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
private List<Envelope> envelopes;
private List<KkotalkApiDTO.Envelope> envelopes;
}
@Schema(name = "BulkSendResponse DTO", description = "문서발송(bulk) response DTO")
@ -85,7 +85,7 @@ public class KkotalkDTO extends KkotalkApiDTO {
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public static class BulkSendResponse {
private List<EnvelopeRes> envelopes;
private List<KkotalkApiDTO.EnvelopeRes> envelopes;
}
@Schema(name = "BulkStatusRequest DTO", description = "문서상태조회[bulk] request DTO")

@ -248,6 +248,7 @@ public class KtGbsDTO {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
@JsonProperty("reqs")
private List<MsgRsltReqsData> reqs;
}

@ -7,6 +7,8 @@ import kr.xit.core.spring.util.MessageUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.*;
import org.egovframe.rte.fdl.cmmn.exception.BaseRuntimeException;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
@ -98,7 +100,10 @@ public class BizRuntimeException extends BaseRuntimeException {
* @param wrappedException Exception
*/
public static BizRuntimeException create(Throwable wrappedException) {
return new BizRuntimeException("BizRuntimeException without message", null, wrappedException);
if( wrappedException != null && wrappedException.getCause() != null){
return new BizRuntimeException(String.valueOf(wrappedException.getCause()), null, wrappedException);
}
return new BizRuntimeException(ObjectUtils.isNotEmpty(wrappedException.getMessage())? wrappedException.getMessage(): "BizRuntimeException 메세지 EMPTY", null, wrappedException);
}
/**

@ -0,0 +1,143 @@
package kr.xit.core.spring.config.support;
import java.io.*;
import java.net.*;
import java.util.*;
import org.apache.http.client.config.*;
import org.apache.http.config.*;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.context.annotation.*;
import org.springframework.http.*;
import org.springframework.http.client.*;
import org.springframework.web.client.*;
import com.fasterxml.jackson.databind.*;
import kr.xit.core.model.*;
import kr.xit.core.support.utils.*;
import lombok.extern.slf4j.*;
@Slf4j
@Configuration
public class RestTemplateConfig {
@Value("${app.restTemplate.socketTimeout:5000}")
private int socketTimeout;
@Value("${app.restTemplate.connectionTimeout:2000}")
private int connectionTimeout;
@Value("${app.restTemplate.requestTimeout:2000}")
private int requestTimeout;
@Value("${app.restTemplate.pool.max:10}")
private int poolMax;
@Value("${app.restTemplate.pool.max-per-route:5}")
private int maxPerRoute;
// FIXME:: RetryTemplate 미사용
// @Value("${app.restTemplate.retry.delay:200}")
// private int retryDelay;
//
// @Value("${app.restTemplate.retry.max-delay:5}")
// private int retryMaxDelay;
//
// @Value("${app.restTemplate.retry.max-attempts:3}")
// private int maxAttempts;
@Bean
public RestTemplate restTemplate() {
ConnectionConfig connectionConfig = ConnectionConfig.custom()
.build();
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(socketTimeout)
.setConnectTimeout(connectionTimeout)
.setConnectionRequestTimeout(requestTimeout)
.build();
PoolingHttpClientConnectionManager connectionPool = new PoolingHttpClientConnectionManager();
connectionPool.setMaxTotal(poolMax);
connectionPool.setDefaultMaxPerRoute(maxPerRoute);
connectionPool.setDefaultConnectionConfig(connectionConfig);
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setConnectionManager(connectionPool)
.setDefaultRequestConfig(requestConfig)
.evictExpiredConnections()
.build();
RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
restTemplate.setErrorHandler(new CustomResponseErrorHandler());
//restTemplate.getInterceptors().add(requestInterceptor);
return restTemplate;
}
// FIXME:: RetryTemplate 미사용
// @Bean
// public ClientHttpRequestInterceptor requestInterceptor(final RetryTemplate retryTemplate) {
// return (request, body, execution) ->
// retryTemplate.execute(context -> execution.execute(request, body));
// }
// @Bean
// public RetryTemplate retryTemplate() {
// BackOffPolicy backOffPolicy = BackOffPolicyBuilder.newBuilder()
// .delay(retryDelay)
// .maxDelay(retryMaxDelay)
// .build();
//
// RetryTemplate retryTemplate = new RetryTemplate();
// retryTemplate.setRetryPolicy(new SimpleRetryPolicy(maxAttempts));
// retryTemplate.setBackOffPolicy(backOffPolicy);
//
// return retryTemplate;
// }
class CustomResponseErrorHandler implements ResponseErrorHandler {
ObjectMapper objectMapper = new ObjectMapper();
public CustomResponseErrorHandler() {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
HttpStatus status = clientHttpResponse.getStatusCode();
return status.is4xxClientError() || status.is5xxServerError();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
String responseAsXmlString = toString(response.getBody());
String responseAsString = JsonUtils.jsonFromXml(responseAsXmlString, ApiResponseDTO.class);
log.error("ResponseBody: {}", responseAsString);
throw new CustomException(responseAsString);
}
@Override
public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
String responseAsXmlString = toString(response.getBody());
String responseAsString = JsonUtils.jsonFromXml(responseAsXmlString, ApiResponseDTO.class);
log.error("URL: {}, HttpMethod: {}, ResponseBody: {}", url, method, responseAsString);
throw new CustomException(responseAsString);
}
String toString(InputStream inputStream) {
Scanner s = new Scanner(inputStream).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
static class CustomException extends IOException {
public CustomException(String message) {
super(message);
}
}
}
}

@ -96,24 +96,33 @@ public class WebClientConfig {
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
return HttpClient.create(connectionProvider())
// SSL 설정 적용
.secure(t -> t.sslContext(sslContext))
.wiretap(this.getClass().getCanonicalName(), LogLevel.DEBUG,
AdvancedByteBufFormat.TEXTUAL)
// 연결 타임 아웃
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
// 응답 읽기 타임 아웃
.responseTimeout(Duration.ofMillis(this.connectTimeout))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS))
.addHandlerLast(
new WriteTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS)))
.doOnConnected(conn -> {
// 쓰기 타임
conn.addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
// 쓰기 타임
conn.addHandlerLast(new WriteTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
})
/*
FIXME:
Request processing failed; nested exception is org.springframework.web.reactive.function.client.WebClientRequestException: failed to resolve 'api.github.com' after 2 queries ;
nested exception is java.net.UnknownHostException: failed to resolve 'api.github.com' after 2 queries ] with root cause
io.netty.resolver.dns.DnsNameResolverTimeoutException: [/168.126.63.2:53] query via UDP timed out after 5000 milliseconds (no stack trace available)
*/
.resolver(DefaultAddressResolverGroup.INSTANCE);
// 예상 가능 DNS 문제 방지
//.resolver(DefaultAddressResolverGroup.INSTANCE)
// 기본 JVM DNS 사용 (프록시 환경에서 더 안정적임)
.resolver(spec -> spec.queryTimeout(Duration.ofSeconds(10)));
}catch(SSLException se){
throw BizRuntimeException.create(se.getMessage());
}

@ -0,0 +1,218 @@
package kr.xit.core.spring.util;
import java.io.*;
import java.net.*;
import java.net.http.HttpRequest;
import java.net.http.*;
import java.nio.charset.*;
import java.time.*;
import java.util.*;
import org.apache.commons.lang3.*;
import org.springframework.http.HttpHeaders;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import com.fasterxml.jackson.core.type.*;
import com.fasterxml.jackson.databind.*;
import kr.xit.biz.ens.model.cmm.*;
import kr.xit.core.consts.*;
import kr.xit.core.exception.*;
import kr.xit.core.spring.config.support.*;
import kr.xit.core.support.utils.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
* description : react Restfull Config
* packageName : kr.xit.core.spring.support
* fileName : ApiWebClientUtil
* author : julim
* date : 2023-09-06
* ======================================================================
*
* ----------------------------------------------------------------------
* 2023-09-06 julim
*
* </pre>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiHttpClientUtil {
private static final String AUTH_TYPE_BEARER = "bearer";
private final ObjectMapper objectMapper = new ObjectMapper();
private final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(10))
.build();
/**
* kakao WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url String
* @param method HttpMethod
* @param body Object
* @param rtnClzz Class<T>
* @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T>
*/
public <T> T exchangeKkopay(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) {
Map<String, String> map = new HashMap<>();
map.put(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
map.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
map.put(HttpHeaders.AUTHORIZATION,
String.format("%s %s", Constants.JwtToken.GRANT_TYPE.getCode(), ensDTO.getKakaoAccessToken()));
map.put(Constants.HeaderName.UUID.getCode(), ensDTO.getKakaoContractUuid());
return exchange(url, method, body, rtnClzz, map);
}
public <T> T exchangeKkopayAsIsCan(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, Map<String, String> headerMap) {
return exchange(url, method, body, rtnClzz, headerMap);
}
/**
* kakaotalk WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url String
* @param method HttpMethod
* @param body Object
* @param rtnClzz Class<T>
* @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T>
*/
public <T> T exchangeKkotalk(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) {
Map<String, String> map = new HashMap<>();
map.put(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
map.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
map.put(HttpHeaders.AUTHORIZATION, String.format("KakaoAK %s", ensDTO.getKakaoDealerRestApiKey()));
map.put(Constants.JwtToken.PARTNER_REST_API_KEY.getCode(), String.format("KakaoAK %s", ensDTO.getKakaoPartnerRestApiKey()));
map.put(Constants.JwtToken.SETTLE_ID.getCode(), ensDTO.getKakaoSettleId());
return exchange(url, method, body, rtnClzz, map);
}
/**
* KT-BC WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url String
* @param method HttpMethod
* @param body Object
* @param rtnClzz Class<T>
* @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T>
*/
public <T> T exchangeKt(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) {
final Map<String,String> headerMap = new HashMap<>();
headerMap.put(HttpHeaders.AUTHORIZATION, String.format("%s %s", AUTH_TYPE_BEARER, ensDTO.getKtAccessToken()));
headerMap.put("client-id", ensDTO.getKtClientId());
headerMap.put("client-tp", ensDTO.getKtClientTp());
return exchange(url, method, body, rtnClzz, headerMap);
}
/**
* KT-GIBIS WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url String
* @param method HttpMethod
* @param body Object
* @param rtnClzz Class<T>
* @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T>
*/
public <T> T exchangeKtGbs(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) {
final Map<String,String> headerMap = new HashMap<>();
headerMap.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
headerMap.put(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", ensDTO.getKtAccessToken()));
return exchange(url, method, body, rtnClzz, headerMap);
}
/**
* <pre>
* WebClient
* GET url (?key=value&key2=value2)
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url call url
* @param method POST|GET
* @param body JSON String type
* @param rtnClzz rtnClzz return type class
* (ex: new KkopayDocDTO.DocStatusResponse().getClass())
* @return T rtnClzz return DTO
* </pre>
*/
public <T> T exchange(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final Map<String,String> headerMap) {
if(HttpMethod.POST.equals(method)) {
final HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.POST(HttpRequest.BodyPublishers.ofString(ObjectUtils.isEmpty(body)? StringUtils.EMPTY: String.valueOf(body)))
.headers(convertHeadersToVarArgs(headerMap))
.build();
try {
final HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
validateSuccess(response);
return JsonUtils.toObject(response.body(), new TypeReference<T>() {});
} catch (InterruptedException e) {
throw BizRuntimeException.create(e);
} catch (IOException e) {
throw BizRuntimeException.create(e);
}
}else{
final HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.GET()
.headers(convertHeadersToVarArgs(headerMap))
.build();
try {
final HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
validateSuccess(response);
return JsonUtils.toObject(response.body(), new TypeReference<T>() {});
} catch (InterruptedException e) {
throw BizRuntimeException.create(e);
} catch (IOException e) {
throw BizRuntimeException.create(e);
}
}
}
private <T> void validateSuccess(final HttpResponse<T> response) {
final HttpStatus status = HttpStatus.resolve(response.statusCode());
if (status == null || status.isError()) {
//log.warn("URI: {}, STATUS: {}", response.uri(), response.statusCode());
//throw BizRuntimeException.create("처리실패");
}
log.info("URI: {}, STATUS: {}, BODY : {}, ", response.uri(), response.statusCode(), response.body());
}
private String[] convertHeadersToVarArgs(Map<String, String> headers) {
if(ObjectUtils.isEmpty(headers)) return new String[]{};
List<String> headerList = new ArrayList<>();
for(Map.Entry<String, String> e : headers.entrySet()){
headerList.add(e.getKey());
headerList.add(e.getValue());
}
return headerList.toArray(new String[0]);
}
}

@ -0,0 +1,213 @@
package kr.xit.core.spring.util;
import java.net.*;
import java.nio.charset.*;
import java.util.*;
import org.apache.commons.lang3.*;
import org.springframework.http.*;
import org.springframework.stereotype.*;
import org.springframework.web.client.*;
import org.springframework.web.util.*;
import kr.xit.biz.ens.model.cmm.*;
import kr.xit.core.consts.*;
import kr.xit.core.exception.*;
import kr.xit.core.model.*;
import kr.xit.core.spring.config.support.*;
import kr.xit.core.support.utils.*;
import lombok.*;
import lombok.extern.slf4j.*;
/**
* <pre>
* description : react Restfull Config
* packageName : kr.xit.core.spring.support
* fileName : ApiWebClientUtil
* author : julim
* date : 2023-09-06
* ======================================================================
*
* ----------------------------------------------------------------------
* 2023-09-06 julim
*
* </pre>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiRestTemplateUtil {
private static final String AUTH_TYPE_BEARER = "bearer";
private final RestTemplate restTemplate;
/**
* kakao WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url String
* @param method HttpMethod
* @param body Object
* @param rtnClzz Class<T>
* @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T>
*/
public <T> T exchangeKkopay(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) {
Map<String, String> map = new HashMap<>();
map.put(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
map.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
map.put(HttpHeaders.AUTHORIZATION,
String.format("%s %s", Constants.JwtToken.GRANT_TYPE.getCode(), ensDTO.getKakaoAccessToken()));
map.put(Constants.HeaderName.UUID.getCode(), ensDTO.getKakaoContractUuid());
return exchange(url, method, body, rtnClzz, map);
}
public <T> T exchangeKkopayAsIsCan(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, Map<String, String> headerMap) {
return exchange(url, method, body, rtnClzz, headerMap);
}
/**
* kakaotalk WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url String
* @param method HttpMethod
* @param body Object
* @param rtnClzz Class<T>
* @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T>
*/
public <T> T exchangeKkotalk(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) {
Map<String, String> map = new HashMap<>();
map.put(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
map.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
map.put(HttpHeaders.AUTHORIZATION, String.format("KakaoAK %s", ensDTO.getKakaoDealerRestApiKey()));
map.put(Constants.JwtToken.PARTNER_REST_API_KEY.getCode(), String.format("KakaoAK %s", ensDTO.getKakaoPartnerRestApiKey()));
map.put(Constants.JwtToken.SETTLE_ID.getCode(), ensDTO.getKakaoSettleId());
return exchange(url, method, body, rtnClzz, map);
}
/**
* KT-BC WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url String
* @param method HttpMethod
* @param body Object
* @param rtnClzz Class<T>
* @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T>
*/
public <T> T exchangeKt(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) {
final Map<String,String> headerMap = new HashMap<>();
headerMap.put(HttpHeaders.AUTHORIZATION, String.format("%s %s", AUTH_TYPE_BEARER, ensDTO.getKtAccessToken()));
headerMap.put("client-id", ensDTO.getKtClientId());
headerMap.put("client-tp", ensDTO.getKtClientTp());
return exchange(url, method, body, rtnClzz, headerMap);
}
/**
* KT-GIBIS WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url String
* @param method HttpMethod
* @param body Object
* @param rtnClzz Class<T>
* @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T>
*/
public <T> T exchangeKtGbs(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) {
final Map<String,String> headerMap = new HashMap<>();
headerMap.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
headerMap.put(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", ensDTO.getKtAccessToken()));
return exchange(url, method, body, rtnClzz, headerMap);
}
/**
* <pre>
* WebClient
* GET url (?key=value&key2=value2)
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url call url
* @param method POST|GET
* @param body JSON String type
* @param rtnClzz rtnClzz return type class
* (ex: new KkopayDocDTO.DocStatusResponse().getClass())
* @return T rtnClzz return DTO
* </pre>
*/
public <T> T exchange(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final Map<String,String> headerMap) {
if(HttpMethod.POST.equals(method)) {
return restTemplate.exchange(
url,
method,
new HttpEntity<>(ObjectUtils.isEmpty(body) ? null: body, getHeaders(headerMap)),
rtnClzz
).getBody();
}else{
return restTemplate.exchange(
url,
//createUrl(url),
method,
new HttpEntity<>(getHeaders(headerMap)),
rtnClzz,
body
).getBody();
}
}
/**
* <pre>
* WebClient form data
* -> application/x-www-form-urlencoded
* GET url (?key=value&key2=value2)
* (.onStatus status.is4xxClientError() || status.is5xxServerError())
* -> {@link WebClientConfig responseFilter}
* @param url call url
* @param method POST|GET
* @param body JSON String type
* @param rtnClzz rtnClzz return type class
* (ex: new KkopayDocDTO.DocStatusResponse().getClass())
* @return T rtnClzz return DTO
* </pre>
*/
public <T> T exchangeFormData(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final Map<String,String> headerMap) {
return exchange(url, method, JsonUtils.toMultiValue(body), rtnClzz, headerMap);
}
public <T> ApiResponseDTO<T> sendError(final Throwable e) {
return ErrorParse.extractError(e.getCause());
}
private URI createUrl(final String endPoint, final String... value) {
return UriComponentsBuilder.fromUriString(endPoint)
.build(value);
}
private HttpHeaders getHeaders(final HttpHeaders headers, final Map<String, String> map) {
if(ObjectUtils.isEmpty(map)) return headers;
for(Map.Entry<String, String> e : map.entrySet()){
headers.add(e.getKey(), e.getValue());
}
return headers;
}
private HttpHeaders getHeaders(final Map<String, String> map) {
HttpHeaders headers = new HttpHeaders();
if(ObjectUtils.isEmpty(map)) return headers;
for(Map.Entry<String, String> e : map.entrySet()){
headers.add(e.getKey(), e.getValue());
}
return headers;
}
}

@ -1,28 +1,26 @@
package kr.xit.core.support.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import kr.xit.core.exception.BizRuntimeException;
import kr.xit.core.spring.util.CoreSpringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.io.*;
import java.util.*;
import javax.xml.parsers.*;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.json.simple.*;
import org.springframework.util.*;
import org.w3c.dom.*;
import org.xml.sax.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.type.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.dataformat.xml.*;
import kr.xit.biz.ens.model.kakao.talk.*;
import kr.xit.core.exception.*;
import kr.xit.core.spring.util.*;
import lombok.*;
/**
@ -87,6 +85,20 @@ public class JsonUtils {
}
}
/**
* Json string -> class
* @return T
* @param str String
* @param typeRef TypeReference<T>
*/
public static <T> T toObject(final String str, TypeReference<T> typeRef) {
try {
return ObjectUtils.isNotEmpty(str)? OM.readValue(str, typeRef) : null;
} catch (JsonProcessingException e) {
throw BizRuntimeException.create(e.getLocalizedMessage());
}
}
/**
* Object -> class
* @param obj Object
@ -101,6 +113,22 @@ public class JsonUtils {
}
}
/**
* Generic Object(List> -> class
* java.util.LinkedHashMap is in module java.base of loader 'bootstrap' error
* @param obj Object
* @param typeRef TypeReference<T>
* @return T
*/
public static <T> T toObjByObj(final Object obj, TypeReference<T> typeRef) {
try {
return ObjectUtils.isNotEmpty(obj)? OM.convertValue(obj, new TypeReference<T>() {
}) : null;
} catch (IllegalArgumentException e) {
throw BizRuntimeException.create(e.getLocalizedMessage());
}
}
/**
* xml String -> cls
* @param xml String
@ -267,6 +295,22 @@ public class JsonUtils {
}
}
/**
* class type XML string -> JSON string
*
* @param xml the XML string to be converted
* @param cls the class type that the XML string should be converted into before transforming to JSON
* @return the JSON string representation of the input XML
*/
public static <T> String jsonFromXml(final String xml, final Class<T> cls) {
try {
XmlMapper xmlMapper = new XmlMapper();
return OM.writeValueAsString(xmlMapper.readValue(xml, cls));
}catch(Exception e) {
throw BizRuntimeException.create(e.getLocalizedMessage());
}
}
/**
* Json .
* @param json String json

@ -125,6 +125,27 @@ logging:
# Spring Security cors 설정 :: CorsConfiguration 설정 값
app:
# //FIXME::restful 연결 설정
restTemplate:
# 데이터 수신시 read timeout - ms
# 초과시 SocketTimeoutException
socketTimeout: 600000
# 서버 연결 허용 시간
connectionTimeout: 600000
# 연결 요청 대기 시간 - ms
# 초과시 ConnectionPoolTimeoutException
requestTimeout: 600000
pool:
max: 10
max-per-route: 5
# //FIXME::오류발생시 재시도 설정 - RetryTemplate 사용시
retry:
# 첫 재시도시 대기 시간 - ms
delay: 200
# 최대 대기시간 - ms
max-delay: 1000
# 최대 허용 횟수
max-attempts: 3
#강제로 swagger-url을 지정해야 하는 경우만 선언
#swagger-url:
cors:

@ -0,0 +1,139 @@
package kr.xit.core.support.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.util.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.*;
import org.mockito.junit.jupiter.*;
import com.fasterxml.jackson.core.*;
import kr.xit.biz.common.*;
import kr.xit.biz.ens.model.kakao.pay.*;
import kr.xit.biz.ens.model.kakao.talk.*;
import kr.xit.core.exception.*;
import kr.xit.core.model.*;
import lombok.*;
import lombok.extern.slf4j.*;
@Slf4j
@ExtendWith(MockitoExtension.class)
public class JsonUtilsTest {
/**
* This class provides methods to test the functionality of JsonUtils.
* The 'toObject' method converts a JSON string into an object of the specified class type.
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
static class TestObject {
private String name;
private int age;
}
@Test
void testSendBulks() throws JsonProcessingException {
String sendJson1 = """
{"envelopes":[{"externalId":"41220110994794765","envelopeId":"EVLP-01JCQC971QBBQQ6N54TKSEP9WT-00"}]}
""";
String sendJson = "{\"envelopes\":[{\"externalId\":\"41220110994794765\",\"envelopeId\":\"EVLP-01JCQC971QBBQQ6N54TKSEP9WT-00\"}]}";
String statusJson = """
{"envelopeStatus":[{"envelopeId":"EVLP-01JCQC971QBBQQ6N54TKSEP9WT-00","externalId":"41220110994794765","status":"RECEIVED","sentAt":"2024-11-15T16:40:57","receivedAt":"2024-11-15T16:40:57","readExpiredAt":"2024-12-31T23:59:59","isNotificationUnavailable":false}]}
""";
ApiResponseDTO kkoStatusList = getKkoStatusList();
List<ApiResponseDTO> apiResults = new ArrayList<>(Collections.singleton(kkoStatusList));
final List<KkopayDocBulkDTO.BulkStatusResponses> resList = new ArrayList<>();
//noinspection rawtypes
for(ApiResponseDTO apiResult : apiResults) {
if(apiResult.getData() != null) {
resList.add(toObjByObj(apiResult.getData(), KkopayDocBulkDTO.BulkStatusResponses.class));
}
}
toObject(sendJson, KkotalkDTO.BulkSendResponse.class);
toObject(statusJson, KkotalkDTO.BulkStatusResponse.class);
}
private static <T> T toObjByObj(Object obj, Class<T> clazz) {
return JsonUtils.toObjByObj(obj, clazz);
}
private static <T> T toObject(String obj, Class<T> clazz) {
return JsonUtils.toObject(obj, clazz);
}
@Test
void testToObject_validJson() {
String json = "{\"name\": \"John\", \"age\": 30}";
TestObject expectedObject = new TestObject("John", 30);
TestObject result = JsonUtils.toObject(json, TestObject.class);
assertEquals(expectedObject, result, "Converting JSON to TestObject did not produce the expected object.");
}
@Test
void testToObject_emptyString() {
String json = "";
assertNull(JsonUtils.toObject(json, TestObject.class), "Empty string should result in a null object.");
}
@Test
void testToObject_nullString() {
assertNull(JsonUtils.toObject(null, TestObject.class), "Null string should result in a null object.");
}
@Test
void testToObject_invalidJson() {
String json = "{\"name\": \"John\", \"age\": \"thirty\"}";
assertThrows(BizRuntimeException.class, () -> JsonUtils.toObject(json, TestObject.class),
"Invalid JSON should throw a BizRuntimeException.");
}
private static ApiResponseDTO getKkoStatusList(){
List<KkopayDocBulkDTO.BulkStatus> resDTO = new ArrayList<>();
resDTO.add(KkopayDocBulkDTO.BulkStatus.builder()
.document_binder_uuid("dkdkdkkdkd")
.error_code(ApiConstants.Error.NOT_FOUND.getCode())
.error_message("요청 정보를 찾을 수 없습니다. documentBinder를 찾을수 없습니다.")
.build()
);
resDTO.add(KkopayDocBulkDTO.BulkStatus.builder()
.document_binder_uuid("susuusus")
.status_data(KkopayDocAttrDTO.DocStatus.builder()
.doc_box_status(ApiConstants.KkopayDocStatus.RECEIVED)
.doc_box_sent_at(1443456743L)
.doc_box_received_at(1443456743L)
.user_notified_at(1443456743L)
.build())
.build()
);
resDTO.add(KkopayDocBulkDTO.BulkStatus.builder()
.document_binder_uuid("djdjfjjf")
.status_data(KkopayDocAttrDTO.DocStatus.builder()
.doc_box_status(ApiConstants.KkopayDocStatus.READ)
.doc_box_sent_at(1443456743L)
.doc_box_received_at(1443456743L)
.doc_box_read_at(1443456743L)
.authenticated_at(1443456743L)
.token_used_at(1443456743L)
.user_notified_at(1443456743L)
.build())
.build()
);
return ApiResponseDTO.success(KkopayDocBulkDTO.BulkStatusResponses.builder()
.documents(resDTO)
.build());
}
}
Loading…
Cancel
Save