feat: 카카오톡 전자우편 API 신규 추가 - D10_2

main
Jonguk. Lim 3 months ago
parent 4545e29e3a
commit adffdca5c5

@ -52,19 +52,19 @@ import lombok.extern.slf4j.Slf4j;
public class AsyncKkopayEltrcDocService extends AbstractService implements public class AsyncKkopayEltrcDocService extends AbstractService implements
IAsyncKkopayEltrcDocService { IAsyncKkopayEltrcDocService {
@Value("${app.contract.kakao.host}") @Value("${app.contract.kakao.api.pay.host}")
private String HOST; private String HOST;
@Value("#{'${app.contract.kakao.api.send}'.split(';')}") @Value("#{'${app.contract.kakao.api.pay.send}'.split(';')}")
private String[] API_SEND; private String[] API_SEND;
@Value("#{'${app.contract.kakao.api.validToken}'.split(';')}") @Value("#{'${app.contract.kakao.api.pay.validToken}'.split(';')}")
private String[] API_VALID_TOKEN; private String[] API_VALID_TOKEN;
@Value("#{'${app.contract.kakao.api.modifyStatus}'.split(';')}") @Value("#{'${app.contract.kakao.api.pay.modifyStatus}'.split(';')}")
private String[] API_MODIFY_STATUS; private String[] API_MODIFY_STATUS;
@Value("#{'${app.contract.kakao.api.findStatus}'.split(';')}") @Value("#{'${app.contract.kakao.api.pay.findStatus}'.split(';')}")
private String[] API_STATUS; private String[] API_STATUS;
@Value("#{'${app.contract.kakao.api.bulksend}'.split(';')}") @Value("#{'${app.contract.kakao.api.pay.bulksend}'.split(';')}")
private String[] API_BULKSEND; private String[] API_BULKSEND;
@Value("#{'${app.contract.kakao.api.bulkstatus}'.split(';')}") @Value("#{'${app.contract.kakao.api.pay.bulkstatus}'.split(';')}")
private String[] API_BULKSTATUS; private String[] API_BULKSTATUS;
private final ApiWebClientUtil webClient; private final ApiWebClientUtil webClient;
@ -104,7 +104,7 @@ public class AsyncKkopayEltrcDocService extends AbstractService implements
if(Objects.requireNonNull(errors).size() > 0) throw BizRuntimeException.create(errors.toString()); if(Objects.requireNonNull(errors).size() > 0) throw BizRuntimeException.create(errors.toString());
return CompletableFuture.supplyAsync(() -> return CompletableFuture.supplyAsync(() ->
webClient.exchangeKko(HOST + API_SEND[0], HttpMethod.valueOf(API_SEND[1]), JsonUtils.toJson(reqDTO), KkopayDocDTO.SendResponse.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode()))) webClient.exchangeKkopay(HOST + API_SEND[0], HttpMethod.valueOf(API_SEND[1]), JsonUtils.toJson(reqDTO), KkopayDocDTO.SendResponse.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode())))
.handle((r, e) -> { .handle((r, e) -> {
if(e != null){ if(e != null){
return webClient.sendError(e); return webClient.sendError(e);
@ -131,7 +131,7 @@ public class AsyncKkopayEltrcDocService extends AbstractService implements
.replace("{tokens}", reqDTO.getToken()); .replace("{tokens}", reqDTO.getToken());
return CompletableFuture.supplyAsync(() -> return CompletableFuture.supplyAsync(() ->
webClient.exchangeKko(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null, KkopayDocDTO.ValidTokenResponse.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode()))) webClient.exchangeKkopay(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null, KkopayDocDTO.ValidTokenResponse.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode())))
.handle((r, e) -> { .handle((r, e) -> {
if(e != null){ if(e != null){
return webClient.sendError(e); return webClient.sendError(e);
@ -161,7 +161,7 @@ public class AsyncKkopayEltrcDocService extends AbstractService implements
final String url = HOST + API_MODIFY_STATUS[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid()); final String url = HOST + API_MODIFY_STATUS[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid());
return CompletableFuture.supplyAsync(() -> return CompletableFuture.supplyAsync(() ->
webClient.exchangeKko(url, HttpMethod.valueOf(API_MODIFY_STATUS[1]), body, Void.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode()))) webClient.exchangeKkopay(url, HttpMethod.valueOf(API_MODIFY_STATUS[1]), body, Void.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode())))
.handle((r, e) -> { .handle((r, e) -> {
if(e != null){ if(e != null){
return webClient.sendError(e); return webClient.sendError(e);
@ -190,7 +190,7 @@ public class AsyncKkopayEltrcDocService extends AbstractService implements
final String url = HOST + API_STATUS[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid()); final String url = HOST + API_STATUS[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid());
return CompletableFuture.supplyAsync(() -> return CompletableFuture.supplyAsync(() ->
webClient.exchangeKko(url, HttpMethod.valueOf(API_STATUS[1]), null, KkopayDocDTO.DocStatusResponse.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode()))) webClient.exchangeKkopay(url, HttpMethod.valueOf(API_STATUS[1]), null, KkopayDocDTO.DocStatusResponse.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode())))
.handle((r, e) -> { .handle((r, e) -> {
if(e != null){ if(e != null){
return webClient.sendError(e); return webClient.sendError(e);
@ -212,7 +212,7 @@ public class AsyncKkopayEltrcDocService extends AbstractService implements
@Async("asyncExecutor") @Async("asyncExecutor")
public CompletableFuture<ApiResponseDTO<KkopayDocBulkDTO.BulkSendResponses>> requestSendBulk(final KkopayDocBulkDTO.BulkSendRequests reqDTO) { public CompletableFuture<ApiResponseDTO<KkopayDocBulkDTO.BulkSendResponses>> requestSendBulk(final KkopayDocBulkDTO.BulkSendRequests reqDTO) {
return CompletableFuture.supplyAsync(() -> return CompletableFuture.supplyAsync(() ->
webClient.exchangeKko(HOST + API_BULKSEND[0], HttpMethod.valueOf(API_BULKSEND[1]), JsonUtils.toJson(reqDTO), KkopayDocBulkDTO.BulkSendResponses.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode()))) webClient.exchangeKkopay(HOST + API_BULKSEND[0], HttpMethod.valueOf(API_BULKSEND[1]), JsonUtils.toJson(reqDTO), KkopayDocBulkDTO.BulkSendResponses.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode())))
.handle((r, e) -> { .handle((r, e) -> {
if(e != null){ if(e != null){
return webClient.sendError(e); return webClient.sendError(e);
@ -251,7 +251,7 @@ public class AsyncKkopayEltrcDocService extends AbstractService implements
} }
return CompletableFuture.supplyAsync(() -> return CompletableFuture.supplyAsync(() ->
webClient.exchangeKko(HOST + API_BULKSTATUS[0], HttpMethod.valueOf(API_BULKSTATUS[1]), JsonUtils.toJson(reqDTO), KkopayDocBulkDTO.BulkStatusResponses.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode()))) webClient.exchangeKkopay(HOST + API_BULKSTATUS[0], HttpMethod.valueOf(API_BULKSTATUS[1]), JsonUtils.toJson(reqDTO), KkopayDocBulkDTO.BulkStatusResponses.class, getEnsRlaybsnmDTO(reqDTO.getSignguCode(), reqDTO.getFfnlgCode())))
.handle((r, e) -> { .handle((r, e) -> {
if(e != null){ if(e != null){
return webClient.sendError(e); return webClient.sendError(e);

@ -52,21 +52,27 @@ import lombok.extern.slf4j.Slf4j;
public class KkopayEltrcDocService extends AbstractService implements public class KkopayEltrcDocService extends AbstractService implements
IKkopayEltrcDocService { IKkopayEltrcDocService {
@Value("${app.contract.kakao.host}") @Value("${app.contract.kakao.api.pay.host}")
private String HOST; private String HOST;
@Value("#{'${app.contract.kakao.api.send}'.split(';')}")
@Value("#{'${app.contract.kakao.api.pay.send}'.split(';')}")
private String[] API_SEND; private String[] API_SEND;
@Value("#{'${app.contract.kakao.api.validToken}'.split(';')}")
@Value("#{'${app.contract.kakao.api.pay.validToken}'.split(';')}")
private String[] API_VALID_TOKEN; private String[] API_VALID_TOKEN;
@Value("#{'${app.contract.kakao.api.modifyStatus}'.split(';')}")
@Value("#{'${app.contract.kakao.api.pay.modifyStatus}'.split(';')}")
private String[] API_MODIFY_STATUS; private String[] API_MODIFY_STATUS;
@Value("#{'${app.contract.kakao.api.findStatus}'.split(';')}")
private String[] API_STATUS; @Value("#{'${app.contract.kakao.api.pay.bulksend}'.split(';')}")
@Value("#{'${app.contract.kakao.api.bulksend}'.split(';')}")
private String[] API_BULKSEND; private String[] API_BULKSEND;
@Value("#{'${app.contract.kakao.api.bulkstatus}'.split(';')}")
@Value("#{'${app.contract.kakao.api.pay.bulkstatus}'.split(';')}")
private String[] API_BULKSTATUS; private String[] API_BULKSTATUS;
@Value("#{'${app.contract.kakao.api.pay.findStatus}'.split(';')}")
private String[] API_STATUS;
private final ApiWebClientUtil webClient; private final ApiWebClientUtil webClient;
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private static final CharSequence DOCUMENT_BINDER_UUID = "{document_binder_uuid}"; private static final CharSequence DOCUMENT_BINDER_UUID = "{document_binder_uuid}";
@ -101,7 +107,7 @@ public class KkopayEltrcDocService extends AbstractService implements
if(Checks.isEmpty(receiver.getBirthday())) Objects.requireNonNull(errors).add("receiver.birthday=받는이 생년월일은 필수입니다."); if(Checks.isEmpty(receiver.getBirthday())) Objects.requireNonNull(errors).add("receiver.birthday=받는이 생년월일은 필수입니다.");
} }
if(!Objects.requireNonNull(errors).isEmpty()) throw BizRuntimeException.create(errors.toString()); if(!Objects.requireNonNull(errors).isEmpty()) throw BizRuntimeException.create(errors.toString());
return webClient.exchangeKko(HOST + API_SEND[0], HttpMethod.valueOf(API_SEND[1]), JsonUtils.toJson(reqDTO), KkopayDocDTO.SendResponse.class, getRlaybsnmInfo(reqDTO)); return webClient.exchangeKkopay(HOST + API_SEND[0], HttpMethod.valueOf(API_SEND[1]), JsonUtils.toJson(reqDTO), KkopayDocDTO.SendResponse.class, getRlaybsnmInfo(reqDTO));
} }
/** /**
@ -119,7 +125,7 @@ public class KkopayEltrcDocService extends AbstractService implements
final String url = HOST final String url = HOST
+ API_VALID_TOKEN[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid()) + API_VALID_TOKEN[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid())
.replace("{tokens}", reqDTO.getToken()); .replace("{tokens}", reqDTO.getToken());
return webClient.exchangeKko(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null, KkopayDocDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO)); return webClient.exchangeKkopay(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null, KkopayDocDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO));
} }
/** /**
@ -140,7 +146,7 @@ public class KkopayEltrcDocService extends AbstractService implements
final String body = "{\"document\": {\"is_detail_read\": true} }"; final String body = "{\"document\": {\"is_detail_read\": true} }";
final String url = HOST + API_MODIFY_STATUS[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid()); final String url = HOST + API_MODIFY_STATUS[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid());
webClient.exchangeKko(url, HttpMethod.valueOf(API_MODIFY_STATUS[1]), body, Void.class, getRlaybsnmInfo(reqDTO)); webClient.exchangeKkopay(url, HttpMethod.valueOf(API_MODIFY_STATUS[1]), body, Void.class, getRlaybsnmInfo(reqDTO));
} }
/** /**
@ -161,7 +167,7 @@ public class KkopayEltrcDocService extends AbstractService implements
validate(reqDTO, null); validate(reqDTO, null);
final String url = HOST + API_STATUS[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid()); final String url = HOST + API_STATUS[0].replace(DOCUMENT_BINDER_UUID, reqDTO.getDocument_binder_uuid());
return webClient.exchangeKko(url, HttpMethod.valueOf(API_STATUS[1]), null, KkopayDocDTO.DocStatusResponse.class, getRlaybsnmInfo(reqDTO)); return webClient.exchangeKkopay(url, HttpMethod.valueOf(API_STATUS[1]), null, KkopayDocDTO.DocStatusResponse.class, getRlaybsnmInfo(reqDTO));
} }
/** /**
@ -218,7 +224,7 @@ public class KkopayEltrcDocService extends AbstractService implements
throw BizRuntimeException.create(errors.toString()); throw BizRuntimeException.create(errors.toString());
} }
return webClient.exchangeKko(HOST + API_BULKSEND[0], HttpMethod.valueOf(API_BULKSEND[1]), JsonUtils.toJson(reqDTO), KkopayDocBulkDTO.BulkSendResponses.class, getRlaybsnmInfo(reqDTO)); return webClient.exchangeKkopay(HOST + API_BULKSEND[0], HttpMethod.valueOf(API_BULKSEND[1]), JsonUtils.toJson(reqDTO), KkopayDocBulkDTO.BulkSendResponses.class, getRlaybsnmInfo(reqDTO));
} }
/** /**
@ -248,7 +254,7 @@ public class KkopayEltrcDocService extends AbstractService implements
if(!errors.isEmpty()) { if(!errors.isEmpty()) {
throw BizRuntimeException.create(errors.toString()); throw BizRuntimeException.create(errors.toString());
} }
return webClient.exchangeKko(HOST + API_BULKSTATUS[0], HttpMethod.valueOf(API_BULKSTATUS[1]), JsonUtils.toJson(reqDTO), KkopayDocBulkDTO.BulkStatusResponses.class, getRlaybsnmInfo(reqDTO)); return webClient.exchangeKkopay(HOST + API_BULKSTATUS[0], HttpMethod.valueOf(API_BULKSTATUS[1]), JsonUtils.toJson(reqDTO), KkopayDocBulkDTO.BulkStatusResponses.class, getRlaybsnmInfo(reqDTO));
} }
@Override @Override
@ -257,7 +263,7 @@ public class KkopayEltrcDocService extends AbstractService implements
.replace("{tokens}", reqDTO.getToken()); .replace("{tokens}", reqDTO.getToken());
// 유효성 검증 // 유효성 검증
final KkopayDocDTO.ValidTokenResponse validTokenRes = webClient.exchangeKko(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null, final KkopayDocDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkopay(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null,
KkopayDocDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO)); KkopayDocDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO));
if(!"USED".equals(validTokenRes.getToken_status())){ if(!"USED".equals(validTokenRes.getToken_status())){
@ -270,7 +276,7 @@ public class KkopayEltrcDocService extends AbstractService implements
// 정상 : HttpStatus.NO_CONTENT(204) return // 정상 : HttpStatus.NO_CONTENT(204) return
// error : body에 error_code, error_message return // error : body에 error_code, error_message return
final KkopayErrorDTO errorDTO = webClient.exchangeKko(url2, HttpMethod.valueOf(API_MODIFY_STATUS[1]), body, KkopayErrorDTO.class, getRlaybsnmInfo(reqDTO)); final KkopayErrorDTO errorDTO = webClient.exchangeKkopay(url2, HttpMethod.valueOf(API_MODIFY_STATUS[1]), body, KkopayErrorDTO.class, getRlaybsnmInfo(reqDTO));
if(errorDTO != null){ if(errorDTO != null){
return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage()); return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage());
} }

@ -0,0 +1,95 @@
package kr.xit.ens.kakao.v2.service;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDocDTO;
import kr.xit.core.model.ApiResponseDTO;
/**
* <pre>
* description :
* packageName : kr.xit.ens.kakao.v1.service
* fileName : IKkopayEltrcDocService
* author : julim
* date : 2023-04-28
* ======================================================================
*
* ----------------------------------------------------------------------
* 2023-04-28 julim
*
* </pre>
*/
public interface IKkotalkEltrcDocService {
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkotalkDocDTO.SendRequest
* @return KkotalkDocDTO.SendResponse
*/
KkotalkDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO);
/**
* <pre>
* (Redirect URL /)
* </pre>
* @param reqDTO KkotalkDocDTO.ValidTokenRequest
* @return KkotalkDocDTO.ValidTokenResponse>
*/
KkotalkDocDTO.ValidTokenResponse validToken(final KkotalkDocDTO.ValidTokenRequest reqDTO);
/**
* <pre>
* API
* -. . (OTT ) API .
* -.
* 1) API .
* 2) API(/v1/envelopes/${ENVELOPE_ID}/read) read_at ) .
* </pre>
* @param reqDTO KkotalkDTO.EnvelopeId
*/
void modifyStatus(final KkotalkDTO.EnvelopeId reqDTO);
/**
* <pre>
* API
* -. .
* : , flow
* : polling , 5 .
* -.doc_box_status
* : RECEIVE(, ) > READ()/EXPIRED
* </pre>
* @param reqDTO KkotalkDTO.EnvelopeId
* @return KkotalkDocDTO.EnvelopeStatusResponse
*/
KkotalkDocDTO.EnvelopeStatusResponse findStatus(final KkotalkDocDTO.EnvelopeId reqDTO);
/**
* <pre>
* (bulk)
* -. (bulk) .
* </pre>
* @param reqDTO KkopayDocBulkDTO.BulkSendRequests
* @return KkopayDocBulkDTO.BulkSendResponses
*/
KkotalkDTO.BulkSendResponse requestSendBulk(final KkotalkDTO.BulkSendRequest reqDTO);
/**
* <pre>
* (bulk) API
* -. .
* : , flow
* : polling , 5 .
* : RECEIVED(,) > READ()/EXPIRED
* </pre>
* @param reqDTO KkotalkDTO.BulkStatusRequest
* @return KkotalkDTO.BulkStatusResponse
*/
KkotalkDTO.BulkStatusResponse findBulkStatus(final KkotalkDTO.BulkStatusRequest reqDTO);
ApiResponseDTO<KkotalkDocDTO.ValidTokenResponse> findMyDocReadyAndMblPage(KkotalkDocDTO.ValidTokenRequest reqDTO);
}

@ -0,0 +1,332 @@
package kr.xit.ens.kakao.v2.service;
import java.util.ArrayList;
import java.util.List;
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.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.v2.KkotalkDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDocDTO;
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;
/**
* <pre>
* description :
* packageName : kr.xit.ens.kakao.v1.service
* fileName : KkopayEltrcDocService
* author : julim
* date : 2023-04-28
* ======================================================================
*
* ----------------------------------------------------------------------
* 2023-04-28 julim
*
* </pre>
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class KkotalkEltrcDocService extends AbstractService implements
IKkotalkEltrcDocService {
@Value("${app.contract.kakao.api.talk.host}")
private String HOST;
@Value("#{'${app.contract.kakao.api.talk.send}'.split(';')}")
private String[] API_SEND;
@Value("#{'${app.contract.kakao.api.talk.bulksend}'.split(';')}")
private String[] API_BULKSEND;
@Value("#{'${app.contract.kakao.api.talk.validToken}'.split(';')}")
private String[] API_VALID_TOKEN;
@Value("#{'${app.contract.kakao.api.talk.modifyStatus}'.split(';')}")
private String[] API_MODIFY_STATUS;
@Value("#{'${app.contract.kakao.api.talk.bulkstatus}'.split(';')}")
private String[] API_BULKSTATUS;
private final ApiWebClientUtil webClient;
private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
private static final CharSequence ENVELOPE_ID = "{ENVELOPE_ID}";
/**
* <pre>
* : POST
* -.
* </pre>
* @param reqDTO KkoPayEltrDocDTO.RequestSendReq
* @return ApiResponseDTO<KkopayDocDTO.SendResponse>
*/
@Override
@TraceLogging
public KkotalkDTO.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 KkotalkDocDTO.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 webClient.exchangeKkopay(
HOST + API_SEND[0].replace("{PRODUCT_CODE}", reqDTO.getProductCode()),
HttpMethod.valueOf(API_SEND[1]),
JsonUtils.toJson(envelope),
KkotalkDTO.SendResponse.class,
getRlaybsnmInfo(reqDTO));
}
/**
* <pre>
* (Redirect URL /) : GET
* </pre>
* @param reqDTO KkopayDocDTO.ValidTokenRequest
* @return ApiResponseDTO<KkopayDocDTO.ValidTokenResponse>
*/
@Override
@TraceLogging
public KkotalkDocDTO.ValidTokenResponse validToken(final KkotalkDocDTO.ValidTokenRequest reqDTO) {
validate(reqDTO, null);
return webClient.exchangeKkotalk(
HOST
+ API_VALID_TOKEN[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId())
.replace("{TOKEN}", reqDTO.getToken()),
HttpMethod.valueOf(API_VALID_TOKEN[1]),
null,
KkotalkDocDTO.ValidTokenResponse.class,
getRlaybsnmInfo(reqDTO));
}
/**
* <pre>
* API : POST
* -. . (OTT ) API .
* -.
* 1) API .
* 2) API(/v1/envelopes/${ENVELOPE_ID}/read) read_at ) .
* </pre>
* @param reqDTO KkopayDocAttrDTO.EnvelopeId
*/
@Override
@TraceLogging
public void modifyStatus(final KkotalkDTO.EnvelopeId reqDTO){
validate(reqDTO.getEnvelopeId(), null);
final String url = HOST + API_MODIFY_STATUS[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId());
webClient.exchangeKkopay(url, HttpMethod.valueOf(API_MODIFY_STATUS[1]), null, Void.class, getRlaybsnmInfo(reqDTO));
}
/**
* <pre>
* API : GET
* -. .
* : , flow
* : polling , 5 .
* : RECEIVE(,) > READ()/EXPIRED
* </pre>
* @param reqDTO KkotalkDTO.EnvelopeId
* @return KkotalkDocDTO.EnvelopeStatusResponse
*/
@Override
@TraceLogging
public KkotalkDocDTO.EnvelopeStatusResponse findStatus(final KkotalkDocDTO.EnvelopeId reqDTO){
validate(reqDTO, null);
KkotalkDTO.BulkStatusResponse res = webClient.exchangeKkotalk(
HOST + API_BULKSTATUS[0],
HttpMethod.valueOf(API_BULKSTATUS[1]),
JsonUtils.toJson(List.of(reqDTO.getEnvelopeId())),
KkotalkDTO.BulkStatusResponse.class,
getRlaybsnmInfo(reqDTO));
return res.getEnvelopeStatus().get(0);
}
/**
* <pre>
* : POST
* -. .
* </pre>
* @param reqDTO KkotalkDTO.BulkSendRequest
* @return KkotalkDTO.BulkSendResponse
*/
@Override
@TraceLogging
public KkotalkDTO.BulkSendResponse requestSendBulk(final KkotalkDTO.BulkSendRequest reqDTO) {
if(Checks.isEmpty(reqDTO.getProductCode())){
throw BizRuntimeException.create("상품 코드는 필수 입니다.");
}
List<String> errors = new ArrayList<>();
List<KkotalkDocDTO.Envelope> envelopes = reqDTO.getEnvelopes();
for(int idx = 0; idx < envelopes.size(); idx++) {
final Set<ConstraintViolation<KkotalkDocDTO.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 KkotalkDocDTO.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());
}
return webClient.exchangeKkotalk(
HOST + API_SEND[0].replace("{PRODUCT_CODE}", reqDTO.getProductCode()),
HttpMethod.valueOf(API_BULKSEND[1]),
JsonUtils.toJson(envelopes),
KkotalkDTO.BulkSendResponse.class,
getRlaybsnmInfo(reqDTO));
}
/**
* <pre>
* (bulk) API : POST
* -. .
* : , flow
* : polling , 5 .
* : RECEIVE(,) > READ()/EXPIRED
* </pre>
* @param reqDTO KkotalkDTO.BulkStatusRequest
* @return KkotalkDTO.BulkStatusResponse
*/
@Override
@TraceLogging
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());
}
return webClient.exchangeKkopay(
HOST + API_BULKSTATUS[0],
HttpMethod.valueOf(API_BULKSTATUS[1]),
JsonUtils.toJson(envelopes),
KkotalkDTO.BulkStatusResponse.class,
getRlaybsnmInfo(reqDTO));
}
@Override
public ApiResponseDTO<KkotalkDocDTO.ValidTokenResponse> findMyDocReadyAndMblPage(final KkotalkDocDTO.ValidTokenRequest reqDTO) {
final String url = HOST + API_VALID_TOKEN[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId())
.replace("{TOKEN}", reqDTO.getToken());
// 유효성 검증
final KkotalkDocDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkotalk(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null,
KkotalkDocDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO));
// FIXME: USED ??
if(!"USED".equals(validTokenRes.getStatus())){
return ApiResponseDTO.error(validTokenRes.getErrorCode(), validTokenRes.getErrorMessage());
}
// 문서상태 변경
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 KkotalkDocDTO.KkotalkErrorDTO errorDTO = webClient.exchangeKkotalk(url2, HttpMethod.valueOf(API_MODIFY_STATUS[1]), null, KkotalkDocDTO.KkotalkErrorDTO.class, getRlaybsnmInfo(reqDTO));
if(errorDTO != null){
return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage());
}
return ApiResponseDTO.success();
}
//-------------------------------------------------------------------------------------------------------------------
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;
}
private CmmEnsRlaybsnmDTO getRlaybsnmInfo(final CmmEnsRequestDTO request){
return CmmEnsUtils.getRlaybsnmInfo(request.getSignguCode(), request.getFfnlgCode(), SndngSeCode.KAKAO);
}
}

@ -0,0 +1,201 @@
package kr.xit.ens.kakao.v2.web;
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.biz.ens.model.kakao.v2.KkotalkDTO;
import kr.xit.biz.ens.model.kakao.v2.KkotalkDocDTO;
import kr.xit.core.model.ApiResponseDTO;
import kr.xit.core.model.IApiResponse;
import kr.xit.ens.kakao.v2.service.IKkotalkEltrcDocService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* <pre>
* description : controller
* packageName : kr.xit.ens.kakao.v2.web
* fileName : KkotalkEltrcDocController
* author : julim
* date : 2024-08-12
* ======================================================================
*
* ----------------------------------------------------------------------
* 2024-08-12 julim
*
* </pre>
*/
@Tag(name = "KkotalkEltrcDocController", description = "카카오톡 전자문서 API")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/api/ens/kakao/v2")
public class KkotalkEltrcDocController {
private final IKkotalkEltrcDocService service;
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkopayDocDTO.SendRequest
* @return ApiResponseDTO<KkopayDocDTO.SendResponse>
*/
@Operation(summary = "문서발송 요청", description = "카카오톡 전자문서 서버로 문서발송 처리를 요청")
@io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = {
@Content(mediaType = "application/json", examples = {
@ExampleObject(
name = "D10",
value = """
{
"productCode": "D10_1",
"envelope": {
"title": "전자문서",
"content": {
"link": "https://nps.or.kr"
},
"guide": "국민연금 공단에서 보내는 문서입니다.",
"payload": "이용기관 페이로드",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"useNonPersonalizedNotification": true,
"phoneNumber": "01099999999",
"name": "홍길동",
"birthday": "20000303",
"externalId": "external_id1"
},
"signguCode": "51110",
"ffnlgCode": "11"
}
"""),
@ExampleObject(
name = "D11",
value = """
{
"productCode": "D11_1",
"envelope": {
"title": "전자문서",
"content": {
"html": "<!DOCTYPEhtml><html><body><h1>MyFirstHeading</h1><p>Myfirstparagraph.</p></body></html>"
},
"guide": "국민연금 공단에서 보내는 문서입니다.",
"readExpiresAt": "2023-12-31T10:00:00",
"reviewExpiresAt": "2023-12-31T13:00:00",
"ci": "${CI}"
},
"signguCode": "51110",
"ffnlgCode": "11"
}
""")
})
})
@PostMapping(value = "/envelopes", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse requestSend(
@RequestBody final KkotalkDTO.SendRequest reqDTO
) {
return ApiResponseDTO.success(service.requestSend(reqDTO));
}
/**
* <pre>
* (Redirect URL /)
* </pre>
* @param reqDTO KkopayDocDTO.ValidTokenRequest
* @return ApiResponseDTO<KkopayDocDTO.ValidTokenResponse>
*/
@Operation(summary = "토큰 유효성 검증", description = "Redirect URL 접속 허용/불허")
@PostMapping(value = "/validToken", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse validToken(
@RequestBody final KkotalkDTO.ValidTokenRequest reqDTO
) {
return ApiResponseDTO.success(service.validToken(reqDTO));
}
/**
* <pre>
* API
* -. . (OTT ) API .
* -.
* 1) API .
* 2) API(/v1/envelopes/${ENVELOPE_ID}/read) read_at ) .
* </pre>
* @param reqDTO KkotalkDocDTO.EnvelopeStatusResponse
* @return ApiResponseDTO<Void>
*/
@Operation(summary = "문서열람처리(문서 상태 변경)", description = "문서열람처리(문서 상태 변경)")
@PostMapping(value = "/modifyStatus", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse modifyStatus(
@RequestBody final KkotalkDocDTO.EnvelopeId reqDTO
) {
service.modifyStatus(reqDTO);
return ApiResponseDTO.empty();
}
/**
* <pre>
* API
* -. .
* : , flow
* : polling , 5 .
* -.doc_box_status
* : SENT() > RECEIVED() > READ()/EXPIRED( )
* </pre>
* @param reqDTO KkotalkDTO.EnvelopeId
* @return ApiResponseDTO<KkotalkDocDTO.EnvelopeStatusResponse>
*/
@Operation(summary = "문서 상태 조회", description = "문서 상태 조회")
@PostMapping(value = "/findStatus", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse findStatus(
@RequestBody final KkotalkDocDTO.EnvelopeId reqDTO
) {
return ApiResponseDTO.success(service.findStatus(reqDTO));
}
// /**
// * <pre>
// * 모바일웹 연계 문서발송 요청
// * -.이용기관 서버에서 전자문서 서버로 문서발송 처리를 요청합니다.
// * </pre>
// * @param reqDTO KkopayDocBulkDTO.BulkSendRequests
// * @return BulkStatusResponses.BulkSendResponses
// */
// /*
// @Operation(summary = "대량 문서발송 요청 -> batch sendBulks 에서 호출", description = "카카오페이 전자문서 서버로 대량 문서발송 처리를 요청 -> batch sendBulks 에서 호출")
// @PostMapping(value = "/documents/bulk", produces = MediaType.APPLICATION_JSON_VALUE)
// public BulkSendResponses requestSendBulk(
// @RequestBody final BulkSendRequests reqDTO
// ) {
// return service.requestSendBulk(reqDTO);
// }
// */
@Operation(summary = "대량 문서발송 요청 -> batch sendBulks 에서 호출", description = "카카오페이 전자문서 서버로 대량 문서발송 처리를 요청 -> batch sendBulks 에서 호출")
@PostMapping(value = "/envelopes/bulk", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse requestSendBulk(
@RequestBody final KkotalkDTO.BulkSendRequest reqDTO
) {
return ApiResponseDTO.success(service.requestSendBulk(reqDTO));
}
/**
* <pre>
*
* -. .
* </pre>
* @param reqDTO KkotalkDocDTO.BulkStatusRequest
* @return KkotalkDocDTO.BulkStatusResponse
*/
@Operation(summary = "대량 문서 상태 조회 요청 -> batch statusBulks 에서 호출", description = "카카오페이 전자문서 서버로 대량 문서 상태 조회 요청 -> batch statusBulks 에서 호출")
@PostMapping(value = "/documents/bulk/status", produces = MediaType.APPLICATION_JSON_VALUE)
public IApiResponse findBulkStatus(
@RequestBody final KkotalkDTO.BulkStatusRequest reqDTO
) {
return ApiResponseDTO.success(service.findBulkStatus(reqDTO));
}
}

@ -14,16 +14,24 @@ app:
maxPoolSize: 10 maxPoolSize: 10
kakao: kakao:
bulk-max-cnt: 10 bulk-max-cnt: 10
host: https://docs-gw.kakaopay.com
# token: dd394da7f66211eb9cbe46e139ceffc2 # token: dd394da7f66211eb9cbe46e139ceffc2
# uuid: CON-41ef0535f67211ebbdedd2e6ed332381 # uuid: CON-41ef0535f67211ebbdedd2e6ed332381
api: api:
pay:
host: https://docs-gw.kakaopay.com
send: /v1/documents;POST send: /v1/documents;POST
bulksend: /v1/documents/bulk;POST
validToken: /v1/{document_binder_uuid}/tokens/{tokens};GET validToken: /v1/{document_binder_uuid}/tokens/{tokens};GET
modifyStatus: /v1/documents/{document_binder_uuid};POST modifyStatus: /v1/documents/{document_binder_uuid};POST
findStatus: /v1/documents/{document_binder_uuid}/status;GET
bulksend: /v1/documents/bulk;POST
bulkstatus: /v1/documents/bulk/status;POST bulkstatus: /v1/documents/bulk/status;POST
findStatus: /v1/documents/{document_binder_uuid}/status;GET
talk:
host: https://edoc-gw.kakao.com
send: /v1/envelopes/{PRODUCT_CODE};POST
bulksend: /v1/bulk/envelopes/{PRODUCT_CODE};POST
validToken: /v1/envelopes/{ENVELOPE_ID}/tokens/{TOKEN}/verify;GET
modifyStatus: /v1/envelopes/{ENVELOPE_ID}/read;POST
bulkstatus: /v1/envelopes/status;POST
nice: nice:
host: https://svc.niceapi.co.kr:22001 host: https://svc.niceapi.co.kr:22001
# signgu-code: 11000 # signgu-code: 11000

@ -88,7 +88,7 @@ public class EnsBatchExtractService extends AbstractService implements
// 모바일 페이지 컨텐트 생성 // 모바일 페이지 컨텐트 생성
if (Checks.isNotEmpty(t.getDocument_binder_uuid())) { if (Checks.isNotEmpty(t.getDocument_binder_uuid())) {
mapper.insertMobilePageManage(t.getExternal_document_uuid()); mapper.insertMobilePageManage(t.getExternal_document_uuid());
code = ApiConstants.DocBoxStatus.SENT.getCode(); code = ApiConstants.KkopayDocStatus.SENT.getCode();
}else{ }else{
code = t.getError_code(); code = t.getError_code();
} }

@ -66,7 +66,7 @@ import lombok.extern.slf4j.Slf4j;
public class EnsBatchSendService extends AbstractService implements IEnsBatchSendService { public class EnsBatchSendService extends AbstractService implements IEnsBatchSendService {
@Value("${app.contract.host}") @Value("${app.contract.host}")
private String apiHost; private String apiHost;
@Value("${app.contract.kakao.api.bulksend}") @Value("${app.contract.kakao.api.pay.bulksend}")
private String apiKkoBulkSend; private String apiKkoBulkSend;
@Value("${app.contract.kt.api.bulksend}") @Value("${app.contract.kt.api.bulksend}")
private String apiKtBcBulkSend; private String apiKtBcBulkSend;

@ -51,7 +51,7 @@ import lombok.RequiredArgsConstructor;
public class EnsBatchStatusService extends AbstractService implements IEnsBatchStatusService { public class EnsBatchStatusService extends AbstractService implements IEnsBatchStatusService {
@Value("${app.contract.host}") @Value("${app.contract.host}")
private String apiHost; private String apiHost;
@Value("${app.contract.kakao.api.bulkstatus}") @Value("${app.contract.kakao.api.pay.bulkstatus}")
private String apiKkoBulkStatus; private String apiKkoBulkStatus;
@Value("${app.contract.kakao.bulk-max-cnt}") @Value("${app.contract.kakao.bulk-max-cnt}")

@ -43,9 +43,9 @@ import lombok.RequiredArgsConstructor;
public class ApiCallTestController { public class ApiCallTestController {
@Value("${app.contract.host}") @Value("${app.contract.host}")
private String apiHost; private String apiHost;
@Value("${app.contract.kakao.api.bulksend}") @Value("${app.contract.kakao.api.pay.bulksend}")
private String apiBulkSend; private String apiBulkSend;
@Value("${app.contract.kakao.api.bulkstatus}") @Value("${app.contract.kakao.api.pay.bulkstatus}")
private String apiBulkStatus; private String apiBulkStatus;
private final ApiWebClientUtil apiWebClient; private final ApiWebClientUtil apiWebClient;
@ -97,7 +97,7 @@ public class ApiCallTestController {
.append("http://localhost:8081") .append("http://localhost:8081")
.append("/api/ens/kakao/v1/documents"); .append("/api/ens/kakao/v1/documents");
return apiWebClient.exchangeKko( return apiWebClient.exchangeKkopay(
url.toString(), url.toString(),
HttpMethod.POST, HttpMethod.POST,
JsonUtils.toJson(reqDTO), JsonUtils.toJson(reqDTO),
@ -154,7 +154,7 @@ public class ApiCallTestController {
) { ) {
final String url = apiHost + apiBulkSend; final String url = apiHost + apiBulkSend;
return apiWebClient.exchangeKko( return apiWebClient.exchangeKkopay(
url, url,
HttpMethod.POST, HttpMethod.POST,
JsonUtils.toJson(reqDTO), JsonUtils.toJson(reqDTO),
@ -190,7 +190,7 @@ public class ApiCallTestController {
) { ) {
final String url = apiHost + apiBulkStatus;; final String url = apiHost + apiBulkStatus;;
return apiWebClient.exchangeKko( return apiWebClient.exchangeKkopay(
url, url,
HttpMethod.POST, HttpMethod.POST,
JsonUtils.toJson(reqDTO), JsonUtils.toJson(reqDTO),

@ -107,11 +107,13 @@ public class ApiConstants {
} }
/** /**
* <pre>
* *
* SENT() > RECEIVED() > READ()/EXPIRED( ) * SENT() > RECEIVED() > READ()/EXPIRED( )
* </pre>
*/ */
@Getter @Getter
public enum DocBoxStatus { public enum KkopayDocStatus {
SENT("SENT") SENT("SENT")
, RECEIVED("RECEIVED") , RECEIVED("RECEIVED")
, READ("READ") , READ("READ")
@ -120,7 +122,28 @@ public class ApiConstants {
private final String code; private final String code;
DocBoxStatus(String code) { KkopayDocStatus(String code) {
this.code = code;
}
}
/**
* <pre>
*
* RECEIVE(, ) > READ()/EXPIRED( )
* </pre>
*/
@Getter
public enum KkotalkDocStatus {
RECEIVED("RECEIVE")
, READ("READ")
, EXPIRED("EXPIRED")
;
private final String code;
KkotalkDocStatus(String code) {
this.code = code; this.code = code;
} }

@ -220,11 +220,11 @@ public class KkopayDocAttrDTO {
* READ - OTT API * READ - OTT API
* EXPIRED - * EXPIRED -
* </pre> * </pre>
* @see ApiConstants.DocBoxStatus * @see ApiConstants.KkopayDocStatus
*/ */
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, maxLength = 20, title = "진행상태(max:20)", example = " ") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, maxLength = 20, title = "진행상태(max:20)", example = " ")
@Size(min = 1, max = 20, message = "진행상태는 필수입니다(max:20)") @Size(min = 1, max = 20, message = "진행상태는 필수입니다(max:20)")
private ApiConstants.DocBoxStatus doc_box_status; private ApiConstants.KkopayDocStatus doc_box_status;
/** /**
* (long max:10) * (long max:10)

@ -0,0 +1,116 @@
package kr.xit.biz.ens.model.kakao.v2;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.Size;
import io.swagger.v3.oas.annotations.media.Schema;
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.experimental.SuperBuilder;
/**
* <pre>
* description : DTO
*
* packageName : kr.xit.ens.model.kakao.v2
* fileName : KkotalkDTO
* author : limju
* date : 2024-08-12
* ======================================================================
*
* ----------------------------------------------------------------------
* 2024-08-12 limju
*
* </pre>
*/
public class KkotalkDTO extends KkotalkDocDTO {
//------------------ envelop ----------------------------------------------------------------------
@Schema(name = "SendRequest DTO", description = "문서발송 request DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public static class SendRequest extends CmmEnsRequestDTO {
/**
* <pre>
* -
* D10_1|D10_2|D11_1|D11_2
* </pre>
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "상품코드", example = "D10_2", allowableValues = {"D10_1","D10_2","D11_1","D11_2"})
@Size(min = 3, max = 5, message = "상품 코드는 필수 입니다(\"D10_1\",\"D10_2\",\"D11_1\",\"D11_2\"")
@Builder.Default
private String productCode = "D10_2";
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
private Envelope envelope;
}
//------------------ envelop ----------------------------------------------------------------------
//------------------ bulk ----------------------------------------------------------------------
@Schema(name = "BulkSendRequest DTO", description = "문서발송[bulk] request DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public static class BulkSendRequest extends CmmEnsRequestDTO {
/**
* <pre>
* -
* D10_1|D10_2|D11_1|D11_2
* </pre>
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "상품코드", example = "D10_2", allowableValues = {"D10_1","D10_2","D11_1","D11_2"})
@Size(min = 3, max = 5, message = "상품 코드는 필수 입니다(\"D10_1\",\"D10_2\",\"D11_1\",\"D11_2\"")
@Builder.Default
private String productCode = "D10_2";
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
private List<Envelope> envelopes;
}
@Schema(name = "BulkSendResponse DTO", description = "문서발송(bulk) response DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public static class BulkSendResponse extends KkotalkErrorDTO {
private List<EnvelopeRes> envelopeIds;
}
@Schema(name = "BulkStatusRequest DTO", description = "문서상태조회[bulk] request DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public static class BulkStatusRequest extends CmmEnsRequestDTO {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
private List<String> envelopes;
}
@Schema(name = "BulkStatusResponse DTO", description = "문서상태조회(bulk) response DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public static class BulkStatusResponse implements IApiResponse {
private List<KkotalkDocDTO.EnvelopeStatusResponse> envelopeStatus;
}
//------------------ bulk ----------------------------------------------------------------------
}

@ -0,0 +1,427 @@
package kr.xit.biz.ens.model.kakao.v2;
import javax.validation.Valid;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
import com.fasterxml.jackson.annotation.JsonInclude;
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.experimental.SuperBuilder;
/**
* <pre>
* description : DTO
* API json .
* => @JsonInclude(JsonInclude.Include.NON_EMPTY)
* packageName : kr.xit.biz.ens.model.kakao.v2
* fileName : KkotalkDocDTO
* author : limju
* date : 2024-08-12
* ======================================================================
*
* ----------------------------------------------------------------------
* 2024-08-12 limju
*
* </pre>
*/
public class KkotalkDocDTO {
//------------------- Envelope ------------------------------------------------------------------------------------------------
@Schema(name = "Envelope", description = "문서발송(단건) 요청 파라메터 DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public static class Envelope implements IApiResponse {
/**
* : - max 40
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "발송할 문서의 제목", example = "문서 제목")
@Size(max = 40, message = "발송할 문서의 제목은 필수 입니다(max:40)")
private String title;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@Valid
private Content content;
/**
* <pre>
* () hash
* D10_2
* </pre>
*/
@Schema(title = "문서 원문(열람정보)에 대한 hash 값", example = "6EFE827AC88914DE471C621AE")
@Pattern(regexp = "^$|^[a-fA-F0-9]{44}$|^[a-fA-F0-9]{64}$", message = "문서 해시값은 44자 또는 64자의 16진수여야 합니다")
private String hash;
/**
* (: 500) -
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "문서 정보 안내 화면에 노출할 문구(최대: 500자)", example = "문서 정보 안내 화면에 노출할 문구")
@Size(min=1, max = 200, message = "문서 정보 안내 화면에 노출할 문구(max=500)")
private String guide;
/**
* payload
*/
@Schema(title = "payload", example = "payload 파라미터 입니다.")
@Size(max = 200, message = "payload(max=200)")
private String payload;
/**
* <pre>
* (: 6 ) -
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </pre>
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "최초열람마감시간(yyyy-MM-dd'T'HH:mm:ss)", example = "2023-12-31T10:00:00")
@Size(min = 19, max = 19, message = "최초열람마감시간(yyyy-MM-dd'T'HH:mm:ss)")
private String readExpiresAt;
/**
* <pre>
*
* : readExpiredAt
* : 9999-12-31'T'23:59:59, null
* : DX0 6
* </pre>
*/
@Schema(title = "재열람 만료 일시(yyyy-MM-dd'T'HH:mm:ss)", example = "2023-12-31T13:00:00")
@Size(max = 19, message = "재열람 만료 일시(yyyy-MM-dd'T'HH:mm:ss)")
private String reviewExpiresAt;
/**
* <pre>
*
* default: false
* </pre>
*/
@Schema(title = "알림톡 내용에 개인정보 제거 여부", example = " ", defaultValue = "false", allowableValues = {"true", "false"})
@Builder.Default
private Boolean useNonPersonalizedNotification = false;
/**
* CI
*/
@Schema(title = "받는이 CI", example = " ")
@Size(max=88, message = "수신자 CI(max=88)")
private String ci;
/**
* <pre>
*
* ci
* </pre>
*/
@Schema(title = "수신자 전화번호", example = "01012345678")
@Pattern(regexp = "^$|^\\d{11}$", message = "수신자 전화번호(max=11)")
private String phoneNumber;
/**
* <pre>
*
* ci
* </pre>
*/
@Schema(title = "수신자 이름", example = "김페이")
@Size(max = 26, message = "수신자 이름(max=26)")
private String name;
/**
* <pre>
* (YYYY-MM-DD )
* ci
* </pre>
*/
@Schema(requiredMode = Schema.RequiredMode.AUTO, title = "수신자 생년월일 (YYYYMMDD 형식)", example = "19801101")
@Pattern(regexp = "^$|^(19\\d{2}|20\\d{2})(0[1-9]|1[0-2])(0[1-9]|[1-2]\\d|3[0-1])$", message = "수신자 생년월일(YYYYMMDD)")
private String birthday;
/**
* <pre>
* - 40
* </pre>
*/
@Schema(requiredMode = Schema.RequiredMode.AUTO, title = "문서매핑용 식별자", example = " ")
@Size(max=40, message = "문서매핑용 식별자(max=40)")
private String externalId;
}
@Schema(name = "Content", description = "문서 원문 웹링크 또는 HTML")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public static class Content {
/**
* <pre>
*
* D10
* 1000 URL
* </pre>
*/
@Schema(title = "본인인증 후 사용자에게 보여줄 웹페이지 주소", example = "http://ipAddress/api/kakaopay/v1/ott")
@Size(max = 1000, message = "본인인증후 사용자에게 보여줄 페이지 주소는 필수입니다(max=1000)")
private String link;
/**
* <pre>
* HTML
* D11 ( 64KB)
*/
@Schema(title = "HTML 전문", example = " ")
@Size(max = 65536, message = "HTML 전문(max=64KB)")
private String html;
}
//------------------- ValidToken ------------------------------------------------------------------------------------------------
@Schema(name = "ValidTokenRequest DTO", description = "카카오톡 전자문서 토큰 유효성 검증 파라메터 DTO")
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public static class ValidTokenRequest extends CmmEnsRequestDTO {
/**
* ID, 34
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "문서 고유 ID, 34자로 고정", example = " ")
// FIXME: 테스트후 주석 제거!!!
//@Size(min = 34, max = 34, message = "문서 고유 ID는 필수입니다(34자)")
private String envelopeId;
/**
* :
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "카카오톡 전자문서 서버에서 생성한 일회용 토큰", example = "CON-cc375944ae3d11ecb91e42193199ee3c")
@NotEmpty(message = "카카오톡 전자문서 서버 토큰은 필수입니다")
private String token;
}
@Schema(name = "ValidTokenResponse DTO", description = "카카오톡 토큰 검증 response DTO")
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class ValidTokenResponse extends KkotalkErrorDTO implements IApiResponse {
/**
* ID, 34
*/
private String envelopeId;
/**
* <pre>
* - 40
* </pre>
*/
private String externalId;
/**
* <pre>
* :
* ,||
* RECEIVE|READ|EXPIRED
* </pre>
* @see ApiConstants.KkotalkDocStatus
*/
private ApiConstants.KkotalkDocStatus status;
/**
* <pre>
* -
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </
*/
private String sentAt;
/**
* <pre>
* -
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </pre>
*/
private String receivedAt;
/**
* <pre>
*
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </pre>
*/
private String readAt;
/**
* <pre>
*
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </pre>
*/
private String authenticatedAt;
/**
* <pre>
*
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </pre>
*/
private String ottVerifiedAt;
/**
* <pre>
*
* </pre>
*/
private Boolean isNotificationUnavailable;
/**
* <pre>
*
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </pre>
*/
private String userNotifiedAt;
/**
* API
*/
private String payload;
}
//------------------ ValidToken ----------------------------------------------------------------------
//------------------ DocStatus ----------------------------------------------------------------------
@Schema(name = "EnvelopeStatusResponse DTO", description = "카카오톡 상태조회 response DTO")
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@EqualsAndHashCode(callSuper = true)
public static class EnvelopeStatusResponse extends ValidTokenResponse implements IApiResponse {
/** /**
* <pre>
*
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </pre>
*/
private String readExpiredAt;
/** /**
* <pre>
*
*
* yyyy-MM-dd'T'HH:mm:ss > DateUtils.getTimeTOfNow()
* </pre>
*/
private String distributionReceivedAt;
/**
*
*/
private String errorCode;
/**
*
*/
private String errorMessage;
}
//------------------ DocStatus ----------------------------------------------------------------------
//------------------ SendResponse ----------------------------------------------------------------------
@Schema(name = "EnvelopeRes DTO", description = "문서발송 응답 DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
//@JsonInclude(JsonInclude.Include.NON_EMPTY)
public static class EnvelopeRes {
/**
* ID, 34
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "문서 고유 ID, 34자로 고정", example = " ")
@Size(min = 34, max = 34, message = "문서 고유 ID는 필수입니다(34자)")
private String envelopeId;
/**
* <pre>
* - 40
* </pre>
*/
private String externalId;
}
@Schema(name = "KkotalkErrorResponse DTO", description = "카카오톡 에러 응답 DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public static class KkotalkErrorDTO {
/**
*
*/
private String errorCode;
/**
*
*/
private String errorMessage;
/**
* (Tracking) ID
*/
private String edocGtid;
}
@Schema(name = "SendResponse DTO", description = "문서발송 응답 DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public static class SendResponse extends KkotalkErrorDTO {
/**
* ID, 34 -
*/
private String envelopeId;
/**
* <pre>
* - 40
* </pre>
*/
private String externalId;
}
//-------------------------------------------------------------------------------------
@Schema(name = "EnvelopeId DTO", description = "EnvelopeId DTO")
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public static class EnvelopeId extends CmmEnsRequestDTO{
/**
* ID, 34
*/
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "문서 고유 ID, 34자로 고정", example = " ")
// FIXME: 테스트후 주석 제거!!!
//@Size(min = 34, max = 34, message = "문서 고유 ID는 필수입니다(34자)")
private String envelopeId;
}
}

@ -1,6 +1,8 @@
package kr.xit.core.consts; package kr.xit.core.consts;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
/** /**
@ -74,7 +76,12 @@ public class Constants {
AUTHORITIES_KEY("role"), AUTHORITIES_KEY("role"),
TOKEN_USER_NAME("userName"), TOKEN_USER_NAME("userName"),
TOKEN_USER_MAIL("userEmail"), TOKEN_USER_MAIL("userEmail"),
TOKEN_USER_ID("userId"); TOKEN_USER_ID("userId"),
PARTNER_REST_API_KEY("Target-Authorization"),
DEALER_REST_API_KEY("Authorization"),
SETTLE_ID("settle-Id"),
;
private final String code; private final String code;

@ -5,6 +5,17 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.util.UriComponentsBuilder;
import kr.xit.biz.ens.model.cmm.CmmEnsRlaybsnmDTO; import kr.xit.biz.ens.model.cmm.CmmEnsRlaybsnmDTO;
import kr.xit.core.consts.Constants; import kr.xit.core.consts.Constants;
import kr.xit.core.exception.ClientErrorException; import kr.xit.core.exception.ClientErrorException;
@ -15,15 +26,6 @@ import kr.xit.core.spring.config.support.WebClientConfig;
import kr.xit.core.support.utils.JsonUtils; import kr.xit.core.support.utils.JsonUtils;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.util.UriComponentsBuilder;
/** /**
* <pre> * <pre>
@ -104,7 +106,7 @@ public class ApiWebClientUtil {
* @param ensDTO CmmEnsRlaybsnmDTO * @param ensDTO CmmEnsRlaybsnmDTO
* @return rtnClzz<T> * @return rtnClzz<T>
*/ */
public <T> T exchangeKko(final String url, final HttpMethod method, final Object body, final Class<T> rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) { 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<String, String> map = new HashMap<>();
map.put(HttpHeaders.AUTHORIZATION, map.put(HttpHeaders.AUTHORIZATION,
@ -114,6 +116,30 @@ public class ApiWebClientUtil {
return exchange(url, method, body, rtnClzz, map); return exchange(url, method, body, rtnClzz, map);
} }
/**
* 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) {
// FIXME: Kkotalk 값 설정
Map<String, String> map = new HashMap<>();
map.put(Constants.JwtToken.PARTNER_REST_API_KEY.getCode(),
String.format("KakaoAK %s", ensDTO.getKakaoAccessToken()));
map.put(HttpHeaders.AUTHORIZATION,
String.format("KakaoAK %s", ensDTO.getKakaoContractUuid()));
map.put(Constants.JwtToken.SETTLE_ID.getCode(), "");
return exchange(url, method, body, rtnClzz, map);
}
/** /**
* KT-BC WebClient * KT-BC WebClient
* (.onStatus status.is4xxClientError() || status.is5xxServerError()) * (.onStatus status.is4xxClientError() || status.is5xxServerError())

@ -1,9 +1,5 @@
package kr.xit.core.support.utils; package kr.xit.core.support.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
@ -13,6 +9,10 @@ import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Date; import java.util.Date;
import org.apache.commons.lang3.StringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@ -89,6 +89,27 @@ public class DateUtils {
return LocalDateTime.now(); return LocalDateTime.now();
} }
/**
* > yyyy-MM-dd'T'HH:mm:ss String
*
* @return yyyy-MM-dd'T'HH:mm:ss String
*/
public static String getTimeTOfnow() {
return LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
}
/**
* yyyy-MM-dd'T'HH:mm:ss String > yyyy-MM-dd HH:mm:ss String
* @param timeT yyyy-MM-dd'T'HH:mm:ss String
*
* @return yyyy-MM-dd HH:mm:ss String
*/
public static String getTimeOfTimeT(String timeT) {
return LocalDateTime.parse(timeT, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))
.format(DateTimeFormatter.ofPattern(DEFAULT_YMD_DT_FMT));
}
/** /**
* LocalDateTime.parse * LocalDateTime.parse
* *

Loading…
Cancel
Save