diff --git a/.github/workflows/mealkitary-main-develop-ci.yml b/.github/workflows/mealkitary-main-develop-ci.yml index 901d7f2..8b85b57 100644 --- a/.github/workflows/mealkitary-main-develop-ci.yml +++ b/.github/workflows/mealkitary-main-develop-ci.yml @@ -55,6 +55,7 @@ jobs: ./mealkitary-infrastructure/adapter-paymentgateway-tosspayments/build/test-results/**/*.xml ./mealkitary-infrastructure/adapter-firebase-notification/build/test-results/**/*.xml ./mealkitary-infrastructure/business-registration-number-validator/adapter-simple-brn-validator/build/test-results/**/*.xml + ./mealkitary-infrastructure/adapter-address-resolver/build/test-results/**/*.xml - name: Jacoco Coverage 리포트 전송 uses: codecov/codecov-action@v3 @@ -67,7 +68,8 @@ jobs: ./mealkitary-infrastructure/adapter-persistence-spring-data-jpa/build/reports/jacoco/test/jacocoTestReport.xml, ./mealkitary-infrastructure/adapter-paymentgateway-tosspayments/build/reports/jacoco/test/jacocoTestReport.xml, ./mealkitary-infrastructure/adapter-firebase-notification/build/reports/jacoco/test/jacocoTestReport.xml, - ./mealkitary-infrastructure/business-registration-number-validator/adapter-simple-brn-validator/build/reports/jacoco/test/jacocoTestReport.xml + ./mealkitary-infrastructure/business-registration-number-validator/adapter-simple-brn-validator/build/reports/jacoco/test/jacocoTestReport.xml, + ./mealkitary-infrastructure/adapter-address-resolver/build/reports/jacoco/test/jacocoTestReport.xml name: mealkitary-codecov verbose: true diff --git a/.github/workflows/mealkitary-test-coverage-automation.yml b/.github/workflows/mealkitary-test-coverage-automation.yml index acd71d2..e25282a 100644 --- a/.github/workflows/mealkitary-test-coverage-automation.yml +++ b/.github/workflows/mealkitary-test-coverage-automation.yml @@ -43,6 +43,7 @@ jobs: ./mealkitary-infrastructure/adapter-paymentgateway-tosspayments/build/test-results/**/*.xml ./mealkitary-infrastructure/adapter-firebase-notification/build/test-results/**/*.xml ./mealkitary-infrastructure/business-registration-number-validator/adapter-simple-brn-validator/build/test-results/**/*.xml + ./mealkitary-infrastructure/adapter-address-resolver/build/test-results/**/*.xml - name: Jacoco Coverage 리포트 전송 uses: codecov/codecov-action@v3 @@ -55,6 +56,8 @@ jobs: ./mealkitary-infrastructure/adapter-persistence-spring-data-jpa/build/reports/jacoco/test/jacocoTestReport.xml, ./mealkitary-infrastructure/adapter-paymentgateway-tosspayments/build/reports/jacoco/test/jacocoTestReport.xml, ./mealkitary-infrastructure/adapter-firebase-notification/build/reports/jacoco/test/jacocoTestReport.xml, - ./mealkitary-infrastructure/business-registration-number-validator/adapter-simple-brn-validator/build/reports/jacoco/test/jacocoTestReport.xml + ./mealkitary-infrastructure/business-registration-number-validator/adapter-simple-brn-validator/build/reports/jacoco/test/jacocoTestReport.xml, + ./mealkitary-infrastructure/adapter-address-resolver/build/reports/jacoco/test/jacocoTestReport.xml + name: mealkitary-codecov verbose: true diff --git a/mealkitary-api/build.gradle.kts b/mealkitary-api/build.gradle.kts index 5ffe9a6..c0ec197 100644 --- a/mealkitary-api/build.gradle.kts +++ b/mealkitary-api/build.gradle.kts @@ -6,6 +6,7 @@ val snippetsDir by extra { file("build/generated-snippets") } val asciidoctorExt: Configuration by configurations.creating bootJar.enabled = true +bootJar.duplicatesStrategy = DuplicatesStrategy.EXCLUDE jar.enabled = false plugins { @@ -21,6 +22,7 @@ dependencies { implementation(project(":mealkitary-infrastructure:adapter-persistence-spring-data-jpa")) implementation(project(":mealkitary-infrastructure:adapter-paymentgateway-tosspayments")) implementation(project(":mealkitary-infrastructure:adapter-firebase-notification")) + implementation(project(":mealkitary-infrastructure:adapter-address-resolver")) implementation( project( ":mealkitary-infrastructure:business-registration-number-validator:adapter-open-api-brn-validator", diff --git a/mealkitary-api/src/main/kotlin/com/mealkitary/shop/web/request/RegisterShopWebRequest.kt b/mealkitary-api/src/main/kotlin/com/mealkitary/shop/web/request/RegisterShopWebRequest.kt index 034c616..2cdc615 100644 --- a/mealkitary-api/src/main/kotlin/com/mealkitary/shop/web/request/RegisterShopWebRequest.kt +++ b/mealkitary-api/src/main/kotlin/com/mealkitary/shop/web/request/RegisterShopWebRequest.kt @@ -8,8 +8,11 @@ data class RegisterShopWebRequest( val title: String? = null, @field:NotBlank(message = "사업자 번호는 필수입니다.") - val brn: String? = null + val brn: String? = null, + + @field:NotBlank(message = "주소는 필수입니다.") + val address: String? = null ) { - fun mapToServiceRequest() = RegisterShopRequest(title!!, brn!!) + fun mapToServiceRequest() = RegisterShopRequest(title!!, brn!!, address!!) } diff --git a/mealkitary-api/src/test/kotlin/com/docs/shop/RegisterShopControllerDocsTest.kt b/mealkitary-api/src/test/kotlin/com/docs/shop/RegisterShopControllerDocsTest.kt index 7006d8f..c00ef51 100644 --- a/mealkitary-api/src/test/kotlin/com/docs/shop/RegisterShopControllerDocsTest.kt +++ b/mealkitary-api/src/test/kotlin/com/docs/shop/RegisterShopControllerDocsTest.kt @@ -28,7 +28,7 @@ class RegisterShopControllerDocsTest : RestDocsSupport() { fun `api docs test - registerShop`() { every { registerShopUseCase.register(any()) } answers { 1L } - val registerShopWebRequest = RegisterShopWebRequest("집밥뚝딱 안양점", "123-23-12345") + val registerShopWebRequest = RegisterShopWebRequest("집밥뚝딱 안양점", "123-23-12345", "경기도 안양시 동안구 벌말로") mvc.perform( RestDocumentationRequestBuilders.post("/shops") @@ -45,6 +45,7 @@ class RegisterShopControllerDocsTest : RestDocsSupport() { requestFields( fieldWithPath("title").type(JsonFieldType.STRING).description("등록 대상 가게 이름"), fieldWithPath("brn").type(JsonFieldType.STRING).description("사업자 번호"), + fieldWithPath("address").type(JsonFieldType.STRING).description("가게 도로명 주소"), ), responseHeaders(headerWithName("Location").description("생성된 가게 리소스 URI")), ) diff --git a/mealkitary-api/src/test/kotlin/com/mealkitary/shop/web/RegisterShopControllerTest.kt b/mealkitary-api/src/test/kotlin/com/mealkitary/shop/web/RegisterShopControllerTest.kt index f62186a..521da7c 100644 --- a/mealkitary-api/src/test/kotlin/com/mealkitary/shop/web/RegisterShopControllerTest.kt +++ b/mealkitary-api/src/test/kotlin/com/mealkitary/shop/web/RegisterShopControllerTest.kt @@ -15,7 +15,7 @@ class RegisterShopControllerTest : WebIntegrationTestSupport() { fun `api integration test - registerShop`() { every { registerShopUseCase.register(any()) } answers { 1L } - val registerShopWebRequest = RegisterShopWebRequest("집밥뚝딱 안양점", "123-23-12345") + val registerShopWebRequest = RegisterShopWebRequest("집밥뚝딱 안양점", "123-23-12345", "경기도 안양시 동안구 벌말로 40") mvc.perform( MockMvcRequestBuilders.post("/shops") @@ -28,7 +28,7 @@ class RegisterShopControllerTest : WebIntegrationTestSupport() { @Test fun `api integration test - 가게 이름이 누락된 경우 400 에러를 발생한다`() { - val registerShopWebRequest = RegisterShopWebRequest(brn = "123-23-12345") + val registerShopWebRequest = RegisterShopWebRequest(brn = "123-23-12345", address = "경기도 안양시 동안구 벌말로 40") mvc.perform( MockMvcRequestBuilders.post("/shops") @@ -44,7 +44,7 @@ class RegisterShopControllerTest : WebIntegrationTestSupport() { @Test fun `api integration test - 사업자 번호가 누락된 경우 400 에러를 발생한다`() { - val registerShopWebRequest = RegisterShopWebRequest(title = "집밥뚝딱 안양점") + val registerShopWebRequest = RegisterShopWebRequest(title = "집밥뚝딱 안양점", address = "경기도 안양시 동안구 벌말로 40") mvc.perform( MockMvcRequestBuilders.post("/shops") @@ -58,6 +58,22 @@ class RegisterShopControllerTest : WebIntegrationTestSupport() { .andExpect(jsonPath("$..errors[0].reason").value("사업자 번호는 필수입니다.")) } + @Test + fun `api integration test - 주소가 누락된 경우 400 에러를 발생한다`() { + val registerShopWebRequest = RegisterShopWebRequest(title = "집밥뚝딱 안양점", brn = "123-23-12345") + + mvc.perform( + MockMvcRequestBuilders.post("/shops") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(registerShopWebRequest)) + ) + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.status").value("400")) + .andExpect(jsonPath("$.message").value("잘못된 입력값입니다.")) + .andExpect(jsonPath("$..errors[0].field").value("address")) + .andExpect(jsonPath("$..errors[0].reason").value("주소는 필수입니다.")) + } + @Test fun `api integration test - JSON 형식이 아닌 경우 400 에러가 발생한다`() { mvc.perform( diff --git a/mealkitary-application/src/main/kotlin/com/mealkitary/shop/application/port/input/RegisterShopRequest.kt b/mealkitary-application/src/main/kotlin/com/mealkitary/shop/application/port/input/RegisterShopRequest.kt index b11e2f1..9598f31 100644 --- a/mealkitary-application/src/main/kotlin/com/mealkitary/shop/application/port/input/RegisterShopRequest.kt +++ b/mealkitary-application/src/main/kotlin/com/mealkitary/shop/application/port/input/RegisterShopRequest.kt @@ -2,5 +2,6 @@ package com.mealkitary.shop.application.port.input data class RegisterShopRequest( val title: String, - val brn: String + val brn: String, + val address: String ) diff --git a/mealkitary-application/src/main/kotlin/com/mealkitary/shop/application/service/RegisterShopService.kt b/mealkitary-application/src/main/kotlin/com/mealkitary/shop/application/service/RegisterShopService.kt index 8da3cab..f36d12c 100644 --- a/mealkitary-application/src/main/kotlin/com/mealkitary/shop/application/service/RegisterShopService.kt +++ b/mealkitary-application/src/main/kotlin/com/mealkitary/shop/application/service/RegisterShopService.kt @@ -3,23 +3,23 @@ package com.mealkitary.shop.application.service import com.mealkitary.shop.application.port.input.RegisterShopRequest import com.mealkitary.shop.application.port.input.RegisterShopUseCase import com.mealkitary.shop.application.port.output.SaveShopPort -import com.mealkitary.shop.domain.shop.factory.ShopBusinessNumberValidator import com.mealkitary.shop.domain.shop.factory.ShopFactory import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service -@Transactional(readOnly = true) +@Transactional class RegisterShopService( private val saveShopPort: SaveShopPort, - shopBusinessNumberValidator: ShopBusinessNumberValidator + private val shopFactory: ShopFactory ) : RegisterShopUseCase { - private val shopFactory = ShopFactory(shopBusinessNumberValidator) - - @Transactional override fun register(registerShopRequest: RegisterShopRequest): Long { - val shop = shopFactory.createOne(registerShopRequest.title, registerShopRequest.brn) + val shop = shopFactory.createOne( + registerShopRequest.title, + registerShopRequest.brn, + registerShopRequest.address + ) return saveShopPort.saveOne(shop) } diff --git a/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/GetShopServiceTest.kt b/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/GetShopServiceTest.kt index 23b9d34..90f93eb 100644 --- a/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/GetShopServiceTest.kt +++ b/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/GetShopServiceTest.kt @@ -1,11 +1,14 @@ package com.mealkitary.shop.application.service +import com.mealkitary.common.model.Address +import com.mealkitary.common.model.Coordinates import com.mealkitary.shop.application.port.input.ShopResponse import com.mealkitary.shop.application.port.output.LoadShopPort import com.mealkitary.shop.domain.shop.Shop import com.mealkitary.shop.domain.shop.ShopBusinessNumber import com.mealkitary.shop.domain.shop.ShopStatus import com.mealkitary.shop.domain.shop.ShopTitle +import com.mealkitary.shop.domain.shop.address.ShopAddress import io.kotest.core.spec.style.AnnotationSpec import io.kotest.matchers.shouldBe import io.mockk.every @@ -24,6 +27,19 @@ class GetShopServiceTest : AnnotationSpec() { ShopTitle.from("집밥뚝딱"), ShopStatus.VALID, ShopBusinessNumber.from("123-45-67890"), + ShopAddress.of( + "1234567890", + Coordinates.of( + 126.99599512792346, + 35.976749396987046 + ), + Address.of( + "region1DepthName", + "region2DepthName", + "region3DepthName", + "roadName" + ) + ), mutableListOf(), mutableListOf() ) diff --git a/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/RegisterShopServiceTest.kt b/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/RegisterShopServiceTest.kt index 2e2e6be..e61ca1c 100644 --- a/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/RegisterShopServiceTest.kt +++ b/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/RegisterShopServiceTest.kt @@ -1,9 +1,18 @@ package com.mealkitary.shop.application.service +import com.mealkitary.common.model.Address +import com.mealkitary.common.model.Coordinates import com.mealkitary.shop.application.port.input.RegisterShopRequest import com.mealkitary.shop.application.port.output.SaveShopPort +import com.mealkitary.shop.domain.product.Product import com.mealkitary.shop.domain.shop.Shop +import com.mealkitary.shop.domain.shop.ShopBusinessNumber +import com.mealkitary.shop.domain.shop.ShopStatus +import com.mealkitary.shop.domain.shop.ShopTitle +import com.mealkitary.shop.domain.shop.address.ShopAddress +import com.mealkitary.shop.domain.shop.factory.AddressResolver import com.mealkitary.shop.domain.shop.factory.ShopBusinessNumberValidator +import com.mealkitary.shop.domain.shop.factory.ShopFactory import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.AnnotationSpec import io.kotest.matchers.collections.shouldBeEmpty @@ -12,19 +21,36 @@ import io.kotest.matchers.throwable.shouldHaveMessage import io.mockk.every import io.mockk.mockk import io.mockk.slot +import java.time.LocalTime class RegisterShopServiceTest : AnnotationSpec() { private val saveShopPort = mockk() + private val shopFactory = mockk() private val shopBusinessNumberValidator = mockk() - private val registerShopService = RegisterShopService(saveShopPort, shopBusinessNumberValidator) + private val addressResolver = mockk() + private val registerShopService = RegisterShopService(saveShopPort, shopFactory) @Test fun `service unit test - 신규 가게를 등록한다`() { val shopSlot = slot() - val request = RegisterShopRequest("집밥뚝딱 안양점", "123-23-12345") + val request = RegisterShopRequest("집밥뚝딱 안양점", "123-23-12345", "경기도 안양시 동안구 벌말로 40") + val expectedShopAddress = + ShopAddress.of("1234567890", Coordinates.of(0.0, 0.0), Address.of("경기도", "안양시 동안구", "벌말로", "40")) + + val mockedShop = Shop( + ShopTitle.from(request.title), + ShopStatus.VALID, + ShopBusinessNumber.from(request.brn), + expectedShopAddress, + emptyList().toMutableList(), + emptyList().toMutableList() + ) + + every { + shopFactory.createOne(request.title, request.brn, request.address) + } returns mockedShop every { saveShopPort.saveOne(capture(shopSlot)) } answers { 1L } - every { shopBusinessNumberValidator.validate(any()) } answers {} val result = registerShopService.register(request) @@ -32,15 +58,23 @@ class RegisterShopServiceTest : AnnotationSpec() { result shouldBe 1L capturedShop.businessNumber.value shouldBe "123-23-12345" capturedShop.title.value shouldBe "집밥뚝딱 안양점" + capturedShop.address shouldBe expectedShopAddress capturedShop.products.shouldBeEmpty() capturedShop.reservableTimes.shouldBeEmpty() } @Test fun `service unit test - 가게 이름 형식에 맞지 않으면 예외를 발생한다`() { - val request = RegisterShopRequest("invalid!#@", "123-23-12345") + val request = RegisterShopRequest("invalid!#@", "123-23-12345", "경기도 안양시 동안구 벌말로 40") + val expectedShopAddress = + ShopAddress.of("1234567890", Coordinates.of(0.0, 0.0), Address.of("경기도", "안양시 동안구", "벌말로", "40")) + + every { + shopFactory.createOne(any(), any(), any()) + } throws IllegalArgumentException("올바른 가게 이름 형식이 아닙니다.(한글, 영문, 공백, 숫자만 포함 가능)") every { saveShopPort.saveOne(any()) } answers { 1L } every { shopBusinessNumberValidator.validate(any()) } answers {} + every { addressResolver.resolveAddress("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress shouldThrow { registerShopService.register(request) @@ -49,7 +83,11 @@ class RegisterShopServiceTest : AnnotationSpec() { @Test fun `service unit test - 사업자 번호 형식에 맞지 않으면 예외를 발생한다`() { - val request = RegisterShopRequest("집밥뚝딱 안양점", "invalid-brn") + val request = RegisterShopRequest("집밥뚝딱 안양점", "invalid-brn", "경기도 안양시 동안구 벌말로 40") + + every { + shopFactory.createOne(any(), any(), any()) + } throws IllegalArgumentException("올바른 사업자번호 형식이 아닙니다.") every { saveShopPort.saveOne(any()) } answers { 1L } every { shopBusinessNumberValidator.validate(any()) } answers {} diff --git a/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/UpdateShopStatusServiceTest.kt b/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/UpdateShopStatusServiceTest.kt index b755b9a..d8e47f7 100644 --- a/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/UpdateShopStatusServiceTest.kt +++ b/mealkitary-application/src/test/kotlin/com/mealkitary/shop/application/service/UpdateShopStatusServiceTest.kt @@ -1,11 +1,14 @@ package com.mealkitary.shop.application.service +import com.mealkitary.common.model.Address +import com.mealkitary.common.model.Coordinates import com.mealkitary.shop.application.port.output.CheckExistenceShopPort import com.mealkitary.shop.application.port.output.LoadShopPort import com.mealkitary.shop.domain.shop.Shop import com.mealkitary.shop.domain.shop.ShopBusinessNumber import com.mealkitary.shop.domain.shop.ShopStatus import com.mealkitary.shop.domain.shop.ShopTitle +import com.mealkitary.shop.domain.shop.address.ShopAddress import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.AnnotationSpec import io.kotest.matchers.shouldBe @@ -25,6 +28,19 @@ class UpdateShopStatusServiceTest : AnnotationSpec() { ShopTitle.from("제목"), ShopStatus.VALID, ShopBusinessNumber.from("123-12-12345"), + ShopAddress.of( + "1234567890", + Coordinates.of( + 126.99599512792346, + 35.976749396987046 + ), + Address.of( + "region1DepthName", + "region2DepthName", + "region3DepthName", + "roadName" + ) + ), mutableListOf(), mutableListOf() ) @@ -44,6 +60,19 @@ class UpdateShopStatusServiceTest : AnnotationSpec() { ShopTitle.from("제목"), ShopStatus.INVALID, ShopBusinessNumber.from("123-12-12345"), + ShopAddress.of( + "1234567890", + Coordinates.of( + 126.99599512792346, + 35.976749396987046 + ), + Address.of( + "region1DepthName", + "region2DepthName", + "region3DepthName", + "roadName" + ) + ), mutableListOf(), mutableListOf() ) @@ -63,6 +92,19 @@ class UpdateShopStatusServiceTest : AnnotationSpec() { ShopTitle.from("제목"), ShopStatus.VALID, ShopBusinessNumber.from("123-12-12345"), + ShopAddress.of( + "1234567890", + Coordinates.of( + 126.99599512792346, + 35.976749396987046 + ), + Address.of( + "region1DepthName", + "region2DepthName", + "region3DepthName", + "roadName" + ) + ), mutableListOf(), mutableListOf() ) diff --git a/mealkitary-domain/src/main/kotlin/com/mealkitary/common/model/Address.kt b/mealkitary-domain/src/main/kotlin/com/mealkitary/common/model/Address.kt new file mode 100644 index 0000000..099d9d3 --- /dev/null +++ b/mealkitary-domain/src/main/kotlin/com/mealkitary/common/model/Address.kt @@ -0,0 +1,34 @@ +package com.mealkitary.common.model + +import javax.persistence.Column +import javax.persistence.Embeddable + +@Embeddable +class Address private constructor( + @Column(name = "region_1depth_name", nullable = false) + val region1DepthName: String, + @Column(name = "region_2depth_name", nullable = false) + val region2DepthName: String, + @Column(name = "region_3depth_name") + val region3DepthName: String, + @Column(name = "road_name") + val roadName: String +) { + + companion object { + fun of( + region1DepthName: String, + region2DepthName: String, + region3DepthName: String, + roadName: String + ): Address { + + return Address( + region1DepthName, + region2DepthName, + region3DepthName, + roadName + ) + } + } +} diff --git a/mealkitary-domain/src/main/kotlin/com/mealkitary/common/model/Coordinates.kt b/mealkitary-domain/src/main/kotlin/com/mealkitary/common/model/Coordinates.kt new file mode 100644 index 0000000..d11bd0f --- /dev/null +++ b/mealkitary-domain/src/main/kotlin/com/mealkitary/common/model/Coordinates.kt @@ -0,0 +1,27 @@ +package com.mealkitary.common.model + +import javax.persistence.Column +import javax.persistence.Embeddable + +@Embeddable +class Coordinates( + @Column(name = "longitude", nullable = false) + val longitude: Double, + @Column(name = "latitude", nullable = false) + val latitude: Double +) { + + companion object { + fun of(longitude: Double, latitude: Double): Coordinates { + checkIsCoordinateRange(longitude, latitude) + + return Coordinates(longitude, latitude) + } + + private fun checkIsCoordinateRange(longitude: Double, latitude: Double) { + if (longitude !in -180.0..180.0 || latitude !in -90.0..90.0) { + throw IllegalArgumentException("유효하지 않은 좌표 범위입니다.") + } + } + } +} diff --git a/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/Shop.kt b/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/Shop.kt index 79dcbe6..352c3cc 100644 --- a/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/Shop.kt +++ b/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/Shop.kt @@ -1,6 +1,7 @@ package com.mealkitary.shop.domain.shop import com.mealkitary.shop.domain.product.Product +import com.mealkitary.shop.domain.shop.address.ShopAddress import java.time.LocalDateTime import java.time.LocalTime import javax.persistence.CascadeType @@ -23,6 +24,7 @@ class Shop( title: ShopTitle, status: ShopStatus, businessNumber: ShopBusinessNumber, + address: ShopAddress, reservableTimes: MutableList, products: MutableList ) { @@ -58,6 +60,8 @@ class Shop( val businessNumber: ShopBusinessNumber = businessNumber + val address: ShopAddress = address + fun checkReservableShop() { if (status.isInvalidStatus()) { throw IllegalStateException("유효하지 않은 가게입니다.") diff --git a/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/address/ShopAddress.kt b/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/address/ShopAddress.kt new file mode 100644 index 0000000..2229f42 --- /dev/null +++ b/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/address/ShopAddress.kt @@ -0,0 +1,34 @@ +package com.mealkitary.shop.domain.shop.address + +import com.mealkitary.common.model.Address +import com.mealkitary.common.model.Coordinates +import javax.persistence.Column +import javax.persistence.Embeddable +import javax.persistence.Embedded + +private const val CITY_CODE_LENGTH = 10 + +@Embeddable +class ShopAddress private constructor( + @Column(name = "city_code", nullable = false) + val cityCode: String, + @Embedded + val coordinates: Coordinates, + @Embedded + val address: Address +) { + + companion object { + fun of(cityCode: String, coordinates: Coordinates, address: Address): ShopAddress { + checkIsCityCodeLength(cityCode) + + return ShopAddress(cityCode, coordinates, address) + } + + private fun checkIsCityCodeLength(cityCode: String) { + if (cityCode.length != CITY_CODE_LENGTH) { + throw IllegalArgumentException("올바른 지역 코드가 아닙니다. (행정동 지역 코드는 10자리)") + } + } + } +} diff --git a/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/factory/AddressResolver.kt b/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/factory/AddressResolver.kt new file mode 100644 index 0000000..83c3e12 --- /dev/null +++ b/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/factory/AddressResolver.kt @@ -0,0 +1,8 @@ +package com.mealkitary.shop.domain.shop.factory + +import com.mealkitary.shop.domain.shop.address.ShopAddress + +interface AddressResolver { + + fun resolveAddress(address: String): ShopAddress +} diff --git a/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/factory/ShopFactory.kt b/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/factory/ShopFactory.kt index 76cc4df..da6efee 100644 --- a/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/factory/ShopFactory.kt +++ b/mealkitary-domain/src/main/kotlin/com/mealkitary/shop/domain/shop/factory/ShopFactory.kt @@ -5,21 +5,28 @@ import com.mealkitary.shop.domain.shop.Shop import com.mealkitary.shop.domain.shop.ShopBusinessNumber import com.mealkitary.shop.domain.shop.ShopStatus import com.mealkitary.shop.domain.shop.ShopTitle +import com.mealkitary.shop.domain.shop.address.ShopAddress +import org.springframework.stereotype.Component import java.time.LocalTime +@Component class ShopFactory( - private val shopBusinessNumberValidator: ShopBusinessNumberValidator + private val shopBusinessNumberValidator: ShopBusinessNumberValidator, + private val addressResolver: AddressResolver ) { - fun createOne(title: String, brn: String): Shop { + fun createOne(title: String, brn: String, address: String): Shop { val shopBusinessNumber = ShopBusinessNumber.from(brn) + val shopAddress: ShopAddress = addressResolver.resolveAddress(address) + shopBusinessNumberValidator.validate(shopBusinessNumber) return Shop( ShopTitle.from(title), ShopStatus.VALID, shopBusinessNumber, + shopAddress, emptyList().toMutableList(), emptyList().toMutableList(), ) diff --git a/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/AddressTest.kt b/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/AddressTest.kt new file mode 100644 index 0000000..2c26e06 --- /dev/null +++ b/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/AddressTest.kt @@ -0,0 +1,28 @@ +package com.mealkitary.shop.domain.shop + +import com.mealkitary.common.model.Address +import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe + +class AddressTest : AnnotationSpec() { + + @Test + fun `올바른 주소를 입력할 경우 객체를 생성할 수 있다`() { + val region1DepthName = "서울" + val region2DepthName = "강남구" + val region3DepthName = "논현동" + val roadName = "논현로" + + val address = Address.of( + region1DepthName, + region2DepthName, + region3DepthName, + roadName + ) + + address.region1DepthName shouldBe region1DepthName + address.region2DepthName shouldBe region2DepthName + address.region3DepthName shouldBe region3DepthName + address.roadName shouldBe roadName + } +} diff --git a/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/CoordinatesTest.kt b/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/CoordinatesTest.kt new file mode 100644 index 0000000..970576a --- /dev/null +++ b/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/CoordinatesTest.kt @@ -0,0 +1,31 @@ +package com.mealkitary.shop.domain.shop + +import com.mealkitary.common.model.Coordinates +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.throwable.shouldHaveMessage + +class CoordinatesTest : AnnotationSpec() { + + @Test + fun `범위를 벗어나는 좌표일 경우 예외를 발생한다`() { + val longitude = -188.023 + val latitude = 999.7412 + + shouldThrow { + Coordinates.of(longitude, latitude) + } shouldHaveMessage "유효하지 않은 좌표 범위입니다." + } + + @Test + fun `올바른 좌표를 입력했을 경우 객체를 생성한다`() { + val longitude = -150.653 + val latitude = 46.492 + + val coordinates = Coordinates.of(longitude, latitude) + + coordinates.longitude shouldBe longitude + coordinates.latitude shouldBe latitude + } +} diff --git a/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/ShopAddressTest.kt b/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/ShopAddressTest.kt new file mode 100644 index 0000000..d490ebd --- /dev/null +++ b/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/ShopAddressTest.kt @@ -0,0 +1,60 @@ +package com.mealkitary.shop.domain.shop + +import com.mealkitary.common.model.Address +import com.mealkitary.common.model.Coordinates +import com.mealkitary.shop.domain.shop.address.ShopAddress +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.throwable.shouldHaveMessage + +class ShopAddressTest : AnnotationSpec() { + + @Test + fun `올바른 값들을 입력했을 경우 객체를 생성할 수 있다`() { + val cityCode = "1234567890" + val coordinates = Coordinates.of( + -150.653, + 46.492 + ) + val address = Address.of( + "region1DepthName", + "region2DepthName", + "region3DepthName", + "roadName" + ) + + val shopAddress = ShopAddress.of( + cityCode, + coordinates, + address + ) + + shopAddress.cityCode shouldBe cityCode + shopAddress.coordinates shouldBe coordinates + shopAddress.address shouldBe address + } + + @Test + fun `지역 코드가 올바르지 않을 경우 예외를 발생한다`() { + val cityCode = "25231491723109" + val coordinates = Coordinates.of( + -150.653, + 46.492 + ) + val address = Address.of( + "region1DepthName", + "region2DepthName", + "region3DepthName", + "roadName" + ) + + shouldThrow { + ShopAddress.of( + cityCode, + coordinates, + address + ) + } shouldHaveMessage "올바른 지역 코드가 아닙니다. (행정동 지역 코드는 10자리)" + } +} diff --git a/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/factory/ShopFactoryTest.kt b/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/factory/ShopFactoryTest.kt index 34a8382..709e531 100644 --- a/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/factory/ShopFactoryTest.kt +++ b/mealkitary-domain/src/test/kotlin/com/mealkitary/shop/domain/shop/factory/ShopFactoryTest.kt @@ -1,5 +1,8 @@ package com.mealkitary.shop.domain.shop.factory +import com.mealkitary.common.model.Address +import com.mealkitary.common.model.Coordinates +import com.mealkitary.shop.domain.shop.address.ShopAddress import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.AnnotationSpec import io.kotest.matchers.shouldBe @@ -10,31 +13,66 @@ import io.mockk.mockk class ShopFactoryTest : AnnotationSpec() { private val shopBusinessNumberValidator = mockk() - private val shopFactory = ShopFactory(shopBusinessNumberValidator) + private val addressResolver = mockk() + private val shopFactory = ShopFactory(shopBusinessNumberValidator, addressResolver) @Test fun `사업자번호가 유효하지 않으면 예외를 발생한다`() { shouldThrow { - shopFactory.createOne("집밥뚝딱 안양점", "32-12-3221") + shopFactory.createOne("집밥뚝딱 안양점", "32-12-3221", "경기도 안양시 동안구 벌말로 40") } shouldHaveMessage "올바른 사업자번호 형식이 아닙니다." } @Test - fun `실제로 유효한 사업자번호와 가게이름이라면 가게를 생성한다`() { + fun `실제로 유효한 사업자번호와 가게이름, 주소라면 가게를 생성한다`() { + val expectedShopAddress = + ShopAddress.of("1234567890", Coordinates.of(0.0, 0.0), Address.of("경기도", "안양시 동안구", "벌말로", "40")) + every { shopBusinessNumberValidator.validate(any()) } answers { } + every { addressResolver.resolveAddress("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress - val shop = shopFactory.createOne("집밥뚝딱 안양점", "321-23-12345") + val shop = shopFactory.createOne("집밥뚝딱 안양점", "321-23-12345", "경기도 안양시 동안구 벌말로 40") shop.title.value shouldBe "집밥뚝딱 안양점" shop.businessNumber.value shouldBe "321-23-12345" + shop.address shouldBe expectedShopAddress } @Test fun `가게 이름이 유효하지 않으면 예외를 발생한다`() { + val expectedShopAddress = + ShopAddress.of("1234567890", Coordinates.of(0.0, 0.0), Address.of("경기도", "안양시 동안구", "벌말로", "40")) + + every { shopBusinessNumberValidator.validate(any()) } answers { } + every { addressResolver.resolveAddress("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress + + shouldThrow { + shopFactory.createOne("집밥뚝딱 ! 안양점", "321-23-12345", "경기도 안양시 동안구 벌말로 40") + } shouldHaveMessage "올바른 가게 이름 형식이 아닙니다.(한글, 영문, 공백, 숫자만 포함 가능)" + } + + @Test + fun `도로명 주소를 받아 주소 객체를 생선한다`() { + val address = "경기도 안양시 동안구 벌말로" + val shopAddress = ShopAddress.of( + "1234567890", + Coordinates.of( + 127.0, + 40.0 + ), + Address.of( + "경기도", + "안양시", + "동안구", + "벌말로" + ) + ) + every { shopBusinessNumberValidator.validate(any()) } answers { } + every { addressResolver.resolveAddress(address) } answers { shopAddress } shouldThrow { - shopFactory.createOne("집밥뚝딱 ! 안양점", "321-23-12345") + shopFactory.createOne("집밥뚝딱 ! 안양점", "321-23-12345", "경기도 안양시 동안구 벌말로 40") } shouldHaveMessage "올바른 가게 이름 형식이 아닙니다.(한글, 영문, 공백, 숫자만 포함 가능)" } } diff --git a/mealkitary-domain/src/testFixtures/kotlin/data/ShopTestData.kt b/mealkitary-domain/src/testFixtures/kotlin/data/ShopTestData.kt index 1389d2a..968438f 100644 --- a/mealkitary-domain/src/testFixtures/kotlin/data/ShopTestData.kt +++ b/mealkitary-domain/src/testFixtures/kotlin/data/ShopTestData.kt @@ -1,10 +1,13 @@ package data +import com.mealkitary.common.model.Address +import com.mealkitary.common.model.Coordinates import com.mealkitary.shop.domain.product.Product import com.mealkitary.shop.domain.shop.Shop import com.mealkitary.shop.domain.shop.ShopBusinessNumber import com.mealkitary.shop.domain.shop.ShopStatus import com.mealkitary.shop.domain.shop.ShopTitle +import com.mealkitary.shop.domain.shop.address.ShopAddress import data.ProductTestData.Companion.defaultProduct import java.time.LocalTime @@ -22,7 +25,20 @@ class ShopTestData { defaultProduct().withId(1L).withName("부대찌개").build(), defaultProduct().withId(2L).withName("닭볶음탕").build() ), - private var shopBusinessNumber: ShopBusinessNumber = ShopBusinessNumber.from("123-45-67890") + private var shopBusinessNumber: ShopBusinessNumber = ShopBusinessNumber.from("123-45-67890"), + private var shopAddress: ShopAddress = ShopAddress.of( + "1234567890", + Coordinates.of( + 126.99599512792346, + 35.976749396987046 + ), + Address.of( + "region1DepthName", + "region2DepthName", + "region3DepthName", + "roadName" + ) + ) ) { fun withTitle(title: String): ShopBuilder { @@ -50,11 +66,17 @@ class ShopTestData { return this } + fun withAddress(shopAddress: ShopAddress): ShopBuilder { + this.shopAddress = shopAddress + return this + } + fun build(): Shop { return Shop( ShopTitle.from(this.title), this.shopStatus, this.shopBusinessNumber, + this.shopAddress, this.reservableTimes.toMutableList(), this.products.toMutableList() ) diff --git a/mealkitary-infrastructure/adapter-address-resolver/build.gradle.kts b/mealkitary-infrastructure/adapter-address-resolver/build.gradle.kts new file mode 100644 index 0000000..2f0606e --- /dev/null +++ b/mealkitary-infrastructure/adapter-address-resolver/build.gradle.kts @@ -0,0 +1,8 @@ +dependencies { + val querydslVersion: String by properties + implementation("com.querydsl:querydsl-jpa:$querydslVersion") + implementation("com.h2database:h2") + implementation(project(":mealkitary-domain")) + implementation(project(":mealkitary-application")) + testImplementation(testFixtures(project(":mealkitary-domain"))) +} diff --git a/mealkitary-infrastructure/adapter-address-resolver/src/main/kotlin/com/mealkitary/address/SimpleAddressResolver.kt b/mealkitary-infrastructure/adapter-address-resolver/src/main/kotlin/com/mealkitary/address/SimpleAddressResolver.kt new file mode 100644 index 0000000..f15e509 --- /dev/null +++ b/mealkitary-infrastructure/adapter-address-resolver/src/main/kotlin/com/mealkitary/address/SimpleAddressResolver.kt @@ -0,0 +1,41 @@ +package com.mealkitary.address + +import com.mealkitary.common.model.Address +import com.mealkitary.common.model.Coordinates +import com.mealkitary.shop.domain.shop.address.ShopAddress +import com.mealkitary.shop.domain.shop.factory.AddressResolver +import org.springframework.stereotype.Component + +private const val ADDRESS_MIN_LENGTH = 2 + +@Component +class SimpleAddressResolver : AddressResolver { + + override fun resolveAddress(address: String): ShopAddress { + val value = address.split(" ") + + if (value.size < ADDRESS_MIN_LENGTH) { + throw IllegalArgumentException("주소 형식이 올바르지 않습니다.") + } + + val region1DepthName = value[0] + val region2DepthName = value[1] + val region3DepthName = value.getOrNull(2) ?: "" + val roadName = value.getOrNull(3) ?: "" + + // TODO: 좌표 및 지역 코드를 카카오 API에서 받아올 예정 + return ShopAddress.of( + "1234567890", + Coordinates.of( + 127.0, + 40.0 + ), + Address.of( + region1DepthName, + region2DepthName, + region3DepthName, + roadName + ) + ) + } +} diff --git a/mealkitary-infrastructure/adapter-address-resolver/src/test/kotlin/SimpleAddressResolverTest.kt b/mealkitary-infrastructure/adapter-address-resolver/src/test/kotlin/SimpleAddressResolverTest.kt new file mode 100644 index 0000000..79a6e3e --- /dev/null +++ b/mealkitary-infrastructure/adapter-address-resolver/src/test/kotlin/SimpleAddressResolverTest.kt @@ -0,0 +1,69 @@ +package com.mealkitary.address + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.throwable.shouldHaveMessage +import org.springframework.stereotype.Component + +@Component +class SimpleAddressResolverTest : AnnotationSpec() { + + @Test + fun `adapter unit test - 문자열 주소값을 받아 주소 객체를 생성한다`() { + val address = "서울특별시 강남구 역삼동 논현로" + val resolver = SimpleAddressResolver() + + val shopAddress = resolver.resolveAddress(address) + + shopAddress.cityCode shouldBe "1234567890" + shopAddress.coordinates.longitude shouldBe 127.0 + shopAddress.coordinates.latitude shouldBe 40.0 + shopAddress.address.region1DepthName shouldBe "서울특별시" + shopAddress.address.region2DepthName shouldBe "강남구" + shopAddress.address.region3DepthName shouldBe "역삼동" + shopAddress.address.roadName shouldBe "논현로" + } + + @Test + fun `adapter unit test - 문자열 주소값이 3등분일 경우 3개의 정보를 가진 주소 객체를 생성한다`() { + val address = "경기도 남양주시 다산동" + val resolver = SimpleAddressResolver() + + val shopAddress = resolver.resolveAddress(address) + + shopAddress.cityCode shouldBe "1234567890" + shopAddress.coordinates.longitude shouldBe 127.0 + shopAddress.coordinates.latitude shouldBe 40.0 + shopAddress.address.region1DepthName shouldBe "경기도" + shopAddress.address.region2DepthName shouldBe "남양주시" + shopAddress.address.region3DepthName shouldBe "다산동" + shopAddress.address.roadName shouldBe "" + } + + @Test + fun `adapter unit test - 문자열 주소값이 2등분일 경우 2개의 정보를 가진 주소 객체를 생성한다`() { + val address = "제주특별자치도 한림읍" + val resolver = SimpleAddressResolver() + + val shopAddress = resolver.resolveAddress(address) + + shopAddress.cityCode shouldBe "1234567890" + shopAddress.coordinates.longitude shouldBe 127.0 + shopAddress.coordinates.latitude shouldBe 40.0 + shopAddress.address.region1DepthName shouldBe "제주특별자치도" + shopAddress.address.region2DepthName shouldBe "한림읍" + shopAddress.address.region3DepthName shouldBe "" + shopAddress.address.roadName shouldBe "" + } + + @Test + fun `adapter unit test - 문자열 주소값이 2등분 이하일 경우 예외를 발생한다`() { + val address = "제주특별자치도" + val resolver = SimpleAddressResolver() + + shouldThrow { + resolver.resolveAddress(address) + } shouldHaveMessage "주소 형식이 올바르지 않습니다." + } +} diff --git a/mealkitary-infrastructure/adapter-persistence-spring-data-jpa/src/main/resources/data.sql b/mealkitary-infrastructure/adapter-persistence-spring-data-jpa/src/main/resources/data.sql index 929806b..ab9372a 100644 --- a/mealkitary-infrastructure/adapter-persistence-spring-data-jpa/src/main/resources/data.sql +++ b/mealkitary-infrastructure/adapter-persistence-spring-data-jpa/src/main/resources/data.sql @@ -1,5 +1,5 @@ insert into shop -values (1, '123-12-12345', 'VALID', '집밥뚝딱 철산점'); +values (1, '경기도', '광명시', '철산동', '철산로', '1234567890', 30.03, 50.05, '123-12-12345', 'VALID', '집밥뚝딱 철산점'); insert into product values (1, '부대찌개', 15800, 1); insert into product @@ -17,7 +17,7 @@ values (1, '18:30'); insert into shop -values (2, '123-12-12345', 'VALID', '집밥뚝딱 안양점'); +values (2, '경기도', '안양시', '동안구', '경수대로', '1234567890', 30.03, 50.05, '123-12-12345', 'VALID', '집밥뚝딱 안양점'); insert into product values (4, '비비고 만두', 3200, 2); insert into product @@ -30,7 +30,7 @@ insert into reservable_time values (2, '19:30'); insert into shop -values (3, '123-12-12345', 'VALID', '집밥뚝딱 숭실대입구점'); +values (3, '서울시', '동작구', '상도동', '', '1234567890', 30.03, 50.05, '123-12-12345', 'VALID', '집밥뚝딱 숭실대입구점'); insert into product values (7, '왕만두', 4900, 3); insert into product @@ -48,7 +48,7 @@ values (3, '23:30'); insert into shop -values (4, '123-12-12345', 'VALID', '집밥뚝딱 다산점'); +values (4, '경기도', '남양주시', '다산동', '', '1234567890', 30.03, 50.05, '123-12-12345', 'VALID', '집밥뚝딱 다산점'); insert into product values (10, '김치찌개', 15800, 4); insert into reservable_time diff --git a/mealkitary-infrastructure/adapter-persistence-spring-data-jpa/src/test/kotlin/com/mealkitary/PersistenceIntegrationTestSupport.kt b/mealkitary-infrastructure/adapter-persistence-spring-data-jpa/src/test/kotlin/com/mealkitary/PersistenceIntegrationTestSupport.kt index f6b6d25..1bdfea1 100644 --- a/mealkitary-infrastructure/adapter-persistence-spring-data-jpa/src/test/kotlin/com/mealkitary/PersistenceIntegrationTestSupport.kt +++ b/mealkitary-infrastructure/adapter-persistence-spring-data-jpa/src/test/kotlin/com/mealkitary/PersistenceIntegrationTestSupport.kt @@ -4,6 +4,7 @@ import com.mealkitary.reservation.application.service.AcceptReservationService import com.mealkitary.reservation.application.service.PayReservationService import com.mealkitary.reservation.application.service.RejectReservationService import com.mealkitary.shop.application.service.RegisterShopService +import com.mealkitary.shop.domain.shop.factory.ShopFactory import com.ninjasquad.springmockk.MockkBean import io.kotest.core.spec.style.AnnotationSpec import io.kotest.extensions.spring.SpringExtension @@ -36,4 +37,7 @@ abstract class PersistenceIntegrationTestSupport : AnnotationSpec() { @MockkBean private lateinit var registerShopService: RegisterShopService + + @MockkBean + private lateinit var shopFactory: ShopFactory } diff --git a/settings.gradle.kts b/settings.gradle.kts index 52db22b..680a318 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,7 +8,8 @@ include( "mealkitary-infrastructure:adapter-paymentgateway-tosspayments", "mealkitary-infrastructure:adapter-firebase-notification", "mealkitary-infrastructure:business-registration-number-validator:adapter-open-api-brn-validator", - "mealkitary-infrastructure:business-registration-number-validator:adapter-simple-brn-validator" + "mealkitary-infrastructure:business-registration-number-validator:adapter-simple-brn-validator", + "mealkitary-infrastructure:adapter-address-resolver" ) pluginManagement { @@ -36,3 +37,5 @@ pluginManagement { } } } +include("mealkitary-infrastructure:adapter-address-resolver") +findProject(":mealkitary-infrastructure:adapter-address-resolver")?.name = "adapter-address-resolver"