diff --git a/pom.xml b/pom.xml index eac3e00..deb26e8 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 1.8 ${basedir}/libs ojdbc8 - 1.6.3 + 1.6.14 1.4.2.Final 1.18.20 @@ -205,6 +205,11 @@ springdoc-openapi-ui ${springdoc.swagger} + + io.swagger.core.v3 + swagger-annotations + 2.2.6 + com.google.code.gson @@ -231,6 +236,29 @@ 2.2.2 + + org.springframework.boot + spring-boot-starter-webflux + + + + com.google.code.gson + gson + 2.9.0 + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + + net.sf.json-lib + json-lib + 2.4 + jdk15 + + diff --git a/src/main/java/cokr/xit/ens/core/aop/ApiResponseDTO.java b/src/main/java/cokr/xit/ens/core/aop/ApiResponseDTO.java new file mode 100644 index 0000000..8672a96 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/aop/ApiResponseDTO.java @@ -0,0 +1,274 @@ +package cokr.xit.ens.core.aop; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.google.gson.GsonBuilder; + +import cokr.xit.ens.core.exception.BizRuntimeException; +import cokr.xit.ens.core.exception.code.ErrorCode; +import cokr.xit.ens.core.utils.Checks; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + *
+ * description : Api 응답
+ *              TODO :: 프로젝트별 json 결과에서 제외하려면 @JsonIgnore 사용
+ * packageName : kr.xit.core.model
+ * fileName    : ApiResponseDTO
+ * author      : julim
+ * date        : 2023-04-28
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-04-28    julim       최초 생성
+ *
+ * 
+ */ +@Schema(name = "ApiResponseDTO", description = "Restful API 결과") +@Data +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@JsonRootName("result") +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponseDTO implements IApiResponse { + private static final String FAIL_STATUS = "fail"; + private static final String ERROR_STATUS = "error"; + + @Schema(example = "true", description = "에러인 경우 false", requiredMode = Schema.RequiredMode.REQUIRED) + private boolean success; + + @Schema(example = " ", description = "HttpStatus.OK", requiredMode = Schema.RequiredMode.REQUIRED) + private String code; + + @Schema(description = "결과 데이타, 오류시 null", example = " ") + private T data; + + @Schema(description = "오류 발생시 오류 메세지", example = " ", requiredMode = Schema.RequiredMode.AUTO) + @Setter + private String message; + + @JsonIgnore + @Schema(example = " ", description = "HttpStatus.OK", requiredMode = Schema.RequiredMode.AUTO) + private HttpStatus httpStatus; + + @Schema(description = "API 실행 결과 데이타 수") + private int count; + + /** + * 비동기 정상 데이타 ApiResponseDTO return + * + * @param future CompletableFuture + * @return ApiResponseDTO + */ + //@SuppressWarnings("unchecked") + @SuppressWarnings("rawtypes") + public static ApiResponseDTO of(final CompletableFuture future) { + try { + if(future.get() instanceof ApiResponseDTO) return (ApiResponseDTO)future.get(); + return new ApiResponseDTO<>(true, future.get(), String.valueOf(HttpStatus.OK.value()), + HttpStatus.OK.name(), HttpStatus.OK); + + } catch (InterruptedException ie){ + // thread pool에 에러 상태 전송 + Thread.currentThread().interrupt(); + throw BizRuntimeException.create(ie); + + } catch (ExecutionException ee) { + throw BizRuntimeException.create(ee); + } + } + + /** + * 정상 데이타 ApiResponseDTO return + * @return ApiResponseDTO + */ + public static ApiResponseDTO success() { + return new ApiResponseDTO<>(true, null, String.valueOf(HttpStatus.OK.value()), HttpStatus.OK.name(), HttpStatus.OK); + } + + /** + * 정상 데이타 ApiResponseDTO return + * @param data T + * @return ApiResponseDTO + */ + public static ApiResponseDTO success(T data) { + return new ApiResponseDTO<>(true, data, String.valueOf(HttpStatus.OK.value()), HttpStatus.OK.name(), HttpStatus.OK); + } + + + /** + * 정상 데이타 ApiResponseDTO return + * @param httpStatus HttpStatus + * @return ApiResponseDTO + */ + public static ApiResponseDTO success(final HttpStatus httpStatus) { + return new ApiResponseDTO<>(true, null, String.valueOf(httpStatus.value()), httpStatus.name(), httpStatus); + } + + /** + * 정상 데이타 ApiResponseDTO return + * @param data T 데이타 + * @param httpStatus HttpStatus + * @return ApiResponseDTO + */ + public static ApiResponseDTO success(final T data, final HttpStatus httpStatus) { + return new ApiResponseDTO<>(true, data, String.valueOf(httpStatus.value()), httpStatus.name(), httpStatus); + } + + /** + * 정상 데이타 ApiResponseDTO return + * @param data T 데이타 + * @param message String + * @return ApiResponseDTO + */ + public static ApiResponseDTO success(final T data, final String message) { + return new ApiResponseDTO<>(true, data, String.valueOf(HttpStatus.OK.value()), message, HttpStatus.OK); + } + + /** + * 정상 return - body empty + * @return ApiResponseDTO + */ + public static ApiResponseDTO empty() { + return new ApiResponseDTO<>(true, null, String.valueOf(HttpStatus.NO_CONTENT.value()), HttpStatus.NO_CONTENT.name(), HttpStatus.OK); + } + + /** + * Error ApiResponseDTO return + * Hibernate Validator에 의해 유효하지 않은 데이터로 인해 API 호출이 거부될때 반환 + * @param bindingResult BindingResult + * @return ApiResponseDTO + */ + public static ApiResponseDTO error(final BindingResult bindingResult) { + Map errors = new HashMap<>(); + + List allErrors = bindingResult.getAllErrors(); + for (ObjectError error : allErrors) { + if (error instanceof FieldError) { + errors.put(((FieldError) error).getField(), error.getDefaultMessage()); + } else { + errors.put( error.getObjectName(), error.getDefaultMessage()); + } + } + return new ApiResponseDTO<>(false, null, FAIL_STATUS, errors.toString(), null); + } + + /** + * Error ApiResponseDTO return + * @param message 에러 메세지 + * @return ApiResponseDTO + */ + public static ApiResponseDTO error(final String message) { + return new ApiResponseDTO<>(false, null, ERROR_STATUS, message, null); + } + + /** + * Error ApiResponseDTO return + * @param errorCode ErrorCode + * @return ApiResponseDTO + */ + public static ApiResponseDTO error(final ErrorCode errorCode) { + return new ApiResponseDTO<>(false, null, errorCode.name(), errorCode.getMessage(), errorCode.getHttpStatus()); + } + + /** + * Error ApiResponseDTO return + * @param e BizRuntimeException + * @return ApiResponseDTO + */ + public static ApiResponseDTO error(final BizRuntimeException e) { + + if (Checks.isNotEmpty(e.getErrorCode())) { + return ApiResponseDTO.error(e.getErrorCode()); + } + return new ApiResponseDTO<>( + false, + null, + org.apache.commons.lang3.StringUtils.defaultString(e.getCode(), org.apache.commons.lang3.StringUtils.EMPTY), + org.apache.commons.lang3.StringUtils.defaultString(e.getMessage(), org.apache.commons.lang3.StringUtils.EMPTY), + e.getHttpStatus()); + } + + /** + * Error ApiResponseDTO return + * @param code 에러코드 + * @param message 에러메세지 + * @return ApiResponseDTO + */ + public static ApiResponseDTO error(final String code, final String message) { + return new ApiResponseDTO<>(false, null, code, message, null); + } + + /** + * Error ApiResponseDTO return + * @param code 에러코드 + * @param message 에러메세지 + * @param httpStatus HttpStatus + * @return ApiResponseDTO + */ + public static ApiResponseDTO error(final String code, final String message, HttpStatus httpStatus) { + return new ApiResponseDTO<>(false, null, code, message, httpStatus); + } + + /** + * + * @param success true|false + * @param data data + * @param code 에러코드 + * @param message 메세지(에러 발생시 필수) + * @param httpStatus HttpStatus + */ + private ApiResponseDTO(final boolean success, final T data, final String code, final String message, final HttpStatus httpStatus) { + this.success = success; + this.data = data; + this.code = code; + this.message = message; + this.httpStatus = httpStatus; + + if(data == null){ + this.count = 0; + + }else { + + if (Collection.class.isAssignableFrom(data.getClass())) { + this.count = (((Collection) data).size()); + + } else { + this.count = 1; + } + } + + if(httpStatus == null){ + if(!success) this.httpStatus = HttpStatus.BAD_REQUEST; + else this.httpStatus = HttpStatus.OK; + } + } + + @Override + public String toString() { + // value가 null값인 경우도 생성 + GsonBuilder builder = new GsonBuilder().serializeNulls(); + builder.disableHtmlEscaping(); + return builder.setPrettyPrinting().create().toJson(this); + } +} diff --git a/src/main/java/cokr/xit/ens/core/aop/EnsResponseVO.java b/src/main/java/cokr/xit/ens/core/aop/EnsResponseVO.java index 069c38e..5b5aebd 100644 --- a/src/main/java/cokr/xit/ens/core/aop/EnsResponseVO.java +++ b/src/main/java/cokr/xit/ens/core/aop/EnsResponseVO.java @@ -1,9 +1,10 @@ package cokr.xit.ens.core.aop; -import cokr.xit.ens.core.exception.code.EnsErrCd; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.annotations.SerializedName; + +import cokr.xit.ens.core.exception.code.EnsErrCd; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; @@ -14,7 +15,7 @@ import lombok.ToString; @ToString @Schema(name = "EnsResponseVO") @NoArgsConstructor -public class EnsResponseVO { +public class EnsResponseVO implements IApiResponse { @Schema(required = true, title = "에러 코드", example = " ") @JsonProperty("errCode") diff --git a/src/main/java/cokr/xit/ens/core/aop/IApiResponse.java b/src/main/java/cokr/xit/ens/core/aop/IApiResponse.java new file mode 100644 index 0000000..f400b91 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/aop/IApiResponse.java @@ -0,0 +1,19 @@ +package cokr.xit.ens.core.aop; + +/** + *
+ * description :
+ *
+ * packageName : kr.xit.core.model
+ * fileName    : IApiResponse
+ * author      : limju
+ * date        : 2023-05-31
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-05-31    limju       최초 생성
+ *
+ * 
+ */ +public interface IApiResponse { +} diff --git a/src/main/java/cokr/xit/ens/core/config/support/CustomJacksonConfig.java b/src/main/java/cokr/xit/ens/core/config/support/CustomJacksonConfig.java new file mode 100644 index 0000000..5c832e9 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/config/support/CustomJacksonConfig.java @@ -0,0 +1,75 @@ +package cokr.xit.ens.core.config.support; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import cokr.xit.ens.core.utils.CoreSpringUtils; +import cokr.xit.ens.core.utils.JsonUtils; + +/** + *
+ * description : Jackson(ObjectMapper) 설정 재정의
+ *               모든 ObjectMapper 사용시 이 설정이 적용되도록 해야 한다.
+ *               - Cannot coerce empty String("") to ...
+ *                 value(but could if coercion was enabled using `CoercionConfig`) 에러 방어
+ * packageName : kr.xit.core.spring.config.support
+ * fileName    : CustomJacksonConfig
+ * author      : julim
+ * date        : 2023-09-13
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-09-13    julim       최초 생성
+ *
+ * 
+ * @see CoreSpringUtils#getObjectMapper() + * @see JsonUtils + * @see WebClientConfig + */ +@Configuration +public class CustomJacksonConfig { + + /** + *
+     * 기본 설정에 override
+     * Cannot coerce empty String("") to ...
+     * value(but could if coercion was enabled using `CoercionConfig`) 에러 방어
+     * 
+ * + * @return + */ + @Bean + @Primary + public ObjectMapper objectMapper() { + ObjectMapper om = new ObjectMapper() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + //.setSerializationInclusion(Include.NON_NULL) + //.setSerializationInclusion(Include.NON_EMPTY) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, false) + .registerModule(new JavaTimeModule()); + + om.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); +// om.coercionConfigFor(LogicalType.Enum) +// .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); +// om.coercionConfigFor(LogicalType.POJO) +// .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); + + return om; + } +} diff --git a/src/main/java/cokr/xit/ens/core/config/support/WebClientConfig.java b/src/main/java/cokr/xit/ens/core/config/support/WebClientConfig.java new file mode 100644 index 0000000..c159d17 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/config/support/WebClientConfig.java @@ -0,0 +1,245 @@ +package cokr.xit.ens.core.config.support; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLException; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import cokr.xit.ens.core.exception.BizRuntimeException; +import cokr.xit.ens.core.exception.ClientErrorException; +import cokr.xit.ens.core.exception.ServerErrorException; +import io.netty.channel.ChannelOption; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.transport.logging.AdvancedByteBufFormat; + +/** + *
+ * description : WebClient configuration
+ *               Spring WebFlux 기반 HTTP Client Config
+ *               logging : - ExchangeFilterFunction 구현 처리 가능(requestFilter, responseFilter)
+ *                         - logging.level.reactor.netty.http.client: DEBUG|ERROR
+ *
+ * packageName : kr.xit.core.spring.config.support
+ * fileName    : WebClientConfig
+ * author      : julim
+ * date        : 2023-09-06
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-09-06    julim       최초 생성
+ *
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Configuration +public class WebClientConfig { + + @Value("${app.contract.connection.timeout:5000}") + private int connectTimeout; + @Value("${app.contract.connection.readTimeout:5000}") + private int readTimeout; + + private final ObjectMapper objectMapper; + DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(); + + /** + * setEncodingMode : GET 요청의 파라미터 셋팅을 하기 위한 URI 템플릿의 인코딩을 위한 설정 + * @return + */ + @Bean + public WebClient webClient() { + factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY); + + return WebClient.builder() + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .uriBuilderFactory(factory) + .clientConnector(new ReactorClientHttpConnector(defaultHttpClient())) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) + .exchangeStrategies(defaultExchangeStrategies()) + .filters(exchangeFilterFunctions -> { + //exchangeFilterFunctions.add(requestFilter()); + exchangeFilterFunctions.add(responseFilter()); + }) + .build(); + } + + /** + *
+     * Http client 생성
+     * - 요청 / 응답 debugging을 위해 wiretap 설정 - AdvancedByteBufFormat.TEXTUAL
+     * @return HttpClient
+     * 
+ */ + @Bean + public HttpClient defaultHttpClient() { + try { + // SSL check bypass + SslContext sslContext = SslContextBuilder + .forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + + return HttpClient.create(connectionProvider()) + .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))); + }catch(SSLException se){ + throw BizRuntimeException.create(se.getMessage()); + } + } + + /** + * maxConnections : connection pool의 갯수 + * pendingAcquireTimeout : 커넥션 풀에서 커넥션을 얻기 위해 기다리는 최대 시간 + * pendingAcquireMaxCount : 커넥션 풀에서 커넥션을 가져오는 시도 횟수 (-1: no limit) + * maxIdleTime : 커넥션 풀에서 idle 상태의 커넥션을 유지하는 시간 + * @return ConnectionProvider + */ + @Bean + public ConnectionProvider connectionProvider() { + return ConnectionProvider.builder("http-pool") + .maxConnections(100) + .pendingAcquireTimeout(Duration.ofMillis(0)) + .pendingAcquireMaxCount(-1) + .maxIdleTime(Duration.ofMillis(2000L)) + .build(); + } + + /** + *
+     * 1. 256KB 보다 큰 HTTP 메시지를 처리 시도 → DataBufferLimitException 에러 발생 방어
+     * 2. messageWriters를 통한 logging(setEnableLoggingRequestDetails(true)
+     *    -> org.springframework.web.reactive.function.client.ExchangeFunctions: DEBUG 하여 활성
+     *    -> defaultHttpClient()의 wiretap 사용으로 비활성
+     * 
+ * @return ExchangeStrategies + */ + @Bean + public ExchangeStrategies defaultExchangeStrategies() { + // 256KB 보다 큰 HTTP 메시지를 처리 시도 → DataBufferLimitException 에러 발생 방어 + return ExchangeStrategies.builder() + .codecs(config -> { + config.defaultCodecs().maxInMemorySize(2 * 1024 * 1024); + config.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON)); + config.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON)); + }) + .build(); + +// es.messageWriters() +// .stream() +// .filter(LoggingCodecSupport.class::isInstance) +// .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true)); + } + + private ExchangeFilterFunction requestFilter() { + return ExchangeFilterFunction.ofRequestProcessor(cr -> { + if (log.isDebugEnabled()) { + StringBuilder sb = new StringBuilder("\n>>>>>>>>>> WebClient Http Request <<<<<<<<<<<<<\n"); + sb.append(logMethodAndUrl(cr)); + sb.append(logHeaders(cr)); + sb.append("-------------------------------------------------------"); + log.debug(sb.toString()); + } + return Mono.just(cr); + }); + } + + /** + * reponse logging && error Handling + * @return ExchangeFilterFunction + */ + private ExchangeFilterFunction responseFilter() { + return ExchangeFilterFunction.ofResponseProcessor(cr -> { + + HttpStatus status = cr.statusCode(); + + if(cr.statusCode().is4xxClientError()) { + return cr.bodyToMono(String.class) + .flatMap(errorBody -> Mono.error(new ClientErrorException(status, errorBody))); + + } else if(cr.statusCode().is5xxServerError()) { + return cr.bodyToMono(String.class) + .flatMap(errorBody -> Mono.error(new ServerErrorException(status, errorBody))); + } + +// if(log.isDebugEnabled()) { +// StringBuilder sb = new StringBuilder( +// "\n>>>>>>>>>> WebClient Http Response <<<<<<<<<<<<<\n"); +// sb.append(logStatus(cr)); +// sb.append(logHeaders(cr)); +// sb.append("-------------------------------------------------------"); +// log.debug(sb.toString()); +// } + return Mono.just(cr); + }); + } + + private static String logStatus(ClientResponse response) { + HttpStatus status = response.statusCode(); + return String.format("Returned staus code %s (%s)", status.value(), status.getReasonPhrase()); + } + + private static String logHeaders(ClientRequest request) { + StringBuilder sb = new StringBuilder(); + + request.headers() + .forEach((name, values) -> + values.forEach(value -> sb.append(name).append(": ").append(value).append("\n")) + ); + return sb.toString(); + } + + private static String logHeaders(ClientResponse response) { + StringBuilder sb = new StringBuilder(); + response.headers() + .asHttpHeaders() + .forEach((name, values) -> + values.forEach(value -> sb.append(name).append(": ").append(value).append("\n")) + ); + return sb.toString(); + } + + private static String logMethodAndUrl(ClientRequest request) { + StringBuilder sb = new StringBuilder(); + sb.append(request.method().name()); + sb.append(" to "); + sb.append(request.url()); + + return sb.append("\n").toString(); + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/BizRuntimeException.java b/src/main/java/cokr/xit/ens/core/exception/BizRuntimeException.java new file mode 100644 index 0000000..285fd71 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/BizRuntimeException.java @@ -0,0 +1,224 @@ +package cokr.xit.ens.core.exception; + +import java.text.MessageFormat; +import java.util.Locale; + +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import cokr.xit.ens.core.exception.code.ErrorCode; +import cokr.xit.ens.core.utils.CoreSpringUtils; +import cokr.xit.ens.core.utils.MessageUtil; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + *
+ * description : egov BaseRuntimeException 상속
+ * packageName : kr.xit.core.exception
+ * fileName    : BizRuntimeException
+ * author      : julim
+ * date        : 2023-04-28
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-04-28    julim       최초 생성
+ *
+ * 
+ */ +@Getter +@Setter +@Slf4j +@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Invalid parameter") +public class BizRuntimeException extends RuntimeException{ + private String code; + private String message; + private ErrorCode errorCode; + + private static final MessageUtil messageUtil = CoreSpringUtils.getMessageUtil(); + + /** + * BizRuntimeException 생성자. + * @param errorCode ErrorCode 지정 + */ + public static BizRuntimeException create(ErrorCode errorCode) { + return new BizRuntimeException(errorCode); + } + + /** + * BizRuntimeException 생성자. + * @param errorCode ErrorCode 지정 + */ + private BizRuntimeException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.code = String.valueOf(errorCode.getHttpStatus().value()); + this.errorCode = errorCode; + } + + /** + * BizRuntimeException 생성자. + * @param errorCode 에러코드 + */ + public static BizRuntimeException of(String errorCode) { + BizRuntimeException e = new BizRuntimeException(); + e.setCode(errorCode); + e.setMessage(messageUtil.getMessage(errorCode)); + return e; + } + + /** + * BizRuntimeException 생성자. + * @param defaultMessage 메세지 지정 + */ + public static BizRuntimeException create(String defaultMessage) { + return new BizRuntimeException(defaultMessage, null, null); + } + + /** + * BizRuntimeException 생성자. + * @param code + * @param defaultMessage 메세지 지정 + */ + public static BizRuntimeException create(String code, String defaultMessage) { + BizRuntimeException e = new BizRuntimeException(); + e.setCode(code); + e.setMessage(defaultMessage); + return e; + } + + public static BizRuntimeException create(String code, Object[] messageParameters) { + BizRuntimeException e = new BizRuntimeException(); + e.setCode(code); + e.setMessage(messageUtil.getMessage(code, messageParameters)); + return e; + } + + /** + * BizRuntimeException 생성자. + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(Throwable wrappedException) { + return new BizRuntimeException("BizRuntimeException without message", null, wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param defaultMessage 메세지 지정 + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(String defaultMessage, Throwable wrappedException) { + return new BizRuntimeException(defaultMessage, null, wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param defaultMessage 메세지 지정(변수지정) + * @param messageParameters 치환될 메세지 리스트 + * @param wrappedException 원인 Exception + */ + private static BizRuntimeException create(String defaultMessage, Object[] messageParameters, Throwable wrappedException) { + return new BizRuntimeException(defaultMessage, messageParameters, wrappedException); + } + + /** + * BizRuntimeException 기본 생성자. + */ + private BizRuntimeException() { + this("BizRuntimeException without message", null, null); + } + + /** + * BizRuntimeException 생성자. + * @param defaultMessage 메세지 지정(변수지정) + * @param messageParameters 치환될 메세지 리스트 + * @param wrappedException 원인 Exception + */ + private BizRuntimeException(String defaultMessage, Object[] messageParameters, Throwable wrappedException) { + super(wrappedException); + if(messageParameters != null) { + message = MessageFormat.format(defaultMessage, messageParameters); + }else{ + message = defaultMessage; + } + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey) { + return new BizRuntimeException(messageSource, messageKey, null, null, Locale.getDefault(), null); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, null, null, Locale.getDefault(), wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + * @param locale 국가/언어지정 + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Locale locale, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, null, null, locale, wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + * @param messageParameters 치환될 메세지 리스트 + * @param locale 국가/언어지정 + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Object[] messageParameters, Locale locale, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, messageParameters, null, locale, wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + * @param messageParameters 치환될 메세지 리스트 + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Object[] messageParameters, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, messageParameters, null, Locale.getDefault(), wrappedException); + } + + /** + * BizRuntimeException 생성자. + * @param messageSource 메세지 리소스 + * @param messageKey 메세지키값 + * @param messageParameters 치환될 메세지 리스트 + * @param defaultMessage 메세지 지정(변수지정) + * @param wrappedException 원인 Exception + */ + public static BizRuntimeException create(MessageSource messageSource, String messageKey, Object[] messageParameters, String defaultMessage, Throwable wrappedException) { + return new BizRuntimeException(messageSource, messageKey, messageParameters, defaultMessage, Locale.getDefault(), wrappedException); + } + + public BizRuntimeException(MessageSource messageSource, String messageKey, Object[] messageParameters, String defaultMessage, Locale locale, Throwable wrappedException) { + super(wrappedException); + code = messageKey; + message = messageSource.getMessage(messageKey, messageParameters, defaultMessage, locale); + } + + public HttpStatus getHttpStatus(){ + return HttpStatus.BAD_REQUEST; + } + + public static void main(String[] args) { + + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/ClientErrorException.java b/src/main/java/cokr/xit/ens/core/exception/ClientErrorException.java new file mode 100644 index 0000000..8f1a5dc --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/ClientErrorException.java @@ -0,0 +1,33 @@ +package cokr.xit.ens.core.exception; + +import org.springframework.http.HttpStatus; + +import cokr.xit.ens.core.utils.ApiWebClientUtil; +import lombok.Getter; + +/** + *
+ * description : WebClient 4xx 에러 : 에러 핸들러에서 사용
+ *
+ * packageName : kr.xit.core.exception
+ * fileName    : ClientErrorException
+ * author      : limju
+ * date        : 2023-05-25
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-05-25    limju       최초 생성
+ *
+ * 
+ * @see ApiWebClientUtil + */ +@Getter +public class ClientErrorException extends RuntimeException { + private final HttpStatus status; + private final String body; + + public ClientErrorException(HttpStatus status, String body) { + this.status = status; + this.body = body; + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/ErrorParse.java b/src/main/java/cokr/xit/ens/core/exception/ErrorParse.java new file mode 100644 index 0000000..6e0d69e --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/ErrorParse.java @@ -0,0 +1,99 @@ +package cokr.xit.ens.core.exception; + +import java.util.concurrent.ExecutionException; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.WebClientRequestException; + +import cokr.xit.ens.core.aop.ApiResponseDTO; +import cokr.xit.ens.core.utils.Checks; +import io.netty.channel.ConnectTimeoutException; +import io.netty.handler.timeout.ReadTimeoutException; + + +/** + *
+ * description :
+ *
+ * packageName : kr.xit.core.exception
+ * fileName    : ErrorParse
+ * author      : limju
+ * date        : 2023-06-01
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-06-01    limju       최초 생성
+ *
+ * 
+ */ +public class ErrorParse { + + @SuppressWarnings("rawtypes") + public static ApiResponseDTO extractError(final Throwable e){ + String errCode = String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()); + String message = Checks.isNotNull(e) ? e.getLocalizedMessage() : StringUtils.EMPTY; + HttpStatus httpStatus = null; + + if(e instanceof BizRuntimeException) { + BizRuntimeException be = (BizRuntimeException) e; + return ApiResponseDTO.error(be.getCode(), be.getMessage(), HttpStatus.BAD_REQUEST); + } + + if(e instanceof ClientErrorException) { + ClientErrorException ce = (ClientErrorException) e; + return ApiResponseDTO.error(String.valueOf(ce.getStatus()), ce.getBody(), ce.getStatus()); + } + + if(e instanceof ServerErrorException) { + ServerErrorException ce = (ServerErrorException) e; + return ApiResponseDTO.error(String.valueOf(ce.getStatus()), ce.getBody(), ce.getStatus()); + } + + // Async(React) Exception 처리 + if(e instanceof WebClientRequestException) { + if (e.getCause() instanceof ConnectTimeoutException || e.getCause() instanceof ReadTimeoutException) { + return getTimeoutException(); + } + } + + if(e instanceof ExecutionException) { + if (e.getCause() instanceof WebClientRequestException) { + message = e.getCause().getMessage(); + + // Timeout 에러 + if (e.getCause().getCause() instanceof ReadTimeoutException || e.getCause().getCause() instanceof ConnectTimeoutException) { + return getTimeoutException(); + } + } + } + + if(e instanceof ReadTimeoutException){ + return getTimeoutException(); + } + + // Async(React) Exception 처리 + if(e.getCause() instanceof WebClientRequestException){ + + // Timeout 에러 + if(e.getCause().getCause() instanceof ReadTimeoutException){ + return getTimeoutException(); + } + + } + + if(Checks.isNotEmpty(e.getCause())) { + message = e.getCause().getMessage(); + } + + return ApiResponseDTO.error(errCode, message, httpStatus); + } + + @SuppressWarnings("rawtypes") + private static ApiResponseDTO getTimeoutException(){ + return ApiResponseDTO.error( + String.valueOf(HttpStatus.REQUEST_TIMEOUT.value()), + HttpStatus.REQUEST_TIMEOUT.getReasonPhrase(), + HttpStatus.REQUEST_TIMEOUT); + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/ServerErrorException.java b/src/main/java/cokr/xit/ens/core/exception/ServerErrorException.java new file mode 100644 index 0000000..36364bf --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/ServerErrorException.java @@ -0,0 +1,33 @@ +package cokr.xit.ens.core.exception; + +import org.springframework.http.HttpStatus; + +import cokr.xit.ens.core.utils.ApiWebClientUtil; +import lombok.Getter; + +/** + *
+ * description : WebClient 5xx 에러 : 에러 핸들러에서 사용
+ *
+ * packageName : kr.xit.core.exception
+ * fileName    : ServerErrorException
+ * author      : limju
+ * date        : 2023-05-25
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-05-25    limju       최초 생성
+ *
+ * 
+ * @see ApiWebClientUtil + */ +@Getter +public class ServerErrorException extends RuntimeException { + private final HttpStatus status; + private final String body; + + public ServerErrorException(HttpStatus status, String body) { + this.status = status; + this.body = body; + } +} diff --git a/src/main/java/cokr/xit/ens/core/exception/code/ErrorCode.java b/src/main/java/cokr/xit/ens/core/exception/code/ErrorCode.java new file mode 100644 index 0000000..d96e400 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/exception/code/ErrorCode.java @@ -0,0 +1,114 @@ +package cokr.xit.ens.core.exception.code; + +import org.springframework.http.HttpStatus; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + *
+ * description : HttpStatus 기준 에러 코드 정의
+ * packageName : kr.xit.core.const
+ * fileName    : ErrorCode
+ * author      : julim
+ * date        : 2023-04-28
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-04-28    julim       최초 생성
+ *
+ * 
+ * @see HttpStatus + */ +@Getter +@RequiredArgsConstructor +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +public enum +ErrorCode { + + /* +200 : OK, 요청 정상 처리 +201 : Created, 생성 요청 성공 +202 : Accepted, 비동기 요청 성공 +204 : No Content, 요청 정상 처리, 응답 데이터 없음. + +실패 +400 : Bad Request, 요청이 부적절 할 때, 유효성 검증 실패, 필수 값 누락 등. +401 : Unauthorized, 인증 실패, 로그인하지 않은 사용자 또는 권한 없는 사용자 처리 +402 : Payment Required +403 : Forbidden, 인증 성공 그러나 자원에 대한 권한 없음. 삭제, 수정시 권한 없음. +404 : Not Found, 요청한 URI에 대한 리소스 없을 때 사용. +405 : Method Not Allowed, 사용 불가능한 Method를 이용한 경우. +406 : Not Acceptable, 요청된 리소스의 미디어 타입을 제공하지 못할 때 사용. +408 : Request Timeout +409 : Conflict, 리소스 상태에 위반되는 행위 시 사용. +413 : Payload Too Large +423 : Locked +428 : Precondition Required +429 : Too Many Requests + +500 : 서버 에러 + + */ + + + BAD_REQUEST(HttpStatus.BAD_REQUEST, "요청 매개변수 오류 입니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "잘못된 요청 입니다"), + FILE_NOT_FOUND(HttpStatus.NOT_FOUND, "파일이 존재하지 않습니다"), + DATA_NOT_FOUND(HttpStatus.NOT_FOUND, "처리 데이타 오류(처리 요청 데이타 미존재)"), + + /* 400 BAD_REQUEST : 잘못된 요청 */ + CANNOT_FOLLOW_MYSELF(HttpStatus.BAD_REQUEST, "자기 자신은 팔로우 할 수 없습니다"), + + /* 401 UNAUTHORIZED : 인증되지 않은 사용자 */ + INVALID_AUTH_TOKEN(HttpStatus.FORBIDDEN, "인가된 사용자가 아닙니다"), + UN_AUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "계정 정보가 존재하지 않습니다"), + AUTH_HEADER_NOT_EXISTS(HttpStatus.UNAUTHORIZED, "헤더에 인증 정보를 찾을 수 없습니다"), + LOGOUT_USER(HttpStatus.UNAUTHORIZED, "로그아웃된 사용자 입니다"), + NOT_EXISTS_SECURITY_AUTH(HttpStatus.UNAUTHORIZED, "Security Context 에 인증 정보가 없습니다"), + + NOT_EXISTS_TOKEN(HttpStatus.UNAUTHORIZED, "인증된 토큰이 없습니다"), + NOT_EXISTS_SAVED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "저장된 인증 토큰이 없습니다"), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효한 토큰이 아닙니다"), + INVALID_ROLE_TOKEN(HttpStatus.UNAUTHORIZED, "사용 권한이 없는 토큰 입니다"), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "유효기간이 경과된 토큰 입니다"), + INVALID_SIGN_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 서명의 토큰 입니다"), + INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰이 유효하지 않습니다"), + MISMATCH_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "리프레시 토큰의 유저 정보가 일치하지 않습니다"), + MISMATCH_REFRESH_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "발급된 refresh token의 정보가 일치하지 않습니다"), + NOT_EXPIRED_TOKEN_YET(HttpStatus.UNAUTHORIZED, "토큰 유효기간이 경과되지 않았습니다"), + + /* 404 NOT_FOUND : Resource 를 찾을 수 없음 */ + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자 정보를 찾을 수 없습니다"), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "로그아웃 된 사용자입니다"), + + /* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */ + DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "데이터가 이미 존재합니다"), + MEMBER_EXISTS(HttpStatus.CONFLICT, "가입되어 있는 회원 입니다"), + + // JPA query error + SQL_DATA_RESOURCE_INVALID(HttpStatus.CONFLICT, "SQL 오류(데이터가 이미 존재합니다)"), + + MPOWER_CONNECT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "MPower DB 접속 에러 입니다"), + MPOWER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "MPower DB 에러 입니다"), + + FORBIDDEN(HttpStatus.FORBIDDEN, "FORBIDDEN"), + INVALID_CODE(HttpStatus.BAD_REQUEST, "유효하지 않은 HttpStatus 상태 코드 입니다"), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR"), + + MISMATCH_PASSWORD(HttpStatus.BAD_REQUEST, "비밀 번호가 일치하지 않습니다."), + + CONNECT_TIMEOUT(HttpStatus.REQUEST_TIMEOUT, "서버 접속 에러입니다(Connect timeout)") + ; + + + private HttpStatus httpStatus; + private String message; + + ErrorCode(HttpStatus httpStatus, String message){ + this.httpStatus = httpStatus; + this.message = message; + } +} diff --git a/src/main/java/cokr/xit/ens/core/utils/ApiWebClientUtil.java b/src/main/java/cokr/xit/ens/core/utils/ApiWebClientUtil.java new file mode 100644 index 0000000..0b9cbe9 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/utils/ApiWebClientUtil.java @@ -0,0 +1,253 @@ +package cokr.xit.ens.core.utils; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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 cokr.xit.ens.core.aop.ApiResponseDTO; +import cokr.xit.ens.core.config.support.WebClientConfig; +import cokr.xit.ens.core.exception.ClientErrorException; +import cokr.xit.ens.core.exception.ErrorParse; +import cokr.xit.ens.modules.kkotalk.model.CmmEnsRlaybsnmDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + *
+ * description : react Restfull Util
+ *               error(.onStatus)는 ExchangeFilterFunction {@link WebClientConfig responseFilter} 에서 처리
+ * packageName : kr.xit.core.spring.util
+ * fileName    : ApiWebClientUtil
+ * author      : julim
+ * date        : 2023-09-06
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-09-06    julim       최초 생성
+ *
+ * 
+ * @see WebClientConfig + * @see kr.xit.core.spring.config.AsyncExecutorConfig + * @see ClientErrorException + * @see ServerErrorException + * @see ErrorParse + * @see kr.xit.core.spring.config.support.CustomJacksonConfig + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ApiWebClientUtil { + private static final String AUTH_TYPE_BEARER = "bearer"; + private final WebClientConfig webClientConfig; + + /** + * WebClient GET 호출 처리 + * 에러(.onStatus status.is4xxClientError() || status.is5xxServerError()) + * -> {@link WebClientConfig responseFilter} 에서 처리 + * @param url String + * @param responseDtoClass Class + * @param headerMap Map + * @return responseDtoClass + */ + public T get(final String url, final Class responseDtoClass, Map headerMap) { + return webClientConfig.webClient() + .method(HttpMethod.GET) + .uri(url) + .headers(httpHeaders -> getHeaders(httpHeaders, headerMap)) + .retrieve() + .bodyToMono(responseDtoClass) + .block(); + } + + /** + * WebClient POST 호출 처리 + * 에러(.onStatus status.is4xxClientError() || status.is5xxServerError()) + * -> {@link WebClientConfig responseFilter} 에서 처리 + * @param url String + * @param requestDto V + * @param responseDtoClass Class + * @param headerMap Map + * @return responseDtoClass + */ + public T post(final String url, final V requestDto, final Class responseDtoClass, Map headerMap) { + return webClientConfig.webClient() + .method(HttpMethod.POST) + .uri(url) + .headers(httpHeaders -> getHeaders(httpHeaders, headerMap)) + .bodyValue(requestDto != null ? requestDto : "") + .retrieve() + .bodyToMono(responseDtoClass) + .block(); + } + + /** + * kakaotalk WebClient 호출 처리 + * 에러(.onStatus status.is4xxClientError() || status.is5xxServerError()) + * -> {@link WebClientConfig responseFilter} 에서 처리 + * @param url String + * @param method HttpMethod + * @param body Object + * @param rtnClzz Class + * @param ensDTO CmmEnsRlaybsnmDTO + * @return rtnClzz + */ + public T exchangeKkotalk(final String url, final HttpMethod method, final Object body, final Class rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) { + + Map map = new HashMap<>(); + map.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + map.put(HttpHeaders.AUTHORIZATION, String.format("KakaoAK %s", ensDTO.getKakaoDealerRestApiKey())); + map.put("Target-Authorization", String.format("KakaoAK %s", ensDTO.getKakaoPartnerRestApiKey())); + map.put("settle-Id", 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 + * @param ensDTO CmmEnsRlaybsnmDTO + * @return rtnClzz + */ + public T exchangeKt(final String url, final HttpMethod method, final Object body, final Class rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) { + final Map 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 + * @param ensDTO CmmEnsRlaybsnmDTO + * @return rtnClzz + */ + public T exchangeKtGbs(final String url, final HttpMethod method, final Object body, final Class rtnClzz, final CmmEnsRlaybsnmDTO ensDTO) { + final Map 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); + } + + /** + *
+     * 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
+     * 
+ */ + public T exchange(final String url, final HttpMethod method, final Object body, final Class rtnClzz, final Map headerMap) { + + return webClientConfig.webClient() + .method(method) + .uri(url) + .headers(httpHeaders -> getHeaders(httpHeaders, headerMap)) + .bodyValue(body != null ? body : "") + .exchangeToMono(res -> res.bodyToMono(rtnClzz)) + .block(); + } + + /** + *
+     * 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
+     * 
+ */ + public T exchangeFormData(final String url, final HttpMethod method, final Object body, final Class rtnClzz, final Map headerMap) { + + return webClientConfig.webClient() + .method(method) + .uri(url) + .headers(httpHeaders -> getHeaders(httpHeaders, headerMap)) + .body(ObjectUtils.isNotEmpty(body)? BodyInserters.fromFormData(JsonUtils.toMultiValue(body)): BodyInserters.empty()) + .exchangeToMono(res -> res.bodyToMono(rtnClzz)) + .block(); + } + + /** + * webclient file data 호출 처리 + * -> multipart/form-data 전송시 + * url에 파라메터를 포함해야 함(?key=value&key2=value2) + * 에러(.onStatus status.is4xxClientError() || status.is5xxServerError()) + * -> WebClientConfig.responseFilter() 에서 처리 + * @param url + * @param method + * @param files + * @param rtnClzz + * @return rtnClzz + */ + public T exchangeFileData(final String url, final HttpMethod method, final List files, final String pFileName, final Class rtnClzz) { + MultipartBodyBuilder builder = new MultipartBodyBuilder(); + for(MultipartFile mf : files) { + //builder.part(mf.getOriginalFilename().split("\\.")[0], mf.getResource()); + builder.part(pFileName, mf.getResource()); + } + + return webClientConfig.webClient() + .method(method) + .uri(url) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(builder.build())) + .exchangeToMono(res -> res.bodyToMono(rtnClzz)) + .block(); +// .blockOptional() +// .orElse(""); + + } + + public ApiResponseDTO 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 map) { + if(ObjectUtils.isEmpty(map)) return headers; + for(Map.Entry e : map.entrySet()){ + headers.add(e.getKey(), e.getValue()); + } + return headers; + } +} diff --git a/src/main/java/cokr/xit/ens/core/utils/CoreSpringUtils.java b/src/main/java/cokr/xit/ens/core/utils/CoreSpringUtils.java new file mode 100644 index 0000000..4cd9f9d --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/utils/CoreSpringUtils.java @@ -0,0 +1,70 @@ +package cokr.xit.ens.core.utils; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.MessageSource; +import org.springframework.core.env.Environment; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + *
+ * description : Get Bean Object
+ *               Filter / Interceptor 등에서 Bean 사용시 필요
+ *               (Bean으로 등록되는 클래스 내에서만 @Autowired / @Resource 등이 동작)
+ * packageName : kr.xit.core.spring.util
+ * fileName    : CoreSpringUtils
+ * author      : julim
+ * date        : 2023-04-28
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-04-28    julim       최초 생성
+ *
+ * 
+ * @see ApplicationContextProvider + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CoreSpringUtils { + public static ApplicationContext getApplicationContext() { + return ApplicationContextProvider.getApplicationContext(); + } + + public static boolean containsBean(String beanName) { + return getApplicationContext().containsBean(beanName); + } + + public static Object getBean(String beanName) { + return getApplicationContext().getBean(beanName); + } + + public static Object getBean(Class clazz) { + return getApplicationContext().getBean(clazz); + } + + /** + * + * @return MessageSourceAccessor + */ + public static MessageUtil getMessageUtil(){ + return (MessageUtil)getBean(MessageUtil.class); + } + + /** + * + * @return MessageSourceAccessor + */ + public static MessageSource getMessageSource(){ + return (MessageSource)getBean(MessageSource.class); + } + + public static Environment getEnvironment(){ + return (Environment)getBean(Environment.class); + } + + public static ObjectMapper getObjectMapper(){ + return (ObjectMapper)getBean(ObjectMapper.class); + } +} diff --git a/src/main/java/cokr/xit/ens/core/utils/JsonUtils.java b/src/main/java/cokr/xit/ens/core/utils/JsonUtils.java new file mode 100644 index 0000000..c7386aa --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/utils/JsonUtils.java @@ -0,0 +1,339 @@ +package cokr.xit.ens.core.utils; + +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 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import cokr.xit.ens.core.config.support.CustomJacksonConfig; +import cokr.xit.ens.core.exception.BizRuntimeException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + + +/** + *
+ * description : JSON Utility
+ *
+ * packageName : kr.xit.core.support.utils
+ * fileName    : JsonUtils
+ * author      : limju
+ * date        : 2023-09-04
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-09-04    limju       최초 생성
+ *
+ * 
+ * @see CustomJacksonConfig + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonUtils { + + private static final ObjectMapper OM = CoreSpringUtils.getObjectMapper(); + + /** + * json string 인지 판별 + * @param str String + * @return boolean + */ + public static boolean isJson(final String str) { + try { + OM.readTree(str); + return true; + } catch (JsonProcessingException e) { + return false; + } + } + + /** + * Object -> json string + * @return String + * @param obj Object + */ + public static String toJson(final Object obj) { + try { + return ObjectUtils.isNotEmpty(obj)? OM.writeValueAsString(obj) : null; + } catch (JsonProcessingException e) { + throw BizRuntimeException.create(e.getLocalizedMessage()); + } + } + + /** + * Json string -> class로 변환 + * @return T + * @param str String + * @param cls Class + */ + public static T toObject(final String str, final Class cls) { + try { + return ObjectUtils.isNotEmpty(str)? OM.readValue(str, cls) : null; + } catch (JsonProcessingException e) { + throw BizRuntimeException.create(e.getLocalizedMessage()); + } + } + + /** + * Object -> class로 변환 + * @param obj Object + * @param cls Class + * @return T + */ + public static T toObjByObj(final Object obj, final Class cls) { + try { + return ObjectUtils.isNotEmpty(obj)? OM.convertValue(obj, cls) : null; + } catch (IllegalArgumentException e) { + throw BizRuntimeException.create(e.getLocalizedMessage()); + } + } + + /** + * xml String -> cls + * @param xml String + * @param cls Class + * @return cls Class + */ + public static T toObjByXml(String xml, final Class cls){ + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = null; + try { + builder = factory.newDocumentBuilder(); + Document document = builder.parse(new InputSource(new StringReader(xml))); + return toObject(OM.writeValueAsString(document), cls); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw BizRuntimeException.create(e.getMessage()); + } + } + + + /** + * Json string -> class list로 변환 + * @return T + * @param str + * @param cls + * @throws IOException + */ + public static List toObjectList(final String str, final Class cls) { + if(ObjectUtils.isNotEmpty(str)){ + try { + return OM.readValue(str, OM.getTypeFactory().constructCollectionType(List.class, cls)); + } catch (JsonProcessingException e) { + throw BizRuntimeException.create(e.getLocalizedMessage()); + } + }else { + return null; + } + } + + /** + * JSON 문자열 -> Map 구조체로 변환 + * @param str String str + * @return Map + */ + public static Map toMap(final String str) { + try { + return ObjectUtils.isNotEmpty(str)? OM.readValue(str, new TypeReference>(){}) : null; + } catch (JsonProcessingException e) { + throw BizRuntimeException.create(e.getLocalizedMessage()); + } + } + + /** + * Object + * -> MultiValueMap return + * @param obj Object + * @return MultiValueMap + */ + public static MultiValueMap toMultiValue(final Object obj){ + if(ObjectUtils.isEmpty(obj)) return null; + MultiValueMap formData = new LinkedMultiValueMap<>(); + JSONObject jsonObj = toObjByObj(obj, JSONObject.class); + for (Object key : jsonObj.keySet()) { + formData.add((String) key, (String) jsonObj.get(key)); + } + return formData; + } + + /** + * Object의 key와 value를 추출 + * -> key배열, value배열의 JSONObject return + * @param obj key, value를 추출할 Object + * @param keyName key배열의 JSON key name + * @param valueName value배열의 JSON key name + * @return JSONObject key배열, value배열의 JSONObject + */ + public static JSONObject extractObjKeyValue(final Object obj, final String keyName, final String valueName){ + return extractJsonKeyValue(toObjByObj(obj, JSONObject.class), keyName, valueName); + } + + /** + * JSONObject의 key와 value를 추출 + * -> key배열, value배열의 JSONObject return + * @param json + * @param keyName key배열의 JSON key name + * @param valueName value배열의 JSON key name + * @return JSONObject key배열, value배열의 JSONObject + */ + public static JSONObject extractJsonKeyValue(final JSONObject json, final String keyName, final String valueName){ + final JSONObject rtnJson = new JSONObject(); + final JSONArray keys = new JSONArray(); + final JSONArray values = new JSONArray(); + for (Object key : json.keySet()) { + Object value = json.get(key); + + if (value instanceof JSONObject) { + JSONObject jo = (JSONObject) value; + extractJsonKeyValue(jo, keyName, valueName); + } else if (value instanceof JSONArray) { + JSONArray ja = (JSONArray) value; + ja.forEach(obj -> extractJsonKeyValue((JSONObject)obj, keyName, valueName)); + } else{ + //System.out.println(key + ", " + value); + keys.add(String.valueOf(key)); + values.add(value); + } + } + rtnJson.put(keyName, keys); + rtnJson.put(valueName, values); + return rtnJson; + } + + /** + * JSONArray의 key와 value를 추출 + * -> key배열, value배열의 JSONObject return + * @param jsons JSONArray + * @param keyName key배열의 JSON key name + * @param valueName value배열의 JSON key name + * @return JSONObject key배열, value배열의 JSONObject + */ + public static JSONObject extractJsonArrayKeyValue(final net.sf.json.JSONArray jsons, final String keyName, final String valueName){ + final JSONObject rtnJson = new JSONObject(); + final JSONArray keys = new JSONArray(); + final JSONArray values = new JSONArray(); + for(int i = 0; i extractJsonKeyValue((JSONObject)obj, keyName, valueName)); + } else { + //System.out.println(key + ", " + value); + keys.add(String.valueOf(key)); + values.add(value); + } + } + rtnJson.put(keyName, keys); + rtnJson.put(valueName, values); + return rtnJson; + } + + /** + * Json 데이터 보기 좋게 변환. + * @param obj Object json + * @return String + */ + public static String jsonEnterConvert(final Object obj) { + + try { + return jsonEnterConvert((JsonUtils.toJson(obj))); + } catch(Exception e) { + return StringUtils.EMPTY; + } + } + + /** + * Json 데이터 보기 좋게 변환. + * @param json String json + * @return String + */ + private static String jsonEnterConvert(String json) { + + if( json == null || json.length() < 2 ) + return json; + + final int len = json.length(); + final StringBuilder sb = new StringBuilder(); + char c; + String tab = ""; + boolean beginEnd = true; + for( int i=0 ; i 0 ) + sb.insert(0, '\n'); + return sb.toString(); + } +} + diff --git a/src/main/java/cokr/xit/ens/core/utils/MessageUtil.java b/src/main/java/cokr/xit/ens/core/utils/MessageUtil.java new file mode 100644 index 0000000..d6e0aa3 --- /dev/null +++ b/src/main/java/cokr/xit/ens/core/utils/MessageUtil.java @@ -0,0 +1,64 @@ +package cokr.xit.ens.core.utils; + +import java.util.Locale; + +import javax.annotation.Resource; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +/** + *
+ * description : Spring MessageSource를 통한 메세지 read
+ *
+ * packageName : kr.xit.core.spring.util
+ * fileName    : MessageUtil
+ * author      : julim
+ * date        : 2023-04-28
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-04-28    julim       최초 생성
+ *
+ * 
+ */ +@Component +public class MessageUtil { + + @Resource(name = "messageSource") + private MessageSource messageSource; + + /** + * messageSource 에 코드값을 넘겨 메시지를 찾아 리턴한다. + * + * @param code + * @return + */ + public String getMessage(String code) { + return this.getMessage(code, new Object[]{}); + } + + /** + * messageSource 에 코드값과 인자를 넘겨 메시지를 찾아 리턴한다. + * + * @param code + * @param args + * @return + */ + public String getMessage(String code, Object[] args) { + return this.getMessage(code, args, LocaleContextHolder.getLocale()); + } + + /** + * messageSource 에 코드값, 인자, 지역정보를 넘겨 메시지를 찾아 리턴한다. + * + * @param code + * @param args + * @param locale + * @return + */ + public String getMessage(String code, Object[] args, Locale locale) { + return messageSource.getMessage(code, args, locale); + } +} diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/ApiConstants.java b/src/main/java/cokr/xit/ens/modules/kkotalk/ApiConstants.java new file mode 100644 index 0000000..726b412 --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/ApiConstants.java @@ -0,0 +1,393 @@ +package cokr.xit.ens.modules.kkotalk; + +import java.util.Arrays; + +import cokr.xit.ens.core.exception.BizRuntimeException; +import lombok.Getter; + +/** + *
+ * description :
+ *
+ * packageName : kr.xit.ens.support.common
+ * fileName    : KakaoConstants
+ * author      : limju
+ * date        : 2023-05-04
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-05-04    limju       최초 생성
+ *
+ * 
+ */ +public class ApiConstants { + + /** + * profile + */ + public static final String PROFILE = System.getProperty("spring.profiles.active"); + /** + * profile : local 여부 + */ + public static final boolean IS_PROFILE_LOCAL = PROFILE.matches("local-.*"); + + /** + * profile : 춘천 시스템 여부 + */ + public static final boolean IS_CCN = PROFILE.matches(".*-ccn"); + public static final String FFNLN_CODE = "11"; + + /** + * 구분자 없는 date-time 포맷 + */ + public static final String FMT_DT_EMPTY_DLT = "yyyyMMddHHmmss"; + + /** + * date-time 표준 포맷 + */ + public static final String FMT_DT_STD = "yyyy-MM-dd HH:mm:ss"; + + /** + *
+     * 문서 조회 버튼의 명칭을 구분하기 위한 값
+     * code(실제 파라미터로 보내는 값) : button_name(내문서함 내부에 표기되는 버튼 명칭)- name(내문서함 내부에서 구분하기 위한 명칭)
+     * Default : 문서확인 - Default
+     * BILL : 문서확인 - 고지서
+     * BILL_PAY : 문서확인후 납부 - 납부가 포함된 고지서
+     * NOTICE : 문서확인 - 안내문
+     * CONTRACT : 문서확인 - 계약서
+     * REPORT : 문서확인 - 리포트
+     * 
+ */ + @Getter + public enum Categories { + DEFAULT("Default") + , BILL("BILL") + , BILL_PAY("BILL_PAY") + , NOTICE("NOTICE") + , CONTRACT("CONTRACT") + , REPORT("REPORT") + ; + + private final String code; + + Categories(String code) { + this.code = code; + } + + } + + /** + *
+     * INVALID_VALUE : http status code : 400
+     *                 파라미터가 형식에 맞지않음 혹은 필수파라미터 누락
+     *                 {"error_code": "INVALID_VALUE", "error_message": "유효하지 않은 값입니다."
+     * UNIDENTIFIED_USER : http status code : 400
+     *                     받는이의 정보로 발송대상을 특정 할 수 없을때
+     *                     {"error_code": "INVALID_VALUE", "error_message": "유효하지 않은 값입니다."
+     * UNAUTHORIZED : http status code : 401
+     *                access token이 유효하지 않거나 잘못된 경우
+     *                {"error_code": "UNAUTHORIZED","error_message": "접근 권한이 없습니다."
+     * FORBIDDEN : http status code : 403
+     *             문서의 대상자가 내문서함에서 수신거부를 한 경우
+     *             {"error_code": "FORBIDDEN","error_message": "허용되지 않는 요청입니다. 수신거부된 사용자 입니다."}
+     * NOT_FOUND : http status code : 404
+     *             "Contract-Uuid" or "document_binder_uuid"가 유효하지 않거나 잘못된 경우
+     *             {"error_code": "NOT_FOUND","error_message": "요청 정보를 찾을 수 없습니다."
+     * INTERNAL_ERROR : http status code : 500
+     *                  카카오페이 서버에러
+     *                  {"error_code": "INTERNAL_SERVER_ERROR","error_message": "서버 에러입니다. 다시 시도해 주세요."}
+     *  
+ * 카카오페이 전자문서 발송 요청 에러 코드 + */ + @Getter + public enum Error { + INVALID_VALUE("INVALID_VALUE") + , UNIDENTIFIED_USER("UNIDENTIFIED_USER") + , UNAUTHORIZED("UNAUTHORIZED") + , FORBIDDEN("FORBIDDEN") + , NOT_FOUND("NOT_FOUND") + , INTERNAL_ERROR("INTERNAL_ERROR") + ; + + private final String code; + + Error(String code) { + this.code = code; + } + + } + + /** + *
+     * 카카오페이 문서 상태
+     * SENT(송신) > RECEIVED(수신) > READ(열람)/EXPIRED(미열람자료의 기한만료)
+     * 
+ */ + @Getter + public enum KkopayDocStatus { + SENT("SENT") + , RECEIVED("RECEIVED") + , READ("READ") + , EXPIRED("EXPIRED") + ; + + private final String code; + + KkopayDocStatus(String code) { + this.code = code; + } + + } + + /** + *
+     * 카카오톡 문서 상태
+     * RECEIVE(수신, 미열람) > READ(열람)/EXPIRED(최초열람만료일시 또는 재열람 만료일시 초과)
+     * 
+ */ + @Getter + public enum KkotalkDocStatus { + RECEIVED("RECEIVE") + , READ("READ") + , EXPIRED("EXPIRED") + ; + + private final String code; + + KkotalkDocStatus(String code) { + this.code = code; + } + + } + + /** + * 발송처리상태 : ENS003 + */ + @Getter + public enum SndngProcessStatus { + ACCEPT("accept"), + ACCEPT_OK("accept-ok"), + ACCEPT_FAIL("accept-fail"), + MAKE_OK("make-ok"), + MAKE_FAIL1("make-fail1"), + MAKE_FAIL2("make-fail2"), + MAKE_FAIL3("make-fail3"), + SENDING1("sending1"), + SENDING2("sending2"), + SEND_OK("send-ok"), + SEND_FAIL1("send-fail1"), + SEND_FAIL2("send-fail2"), + SEND_FAIL3("send-fail3"), + CLOSE("close") + + ; + + private final String code; + + SndngProcessStatus(String code) { + this.code = code; + } + + } + + /** + * 발송구분코드 + */ + @Getter + public enum SndngSeCode { + KAKAO("KKO-MY-DOC", "카카오"), + KAKAO_NEW("KKO-NEW", "카카오NEW"), + KT_BC("KT-BC", "공공알림문자"), + KT_GIBIS("KT-GIBIS", "GIBIS알림문자"), + ; + + private final String code; + private final String desc; + + SndngSeCode(final String code, final String desc) { + this.code = code; + this.desc = desc; + } + + public static SndngSeCode getSndngSeCode(final String code){ + return Arrays.stream(SndngSeCode.values()) + .filter(ssc -> ssc.getCode().equals(code)) + .findFirst() + .orElseThrow(() -> BizRuntimeException.create(String.format("미정의된 문서 중계자가[%s]", code))); + } + } + + /** + * SignguCode + */ + @Getter + public enum SignguCode { + /** + * 춘천 + */ + CHUNCHEON("51110"), + /** + * 천안동남구청 + */ + CHEONAN_ES("44131"), + /** + * 천안서북 + */ + CHEONAN_WN("44133"), + ; + + private final String code; + + SignguCode(String code) { + this.code = code; + } + + } + + public enum NiceCiWrkDiv { + TOKEN, + PUBLIC_KEY, + SYM_KEY, + CI + } + + public enum KtServiceCode { + SISUL, + CHUMO; + + public static KtServiceCode compare(final String en){ + return valueOf(en); + } + } + + /** + *
+     * 춘천의 발송처리상태 : ENS003 매핑
+     * 01: 요청, 11: 접수완료, 19: 접수실패
+     * 22: 제작완료, 29: 제작실패, 08: 발송완료, 09: 마감, 99: 발송실패
+     * 
+ */ + @Getter + public enum MappingCcnSndngProcessStatus { + ACCEPT("accept", "01"), + ACCEPT_OK("accept-ok", "11"), + ACCEPT_FAIL("accept-fail", "19"), + MAKE_OK("make-ok", "22"), + MAKE_FAIL1("make-fail1", "29"), + MAKE_FAIL2("make-fail2", "29"), + MAKE_FAIL3("make-fail3", "29"), + SENDING1("sending1", "08"), + SENDING2("sending2", "08"), + SEND_OK("send-ok", "08"), + SEND_FAIL1("send-fail1", "99"), + SEND_FAIL2("send-fail2", "99"), + SEND_FAIL3("send-fail3", "99"), + CLOSE("close", "09") + + ; + + private final String ensCode; + private final String trfCode; + + MappingCcnSndngProcessStatus(String ensCode, String trfCode) { + this.ensCode = ensCode; + this.trfCode = trfCode; + } + + /** + *
+         * traffic 상태코드를 ens 상태코드로 매핑
+         * @param trfCode
+         * @return traffic 상태코드에 매핑된 ens 상태코드
+         * 
+ */ + public static String fromTraffic(final String trfCode){ + return Arrays.stream(MappingCcnSndngProcessStatus.values()) + .filter(en -> en.getTrfCode().equals(trfCode)) + .findFirst() + .map(MappingCcnSndngProcessStatus::getEnsCode) + .orElseThrow(() -> BizRuntimeException.create(String.format("미정의된 상태코드가[%s]", trfCode))); + } + + /** + *
+         * ens 상태코드를 traffic 상태코드로 매핑
+         * @param ensCode
+         * @return ens 상태코드에 매핑된 traffic 상태코드
+         * 
+ */ + public static String fromEns(final String ensCode){ + return Arrays.stream(MappingCcnSndngProcessStatus.values()) + .filter(en -> en.getEnsCode().equals(ensCode)) + .findFirst() + .map(MappingCcnSndngProcessStatus::getTrfCode) + .orElseThrow(() -> BizRuntimeException.create(String.format("미정의된 상태코드가[%s]", ensCode))); + } + } + + /** + *
+     * 천안의 발송처리상태 : ENS003 매핑
+     * 01: 요청, 11: 접수완료, 19: 접수실패
+     * 22: 제작완료, 29: 제작실패, 08: 발송완료, 09: 마감, 99: 발송실패
+     * 
+ */ + @Getter + public enum MappingCanSndngProcessStatus { + ACCEPT("accept", "accept"), + ACCEPT_OK("accept-ok", "acptok"), + ACCEPT_FAIL("accept-fail", "acptfail"), + MAKE_OK("make-ok", "ensmake"), + MAKE_FAIL1("make-fail1", "makefail"), + MAKE_FAIL2("make-fail2", "makefail"), + MAKE_FAIL3("make-fail3", "makefail"), + SENDING1("sending1", "ensok"), + SENDING2("sending2", "ensok"), + SEND_OK("send-ok", "ensopen"), + SEND_FAIL1("send-fail1", "ensfail"), + SEND_FAIL2("send-fail2", "ensfail"), + SEND_FAIL3("send-fail3", "ensfail"), + CLOSE("close", "ensclose") + + ; + + private final String ensCode; + private final String trfCode; + + MappingCanSndngProcessStatus(String ensCode, String trfCode) { + this.ensCode = ensCode; + this.trfCode = trfCode; + } + + /** + *
+         * traffic 상태코드를 ens 상태코드로 매핑
+         * @param trfCode
+         * @return traffic 상태코드에 매핑된 ens 상태코드
+         * 
+ */ + public static String fromTraffic(final String trfCode){ + return Arrays.stream(MappingCanSndngProcessStatus.values()) + .filter(en -> en.getTrfCode().equals(trfCode)) + .findFirst() + .map(MappingCanSndngProcessStatus::getEnsCode) + .orElseThrow(() -> BizRuntimeException.create(String.format("미정의된 상태코드가[%s]", trfCode))); + } + + /** + *
+         * ens 상태코드를 traffic 상태코드로 매핑
+         * @param ensCode
+         * @return ens 상태코드에 매핑된 traffic 상태코드
+         * 
+ */ + public static String fromEns(final String ensCode){ + return Arrays.stream(MappingCanSndngProcessStatus.values()) + .filter(en -> en.getEnsCode().equals(ensCode)) + .findFirst() + .map(MappingCanSndngProcessStatus::getTrfCode) + .orElseThrow(() -> BizRuntimeException.create(String.format("미정의된 상태코드가[%s]", ensCode))); + } + } +} diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/CmmEnsUtils.java b/src/main/java/cokr/xit/ens/modules/kkotalk/CmmEnsUtils.java new file mode 100644 index 0000000..30124b2 --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/CmmEnsUtils.java @@ -0,0 +1,285 @@ +// package cokr.xit.ens.modules.kkotalk; +// +// import java.nio.charset.StandardCharsets; +// import java.security.InvalidAlgorithmParameterException; +// import java.security.InvalidKeyException; +// import java.security.KeyFactory; +// import java.security.MessageDigest; +// import java.security.NoSuchAlgorithmException; +// import java.security.spec.InvalidKeySpecException; +// import java.security.spec.X509EncodedKeySpec; +// import java.util.ArrayList; +// import java.util.Base64; +// import java.util.List; +// import java.util.Locale; +// import java.util.Random; +// import java.util.Set; +// import java.util.UUID; +// +// import javax.crypto.BadPaddingException; +// import javax.crypto.Cipher; +// import javax.crypto.IllegalBlockSizeException; +// import javax.crypto.Mac; +// import javax.crypto.NoSuchPaddingException; +// import javax.crypto.SecretKey; +// import javax.crypto.spec.IvParameterSpec; +// import javax.crypto.spec.SecretKeySpec; +// import javax.validation.ConstraintViolation; +// import javax.validation.Validation; +// import javax.validation.Validator; +// +// import org.apache.commons.lang3.ObjectUtils; +// import org.apache.commons.lang3.StringUtils; +// import org.springframework.util.Base64Utils; +// +// import com.sun.org.apache.xalan.internal.xsltc.compiler.util.ErrorMsg; +// +// import cokr.xit.ens.core.exception.BizRuntimeException; +// import cokr.xit.ens.core.utils.CoreSpringUtils; +// import cokr.xit.ens.core.utils.MessageUtil; +// import cokr.xit.ens.modules.kkotalk.model.CmmEnsRequestDTO; +// import cokr.xit.ens.modules.kkotalk.model.CmmEnsRlaybsnmDTO; +// import lombok.AccessLevel; +// import lombok.NoArgsConstructor; +// +// /** +// *
+//  * description : ENS 공통 method
+//  *
+//  * packageName : kr.xit.ens.cmm
+//  * fileName    : CmmNiceCiUtils
+//  * author      : limju
+//  * date        : 2023-09-19
+//  * ======================================================================
+//  * 변경일         변경자        변경 내용
+//  * ----------------------------------------------------------------------
+//  * 2023-09-19    limju       최초 생성
+//  *
+//  * 
+// */ +// +// @NoArgsConstructor(access = AccessLevel.PRIVATE) +// public class CmmEnsUtils { +// private static final MessageUtil messageUtil = CoreSpringUtils.getMessageUtil(); +// private static final ICmmEnsCacheService cacheService = ApiSpringUtils.getCmmEnsCacheService(); +// private static final IBizKtBcService bizKtService = ApiSpringUtils.getBizKtMmsService(); +// +// /** +// * 문서 중개자 인증 정보 조회 +// * @param signguCode string +// * @param ffnlgCode String +// * @param seCode SndngSeCode 문서중개자 구분 코드 +// * @return CmmEnsRlaybsnmDTO 문서중개자 정보 +// */ +// public static CmmEnsRlaybsnmDTO getRlaybsnmInfo(final String signguCode, final String ffnlgCode, final +// SndngSeCode seCode) { +// CmmEnsRequestDTO ensDTO = CmmEnsRequestDTO.builder() +// .signguCode(signguCode) +// .ffnlgCode(ffnlgCode) +// .profile(ApiConstants.IS_PROFILE_LOCAL? "local" : "prod") +// .build(); +// +// final CmmEnsRlaybsnmDTO dto = cacheService.getRlaybsnmInfoCache(ensDTO); +// if(ObjectUtils.isEmpty(dto)) throw BizRuntimeException.create(messageUtil.getMessage("fail.api.rlaybsnm.info")); +// +// // KT인 경우 토큰유효기간 check +// if(SndngSeCode.KT_BC.equals(seCode)){ +// +// if(StringUtils.isNotEmpty(dto.getKtTokenExpiresIn()) +// && DateUtils.getTodayAndNowTime(ApiConstants.FMT_DT_STD).compareTo(dto.getKtTokenExpiresIn()) < 0 +// && ObjectUtils.isNotEmpty(dto.getKtAccessToken()) +// ) return dto; +// +// // 유효기간이 경과된 경우 재발급 +// bizKtService.requestToken( +// KtMnsRequest.builder() +// .signguCode(signguCode) +// .ffnlgCode(ffnlgCode) +// .profile(ApiConstants.IS_PROFILE_LOCAL? "local" : "prod") +// .build() +// ); +// return cacheService.getRlaybsnmInfoCache(ensDTO); +// } +// return dto; +// } +// +// /** +// *
+//      * parameter validation check
+//      * invalid parameter message -> String으로 BizRuntimeException throw
+//      * @param t T
+//      * 
+// */ +// public static void validate(T t) { +// Locale.setDefault(Locale.KOREA); +// final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); +// final Set> list = validator.validate(t); +// +// if (!list.isEmpty()) { +// throw BizRuntimeException.create( +// list.stream() +// .map(row -> String.format("%s=%s", row.getPropertyPath(), row.getMessageTemplate())) +// //.map(row -> String.format("%s=%s", row.getPropertyPath(), row.get()) ? row.getMessage(): row.getMessageTemplate())) +// .toList().toString()); +// } +// } +// +// /** +// * parameter validation check +// * @param t T +// * @return List +// */ +// public static List getValidateErrors(T t) { +// Locale.setDefault(Locale.KOREA); +// final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); +// final Set> list = validator.validate(t); +// +// if (!list.isEmpty()) { +// return list.stream() +// .map(row -> new ErrorMsg(String.format("%s=%s", row.getPropertyPath(), row.getMessageTemplate()))) +// .toList(); +// } +// return new ArrayList<>(); +// } +// +// /** +// * length 길이의 UUID String return -> '-' remove +// * @param length +// * @return +// */ +// public static String generateLengthUuid(int length) { +// final String allChars = UUID.randomUUID().toString().replace("-", ""); +// final Random random = new Random(); +// final char[] otp = new char[length]; +// for (int i = 0; i < length; i++) { +// otp[i] = allChars.charAt(random.nextInt(allChars.length())); +// } +// return String.valueOf(otp); +// } +// +// /** +// * 공개키로 암호화를 수행 +// * +// * @param publicKeyString String +// * @param symkeyRegInfo String +// * @return String +// */ +// public static String encSymkeyRegInfo(String publicKeyString, String symkeyRegInfo) { +// try { +// KeyFactory keyFactory = KeyFactory.getInstance("RSA"); +// byte[] cipherEnc = Base64.getDecoder().decode(publicKeyString); +// X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(cipherEnc); +// java.security.PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); +// +// Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); +// cipher.init(Cipher.ENCRYPT_MODE, publicKey); +// byte[] bytePlain = cipher.doFinal(symkeyRegInfo.getBytes()); +// +// return Base64Utils.encodeToString(bytePlain); +// } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | +// IllegalBlockSizeException | BadPaddingException | InvalidKeyException e){ +// throw BizRuntimeException.create(e.getMessage()); +// } +// } +// +// /** +// * sha256 암호화 +// * +// * @param text String +// * @return String +// */ +// public static String hexSha256(final String text) { +// final StringBuilder sbuf = new StringBuilder(); +// +// try { +// final MessageDigest mDigest = MessageDigest.getInstance("SHA-256"); +// mDigest.update(text.getBytes()); +// +// final byte[] msgStr = mDigest.digest(); +// +// for(final byte tmpStrByte : msgStr) { +// final String tmpEncTxt = Integer.toString((tmpStrByte & 0xff) + 0x100, 16) +// .substring(1); +// +// sbuf.append(tmpEncTxt); +// } +// } catch (NoSuchAlgorithmException nae){ +// throw BizRuntimeException.create(nae.getMessage()); +// } +// return sbuf.toString(); +// } +// +// +// /** +// *
+//      * AES 암호화 -> Base64 encoding return
+//      * -> Nice ci 데이타 암호화
+//      *
+//      * @param key String
+//      * @param iv String
+//      * @param planText String
+//      * @return String  Base64 encoding data
+//      * 
+// */ +// public static String encodeAesData(final String key, final String iv, final String planText) { +// final SecretKey secureKey = new SecretKeySpec(key.getBytes(), "AES"); +// try { +// final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); +// cipher.init(Cipher.ENCRYPT_MODE, secureKey, new IvParameterSpec(iv.getBytes())); +// +// final byte[] encData = cipher.doFinal(planText.trim().getBytes(StandardCharsets.UTF_8)); +// return Base64.getEncoder().encodeToString(encData); +// +// }catch (NoSuchPaddingException | NoSuchAlgorithmException | +// InvalidAlgorithmParameterException | InvalidKeyException | +// IllegalBlockSizeException | BadPaddingException e){ +// throw BizRuntimeException.create(e.getMessage()); +// } +// } +// +// /** +// *
+//      * Hmac 무결성체크값(integrity_value) 생성
+//      * @param hmacKey String
+//      * @param message String
+//      * @return String
+//      * 
+// */ +// public static String encodeHmacSha256(final String hmacKey, final String message) { +// try { +// final Mac mac = Mac.getInstance("HmacSHA256"); +// final SecretKeySpec sks = new SecretKeySpec(hmacKey.getBytes(), "HmacSHA256"); +// mac.init(sks); +// final byte[] hmac256 = mac.doFinal(message.getBytes()); +// return Base64.getEncoder().encodeToString(hmac256); +// +// }catch (NoSuchAlgorithmException|InvalidKeyException e){ +// throw BizRuntimeException.create(e.getMessage()); +// } +// } +// +// /** +// * AES 복호화 +// * @param encData String +// * @param key String +// * @param iv String +// * @return String +// */ +// public static String decodeAesData(String encData, String key, String iv) { +// +// final byte[] respDataEnc = Base64.getDecoder().decode(encData.getBytes()); +// final SecretKey secureKey = new SecretKeySpec(key.getBytes(), "AES"); +// +// try { +// final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); +// cipher.init(Cipher.DECRYPT_MODE, secureKey, new IvParameterSpec(iv.getBytes())); +// final byte[] decrypted = cipher.doFinal(respDataEnc); +// return new String(decrypted); +// +// }catch (NoSuchPaddingException | NoSuchAlgorithmException | +// InvalidAlgorithmParameterException | InvalidKeyException | +// IllegalBlockSizeException | BadPaddingException e){ +// throw BizRuntimeException.create(e.getMessage()); +// } +// } +// } diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/model/CmmEnsRequestDTO.java b/src/main/java/cokr/xit/ens/modules/kkotalk/model/CmmEnsRequestDTO.java new file mode 100644 index 0000000..898cefa --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/model/CmmEnsRequestDTO.java @@ -0,0 +1,67 @@ +package cokr.xit.ens.modules.kkotalk.model; + +import javax.validation.constraints.Size; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + *
+ * description : 전자고지 문서중계자 Request 공통 DTO
+ *
+ * packageName : kr.xit.biz.ens.model.kt
+ * fileName    : CmmEnsRequestDTO
+ * author      : limju
+ * date        : 2023-09-22
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-09-22    limju       최초 생성
+ *
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +@JsonInclude(Include.NON_NULL) +public class CmmEnsRequestDTO { + + /** + * 시군구 코드 + */ + @Schema(requiredMode = RequiredMode.REQUIRED, title = "시군구코드", example = "51110") + @Size(min = 1, max = 10, message = "시군구 코드는 필수 입니다") + @JsonProperty("signguCode") + private String signguCode; + + /** + * 과태료 코드 + */ + @Schema(requiredMode = RequiredMode.REQUIRED, title = "과태료코드", example = "11") + @Size(min = 1, max = 2, message = "과태료 코드는 필수 입니다") + @JsonProperty("ffnlgCode") + private String ffnlgCode = "11"; + + /** + * active profile + */ + @Schema(requiredMode = RequiredMode.AUTO, title = "profile", example = "local") + @JsonProperty("profile") + private String profile; + + /** + * 1차 발송 + */ + @Schema(hidden = true, requiredMode = RequiredMode.AUTO, title = "1차 발송", example = "KKO-MY-DOC") + @JsonProperty("try1") + private String try1; +} diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/model/CmmEnsRlaybsnmDTO.java b/src/main/java/cokr/xit/ens/modules/kkotalk/model/CmmEnsRlaybsnmDTO.java new file mode 100644 index 0000000..568ec56 --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/model/CmmEnsRlaybsnmDTO.java @@ -0,0 +1,174 @@ +package cokr.xit.ens.modules.kkotalk.model; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + *
+ * description : 전자고지 문서중계자 정보 DTO
+ *
+ * packageName : kr.xit.biz.ens.model.kt
+ * fileName    : KtMmsDTO
+ * author      : limju
+ * date        : 2023-09-22
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-09-22    limju       최초 생성
+ *
+ * 
+ */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CmmEnsRlaybsnmDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 시군구 코드 + */ + private String signguCode; + /** + * 과태료 코드 + */ + private String ffnlgCode; + /** + * profile + */ + private String profile; + /** + * 시군구 명 + */ + private String signguNm; + /** + * 과태료 명 + */ + private String ffnlgNm; + /** + * KAKAO CLIENT ID + */ + private String kakaoClientId; + /** + * KAKAO 상품 코드 + */ + private String kakaoProductCd; + /** + * KAKAO ACCESS TOKEN + */ + private String kakaoAccessToken; + /** + * KAKAO CONTRACT UUID + */ + private String kakaoContractUuid; + /** + * KAKAO_NEW PARTNER KEY + */ + private String kakaoPartnerRestApiKey; + /** + * KAKAO_NEW DEALER KEY + */ + private String kakaoDealerRestApiKey; + /** + * KAKAO_NEW SETTLE ID + */ + private String kakaoSettleId; + /** + * KT client id + */ + private String ktClientId; + /** + * KT client tp + */ + private String ktClientTp; + /** + * KT Scope + */ + private String ktScope; + /** + * KT Service code + */ + private String ktServiceCode; + /** + * KT service client ID + */ + private String ktSvcClientId; + /** + * KT service client secret + */ + private String ktSvcClientSecret; + /** + * KT service cerf key + */ + private String ktSvcCerfKey; + /** + * KT_ACCESS_TOKEN + */ + private String ktAccessToken; + + /** + * KT 토큰 유효 기간 + */ + private String ktTokenExpiresIn; + /** + * KT 토큰 식별자 + */ + private String ktTokenJti; + /** + * postplus apiKey + */ + private String pplusApiKey; + + /** + * EPost service key + */ + private String epostServiceKey; + + /** + * 발송인 명 + */ + private String senderNm; + /** + * 발송인 우편번호 + */ + private String senderZipNo; + /** + * 발송인 주소 + */ + private String senderAddr; + /** + * 발송인 상세 주소 + */ + private String senderDetailAddr; + + /** + * 등록 일시 + */ + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(pattern = "yyyy-MM-dd kk:mm:ss") + private LocalDateTime registDt; + /** + * 등록자 + */ + private String register; + /** + * 수정 일시 + */ + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonFormat(pattern = "yyyy-MM-dd kk:mm:ss") + private LocalDateTime updtDt; + /** + * 수정자 + */ + private String updusr; +} diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/model/KkotalkApiDTO.java b/src/main/java/cokr/xit/ens/modules/kkotalk/model/KkotalkApiDTO.java new file mode 100644 index 0000000..dac233e --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/model/KkotalkApiDTO.java @@ -0,0 +1,416 @@ +package cokr.xit.ens.modules.kkotalk.model; + +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 cokr.xit.ens.core.aop.IApiResponse; +import cokr.xit.ens.modules.kkotalk.ApiConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + *
+ * description : 카카오톡 전자문서 DTO
+ *               API 호출시 속성값이 없는 경우 속성을 json에 포함시키지 않는다.
+ *               => @JsonInclude(JsonInclude.Include.NON_EMPTY) 설정
+ * packageName : kr.xit.biz.ens.model.kakao.talk
+ * fileName    : KkotalkApiDTO
+ * author      : limju
+ * date        : 2024-08-12
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2024-08-12    limju       최초 생성
+ *
+ * 
+ */ +public class KkotalkApiDTO { + + //------------------- Envelope ------------------------------------------------------------------------------------------------ + @Schema(name = "Envelope", description = "문서발송(단건) 요청 파라메터 DTO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @SuperBuilder + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public static class Envelope { + /** + * 발송할 문서의 제목 : 필수 - 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; + + /** + *
+         * 문서 원문(열람정보)에 대한 hash 값
+         * D10_2 상품 사용시 필수
+         * 
+ */ + @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 = 500, message = "문서 정보 안내 화면에 노출할 문구(max=500)") + private String guide; + + /** + * payload + */ + @Schema(title = "payload", example = "payload 파라미터 입니다.") + @Size(max = 200, message = "payload(max=200)") + private String payload; + + /** + *
+         * 최초 열람 만료 일시 지정(최대: 요청 일시로부터 6개월 이내) - 필수
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+ */ + @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; + + /** + *
+         * 재열람 만료 일시 지정
+         * 최소: readExpiredAt 이후의 시각
+         * 최대값: 9999-12-31'T'23:59:59, null 값이면 무제한
+         * 비고: DX0 상품인 경우 최대값은 요청일로부터 6개월 이내
+         * 
+ */ + @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; + + /** + *
+         * 알림톡 내용에 개인정보 제거 여부
+         * default: false
+         * 
+ */ + @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; + + /** + *
+         * 수신자 전화번호
+         * ci 미전송시 필수
+         * 
+ */ + @Schema(title = "수신자 전화번호", example = "01012345678") + @Pattern(regexp = "^$|^\\d{11}$", message = "수신자 전화번호(max=11)") + private String phoneNumber; + + /** + *
+         * 수신자 이름
+         * ci 미전송시 필수
+         * 
+ */ + @Schema(title = "수신자 이름", example = "김페이") + @Size(max = 26, message = "수신자 이름(max=26)") + private String name; + + /** + *
+         * 수신자 생년월일 (YYYY-MM-DD 형식)
+         * ci 미전송시 필수
+         * 
+ */ + @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; + + /** + *
+         * 문서매핑용 식별자 - 최대 40자
+         * 
+ */ + @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 { + /** + *
+         * 본인인증 후 사용자에게 보여줄 웹페이지 주소
+         * D10 상품 사용시 필수
+         * 1000자 이하의 URL 형식
+         * 
+ */ + @Schema(title = "본인인증 후 사용자에게 보여줄 웹페이지 주소", example = "http://ipAddress/api/kakaopay/v1/ott") + @Size(max = 1000, message = "본인인증후 사용자에게 보여줄 페이지 주소는 필수입니다(max=1000)") + private String link; + + /** + *
+         * 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 = " ")
+        @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;
+
+        /**
+         * 
+         * 문서매핑용 식별자 - 최대 40자
+         * 
+ */ + private String externalId; + + /** + *
+         * 진행상태 : 토큰검증시 필수
+         * 수신,미열람|열람|최초열람만료일시 또는 재열람 만료일시 초과
+         * RECEIVE|READ|EXPIRED
+         * 
+ * @see ApiConstants.KkotalkDocStatus + */ + private ApiConstants.KkotalkDocStatus status; + + /** + *
+         * 문서 송신 일시 - 토큰검증시 필수
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+         * 문서 수신 일시 - 토큰검증시 필수
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+ */ + private String receivedAt; + + /** + *
+         * 문서 열람 일시
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+ */ + private String readAt; + + /** + *
+         * 문서 열람 인증 일시
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+ */ + private String authenticatedAt; + + /** + *
+         * 토큰 검증 일시
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+ */ + private String ottVerifiedAt; + + /** + *
+         * 사용자의 알림톡 수신 가능 여부
+         * 
+ */ + private Boolean isNotificationUnavailable; + + /** + *
+         * 사용자의 알림톡 수신 일시
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+ */ + 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 { + /** /** + *
+         * 문서 열람 만료 일시
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+ */ + private String readExpiredAt; + + /** /** + *
+         * 유통정보의 수신 시각
+         * 공인전자주소 활성화와 문서 수신이 모두 완료된 시각
+         * yyyy-MM-dd'T'HH:mm:ss 형식 > DateUtils.getTimeTOfNow()
+         * 
+ */ + private String distributionReceivedAt; + } + //------------------ DocStatus ---------------------------------------------------------------------- + + + //------------------ SendResponse ---------------------------------------------------------------------- + @Schema(name = "EnvelopeRes DTO", description = "문서발송 응답 DTO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @SuperBuilder + @EqualsAndHashCode(callSuper = false) + //@JsonInclude(JsonInclude.Include.NON_EMPTY) + public static class EnvelopeRes extends KkotalkErrorDTO{ + /** + * 문서 고유 ID, 34자로 고정 + */ + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "문서 고유 ID, 34자로 고정", example = " ") + @Size(min = 34, max = 34, message = "문서 고유 ID는 필수입니다(34자)") + private String envelopeId; + + /** + *
+         * 문서매핑용 식별자 - 최대 40자
+         * 
+ */ + 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; + + /** + *
+         * 문서매핑용 식별자 - 최대 40자
+         * 
+ */ + private String externalId; + } + + //------------------------------------------------------------------------------------- + + @Schema(name = "EnvelopeId DTO", description = "EnvelopeId DTO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @SuperBuilder + @EqualsAndHashCode(callSuper = false) + public static class EnvelopeId extends CmmEnsRequestDTO{ + /** + * 문서 고유 ID, 34자로 고정 + */ + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "문서 고유 ID, 34자로 고정", example = " ") + @Size(min = 34, max = 34, message = "문서 고유 ID는 필수입니다(34자)") + private String envelopeId; + } + +} diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/model/KkotalkDTO.java b/src/main/java/cokr/xit/ens/modules/kkotalk/model/KkotalkDTO.java new file mode 100644 index 0000000..ea1d8c0 --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/model/KkotalkDTO.java @@ -0,0 +1,111 @@ +package cokr.xit.ens.modules.kkotalk.model; + +import java.util.List; + +import javax.validation.Valid; +import javax.validation.constraints.Size; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + *
+ * description : 카카오톡 전자문서 요청 파라메터 및 응답 DTO
+ *
+ * packageName : kr.xit.ens.model.kakao.talk
+ * fileName    : KkotalkDTO
+ * author      : limju
+ * date        : 2024-08-12
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2024-08-12    limju       최초 생성
+ *
+ * 
+ */ +public class KkotalkDTO extends KkotalkApiDTO { + + //------------------ envelop ---------------------------------------------------------------------- + @Schema(name = "SendRequest DTO", description = "문서발송 request DTO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @SuperBuilder + @EqualsAndHashCode(callSuper = false) + public static class SendRequest extends CmmEnsRequestDTO { + /** + *
+         * 상품 코드 - 필수
+         * D10_1|D10_2|D11_1|D11_2
+         * 
+ */ + @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\"") + 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 { + /** + *
+         * 상품 코드 - 필수
+         * D10_1|D10_2|D11_1|D11_2
+         * 
+ */ + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, title = "상품코드", example = "D10_1", allowableValues = {"D10_1","D10_2","D11_1","D11_2"}) + @Size(min = 3, max = 5, message = "상품 코드는 필수 입니다(\"D10_1\",\"D10_2\",\"D11_1\",\"D11_2\"") + private String productCode = "D10_2"; + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + private List envelopes; + } + + @Schema(name = "BulkSendResponse DTO", description = "문서발송(bulk) response DTO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @SuperBuilder + @EqualsAndHashCode(callSuper = false) + public static class BulkSendResponse { + private List envelopes; + } + + @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 envelopes; + } + + @Schema(name = "BulkStatusResponse DTO", description = "문서상태조회(bulk) response DTO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @SuperBuilder + public static class BulkStatusResponse { + private List envelopeStatus; + } + //------------------ bulk ---------------------------------------------------------------------- + +} diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/service/IKkotalkEltrcDocService.java b/src/main/java/cokr/xit/ens/modules/kkotalk/service/IKkotalkEltrcDocService.java new file mode 100644 index 0000000..e693444 --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/service/IKkotalkEltrcDocService.java @@ -0,0 +1,94 @@ +package cokr.xit.ens.modules.kkotalk.service; + +import cokr.xit.ens.modules.kkotalk.model.KkotalkApiDTO; +import cokr.xit.ens.modules.kkotalk.model.KkotalkDTO; + +/** + *
+ * description : 카카오 페이 전자 문서 발송 요청 인터 페이스
+ * packageName : kr.xit.ens.kakao.talk.service
+ * fileName    : IKkopayEltrcDocService
+ * author      : julim
+ * date        : 2023-04-28
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-04-28    julim       최초 생성
+ *
+ * 
+ */ +public interface IKkotalkEltrcDocService { + + /** + *
+     * 모바일웹 연계 문서발송 요청
+     * -.이용기관 서버에서 전자문서 서버로 문서발송 처리를 요청합니다.
+     * 
+ * @param reqDTO KkotalkApiDTO.SendRequest + * @return KkotalkApiDTO.SendResponse + */ + KkotalkDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO); + + /** + *
+     * 토큰 유효성 검증(Redirect URL  접속 허용/불허)
+     * 
+ * @param reqDTO KkotalkApiDTO.ValidTokenRequest + * @return KkotalkApiDTO.ValidTokenResponse> + */ + KkotalkApiDTO.ValidTokenResponse validToken(final KkotalkApiDTO.ValidTokenRequest reqDTO); + + /** + *
+     * 문서 열람 처리 API
+     * -.문서에 대해서 열람 상태로 변경. 사용자가 문서열람 시(OTT 검증 완료 후 페이지 로딩 완료 시점) 반드시 문서 열람 상태 변경 API를 호출해야 함.
+     * -.미 호출 시 아래와 같은 문제 발생
+     * 1)유통증명시스템을 사용하는 경우 해당 API를 호출한 시점으로 열람정보가 등록되어 미 호출 시 열람정보가 등록 되지 않음.
+     * 2)문서상태조회 API(/v1/envelopes/${ENVELOPE_ID}/read) 호출 시 read_at최초 열람시간) 데이터가 내려가지 않음.
+     * 
+ * @param reqDTO KkotalkDTO.EnvelopeId + */ + void modifyStatus(final KkotalkDTO.EnvelopeId reqDTO); + + + /** + *
+     * 문서 상태 조회 API
+     * -.이용기관 서버에서 카카오페이 전자문서 서버로 문서 상태에 대한 조회를 요청 합니다.
+     * : 발송된 문서의 진행상태를 알고 싶은 경우, flow와 상관없이 요청 가능
+     * : polling 방식으로 호출할 경우, 호출 간격은 5초를 권장.
+     * -.doc_box_status 상태변경순서
+     * : RECEIVE(수신, 미처리) > READ(열람)/EXPIRED
+     * 
+ * @param reqDTO KkotalkDTO.EnvelopeId + * @return KkotalkApiDTO.EnvelopeStatusResponse + */ + KkotalkApiDTO.EnvelopeStatusResponse findStatus(final KkotalkApiDTO.EnvelopeId reqDTO); + + /** + *
+     * 대량(bulk) 문서발송 요청
+     * -.이용기관 서버에서 카카오페이 내문서함 서버로 대량(bulk) 문서발송 처리를 요청합니다.
+     * 
+ * @param reqDTO KkopayDocBulkDTO.BulkSendRequests + * @return KkopayDocBulkDTO.BulkSendResponses + */ + KkotalkDTO.BulkSendResponse requestSendBulk(final KkotalkDTO.BulkSendRequest reqDTO); + + /** + *
+     * 대량(bulk) 문서 상태 조회 API
+     * -.이용기관 서버에서 카카오페이 전자문서 서버로 문서 상태에 대한 조회를 요청 합니다.
+     * : 발송된 문서의 진행상태를 알고 싶은 경우, flow와 상관없이 요청 가능
+     * : polling 방식으로 호출할 경우, 호출 간격은 5초를 권장.
+     * : RECEIVED(수신,미수신) > READ(열람)/EXPIRED
+     * 
+ * @param reqDTO KkotalkDTO.BulkStatusRequest + * @return KkotalkDTO.BulkStatusResponse + */ + KkotalkDTO.BulkStatusResponse findBulkStatus(final KkotalkDTO.BulkStatusRequest reqDTO); + + + //KkotalkApiDTO.ValidTokenResponse findKkotalkReadyAndMblPage(KkotalkApiDTO.ValidTokenRequest reqDTO); +} + diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/service/KkotalkEltrcDocService.java b/src/main/java/cokr/xit/ens/modules/kkotalk/service/KkotalkEltrcDocService.java new file mode 100644 index 0000000..6410882 --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/service/KkotalkEltrcDocService.java @@ -0,0 +1,317 @@ +package cokr.xit.ens.modules.kkotalk.service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +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 cokr.xit.ens.core.exception.BizRuntimeException; +import cokr.xit.ens.core.utils.ApiWebClientUtil; +import cokr.xit.ens.core.utils.Checks; +import cokr.xit.ens.core.utils.JsonUtils; +import cokr.xit.ens.modules.kkotalk.model.CmmEnsRequestDTO; +import cokr.xit.ens.modules.kkotalk.model.CmmEnsRlaybsnmDTO; +import cokr.xit.ens.modules.kkotalk.model.KkotalkApiDTO; +import cokr.xit.ens.modules.kkotalk.model.KkotalkDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + *
+ * description : 카카오 페이 전자 문서 발송 요청 서비스
+ * packageName : kr.xit.ens.kakao.talk.service
+ * fileName    : KkopayEltrcDocService
+ * author      : julim
+ * date        : 2023-04-28
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2023-04-28    julim       최초 생성
+ *
+ * 
+ */ +@Slf4j +@RequiredArgsConstructor +@Component +public class KkotalkEltrcDocService implements + IKkotalkEltrcDocService { + + @Value("${contract.kakao.talk.host}") + private String HOST; + + @Value("#{'${contract.kakao.talk.send}'.split(';')}") + private String[] API_SEND; + + @Value("#{'${contract.kakao.talk.bulksend}'.split(';')}") + private String[] API_BULKSEND; + + @Value("#{'${contract.kakao.talk.validToken}'.split(';')}") + private String[] API_VALID_TOKEN; + + @Value("#{'${contract.kakao.talk.modifyStatus}'.split(';')}") + private String[] API_MODIFY_STATUS; + + + @Value("#{'${contract.kakao.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}"; + + + /** + *
+     * 모바일웹 연계 문서발송 요청 : POST
+     * -.이용기관 서버에서 전자문서 서버로 문서발송 처리 요청
+     * 
+ * @param reqDTO KkoPayEltrDocDTO.RequestSendReq + * @return ApiResponseDTO + */ + @Override + public KkotalkDTO.SendResponse requestSend(final KkotalkDTO.SendRequest reqDTO) { + if(Checks.isEmpty(reqDTO.getProductCode())){ + throw BizRuntimeException.create("상품 코드는 필수 입니다."); + } + List 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 webClient.exchangeKkotalk( + HOST + API_SEND[0].replace("{PRODUCT_CODE}", reqDTO.getProductCode()), + HttpMethod.valueOf(API_SEND[1]), + JsonUtils.toJson(envelope), + KkotalkDTO.SendResponse.class, + getRlaybsnmInfo(reqDTO)); + } + + /** + *
+     * 토큰 유효성 검증(Redirect URL  접속 허용/불허) : GET
+     * 
+ * @param reqDTO KkopayDocDTO.ValidTokenRequest + * @return ApiResponseDTO + */ + @Override + public KkotalkApiDTO.ValidTokenResponse validToken(final KkotalkApiDTO.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, + KkotalkApiDTO.ValidTokenResponse.class, + getRlaybsnmInfo(reqDTO)); + } + + /** + *
+     * 문서 열람 처리 API : POST
+     * -.문서에 대해서 열람 상태로 변경. 사용자가 문서열람 시(OTT 검증 완료 후 페이지 로딩 완료 시점) 반드시 문서 열람 상태 변경 API를 호출해야 함.
+     * -.미 호출 시 아래와 같은 문제 발생
+     * 1)유통증명시스템을 사용하는 경우 해당 API를 호출한 시점으로 열람정보가 등록되어 미 호출 시 열람정보가 등록 되지 않음.
+     * 2)문서상태조회 API(/v1/envelopes/${ENVELOPE_ID}/read) 호출 시 read_at최초 열람시간) 데이터가 내려가지 않음.
+     * 
+ * @param reqDTO KkopayDocAttrDTO.EnvelopeId + */ + @Override + 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.exchangeKkotalk(url, HttpMethod.valueOf(API_MODIFY_STATUS[1]), null, Void.class, getRlaybsnmInfo(reqDTO)); + } + + /** + *
+     * 문서 상태 조회 API : GET
+     * -.이용기관 서버에서 카카오페이 전자문서 서버로 문서 상태에 대한 조회를 요청 합니다.
+     * : 발송된 문서의 진행상태를 알고 싶은 경우, flow와 상관없이 요청 가능
+     * : polling 방식으로 호출할 경우, 호출 간격은 5초를 권장.
+     * : RECEIVE(수신,미수신) > READ(열람)/EXPIRED
+     * 
+ * @param reqDTO KkotalkDTO.EnvelopeId + * @return KkotalkApiDTO.EnvelopeStatusResponse + */ + @Override + public KkotalkApiDTO.EnvelopeStatusResponse findStatus(final KkotalkApiDTO.EnvelopeId reqDTO){ + validate(reqDTO, null); + + String param = "{\"envelopeIds\":" + JsonUtils.toJson(Collections.singletonList(reqDTO.getEnvelopeId())) + "}"; + KkotalkDTO.BulkStatusResponse res = webClient.exchangeKkotalk( + HOST + API_BULKSTATUS[0], + HttpMethod.valueOf(API_BULKSTATUS[1]), + param, + KkotalkDTO.BulkStatusResponse.class, + getRlaybsnmInfo(reqDTO)); + return res.getEnvelopeStatus().get(0); + } + + /** + *
+     * 모바일웹 연계 문서발송 요청 : POST
+     * -.이용기관 서버에서 전자문서 서버로 문서발송 처리를 요청합니다.
+     * 
+ * @param reqDTO KkotalkDTO.BulkSendRequest + * @return KkotalkDTO.BulkSendResponse + */ + @Override + public KkotalkDTO.BulkSendResponse requestSendBulk(final KkotalkDTO.BulkSendRequest reqDTO) { + if(Checks.isEmpty(reqDTO.getProductCode())){ + throw BizRuntimeException.create("상품 코드는 필수 입니다."); + } + + List errors = new ArrayList<>(); + + List envelopes = reqDTO.getEnvelopes(); + for(int idx = 0; idx < envelopes.size(); idx++) { + final Set> 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())) + .collect(Collectors.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) + "}"; + return webClient.exchangeKkotalk( + HOST + API_BULKSEND[0].replace("{PRODUCT_CODE}", reqDTO.getProductCode()), + HttpMethod.valueOf(API_BULKSEND[1]), + param, + KkotalkDTO.BulkSendResponse.class, + getRlaybsnmInfo(reqDTO)); + } + + /** + *
+     * 대량(bulk) 문서 상태 조회 API : POST
+     * -.이용기관 서버에서 카카오페이 전자문서 서버로 문서 상태에 대한 조회를 요청 합니다.
+     * : 발송된 문서의 진행상태를 알고 싶은 경우, flow와 상관없이 요청 가능
+     * : polling 방식으로 호출할 경우, 호출 간격은 5초를 권장.
+     * : RECEIVE(수신,미수신) > READ(열람)/EXPIRED
+     * 
+ * @param reqDTO KkotalkDTO.BulkStatusRequest + * @return KkotalkDTO.BulkStatusResponse + */ + @Override + public KkotalkDTO.BulkStatusResponse findBulkStatus(final KkotalkDTO.BulkStatusRequest reqDTO) { + List errors = new ArrayList<>(); + + List 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) + "}"; + return webClient.exchangeKkotalk( + HOST + API_BULKSTATUS[0], + HttpMethod.valueOf(API_BULKSTATUS[1]), + param, + KkotalkDTO.BulkStatusResponse.class, + getRlaybsnmInfo(reqDTO)); + } + + // @Override + // public KkotalkApiDTO.ValidTokenResponse findKkotalkReadyAndMblPage(final KkotalkApiDTO.ValidTokenRequest reqDTO) { + // final String url = HOST + API_VALID_TOKEN[0].replace(ENVELOPE_ID, reqDTO.getEnvelopeId()) + // .replace("{TOKEN}", reqDTO.getToken()); + // + // // 유효성 검증 + // final KkotalkApiDTO.ValidTokenResponse validTokenRes = webClient.exchangeKkotalk(url, HttpMethod.valueOf(API_VALID_TOKEN[1]), null, + // KkotalkApiDTO.ValidTokenResponse.class, getRlaybsnmInfo(reqDTO)); + // + // // 문서상태 변경 + // 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)); + // if(errorDTO != null){ + // return ApiResponseDTO.error(errorDTO.getErrorCode(), errorDTO.getErrorMessage()); + // } + // return ApiResponseDTO.success(); + // } + + //------------------------------------------------------------------------------------------------------------------- + private static List validate(T t, List errList) { + final Set> list = validator.validate(t); + + if(!list.isEmpty()) { + final List errors = list.stream() + .map(row -> String.format("%s=%s", row.getPropertyPath(), row.getMessageTemplate())) + .collect(Collectors.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 null;//CmmEnsUtils.getRlaybsnmInfo(request.getSignguCode(), request.getFfnlgCode(), SndngSeCode.KAKAO); + } +} diff --git a/src/main/java/cokr/xit/ens/modules/kkotalk/web/KkotalkEltrcDocController.java b/src/main/java/cokr/xit/ens/modules/kkotalk/web/KkotalkEltrcDocController.java new file mode 100644 index 0000000..94bca13 --- /dev/null +++ b/src/main/java/cokr/xit/ens/modules/kkotalk/web/KkotalkEltrcDocController.java @@ -0,0 +1,256 @@ +package cokr.xit.ens.modules.kkotalk.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 cokr.xit.ens.core.aop.ApiResponseDTO; +import cokr.xit.ens.core.aop.IApiResponse; +import cokr.xit.ens.modules.kkotalk.model.KkotalkApiDTO; +import cokr.xit.ens.modules.kkotalk.model.KkotalkDTO; +import cokr.xit.ens.modules.kkotalk.service.IKkotalkEltrcDocService; +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + *
+ * description : 카카오톡 전자 문서 발송 controller
+ * packageName : kr.xit.ens.kakao.talk.web
+ * fileName    : KkotalkEltrcDocController
+ * author      : julim
+ * date        : 2024-08-12
+ * ======================================================================
+ * 변경일         변경자        변경 내용
+ * ----------------------------------------------------------------------
+ * 2024-08-12    julim       최초 생성
+ *
+ * 
+ */ +@Tag(name = "KkotalkEltrcDocController", description = "카카오톡 전자문서 API") +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping(value = "/api/ens/kakao/v2") +public class KkotalkEltrcDocController { + private final IKkotalkEltrcDocService service; + + /** + *
+     * 모바일웹 연계 문서발송 요청
+     * -.이용기관 서버에서 전자문서 서버로 문서발송 처리를 요청합니다.
+     * 
+ * @param reqDTO KkopayDocDTO.SendRequest + * @return ApiResponseDTO + */ + @Operation(summary = "문서발송 요청", description = "카카오톡 전자문서 서버로 문서발송 처리를 요청") + @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = { + @Content(mediaType = "application/json", examples = { + @ExampleObject( + name = "D10", + value = "{\n" + + " \"productCode\": \"D10_1\",\n" + + " \"envelope\": {\n" + + " \"title\": \"전자문서\",\n" + + " \"content\": {\n" + + " \"link\": \"https://nps.or.kr\"\n" + + " },\n" + + " \"guide\": \"국민연금 공단에서 보내는 문서입니다.\",\n" + + " \"payload\": \"이용기관 페이로드\",\n" + + " \"readExpiresAt\": \"2023-12-31T10:00:00\",\n" + + " \"reviewExpiresAt\": \"2023-12-31T13:00:00\",\n" + + " \"useNonPersonalizedNotification\": true,\n" + + " \"phoneNumber\": \"01099999999\",\n" + + " \"name\": \"홍길동\",\n" + + " \"birthday\": \"20000303\",\n" + + " \"externalId\": \"external_id1\"\n" + + " },\n" + + " \"signguCode\": \"51110\",\n" + + " \"ffnlgCode\": \"11\"\n" + + "}" + ), + @ExampleObject( + name = "D11", + value = "{\n" + + " \"productCode\": \"D11_1\",\n" + + " \"envelope\": {\n" + + " \"title\": \"전자문서\",\n" + + " \"content\": {\n" + + " \"html\": \"

MyFirstHeading

Myfirstparagraph.

\"\n" + + " },\n" + + " \"guide\": \"국민연금 공단에서 보내는 문서입니다.\",\n" + + " \"readExpiresAt\": \"2023-12-31T10:00:00\",\n" + + " \"reviewExpiresAt\": \"2023-12-31T13:00:00\",\n" + + " \"ci\": \"${CI}\"\n" + + " },\n" + + " \"signguCode\": \"51110\",\n" + + " \"ffnlgCode\": \"11\"\n" + + "}" + ) + }) +}) @PostMapping(value = "/envelopes", produces = MediaType.APPLICATION_JSON_VALUE) + public IApiResponse requestSend( + @RequestBody final KkotalkDTO.SendRequest reqDTO + ) { + return ApiResponseDTO.success(service.requestSend(reqDTO)); + } + + /** + *
+     * 토큰 유효성 검증(Redirect URL  접속 허용/불허)
+     * 
+ * @param reqDTO KkopayDocDTO.ValidTokenRequest + * @return ApiResponseDTO + */ + @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)); + } + + /** + *
+     * 문서 열람처리 API
+     * -.문서에 대해서 열람 상태로 변경. 사용자가 문서열람 시(OTT 검증 완료 후 페이지 로딩 완료 시점) 반드시 문서 열람 상태 변경 API를 호출해야 함.
+     * -.미 호출 시 아래와 같은 문제 발생
+     * 1)유통증명시스템을 사용하는 경우 해당 API를 호출한 시점으로 열람정보가 등록되어 미 호출 시 열람정보가 등록 되지 않음.
+     * 2)문서상태조회 API(/v1/envelopes/${ENVELOPE_ID}/read) 호출 시 read_at최초 열람시간) 데이터가 내려가지 않음.
+     * 
+ * @param reqDTO KkotalkApiDTO.EnvelopeStatusResponse + * @return ApiResponseDTO + */ + @Operation(summary = "문서열람처리(문서 상태 변경)", description = "문서열람처리(문서 상태 변경)") + @PostMapping(value = "/modifyStatus", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public IApiResponse modifyStatus( + @RequestBody final KkotalkApiDTO.EnvelopeId reqDTO + ) { + service.modifyStatus(reqDTO); + return ApiResponseDTO.empty(); + } + + /** + *
+     * 문서 상태 조회 API
+     * -.이용기관 서버에서 카카오페이 전자문서 서버로 문서 상태에 대한 조회를 요청 합니다.
+     * : 발송된 문서의 진행상태를 알고 싶은 경우, flow와 상관없이 요청 가능
+     * : polling 방식으로 호출할 경우, 호출 간격은 5초를 권장.
+     * -.doc_box_status 상태변경순서
+     * : SENT(송신) > RECEIVED(수신) > READ(열람)/EXPIRED(미열람자료의 기한만료)
+     * 
+ * @param reqDTO KkotalkDTO.EnvelopeId + * @return ApiResponseDTO + */ + @Operation(summary = "문서 상태 조회", description = "문서 상태 조회") + @PostMapping(value = "/findStatus", produces = MediaType.APPLICATION_JSON_VALUE) + public IApiResponse findStatus( + @RequestBody final KkotalkApiDTO.EnvelopeId reqDTO + ) { + return ApiResponseDTO.success(service.findStatus(reqDTO)); + } + + @Operation(summary = "대량 문서발송 요청 -> batch sendBulks 에서 호출", description = "카카오페이 전자문서 서버로 대량 문서발송 처리를 요청 -> batch sendBulks 에서 호출") + @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = { + @Content(mediaType = "application/json", examples = { + @ExampleObject( + name = "D10", + value = "{\n" + + " \"productCode\": \"D10_1\",\n" + + " \"signguCode\": \"51110\",\n" + + " \"ffnlgCode\": \"11\",\n" + + " \"envelopes\": [\n" + + " {\n" + + " \"title\": \"전자문서\",\n" + + " \"content\": {\n" + + " \"html\": \"

MyFirstHeading

Myfirstparagraph.

\"\n" + + " },\n" + + " \"guide\": \"국민연금 공단에서 보내는 문서입니다.\",\n" + + " \"readExpiresAt\": \"2024-12-31T10:00:00\",\n" + + " \"reviewExpiresAt\": \"2025-03-31T13:00:00\",\n" + + " \"phoneNumber\": \"01099999999\",\n" + + " \"name\": \"홍길동\",\n" + + " \"birthday\": \"20000303\",\n" + + " \"externalId\": \"external_id1\"\n" + + " },\n" + + " {\n" + + " \"title\": \"전자문서\",\n" + + " \"content\": {\n" + + " \"html\": \"

MyFirstHeading

Myfirstparagraph.

\"\n" + + " },\n" + + " \"guide\": \"국민연금 공단에서 보내는 문서입니다.\",\n" + + " \"readExpiresAt\": \"2024-12-31T10:00:00\",\n" + + " \"reviewExpiresAt\": \"2025-03-31T13:00:00\",\n" + + " \"hash\": \"b0c34fdc5e2ecb0335919fdad3b2ada28fa3ab90ec16e9055c3e9e05c431c6e8\",\n" + + " \"ci\": \"vMtqVxJX56lBgbf9heK3QTc+jVndTfK77i/UJKAzPmBG4n9CazCdd/8YytlFZnN4qofIqgxHpSoiG0yYzgEpJg==\",\n" + + " \"externalId\": \"external_id2\"\n" + + " }\n" + + " ]\n" + + "}" + ), + @ExampleObject( + name = "D11", + value = "{\n" + + " \"productCode\": \"D11_1\",\n" + + " \"signguCode\": \"51110\",\n" + + " \"ffnlgCode\": \"11\",\n" + + " \"envelopes\": [\n" + + " {\n" + + " \"title\": \"전자문서\",\n" + + " \"content\": {\n" + + " \"link\": \"https://nps.or.kr\"\n" + + " },\n" + + " \"guide\": \"국민연금 공단에서 보내는 문서입니다.\",\n" + + " \"payload\": \"이용기관 페이로드\",\n" + + " \"readExpiresAt\": \"2024-12-31T10:00:00\",\n" + + " \"reviewExpiresAt\": \"2025-03-31T13:00:00\",\n" + + " \"phoneNumber\": \"01099999999\",\n" + + " \"name\": \"홍길동\",\n" + + " \"birthday\": \"20000303\",\n" + + " \"externalId\": \"external_id1\"\n" + + " },\n" + + " {\n" + + " \"title\": \"전자문서\",\n" + + " \"content\": {\n" + + " \"link\": \"https://nps.or.kr\"\n" + + " },\n" + + " \"guide\": \"국민연금 공단에서 보내는 문서입니다.\",\n" + + " \"payload\": \"이용기관 페이로드\",\n" + + " \"readExpiresAt\": \"2024-12-31T10:00:00\",\n" + + " \"reviewExpiresAt\": \"2025-03-31T13:00:00\",\n" + + " \"ci\": \"${CI}\",\n" + + " \"externalId\": \"external_id2\"\n" + + " }\n" + + " ]\n" + + "}" + ) + }) + }) + @PostMapping(value = "/envelopes/bulk", produces = MediaType.APPLICATION_JSON_VALUE) + public IApiResponse requestSendBulk( + @RequestBody final KkotalkDTO.BulkSendRequest reqDTO + ) { + return ApiResponseDTO.success(service.requestSendBulk(reqDTO)); + } + + /** + *
+     * 모바일웹 연계 문서발송 요청
+     * -.이용기관 서버에서 전자문서 서버로 문서발송 처리를 요청합니다.
+     * 
+ * @param reqDTO KkotalkApiDTO.BulkStatusRequest + * @return KkotalkApiDTO.BulkStatusResponse + */ + @Operation(summary = "대량 문서 상태 조회 요청 -> batch statusBulks 에서 호출", description = "카카오페이 전자문서 서버로 대량 문서 상태 조회 요청 -> batch statusBulks 에서 호출") + @PostMapping(value = "/envelopes/bulk/status", produces = MediaType.APPLICATION_JSON_VALUE) + public IApiResponse findBulkStatus( + @RequestBody final KkotalkDTO.BulkStatusRequest reqDTO + ) { + return ApiResponseDTO.success(service.findBulkStatus(reqDTO)); + } +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 1e96ba8..f155e88 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -58,3 +58,9 @@ spring: # enable-auto-commit: true # auto-commit-interval: 1000 +contract: + kakao: + talk: + host: https://test-edoc-gw.kakao.com + send: /v1/envelopes/{PRODUCT_CODE}T;POST + bulksend: /v1/bulk/envelopes/{PRODUCT_CODE}T;POST diff --git a/src/main/resources/config/conf-contract.yml b/src/main/resources/config/conf-contract.yml index 82501d5..bbb64bf 100644 --- a/src/main/resources/config/conf-contract.yml +++ b/src/main/resources/config/conf-contract.yml @@ -27,6 +27,13 @@ contract: notice: /iup/kakao/notice prepay: /iup/kakao/prepay payresult: /iup/kakao/pay-result + 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 alimtalk: biztalk: