diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 591e1ba..ca6dedb 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -7,15 +7,30 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
index b288176..17bfa87 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,6 +2,9 @@
+
+
+
diff --git a/codegen/.gitignore b/codegen/.gitignore
new file mode 100644
index 0000000..d26fe8b
--- /dev/null
+++ b/codegen/.gitignore
@@ -0,0 +1,42 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+.generated
+resources/swagger/
+resources/swagger/**
+
+swagger-codegen/**
diff --git a/codegen/build.gradle b/codegen/build.gradle
new file mode 100644
index 0000000..01ecb11
--- /dev/null
+++ b/codegen/build.gradle
@@ -0,0 +1,19 @@
+plugins {
+ id 'java-library'
+}
+
+
+repositories {
+ mavenCentral()
+}
+
+tasks.withType(JavaCompile) {
+ options.encoding = 'UTF-8'
+ sourceCompatibility = '1.8'
+ targetCompatibility = '1.8'
+}
+
+
+dependencies {
+ implementation "org.openapitools:openapi-generator:5.3.1"
+}
diff --git a/codegen/src/main/java/org/masil/seoulyeok/oas/codegen/spring/SpringCodeGenerator.java b/codegen/src/main/java/org/masil/seoulyeok/oas/codegen/spring/SpringCodeGenerator.java
new file mode 100644
index 0000000..0955e74
--- /dev/null
+++ b/codegen/src/main/java/org/masil/seoulyeok/oas/codegen/spring/SpringCodeGenerator.java
@@ -0,0 +1,38 @@
+package org.masil.seoulyeok.oas.codegen.spring;
+
+import io.swagger.v3.oas.models.media.Schema;
+import org.openapitools.codegen.CodegenModel;
+import org.openapitools.codegen.languages.SpringCodegen;
+
+public class SpringCodeGenerator extends SpringCodegen {
+
+
+ public SpringCodeGenerator() {
+ super();
+ templateDir = "trevari";
+ }
+
+ @Override
+ public String getName() {
+ return "trevari";
+ }
+
+ @Override
+ public void processOpts() {
+ super.processOpts();
+ // imports for pojos
+ importMapping.remove("ApiModelProperty");
+ importMapping.remove("ApiModel");
+ }
+
+ @Override
+ public CodegenModel fromModel(String name, Schema model) {
+ super.fromModel(name, model);
+ CodegenModel codegenModel = super.fromModel(name, model);
+ codegenModel.imports.remove("ApiModel");
+ codegenModel.imports.remove("ApiModelProperty");
+ return codegenModel;
+ }
+
+
+}
diff --git a/codegen/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/codegen/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
new file mode 100644
index 0000000..09bdeda
--- /dev/null
+++ b/codegen/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
@@ -0,0 +1 @@
+org.masil.seoulyeok.oas.codegen.spring.SpringCodeGenerator
diff --git a/codegen/src/main/resources/trevari/api.mustache b/codegen/src/main/resources/trevari/api.mustache
new file mode 100644
index 0000000..0ef4d8d
--- /dev/null
+++ b/codegen/src/main/resources/trevari/api.mustache
@@ -0,0 +1,135 @@
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package {{package}};
+
+{{#imports}}import {{import}};
+{{/imports}}
+
+{{#jdk8-no-delegate}}
+{{#virtualService}}
+import io.virtualan.annotation.ApiVirtual;
+import io.virtualan.annotation.VirtualService;
+{{/virtualService}}
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+{{/jdk8-no-delegate}}
+import org.springframework.http.ResponseEntity;
+{{#useBeanValidation}}
+import org.springframework.validation.annotation.Validated;
+{{/useBeanValidation}}
+{{#vendorExtensions.x-spring-paginated}}
+import org.springframework.data.domain.Pageable;
+{{/vendorExtensions.x-spring-paginated}}
+import org.springframework.web.bind.annotation.*;
+{{#jdk8-no-delegate}}
+ {{^reactive}}
+import org.springframework.web.context.request.NativeWebRequest;
+ {{/reactive}}
+{{/jdk8-no-delegate}}
+import org.springframework.web.multipart.MultipartFile;
+{{#reactive}}
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import org.springframework.http.codec.multipart.Part;
+{{/reactive}}
+
+
+{{#useBeanValidation}}
+import javax.validation.Valid;
+import javax.validation.constraints.*;
+{{/useBeanValidation}}
+import java.util.List;
+import java.util.Map;
+{{#jdk8-no-delegate}}
+import java.util.Optional;
+{{/jdk8-no-delegate}}
+{{^jdk8-no-delegate}}
+ {{#useOptional}}
+import java.util.Optional;
+ {{/useOptional}}
+{{/jdk8-no-delegate}}
+{{#async}}
+import java.util.concurrent.{{^jdk8}}Callable{{/jdk8}}{{#jdk8}}CompletableFuture{{/jdk8}};
+{{/async}}
+{{>generatedAnnotation}}
+{{#useBeanValidation}}
+@Validated
+{{/useBeanValidation}}
+
+{{#operations}}
+{{#virtualService}}
+@VirtualService
+{{/virtualService}}
+@RestController
+public interface {{classname}} {
+{{#jdk8-default-interface}}
+ {{^isDelegate}}
+ {{^reactive}}
+
+ default Optional getRequest() {
+ return Optional.empty();
+ }
+ {{/reactive}}
+ {{/isDelegate}}
+ {{#isDelegate}}
+
+ default {{classname}}Delegate getDelegate() {
+ return new {{classname}}Delegate() {};
+ }
+ {{/isDelegate}}
+{{/jdk8-default-interface}}
+{{#operation}}
+
+ /**
+ * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}}
+ {{#notes}}
+ * {{.}}
+ {{/notes}}
+ *
+ {{#allParams}}
+ * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
+ {{/allParams}}
+ * @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
+ * or {{/-last}}{{/responses}}
+ {{#isDeprecated}}
+ * @deprecated
+ {{/isDeprecated}}
+ {{#externalDocs}}
+ * {{description}}
+ * @see {{summary}} Documentation
+ {{/externalDocs}}
+ */
+ {{#virtualService}}
+
+ {{/virtualService}}
+
+ @{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}}Mapping(
+ value = "{{{path}}}"{{#singleContentTypes}}{{#hasProduces}},
+ produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}},
+ consumes = "{{{vendorExtensions.x-contentType}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}},
+ produces = { {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}},
+ consumes = { {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }{{/hasConsumes}}{{/singleContentTypes}}
+ )
+ {{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}} {{#delegate-method}}_{{/delegate-method}}{{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{>cookieParams}}{{^-last}},{{/-last}}{{#-last}}{{#reactive}}, {{/reactive}}{{/-last}}{{/allParams}}{{#reactive}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}}{{#unhandledException}} throws Exception{{/unhandledException}} {
+ {{#delegate-method}}
+ return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}});
+ }
+
+ // Override this method
+ {{#jdk8-default-interface}}default {{/jdk8-default-interface}} {{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#reactive}}Flux{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}} {
+ {{/delegate-method}}
+ {{^isDelegate}}
+ {{>methodBody}}
+ {{/isDelegate}}
+ {{#isDelegate}}
+ return getDelegate().{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}});
+ {{/isDelegate}}
+ }{{/jdk8-default-interface}}
+
+{{/operation}}
+}
+{{/operations}}
diff --git a/codegen/src/main/resources/trevari/bodyParams.mustache b/codegen/src/main/resources/trevari/bodyParams.mustache
new file mode 100644
index 0000000..e4f03e8
--- /dev/null
+++ b/codegen/src/main/resources/trevari/bodyParams.mustache
@@ -0,0 +1 @@
+{{#isBodyParam}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}}
\ No newline at end of file
diff --git a/codegen/src/main/resources/trevari/cookieParams.mustache b/codegen/src/main/resources/trevari/cookieParams.mustache
new file mode 100644
index 0000000..1cab1e3
--- /dev/null
+++ b/codegen/src/main/resources/trevari/cookieParams.mustache
@@ -0,0 +1 @@
+{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}} @CookieValue("{{baseName}}") {{>optionalDataType}} {{paramName}}{{/isCookieParam}}
\ No newline at end of file
diff --git a/codegen/src/main/resources/trevari/formParams.mustache b/codegen/src/main/resources/trevari/formParams.mustache
new file mode 100644
index 0000000..5ca0fa7
--- /dev/null
+++ b/codegen/src/main/resources/trevari/formParams.mustache
@@ -0,0 +1 @@
+{{#isFormParam}}{{^isFile}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{dataType}}} {{paramName}}{{/isFile}}{{#isFile}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#isArray}}List<{{/isArray}}{{#reactive}}Flux{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{#isArray}}>{{/isArray}} {{baseName}}{{/isFile}}{{/isFormParam}}
\ No newline at end of file
diff --git a/codegen/src/main/resources/trevari/headerParams.mustache b/codegen/src/main/resources/trevari/headerParams.mustache
new file mode 100644
index 0000000..75a39dc
--- /dev/null
+++ b/codegen/src/main/resources/trevari/headerParams.mustache
@@ -0,0 +1 @@
+{{#isHeaderParam}}@RequestHeader(value="{{baseName}}", required={{#required}}true{{/required}}{{^required}}false{{/required}}) {{>optionalDataType}} {{paramName}}{{/isHeaderParam}}
\ No newline at end of file
diff --git a/codegen/src/main/resources/trevari/model.mustache b/codegen/src/main/resources/trevari/model.mustache
new file mode 100644
index 0000000..78c3c9a
--- /dev/null
+++ b/codegen/src/main/resources/trevari/model.mustache
@@ -0,0 +1,43 @@
+package {{package}};
+
+import java.util.Objects;
+{{#imports}}import {{import}};
+{{/imports}}
+{{#openApiNullable}}
+import org.openapitools.jackson.nullable.JsonNullable;
+{{/openApiNullable}}
+{{#serializableModel}}
+import java.io.Serializable;
+{{/serializableModel}}
+{{#useBeanValidation}}
+import javax.validation.Valid;
+import javax.validation.constraints.*;
+{{/useBeanValidation}}
+{{#performBeanValidation}}
+import org.hibernate.validator.constraints.*;
+{{/performBeanValidation}}
+{{#jackson}}
+{{#withXml}}
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+{{/withXml}}
+{{/jackson}}
+{{#withXml}}
+import javax.xml.bind.annotation.*;
+{{/withXml}}
+{{^parent}}
+{{#hateoas}}
+import org.springframework.hateoas.RepresentationModel;
+{{/hateoas}}
+{{/parent}}
+
+{{#models}}
+{{#model}}
+{{#isEnum}}
+{{>enumOuterClass}}
+{{/isEnum}}
+{{^isEnum}}
+{{>pojo}}
+{{/isEnum}}
+{{/model}}
+{{/models}}
diff --git a/codegen/src/main/resources/trevari/pathParams.mustache b/codegen/src/main/resources/trevari/pathParams.mustache
new file mode 100644
index 0000000..4bf8e25
--- /dev/null
+++ b/codegen/src/main/resources/trevari/pathParams.mustache
@@ -0,0 +1 @@
+{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}@PathVariable("{{baseName}}") {{>optionalDataType}} {{paramName}}{{/isPathParam}}
\ No newline at end of file
diff --git a/codegen/src/main/resources/trevari/pojo.mustache b/codegen/src/main/resources/trevari/pojo.mustache
new file mode 100644
index 0000000..2dcb88d
--- /dev/null
+++ b/codegen/src/main/resources/trevari/pojo.mustache
@@ -0,0 +1,167 @@
+/**
+ * {{#description}}{{.}}{{/description}}{{^description}}{{classname}}{{/description}}
+ */{{#description}}
+//{{{description}}}
+{{/description}}
+{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}}{{>additionalModelTypeAnnotations}}
+public class {{classname}} {{#parent}}extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}}extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}} {{#serializableModel}}implements Serializable{{/serializableModel}} {
+{{#serializableModel}}
+ private static final long serialVersionUID = 1L;
+
+{{/serializableModel}}
+ {{#vars}}
+ {{#isEnum}}
+ {{^isContainer}}
+{{>enumClass}}
+ {{/isContainer}}
+ {{#isContainer}}
+ {{#mostInnerItems}}
+{{>enumClass}}
+ {{/mostInnerItems}}
+ {{/isContainer}}
+ {{/isEnum}}
+ {{#jackson}}
+ @JsonProperty("{{baseName}}"){{#withXml}}
+ @JacksonXmlProperty({{#isXmlAttribute}}isAttribute = true, {{/isXmlAttribute}}{{#xmlNamespace}}namespace="{{xmlNamespace}}", {{/xmlNamespace}}localName = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"){{/withXml}}
+ {{/jackson}}
+ {{#gson}}
+ @SerializedName("{{baseName}}")
+ {{/gson}}
+ {{#isContainer}}
+ {{#useBeanValidation}}@Valid{{/useBeanValidation}}
+ {{#openApiNullable}}
+ private {{>nullableDataType}} {{name}} = {{#isNullable}}JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#required}}{{{defaultValue}}}{{/required}}{{^required}}null{{/required}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ private {{>nullableDataType}} {{name}} = {{#required}}{{{defaultValue}}}{{/required}}{{^required}}null{{/required}};
+ {{/openApiNullable}}
+ {{/isContainer}}
+ {{^isContainer}}
+ {{#isDate}}
+ @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE)
+ {{/isDate}}
+ {{#isDateTime}}
+ @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME)
+ {{/isDateTime}}
+ {{#openApiNullable}}
+ private {{>nullableDataType}} {{name}}{{#isNullable}} = JsonNullable.undefined(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ private {{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{/isContainer}}
+
+ {{/vars}}
+ {{#vars}}
+ public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {
+ {{#openApiNullable}}
+ this.{{name}} = {{#isNullable}}JsonNullable.of({{name}}){{/isNullable}}{{^isNullable}}{{name}}{{/isNullable}};
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ this.{{name}} = {{name}};
+ {{/openApiNullable}}
+ return this;
+ }
+ {{#isArray}}
+
+ public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {
+ {{#openApiNullable}}
+ {{^required}}
+ if (this.{{name}} == null{{#isNullable}} || !this.{{name}}.isPresent(){{/isNullable}}) {
+ this.{{name}} = {{#isNullable}}JsonNullable.of({{{defaultValue}}}){{/isNullable}}{{^isNullable}}{{{defaultValue}}}{{/isNullable}};
+ }
+ {{/required}}
+ this.{{name}}{{#isNullable}}.get(){{/isNullable}}.add({{name}}Item);
+ {{/openApiNullable}}
+ {{^openApiNullable}}
+ if (this.{{name}} == null) {
+ this.{{name}} = {{{defaultValue}}};
+ }
+ this.{{name}}.add({{name}}Item);
+ {{/openApiNullable}}
+ return this;
+ }
+ {{/isArray}}
+ {{#isMap}}
+
+ public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
+ {{^required}}
+ if (this.{{name}} == null) {
+ this.{{name}} = {{{defaultValue}}};
+ }
+ {{/required}}
+ this.{{name}}.put(key, {{name}}Item);
+ return this;
+ }
+ {{/isMap}}
+
+ /**
+ {{#description}}
+ * {{{description}}}
+ {{/description}}
+ {{^description}}
+ * Get {{name}}
+ {{/description}}
+ {{#minimum}}
+ * minimum: {{minimum}}
+ {{/minimum}}
+ {{#maximum}}
+ * maximum: {{maximum}}
+ {{/maximum}}
+ * @return {{name}}
+ */
+ {{#vendorExtensions.x-extra-annotation}}
+ {{{vendorExtensions.x-extra-annotation}}}
+ {{/vendorExtensions.x-extra-annotation}}
+
+{{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}} public {{>nullableDataType}} {{getter}}() {
+ return {{name}};
+ }
+
+ public void {{setter}}({{>nullableDataType}} {{name}}) {
+ this.{{name}} = {{name}};
+ }
+
+ {{/vars}}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }{{#hasVars}}
+ {{classname}} {{classVarName}} = ({{classname}}) o;
+ return {{#vars}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{^-last}} &&
+ {{/-last}}{{/vars}}{{#parent}} &&
+ super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}}
+ return true;{{/hasVars}}
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash({{#vars}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}});
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class {{classname}} {\n");
+ {{#parent}}sb.append(" ").append(toIndentedString(super.toString())).append("\n");{{/parent}}
+ {{#vars}}sb.append(" {{name}}: ").append(toIndentedString({{name}})).append("\n");
+ {{/vars}}sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+}
diff --git a/codegen/src/main/resources/trevari/queryParams.mustache b/codegen/src/main/resources/trevari/queryParams.mustache
new file mode 100644
index 0000000..c12032e
--- /dev/null
+++ b/codegen/src/main/resources/trevari/queryParams.mustache
@@ -0,0 +1 @@
+{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}} {{#useBeanValidation}}@Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{>optionalDataType}} {{paramName}}{{/isQueryParam}}
\ No newline at end of file
diff --git a/refs/build.gradle b/refs/build.gradle
new file mode 100644
index 0000000..112fe73
--- /dev/null
+++ b/refs/build.gradle
@@ -0,0 +1,12 @@
+plugins {
+ id 'java-library'
+}
+
+group = 'org.masil.seoulyeok'
+
+allprojects {
+ repositories {
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
+ }
+}
diff --git a/refs/destination/build.gradle b/refs/destination/build.gradle
new file mode 100644
index 0000000..dd4939a
--- /dev/null
+++ b/refs/destination/build.gradle
@@ -0,0 +1,25 @@
+plugins {
+ id 'java-library'
+ id "io.freefair.lombok" version "6.4.3"
+
+}
+
+group = 'org.masil.seoulyeok'
+
+dependencies {
+ implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36'
+ implementation 'com.github.moimp:domain-core:0.0.2'
+ implementation 'com.github.moimp.clocks:clocks:1.0.0'
+ implementation 'com.github.moimp.clocks:clocks-frozen:1.0.0'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
+ testImplementation('org.junit.jupiter:junit-jupiter')
+ testImplementation 'org.assertj:assertj-core:3.20.2'
+
+}
+
+repositories {
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
+}
diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Address.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Address.java
new file mode 100644
index 0000000..a52391f
--- /dev/null
+++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Address.java
@@ -0,0 +1,9 @@
+package org.masil.seoulyeok.events.destination;
+
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class Address {
+
+ String value;
+}
diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Destination.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Destination.java
new file mode 100644
index 0000000..1112464
--- /dev/null
+++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/Destination.java
@@ -0,0 +1,53 @@
+package org.masil.seoulyeok.events.destination;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+
+@EqualsAndHashCode
+@Getter
+public class Destination implements DestinationTarget {
+
+ public static Destination create(Address address, DestinationType type) {
+ DestinationId id = DestinationId.of(1L);
+ return new Destination(id, address, type, DestinationStatus.ACTIVE);
+ }
+
+ public static Destination of(DestinationId id, Address address, DestinationType type) {
+ return new Destination(id, address, type, DestinationStatus.ACTIVE);
+ }
+
+ public static Destination of(DestinationId id, Address address, DestinationType type, DestinationStatus status) {
+ return new Destination(id, address, type, status);
+ }
+
+ private final DestinationId id;
+ private final Address address;
+ private final DestinationType type;
+ private DestinationStatus status;
+
+ private Destination(DestinationId id, Address address, DestinationType type, DestinationStatus status) {
+ this.id = id;
+ this.address = address;
+ this.type = type;
+ this.status = status;
+ }
+
+ @Override
+ public DestinationId getId() {
+ return id;
+ }
+
+ @Override
+ public Address getAddress() {
+ return address;
+ }
+
+ @Override
+ public DestinationType getType() {
+ return type;
+ }
+
+ public void inactive() {
+ status = DestinationStatus.INACTIVE;
+ }
+}
diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationId.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationId.java
new file mode 100644
index 0000000..cb4185a
--- /dev/null
+++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationId.java
@@ -0,0 +1,14 @@
+package org.masil.seoulyeok.events.destination;
+
+import com.likelen.identifier.core.LongId;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class DestinationId implements LongId {
+ Long value;
+
+ @Override
+ public Long get() {
+ return value;
+ }
+}
diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationStatus.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationStatus.java
new file mode 100644
index 0000000..fec556a
--- /dev/null
+++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationStatus.java
@@ -0,0 +1,5 @@
+package org.masil.seoulyeok.events.destination;
+
+public enum DestinationStatus {
+ ACTIVE, INACTIVE
+}
diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationTarget.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationTarget.java
new file mode 100644
index 0000000..c61962e
--- /dev/null
+++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationTarget.java
@@ -0,0 +1,7 @@
+package org.masil.seoulyeok.events.destination;
+
+public interface DestinationTarget {
+ DestinationId getId();
+ Address getAddress();
+ DestinationType getType();
+}
diff --git a/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationType.java b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationType.java
new file mode 100644
index 0000000..c7b1291
--- /dev/null
+++ b/refs/destination/src/main/java/org.masil.seoulyeok/events/destination/DestinationType.java
@@ -0,0 +1,6 @@
+package org.masil.seoulyeok.events.destination;
+
+public enum DestinationType {
+
+ SIMPLE_QUEUE, SUBSCRIBER_QUEUE
+}
diff --git a/refs/destination/src/test/java/org/masil/seoulyeok/events/destination/DestinationTest.java b/refs/destination/src/test/java/org/masil/seoulyeok/events/destination/DestinationTest.java
new file mode 100644
index 0000000..97fff7d
--- /dev/null
+++ b/refs/destination/src/test/java/org/masil/seoulyeok/events/destination/DestinationTest.java
@@ -0,0 +1,21 @@
+package org.masil.seoulyeok.events.destination;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.masil.seoulyeok.events.destination.*;
+
+class DestinationTest {
+
+ Destination sut;
+
+ @Test
+ void inactive_상태로_갈_수_있다() {
+ sut = Destination.of(DestinationId.of(1L), Address.of("some_address"), DestinationType.SIMPLE_QUEUE);
+ assertThat(sut.getStatus()).isEqualTo(DestinationStatus.ACTIVE);
+
+ sut.inactive();
+ assertThat(sut.getStatus()).isEqualTo(DestinationStatus.INACTIVE);
+ }
+}
diff --git a/refs/event/build.gradle b/refs/event/build.gradle
new file mode 100644
index 0000000..7c40ecc
--- /dev/null
+++ b/refs/event/build.gradle
@@ -0,0 +1,20 @@
+plugins {
+ id 'java-library'
+ id "io.freefair.lombok" version "6.4.3"
+
+}
+
+group = 'org.masil.seoulyeok'
+
+dependencies {
+ implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36'
+ implementation 'com.github.moimp:domain-core:0.0.2'
+ implementation 'com.github.moimp.clocks:clocks:1.0.0'
+ implementation 'com.github.moimp.clocks:clocks-frozen:1.0.0'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
+ testImplementation('org.junit.jupiter:junit-jupiter')
+ testImplementation 'org.assertj:assertj-core:3.20.2'
+
+}
diff --git a/refs/event/src/main/java/org.masil.seoulyeok.events/Event.java b/refs/event/src/main/java/org.masil.seoulyeok.events/Event.java
new file mode 100644
index 0000000..6490402
--- /dev/null
+++ b/refs/event/src/main/java/org.masil.seoulyeok.events/Event.java
@@ -0,0 +1,29 @@
+package org.masil.seoulyeok.events;
+
+import com.trevari.domain.core.DomainEventId;
+import lombok.Value;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+
+@Value(staticConstructor = "of")
+public class Event {
+ DomainEventId id;
+ String eventType;
+ String payload;
+ String payloadVersion;
+
+ LocalDateTime occurredAt;
+
+ public Event(DomainEventId id, String eventType, String payload, String payloadVersion, LocalDateTime occurredAt) {
+ Objects.requireNonNull(id);
+ Objects.requireNonNull(eventType);
+ Objects.requireNonNull(payloadVersion);
+ this.id = id;
+ this.eventType = eventType;
+ this.payload = payload;
+ this.payloadVersion = payloadVersion;
+ this.occurredAt = occurredAt;
+ }
+}
diff --git a/schema/schema.sql b/schema/schema.sql
index bc70b15..9538018 100644
--- a/schema/schema.sql
+++ b/schema/schema.sql
@@ -21,3 +21,23 @@ CREATE TABLE IF NOT EXISTS pulled_offset
created_at TIMESTAMPTZ DEFAULT NOW(),
version INT not null
);
+
+CREATE TABLE IF NOT EXISTS destination
+(
+ id BIGINT PRIMARY KEY,
+ address VARCHAR(255) NOT NULL,
+ type VARCHAR(50) NOT NULL,
+ status VARCHAR(50),
+ version BIGINT NOT NULL
+ );
+
+CREATE TABLE IF NOT EXISTS relay_message
+(
+ id BIGINT PRIMARY KEY,
+ destination_id BIGINT NOT NULL,
+ message_payload json,
+ created_at TIMESTAMPTZ,
+ relied_at TIMESTAMPTZ,
+ payload_version VARCHAR(50),
+ version INT NOT NULL
+ );
diff --git a/seoulyeok-message-relay/api/Dockerfile b/seoulyeok-message-relay/api/Dockerfile
new file mode 100644
index 0000000..ad51613
--- /dev/null
+++ b/seoulyeok-message-relay/api/Dockerfile
@@ -0,0 +1,9 @@
+FROM amazoncorretto:11-alpine
+
+ARG FROM_JAR=build/libs/api.jar
+
+COPY ${FROM_JAR} app.jar
+
+EXPOSE 5000
+
+ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/seoulyeok-message-relay/api/build.gradle b/seoulyeok-message-relay/api/build.gradle
new file mode 100644
index 0000000..782fcf0
--- /dev/null
+++ b/seoulyeok-message-relay/api/build.gradle
@@ -0,0 +1,117 @@
+import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
+
+buildscript {
+ dependencies {
+ classpath project(':codegen')
+ }
+}
+
+plugins {
+ id 'java-library'
+ id "org.springframework.boot"
+ id "io.spring.dependency-management"
+ id "org.openapi.generator" version "5.2.1"
+ id "io.freefair.lombok"
+
+
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(11)
+ }
+}
+
+ext {
+ set('springCloudVersion', "2.4.1")
+}
+
+
+group 'org.masil.seoulyeok.events.relay'
+
+project.sourceSets.test.resources.srcDirs = [ "${projectDir}/src/test/resources", "${rootDir}/schema" ]
+
+
+dependencies {
+
+ implementation('io.awspring.cloud:spring-cloud-starter-aws-messaging:2.4.1') {
+ exclude group: 'com.amazonaws', module: 'aws-java-sdk-s3'
+ }
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-test'
+
+ //fixed for swagger
+ compileOnly 'io.swagger:swagger-annotations:1.6.2'
+ compileOnly 'jakarta.validation:jakarta.validation-api:2.0.2'
+ compileOnly 'org.openapitools:jackson-databind-nullable:0.1.0'
+
+ implementation 'org.springframework.cloud:spring-cloud-config-client:3.1.3'
+
+ implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36'
+
+ implementation 'com.github.moimp:domain-core:0.0.2'
+
+ implementation project(':seoulyeok-message-relay:messages')
+
+ implementation 'org.json:json:20220320'
+}
+
+def swaggerRoot = "$projectDir/src/main/swagger-config".toString()
+def swaggerAPIOutput = "$projectDir/src/main".toString()
+
+openApiValidate {
+ inputSpec="$swaggerRoot/api.yml".toString()
+ recommend=true
+}
+
+openApiGenerate {
+ inputSpec="$swaggerRoot/api.yml".toString()
+ generatorName = "trevari"
+ configFile= "$swaggerRoot/config.json".toString()
+ outputDir= "$swaggerAPIOutput".toString()
+ configOptions = [
+ "sourceFolder" : "swagger-codegen"
+ ]
+}
+tasks.named("openApiGenerate") {
+ def outputDir = openApiGenerate.outputDir.get()
+ it.doFirst {
+ delete outputDir +"/"+ openApiGenerate.configOptions["sourceFolder"].get()
+ }
+
+ it.doLast {
+ delete "${outputDir}/pom.xml"
+ delete "${outputDir}/README.md"
+ delete "${outputDir}/.openapi-generator-ignore"
+ delete "${outputDir}/.openapi-generator"
+ }
+}
+
+task openApiYmlGenerate(type: GenerateTask) {
+ inputSpec = "$swaggerRoot/api.yml".toString()
+ generatorName = "openapi-yaml".toString()
+ outputDir = "$swaggerAPIOutput/resources/swagger/".toString()
+ configOptions = [
+ outputFile: "openapi.yml"
+ ]
+
+ it.doFirst {
+ delete "$swaggerAPIOutput/resources/swagger/"
+ }
+}
+
+tasks.openApiGenerate.dependsOn tasks.openApiValidate
+tasks.compileJava.dependsOn tasks.openApiGenerate, tasks.openApiYmlGenerate
+sourceSets.main.java.srcDir "${openApiGenerate.outputDir.get()}/${openApiGenerate.configOptions['sourceFolder'].get()}"
+sourceSets.main.resources.srcDir "${openApiGenerate.outputDir.get()}/src/main/resources"
+
+
+test {
+ useJUnitPlatform()
+}
+
+dependencyManagement {
+ imports {
+ mavenBom "io.awspring.cloud:spring-cloud-aws-dependencies:${springCloudVersion}"
+ }
+}
diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/App.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/App.java
new file mode 100644
index 0000000..ed7a241
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/App.java
@@ -0,0 +1,23 @@
+package org.masil.seoulyeok.events.relay;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@SpringBootApplication
+public class App {
+
+ @RestController
+ public static class HelloController {
+
+ @GetMapping(value = "/annyeng")
+ public String hello() {
+ return "hello";
+ }
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(App.class, args);
+ }
+}
diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/GlobalExceptionHandler.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/GlobalExceptionHandler.java
new file mode 100644
index 0000000..a665e28
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/GlobalExceptionHandler.java
@@ -0,0 +1,19 @@
+package org.masil.seoulyeok.events.relay.api;
+
+import org.masil.seoulyeok.events.relay.models.Result;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(IllegalArgumentException.class)
+ public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) {
+ return ResponseEntity.internalServerError()
+ .body(new Result()
+ .code(-1)
+ .message(e.getMessage())
+ );
+ }
+}
diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/SendMessagesController.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/SendMessagesController.java
new file mode 100644
index 0000000..b2ff6f0
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/api/SendMessagesController.java
@@ -0,0 +1,21 @@
+package org.masil.seoulyeok.events.relay.api;
+
+import org.masil.seoulyeok.events.relay.application.MessageSender;
+import org.masil.seoulyeok.events.relay.models.RelayRequest;
+import org.masil.seoulyeok.events.relay.models.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequiredArgsConstructor
+@RestController
+public class SendMessagesController implements RelayApi {
+
+ private final MessageSender sender;
+
+ @Override
+ public ResponseEntity deliveryToDestination(RelayRequest relayRequest) {
+ sender.doWork(relayRequest);
+ return ResponseEntity.ok(new Result().code(0).message("ok"));
+ }
+}
diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/AmazonSQSMessageSender.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/AmazonSQSMessageSender.java
new file mode 100644
index 0000000..b863821
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/AmazonSQSMessageSender.java
@@ -0,0 +1,47 @@
+package org.masil.seoulyeok.events.relay.application;
+
+import com.trevari.domain.core.Serializer;
+import org.masil.seoulyeok.events.relay.config.MessageConfig;
+import org.masil.seoulyeok.events.relay.models.RelayRequest;
+import com.trevari.messages.GeneralMessageEnvelop;
+import io.awspring.cloud.messaging.core.QueueMessagingTemplate;
+import lombok.RequiredArgsConstructor;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.springframework.messaging.MessagingException;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class AmazonSQSMessageSender implements MessageSender {
+
+ private final QueueMessagingTemplate queueMessagingTemplate;
+ private final MessageConfig config;
+
+ @Override
+ public void doWork(RelayRequest relayRequest) {
+ String messagePayload = relayRequest.getPayload();
+ if (!isValid(messagePayload)) {
+ throw new IllegalArgumentException("MessagePayload is not Json Type messagePayload: " + messagePayload);
+ }
+ String queue = config.getFrontControllerQueueName();
+
+ try {
+ String serialize = Serializer.getInstance().serialize(relayRequest);
+
+ queueMessagingTemplate.convertAndSend(queue, new GeneralMessageEnvelop<>(serialize));
+ } catch (MessagingException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+
+ private boolean isValid(String json) {
+ try {
+ new JSONObject(json);
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/MessageSender.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/MessageSender.java
new file mode 100644
index 0000000..1d1aa1d
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/application/MessageSender.java
@@ -0,0 +1,7 @@
+package org.masil.seoulyeok.events.relay.application;
+
+import org.masil.seoulyeok.events.relay.models.RelayRequest;
+
+public interface MessageSender {
+ void doWork(RelayRequest relayRequest);
+}
diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/AmazonSQSConfig.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/AmazonSQSConfig.java
new file mode 100644
index 0000000..cecf1c6
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/AmazonSQSConfig.java
@@ -0,0 +1,24 @@
+package org.masil.seoulyeok.events.relay.config;
+
+import com.amazonaws.services.sqs.AmazonSQSAsync;
+import io.awspring.cloud.messaging.core.QueueMessagingTemplate;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class AmazonSQSConfig implements MessageConfig {
+
+ @Value("${aws.sqs.pulled-events-received}")
+ private String frontControllerQueueName;
+
+ @Bean
+ public QueueMessagingTemplate queueMessagingTemplate(AmazonSQSAsync amazonSQSAsync) {
+ return new QueueMessagingTemplate(amazonSQSAsync);
+ }
+
+ @Override
+ public String getFrontControllerQueueName() {
+ return frontControllerQueueName;
+ }
+}
diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/LongIdGeneratorFactory.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/LongIdGeneratorFactory.java
new file mode 100644
index 0000000..e800694
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/LongIdGeneratorFactory.java
@@ -0,0 +1,56 @@
+package org.masil.seoulyeok.events.relay.config;
+
+import com.trevari.identity.generator.IdGeneratorFactory;
+import com.trevari.identity.generator.LongIdGenerator;
+import com.trevari.identity.generator.LongIdGeneratorHolder;
+import com.trevari.identity.generator.LongValueGenerator;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.config.AbstractFactoryBean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+import java.util.Arrays;
+import java.util.concurrent.ThreadLocalRandom;
+
+@Configuration
+@RequiredArgsConstructor
+public class LongIdGeneratorFactory extends AbstractFactoryBean {
+
+ @Getter
+ @Setter
+ @Value("${id-generator.url}")
+ private String url;
+
+ private final Environment environment;
+
+ @Override
+ public Class> getObjectType() {
+ return LongIdGenerator.class;
+ }
+
+ @Override
+ protected LongIdGenerator createInstance() {
+ IdGeneratorFactory idGeneratorFactory;
+ if (Arrays.asList(environment.getActiveProfiles()).contains("production")) {
+ idGeneratorFactory = new IdGeneratorFactory(url);
+ } else {
+ idGeneratorFactory = new IdGeneratorFactory(new DummyLongValueGenerator());
+ }
+ LongIdGenerator longIdGenerator = idGeneratorFactory.create();
+ LongIdGeneratorHolder.set(longIdGenerator);
+ return longIdGenerator;
+ }
+
+ private static class DummyLongValueGenerator implements LongValueGenerator {
+ static final long SOURCE = 10000000;
+ static final long BOUND = 99999999;
+
+ @Override
+ public long gen() {
+ return ThreadLocalRandom.current().nextLong(SOURCE, BOUND);
+ }
+ }
+}
diff --git a/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/MessageConfig.java b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/MessageConfig.java
new file mode 100644
index 0000000..13e89c2
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/java/org/masil/seoulyeok/events/relay/config/MessageConfig.java
@@ -0,0 +1,6 @@
+package org.masil.seoulyeok.events.relay.config;
+
+public interface MessageConfig {
+
+ String getFrontControllerQueueName();
+}
diff --git a/seoulyeok-message-relay/api/src/main/resources/application-local.yml b/seoulyeok-message-relay/api/src/main/resources/application-local.yml
new file mode 100644
index 0000000..d45b61e
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/resources/application-local.yml
@@ -0,0 +1,23 @@
+server:
+ port: 8071
+
+spring:
+ cloud:
+ config:
+ enabled: false
+cloud:
+ aws:
+ region:
+ static: ap-northeast-2
+ stack:
+ auto: false
+ credentials:
+ access-key: ${AWS_ACCESS_KEY_ID}
+ secret-key: ${AWS_SECRET_ACCESS_KEY}
+
+id-generator:
+ url: "https://not-exists-idgen-url.com"
+
+aws:
+ sqs:
+ pulled-events-received: PULLED-EVENTS-RECEIVED-DEV
diff --git a/seoulyeok-message-relay/api/src/main/resources/application-production.yml b/seoulyeok-message-relay/api/src/main/resources/application-production.yml
new file mode 100644
index 0000000..bb1d06c
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/resources/application-production.yml
@@ -0,0 +1,5 @@
+spring:
+ application:
+ name: message-relay-api
+ config:
+ import: "optional:configserver:https://config.trevari.co.kr"
diff --git a/seoulyeok-message-relay/api/src/main/resources/application.yml b/seoulyeok-message-relay/api/src/main/resources/application.yml
new file mode 100644
index 0000000..d5c85e8
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/resources/application.yml
@@ -0,0 +1,5 @@
+spring:
+ profiles:
+ active: local
+
+
diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator-ignore b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator-ignore
new file mode 100644
index 0000000..7484ee5
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/FILES b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/FILES
new file mode 100644
index 0000000..027c423
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/FILES
@@ -0,0 +1,3 @@
+.openapi-generator-ignore
+README.md
+openapi.yml
diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/VERSION b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/VERSION
new file mode 100644
index 0000000..7d3cdbf
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/resources/swagger/.openapi-generator/VERSION
@@ -0,0 +1 @@
+5.3.1
\ No newline at end of file
diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/README.md b/seoulyeok-message-relay/api/src/main/resources/swagger/README.md
new file mode 100644
index 0000000..6b0f684
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/resources/swagger/README.md
@@ -0,0 +1,2 @@
+# OpenAPI YAML
+This is a OpenAPI YAML built by the [openapi-generator](https://github.com/openapitools/openapi-genreator) project.
\ No newline at end of file
diff --git a/seoulyeok-message-relay/api/src/main/resources/swagger/openapi.yml b/seoulyeok-message-relay/api/src/main/resources/swagger/openapi.yml
new file mode 100644
index 0000000..9095dca
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/resources/swagger/openapi.yml
@@ -0,0 +1,105 @@
+openapi: 3.0.0
+info:
+ license:
+ name: MIT
+ title: notification
+ version: 1.0.2
+servers:
+- url: /
+paths:
+ /apis/events/relay:
+ post:
+ description: 특정 메세지를 목적지로 전달합니다
+ operationId: deliveryToDestination
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RelayRequest'
+ description: deliveryToDestination
+ required: true
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Result'
+ description: "데이터 전송 성공 code = 0, meg: ok"
+ "400":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Result'
+ description: "입력이 유효하지 않습니다 code = -1, meg: fail"
+ "500":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Result'
+ description: "시스템 오류 발생 code = -1, meg: fail"
+ summary: 특정 메세지를 목적지로 전달합니다
+ tags:
+ - relay
+components:
+ schemas:
+ RelayRequest:
+ example:
+ payloadVersion: 1.0.0
+ payload: "{ \"eventId\": 1 }"
+ destinationId: 54321
+ properties:
+ payload:
+ example: "{ \"eventId\": 1 }"
+ type: string
+ destinationId:
+ example: 54321
+ format: int64
+ type: integer
+ payloadVersion:
+ example: 1.0.0
+ pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\\
+ d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\
+ +([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
+ type: string
+ required:
+ - destinationId
+ - payload
+ - payloadVersion
+ type: object
+ RelayClientRequest:
+ properties:
+ destinationType:
+ example: SIMPLE_QUEUE
+ type: string
+ messagePayload:
+ example: "{ \"eventId\": 1 }"
+ type: string
+ destinationName:
+ example: settled.fifo
+ type: string
+ payloadVersion:
+ example: 1.0.0
+ pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\\
+ d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\
+ +([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
+ type: string
+ required:
+ - destinationName
+ - destinationType
+ - messagePayload
+ - payloadVersion
+ type: object
+ Result:
+ example:
+ code: 1
+ message: success | fail
+ properties:
+ code:
+ example: 1
+ type: integer
+ message:
+ example: success | fail
+ type: string
+ required:
+ - value
+ type: object
diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/ApiUtil.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/ApiUtil.java
new file mode 100644
index 0000000..baa04b1
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/ApiUtil.java
@@ -0,0 +1,19 @@
+package org.masil.seoulyeok.events.relay.api;
+
+import org.springframework.web.context.request.NativeWebRequest;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class ApiUtil {
+ public static void setExampleResponse(NativeWebRequest req, String contentType, String example) {
+ try {
+ HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class);
+ res.setCharacterEncoding("UTF-8");
+ res.addHeader("Content-Type", contentType);
+ res.getWriter().print(example);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/RelayApi.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/RelayApi.java
new file mode 100644
index 0000000..9732fbe
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/api/RelayApi.java
@@ -0,0 +1,41 @@
+/**
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.3.1).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+package org.masil.seoulyeok.events.relay.api;
+
+import org.masil.seoulyeok.events.relay.models.RelayRequest;
+import org.masil.seoulyeok.events.relay.models.Result;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+
+import javax.validation.Valid;
+
+@javax.annotation.Generated(value = "com.trevari.oas.codegen.spring.TrevariSpringCodeGenerator")
+@Validated
+
+@RestController
+public interface RelayApi {
+
+ /**
+ * POST /apis/events/relay : 특정 메세지를 목적지로 전달합니다
+ * 특정 메세지를 목적지로 전달합니다
+ *
+ * @param relayRequest deliveryToDestination (required)
+ * @return 데이터 전송 성공 code = 0, meg: ok (status code 200)
+ * or 입력이 유효하지 않습니다 code = -1, meg: fail (status code 400)
+ * or 시스템 오류 발생 code = -1, meg: fail (status code 500)
+ */
+
+ @PostMapping(
+ value = "/apis/events/relay",
+ produces = { "application/json" },
+ consumes = { "application/json" }
+ )
+ ResponseEntity deliveryToDestination(@Valid @RequestBody RelayRequest relayRequest);
+
+}
diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayClientRequest.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayClientRequest.java
new file mode 100644
index 0000000..eef27a3
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayClientRequest.java
@@ -0,0 +1,156 @@
+package org.masil.seoulyeok.events.relay.models;
+
+import java.util.Objects;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+import javax.validation.constraints.*;
+
+/**
+ * RelayClientRequest
+ */@javax.annotation.Generated(value = "com.trevari.oas.codegen.spring.TrevariSpringCodeGenerator")
+public class RelayClientRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @JsonProperty("destinationType")
+ private String destinationType;
+
+ @JsonProperty("messagePayload")
+ private String messagePayload;
+
+ @JsonProperty("destinationName")
+ private String destinationName;
+
+ @JsonProperty("payloadVersion")
+ private String payloadVersion;
+
+ public RelayClientRequest destinationType(String destinationType) {
+ this.destinationType = destinationType;
+ return this;
+ }
+
+ /**
+ * Get destinationType
+ * @return destinationType
+ */
+
+ @NotNull
+
+
+ public String getDestinationType() {
+ return destinationType;
+ }
+
+ public void setDestinationType(String destinationType) {
+ this.destinationType = destinationType;
+ }
+
+ public RelayClientRequest messagePayload(String messagePayload) {
+ this.messagePayload = messagePayload;
+ return this;
+ }
+
+ /**
+ * Get messagePayload
+ * @return messagePayload
+ */
+
+ @NotNull
+
+
+ public String getMessagePayload() {
+ return messagePayload;
+ }
+
+ public void setMessagePayload(String messagePayload) {
+ this.messagePayload = messagePayload;
+ }
+
+ public RelayClientRequest destinationName(String destinationName) {
+ this.destinationName = destinationName;
+ return this;
+ }
+
+ /**
+ * Get destinationName
+ * @return destinationName
+ */
+
+ @NotNull
+
+
+ public String getDestinationName() {
+ return destinationName;
+ }
+
+ public void setDestinationName(String destinationName) {
+ this.destinationName = destinationName;
+ }
+
+ public RelayClientRequest payloadVersion(String payloadVersion) {
+ this.payloadVersion = payloadVersion;
+ return this;
+ }
+
+ /**
+ * Get payloadVersion
+ * @return payloadVersion
+ */
+
+ @NotNull
+
+@Pattern(regexp = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$")
+ public String getPayloadVersion() {
+ return payloadVersion;
+ }
+
+ public void setPayloadVersion(String payloadVersion) {
+ this.payloadVersion = payloadVersion;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RelayClientRequest relayClientRequest = (RelayClientRequest) o;
+ return Objects.equals(this.destinationType, relayClientRequest.destinationType) &&
+ Objects.equals(this.messagePayload, relayClientRequest.messagePayload) &&
+ Objects.equals(this.destinationName, relayClientRequest.destinationName) &&
+ Objects.equals(this.payloadVersion, relayClientRequest.payloadVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(destinationType, messagePayload, destinationName, payloadVersion);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class RelayClientRequest {\n");
+
+ sb.append(" destinationType: ").append(toIndentedString(destinationType)).append("\n");
+ sb.append(" messagePayload: ").append(toIndentedString(messagePayload)).append("\n");
+ sb.append(" destinationName: ").append(toIndentedString(destinationName)).append("\n");
+ sb.append(" payloadVersion: ").append(toIndentedString(payloadVersion)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+}
+
diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayRequest.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayRequest.java
new file mode 100644
index 0000000..e25fa5c
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/RelayRequest.java
@@ -0,0 +1,130 @@
+package org.masil.seoulyeok.events.relay.models;
+
+import java.util.Objects;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+import javax.validation.constraints.*;
+
+/**
+ * RelayRequest
+ */@javax.annotation.Generated(value = "com.trevari.oas.codegen.spring.TrevariSpringCodeGenerator")
+public class RelayRequest implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @JsonProperty("payload")
+ private String payload;
+
+ @JsonProperty("destinationId")
+ private Long destinationId;
+
+ @JsonProperty("payloadVersion")
+ private String payloadVersion;
+
+ public RelayRequest payload(String payload) {
+ this.payload = payload;
+ return this;
+ }
+
+ /**
+ * Get payload
+ * @return payload
+ */
+
+ @NotNull
+
+
+ public String getPayload() {
+ return payload;
+ }
+
+ public void setPayload(String payload) {
+ this.payload = payload;
+ }
+
+ public RelayRequest destinationId(Long destinationId) {
+ this.destinationId = destinationId;
+ return this;
+ }
+
+ /**
+ * Get destinationId
+ * @return destinationId
+ */
+
+ @NotNull
+
+
+ public Long getDestinationId() {
+ return destinationId;
+ }
+
+ public void setDestinationId(Long destinationId) {
+ this.destinationId = destinationId;
+ }
+
+ public RelayRequest payloadVersion(String payloadVersion) {
+ this.payloadVersion = payloadVersion;
+ return this;
+ }
+
+ /**
+ * Get payloadVersion
+ * @return payloadVersion
+ */
+
+ @NotNull
+
+@Pattern(regexp = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$")
+ public String getPayloadVersion() {
+ return payloadVersion;
+ }
+
+ public void setPayloadVersion(String payloadVersion) {
+ this.payloadVersion = payloadVersion;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RelayRequest relayRequest = (RelayRequest) o;
+ return Objects.equals(this.payload, relayRequest.payload) &&
+ Objects.equals(this.destinationId, relayRequest.destinationId) &&
+ Objects.equals(this.payloadVersion, relayRequest.payloadVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(payload, destinationId, payloadVersion);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class RelayRequest {\n");
+
+ sb.append(" payload: ").append(toIndentedString(payload)).append("\n");
+ sb.append(" destinationId: ").append(toIndentedString(destinationId)).append("\n");
+ sb.append(" payloadVersion: ").append(toIndentedString(payloadVersion)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+}
+
diff --git a/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/Result.java b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/Result.java
new file mode 100644
index 0000000..bf002ca
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/swagger-codegen/org/masil/seoulyeok/events/relay/models/Result.java
@@ -0,0 +1,101 @@
+package org.masil.seoulyeok.events.relay.models;
+
+import java.util.Objects;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+
+/**
+ * Result
+ */@javax.annotation.Generated(value = "com.trevari.oas.codegen.spring.TrevariSpringCodeGenerator")
+public class Result implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @JsonProperty("code")
+ private Integer code;
+
+ @JsonProperty("message")
+ private String message;
+
+ public Result code(Integer code) {
+ this.code = code;
+ return this;
+ }
+
+ /**
+ * Get code
+ * @return code
+ */
+
+
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public void setCode(Integer code) {
+ this.code = code;
+ }
+
+ public Result message(String message) {
+ this.message = message;
+ return this;
+ }
+
+ /**
+ * Get message
+ * @return message
+ */
+
+
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Result result = (Result) o;
+ return Objects.equals(this.code, result.code) &&
+ Objects.equals(this.message, result.message);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(code, message);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class Result {\n");
+
+ sb.append(" code: ").append(toIndentedString(code)).append("\n");
+ sb.append(" message: ").append(toIndentedString(message)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+}
+
diff --git a/seoulyeok-message-relay/api/src/main/swagger-config/.gitignore b/seoulyeok-message-relay/api/src/main/swagger-config/.gitignore
new file mode 100644
index 0000000..aa6d44c
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/swagger-config/.gitignore
@@ -0,0 +1 @@
+.generated
\ No newline at end of file
diff --git a/seoulyeok-message-relay/api/src/main/swagger-config/api.yml b/seoulyeok-message-relay/api/src/main/swagger-config/api.yml
new file mode 100644
index 0000000..4ee1c33
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/swagger-config/api.yml
@@ -0,0 +1,97 @@
+openapi: "3.0.0"
+info:
+ version: 1.0.2
+ title: notification
+ license:
+ name: MIT
+paths:
+ /apis/events/relay:
+ post:
+ tags:
+ - relay
+ summary: "특정 메세지를 목적지로 전달합니다"
+ description: "특정 메세지를 목적지로 전달합니다"
+ operationId: "deliveryToDestination"
+ requestBody:
+ description: "deliveryToDestination"
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/RelayRequest"
+
+ responses:
+ '200':
+ description: "데이터 전송 성공 code = 0, meg: ok"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Result"
+ '400':
+ description: "입력이 유효하지 않습니다 code = -1, meg: fail"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Result"
+
+ '500':
+ description: "시스템 오류 발생 code = -1, meg: fail"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Result"
+
+components:
+ schemas:
+ RelayRequest:
+ type: "object"
+ required: [
+ payload,
+ destinationId,
+ payloadVersion
+ ]
+ properties:
+ payload:
+ type: 'string'
+ example: "{ \"eventId\": 1 }"
+ destinationId:
+ type: 'integer'
+ format: 'int64'
+ example: '54321'
+ payloadVersion:
+ type: 'string'
+ example: "1.0.0"
+ pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+ RelayClientRequest:
+ type: "object"
+ required: [
+ destinationType,
+ messagePayload,
+ destinationName,
+ payloadVersion
+ ]
+ properties:
+ destinationType:
+ type: 'string'
+ example: "SIMPLE_QUEUE"
+ messagePayload:
+ type: 'string'
+ example: "{ \"eventId\": 1 }"
+ destinationName:
+ type: 'string'
+ example: 'settled.fifo'
+ payloadVersion:
+ type: 'string'
+ example: "1.0.0"
+ pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
+ Result:
+ type: "object"
+ required:
+ - value
+ properties:
+ code:
+ type: "integer"
+ example: 1
+ message:
+ type: "string"
+ example: "success | fail"
diff --git a/seoulyeok-message-relay/api/src/main/swagger-config/config.json b/seoulyeok-message-relay/api/src/main/swagger-config/config.json
new file mode 100644
index 0000000..399cee6
--- /dev/null
+++ b/seoulyeok-message-relay/api/src/main/swagger-config/config.json
@@ -0,0 +1,23 @@
+{
+
+ "library": "spring-boot",
+ "interfaceOnly" : true,
+ "basePackage" : "com.trevari.events.relay",
+ "configPackage" : "com.trevari.events.relay.config",
+ "modelPackage": "com.trevari.events.relay.models",
+ "apiPackage": "com.trevari.events.relay.api",
+ "invokerPackage": "com.trevari.events.relay.api",
+ "useTags" : true,
+ "skipDefaultInterface" : true,
+ "dateLibrary": "java8",
+ "useBeanValidation" : true,
+ "serializableModel" : true,
+ "implicitHeaders" : false,
+ "swaggerDocketConfig": false,
+ "hideGenerationTimestamp": true,
+
+ "useSpringfox": false,
+ "reactive": false,
+ "openApiNullable" : false
+
+}
diff --git a/seoulyeok-message-relay/application/Dockerfile b/seoulyeok-message-relay/application/Dockerfile
new file mode 100644
index 0000000..d5b3b2a
--- /dev/null
+++ b/seoulyeok-message-relay/application/Dockerfile
@@ -0,0 +1,9 @@
+FROM amazoncorretto:11-alpine
+
+ARG FROM_JAR=build/libs/message-relay.jar
+
+COPY ${FROM_JAR} app.jar
+
+EXPOSE 5000
+
+ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/seoulyeok-message-relay/application/build.gradle b/seoulyeok-message-relay/application/build.gradle
new file mode 100644
index 0000000..f69d328
--- /dev/null
+++ b/seoulyeok-message-relay/application/build.gradle
@@ -0,0 +1,48 @@
+plugins {
+ id 'java'
+ id "io.freefair.lombok"
+ id "org.springframework.boot"
+ id "io.spring.dependency-management"
+
+}
+
+group 'org.masil.seoulyeok.events.relay'
+
+dependencies {
+
+ implementation project(':seoulyeok-message-relay:domain')
+ implementation project(':seoulyeok-message-relay:persistence')
+ implementation project(':refs:destination')
+ implementation project(':refs:event')
+
+ implementation('io.awspring.cloud:spring-cloud-starter-aws-messaging:2.4.1') {
+ exclude group: 'com.amazonaws', module: 'aws-java-sdk-s3'
+ }
+
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-test'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
+
+ // for repository test
+ testImplementation 'io.zonky.test:embedded-database-spring-test:2.1.0'
+ testImplementation 'org.mockito:mockito-inline:4.6.1'
+ testImplementation 'org.awaitility:awaitility:4.2.0'
+
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
+
+ implementation 'com.github.LenKIM.identifier:identifier-generator:0.0.36'
+ implementation 'com.github.moimp:domain-core:0.0.2'
+ implementation 'com.github.moimp.clocks:clocks:1.0.0'
+ implementation 'com.github.moimp.clocks:clocks-frozen:1.0.0'
+
+ implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ implementation 'org.springframework.boot:spring-boot-devtools'
+
+ implementation 'org.springframework.cloud:spring-cloud-config-client:3.1.3'
+
+}
+
+test {
+ useJUnitPlatform()
+}
diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/MessageRelayApplication.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/MessageRelayApplication.java
new file mode 100644
index 0000000..5588640
--- /dev/null
+++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/MessageRelayApplication.java
@@ -0,0 +1,22 @@
+package org.masil.seoulyeok.events.relay;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@SpringBootApplication
+public class MessageRelayApplication {
+
+ @RestController
+ public static class HelloController {
+ @GetMapping(value = {"/annyeng"})
+ public String hello() {
+ return "hello";
+ }
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(MessageRelayApplication.class, args);
+ }
+}
diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationFetcher.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationFetcher.java
new file mode 100644
index 0000000..76e3f09
--- /dev/null
+++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationFetcher.java
@@ -0,0 +1,19 @@
+package org.masil.seoulyeok.events.relay.application;
+
+import org.masil.seoulyeok.events.destination.Destination;
+import org.masil.seoulyeok.events.relay.port.out.LoadDestinationPort;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class DestinationFetcher {
+
+ private final LoadDestinationPort loadPort;
+
+ public List findAllActiveDestination() {
+ return loadPort.loadAllActiveDestination();
+ }
+}
diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChanger.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChanger.java
new file mode 100644
index 0000000..d1fe458
--- /dev/null
+++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/DestinationStatusChanger.java
@@ -0,0 +1,23 @@
+package org.masil.seoulyeok.events.relay.application;
+
+import org.masil.seoulyeok.events.destination.Destination;
+import org.masil.seoulyeok.events.destination.DestinationId;
+import org.masil.seoulyeok.events.relay.port.out.CommandDestinationPort;
+import org.masil.seoulyeok.events.relay.port.out.LoadDestinationPort;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class DestinationStatusChanger {
+
+ private final LoadDestinationPort loadPort;
+ private final CommandDestinationPort commandPort;
+
+ public void inactivateBy(DestinationId destinationId) {
+ Destination destination = loadPort.loadById(destinationId);
+ destination.inactive();
+
+ commandPort.update(destination);
+ }
+}
diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdater.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdater.java
new file mode 100644
index 0000000..232f494
--- /dev/null
+++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelaidMessageUpdater.java
@@ -0,0 +1,18 @@
+package org.masil.seoulyeok.events.relay.application;
+
+import org.masil.seoulyeok.events.relay.port.out.RelayMessageMarkPort;
+import java.time.LocalDateTime;
+import lombok.RequiredArgsConstructor;
+import org.masil.seoulyeok.events.relaymessage.RelayMessageId;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class RelaidMessageUpdater {
+
+ private final RelayMessageMarkPort messageMarkPort;
+
+ public boolean setMarkRelaidMessage(RelayMessageId relayMessageId, LocalDateTime publish) {
+ return messageMarkPort.setReliedMark(relayMessageId, publish);
+ }
+}
diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelayProcessor.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelayProcessor.java
new file mode 100644
index 0000000..76355eb
--- /dev/null
+++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/RelayProcessor.java
@@ -0,0 +1,57 @@
+package org.masil.seoulyeok.events.relay.application;
+
+import org.masil.seoulyeok.events.destination.Destination;
+import org.masil.seoulyeok.events.publisher.PublishResult;
+import org.masil.seoulyeok.events.relay.application.alert.SlackAlertNotifier;
+import org.masil.seoulyeok.events.relay.application.config.RelayProcessorConfig;
+import org.masil.seoulyeok.events.relay.application.publisher.PublisherContainers;
+import lombok.RequiredArgsConstructor;
+import org.masil.seoulyeok.events.relaymessage.TopRelayMessage;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class RelayProcessor {
+
+ private final DestinationFetcher fetcher;
+ private final TopMessageFinder topMessageFinder;
+ private final PublisherContainers publisherContainers;
+ private final RelayProcessorConfig relayProcessorConfig;
+ private final RelaidMessageUpdater updater;
+ private final DestinationStatusChanger changer;
+ private final SlackAlertNotifier notifier;
+
+ public void doProcess() {
+ List activeDestinations = fetcher.findAllActiveDestination();
+ int batchSize = relayProcessorConfig.getBatchSize();
+ for (Destination activeDestination : activeDestinations) {
+ for (int i = 0; i < batchSize; i++) {
+ TopRelayMessage topRelayMessage = topMessageFinder.find(activeDestination.getId());
+ if (TopRelayMessage.EMPTY.equals(topRelayMessage)) {
+ break;
+ }
+ PublishResult result = this.publish(topRelayMessage, activeDestination);
+ if (!result.isSuccess()) {
+ handleOnFailure(result);
+ break;
+ }
+ handleOnSuccess(result);
+ }
+ }
+ }
+
+ private void handleOnSuccess(PublishResult result) {
+ updater.setMarkRelaidMessage(result.getRelayMessageId(), result.getPublishedAt());
+ }
+
+ private void handleOnFailure(PublishResult result) {
+ changer.inactivateBy(result.getDestinationId());
+ notifier.doNotify(result.getRelayMessageId(), result.getDestinationId());
+ }
+
+ private PublishResult publish(TopRelayMessage topRelayMessage, Destination destination) {
+ return publisherContainers.publish(topRelayMessage, destination);
+ }
+}
diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/SchedulerClient.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/SchedulerClient.java
new file mode 100644
index 0000000..12586e4
--- /dev/null
+++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/SchedulerClient.java
@@ -0,0 +1,19 @@
+package org.masil.seoulyeok.events.relay.application;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class SchedulerClient {
+ private final RelayProcessor processor;
+
+ @Scheduled(fixedDelay = 5000)
+ public void execute() {
+
+ processor.doProcess();
+ }
+}
diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/TopMessageFinder.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/TopMessageFinder.java
new file mode 100644
index 0000000..b68735b
--- /dev/null
+++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/TopMessageFinder.java
@@ -0,0 +1,22 @@
+package org.masil.seoulyeok.events.relay.application;
+
+import org.masil.seoulyeok.events.destination.DestinationId;
+import org.masil.seoulyeok.events.relay.port.out.QueryRelayMessagePort;
+import lombok.RequiredArgsConstructor;
+import org.masil.seoulyeok.events.relaymessage.TopRelayMessage;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class TopMessageFinder {
+
+ private final QueryRelayMessagePort queryRelayMessagePort;
+
+ public TopRelayMessage find(DestinationId destinationId) {
+ if (!queryRelayMessagePort.existsByDestinationId(destinationId)) {
+ return TopRelayMessage.EMPTY;
+ }
+ return queryRelayMessagePort.findTopMessageByDestinationId(destinationId);
+ }
+}
+
diff --git a/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/FakeSlackNotifier.java b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/FakeSlackNotifier.java
new file mode 100644
index 0000000..df25bca
--- /dev/null
+++ b/seoulyeok-message-relay/application/src/main/java/org/masil/seoulyeok/events/relay/application/alert/FakeSlackNotifier.java
@@ -0,0 +1,38 @@
+package org.masil.seoulyeok.events.relay.application.alert;
+
+import java.util.HashMap;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.masil.seoulyeok.events.destination.DestinationId;
+import org.masil.seoulyeok.events.relaymessage.RelayMessageId;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+@RequiredArgsConstructor
+public class FakeSlackNotifier {
+
+ public static final String RELAY_FAIL_MESSAGE = ":exclamation: Message Relay 에 실패하였습니다.\n ```\nmessageId => [%s], destinationId => [%s]```";
+
+ @Value("${alert.slack.channel}")
+ private String ALERT_SLACK_CHANNEL;
+
+ private final RestTemplate rest;
+
+ public String doNotify(RelayMessageId messageId, DestinationId destinationId) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+ Map body = new HashMap<>();
+ body.put("text", getAlertMessage(messageId, destinationId));
+ HttpEntity