Skip to content

Commit

Permalink
Merge pull request #86 from le2sky/feat/84
Browse files Browse the repository at this point in the history
[기능 구현] 카카오 주소 조회 api 연동 (issue#84)
  • Loading branch information
le2sky authored Oct 11, 2023
2 parents 7204ea2 + 5cd6b71 commit 194220c
Show file tree
Hide file tree
Showing 18 changed files with 291 additions and 26 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/mealkitary-main-develop-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ jobs:
./mealkitary-infrastructure/adapter-firebase-notification/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-business-registration-number-validator/simple-brn-validator/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-business-registration-number-validator/open-api-brn-validator/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-address-resolver/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-address-resolver/simple-address-resolver/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-address-resolver/kakao-api-address-resolver/build/test-results/**/*.xml
- name: Jacoco Coverage 리포트 전송
uses: codecov/codecov-action@v3
Expand All @@ -71,7 +72,8 @@ jobs:
./mealkitary-infrastructure/adapter-firebase-notification/build/reports/jacoco/test/jacocoTestReport.xml,
./mealkitary-infrastructure/adapter-business-registration-number-validator/simple-brn-validator/build/reports/jacoco/test/jacocoTestReport.xml,
./mealkitary-infrastructure/adapter-business-registration-number-validator/open-api-brn-validator/build/reports/jacoco/test/jacocoTestReport.xml,
./mealkitary-infrastructure/adapter-address-resolver/build/reports/jacoco/test/jacocoTestReport.xml
./mealkitary-infrastructure/adapter-address-resolver/simple-address-resolver/build/reports/jacoco/test/jacocoTestReport.xml,
./mealkitary-infrastructure/adapter-address-resolver/kakao-api-address-resolver/build/reports/jacoco/test/jacocoTestReport.xml
name: mealkitary-codecov
verbose: true

Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/mealkitary-test-coverage-automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ jobs:
./mealkitary-infrastructure/adapter-firebase-notification/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-business-registration-number-validator/simple-brn-validator/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-business-registration-number-validator/open-api-brn-validator/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-address-resolver/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-address-resolver/simple-address-resolver/build/test-results/**/*.xml
./mealkitary-infrastructure/adapter-address-resolver/kakao-api-address-resolver/build/test-results/**/*.xml
- name: Jacoco Coverage 리포트 전송
uses: codecov/codecov-action@v3
Expand All @@ -59,6 +60,7 @@ jobs:
./mealkitary-infrastructure/adapter-firebase-notification/build/reports/jacoco/test/jacocoTestReport.xml,
./mealkitary-infrastructure/adapter-business-registration-number-validator/simple-brn-validator/build/reports/jacoco/test/jacocoTestReport.xml,
./mealkitary-infrastructure/adapter-business-registration-number-validator/open-api-brn-validator/build/reports/jacoco/test/jacocoTestReport.xml,
./mealkitary-infrastructure/adapter-address-resolver/build/reports/jacoco/test/jacocoTestReport.xml
./mealkitary-infrastructure/adapter-address-resolver/simple-address-resolver/build/reports/jacoco/test/jacocoTestReport.xml,
./mealkitary-infrastructure/adapter-address-resolver/kakao-api-address-resolver/build/reports/jacoco/test/jacocoTestReport.xml
name: mealkitary-codecov
verbose: true
11 changes: 10 additions & 1 deletion mealkitary-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ dependencies {
implementation(project(":mealkitary-infrastructure:adapter-paymentgateway-tosspayments"))
implementation(project(":mealkitary-infrastructure:adapter-firebase-notification"))
implementation(project(":mealkitary-infrastructure:adapter-configuration"))
implementation(project(":mealkitary-infrastructure:adapter-address-resolver"))
implementation(
project(
":mealkitary-infrastructure:adapter-address-resolver:kakao-api-address-resolver",
)
)
implementation(
project(
":mealkitary-infrastructure:adapter-address-resolver:simple-address-resolver",
)
)
implementation(
project(
":mealkitary-infrastructure:adapter-business-registration-number-validator:open-api-brn-validator",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class RegisterShopServiceTest : AnnotationSpec() {
} throws IllegalArgumentException("올바른 가게 이름 형식이 아닙니다.(한글, 영문, 공백, 숫자만 포함 가능)")
every { saveShopPort.saveOne(any()) } answers { 1L }
every { shopBusinessNumberValidator.validate(any()) } answers {}
every { addressResolver.resolveAddress("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress
every { addressResolver.resolve("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress

shouldThrow<IllegalArgumentException> {
registerShopService.register(request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import com.mealkitary.shop.domain.shop.address.ShopAddress

interface AddressResolver {

fun resolveAddress(address: String): ShopAddress
fun resolve(fullAddress: String): ShopAddress
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ class ShopFactory(
private val addressResolver: AddressResolver
) {

fun createOne(title: String, brn: String, address: String): Shop {
fun createOne(title: String, brn: String, fullAddress: String): Shop {
val shopBusinessNumber = ShopBusinessNumber.from(brn)

val shopAddress: ShopAddress = addressResolver.resolveAddress(address)
val shopAddress: ShopAddress = addressResolver.resolve(fullAddress)

shopBusinessNumberValidator.validate(shopBusinessNumber)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ShopFactoryTest : AnnotationSpec() {
ShopAddress.of("1234567890", Coordinates.of(0.0, 0.0), Address.of("경기도", "안양시 동안구", "벌말로", "40"))

every { shopBusinessNumberValidator.validate(any()) } answers { }
every { addressResolver.resolveAddress("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress
every { addressResolver.resolve("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress

val shop = shopFactory.createOne("집밥뚝딱 안양점", "321-23-12345", "경기도 안양시 동안구 벌말로 40")

Expand All @@ -44,7 +44,7 @@ class ShopFactoryTest : AnnotationSpec() {
ShopAddress.of("1234567890", Coordinates.of(0.0, 0.0), Address.of("경기도", "안양시 동안구", "벌말로", "40"))

every { shopBusinessNumberValidator.validate(any()) } answers { }
every { addressResolver.resolveAddress("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress
every { addressResolver.resolve("경기도 안양시 동안구 벌말로 40") } returns expectedShopAddress

shouldThrow<IllegalArgumentException> {
shopFactory.createOne("집밥뚝딱 ! 안양점", "321-23-12345", "경기도 안양시 동안구 벌말로 40")
Expand All @@ -69,7 +69,7 @@ class ShopFactoryTest : AnnotationSpec() {
)

every { shopBusinessNumberValidator.validate(any()) } answers { }
every { addressResolver.resolveAddress(address) } answers { shopAddress }
every { addressResolver.resolve(address) } answers { shopAddress }

shouldThrow<IllegalArgumentException> {
shopFactory.createOne("집밥뚝딱 ! 안양점", "321-23-12345", "경기도 안양시 동안구 벌말로 40")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dependencies {
val mockWebServerVersion: String by properties
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation(project(":mealkitary-domain"))
testImplementation("com.squareup.okhttp3:mockwebserver:$mockWebServerVersion")
testImplementation("io.projectreactor:reactor-test")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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.context.annotation.Primary
import org.springframework.stereotype.Component

@Primary
@Component
class KakaoApiAddressResolver(
private val kakaoApiWebClient: KakaoApiWebClient
) : AddressResolver {

override fun resolve(fullAddress: String): ShopAddress {
val kakaoApiAddressResponse = kakaoApiWebClient.requestAddress(fullAddress)

val (x, y, address, roadAddress) = kakaoApiAddressResponse.document

val (longitude, latitude) = listOf(x, y).map {
it.toDoubleOrNull() ?: throw IllegalArgumentException("유효하지 않은 좌표 범위입니다.")
}

return ShopAddress.of(
roadAddress.h_code,
Coordinates.of(
longitude,
latitude
),
Address.of(
address.region_1depth_name,
address.region_2depth_name,
address.region_3depth_name,
roadAddress.road_name
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.mealkitary.address

import com.mealkitary.address.payload.KakaoApiAddressResponse
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient

private const val KAKAO_API_BASE_URL = "/v2/local/search/address"
private const val FORMAT = "json"

@Component
class KakaoApiWebClient(
private val webClient: WebClient,
@Value("\${kakaoapi.address.serviceKey}")
private val serviceKey: String,
) {

fun requestAddress(query: String): KakaoApiAddressResponse {
val kakaoApiAddressResponse = webClient.get()
.uri { uriBuilder ->
uriBuilder.path("$KAKAO_API_BASE_URL.$FORMAT")
.queryParam("query", query)
.build()
}
.header("Authorization", "KakaoAK $serviceKey")
.retrieve()
.bodyToMono(KakaoApiAddressResponse::class.java)
.block()

return kakaoApiAddressResponse!!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mealkitary.address.payload

data class KakaoApiAddressResponse(
val document: Document
) {
data class Document(
val x: String,
val y: String,
val address: Address,
val road_address: RoadAddress
)

data class Address(
val region_1depth_name: String,
val region_2depth_name: String,
val region_3depth_name: String,
)

data class RoadAddress(
val road_name: String,
val h_code: String
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.mealkitary.addess

import com.mealkitary.address.KakaoApiAddressResolver
import com.mealkitary.address.KakaoApiWebClient
import com.mealkitary.address.payload.KakaoApiAddressResponse
import io.kotest.core.spec.style.AnnotationSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk

class KakaoApiAddressResolverTest : AnnotationSpec() {

private val kakaoApiWebClient = mockk<KakaoApiWebClient>()
private val kakaoApiAddressResolver = KakaoApiAddressResolver(kakaoApiWebClient)

@Test
fun `Kakao API를 통해 해당하는 주소 정보를 받아온다`() {
val address = "경기도 남양주시 다산중앙로82번안길 132-12"
val response = KakaoApiAddressResponse(
document = KakaoApiAddressResponse.Document(
x = "127.166069448936",
y = "37.6120947950094",
address = KakaoApiAddressResponse.Address(
region_1depth_name = "경기",
region_2depth_name = "남양주시",
region_3depth_name = "다산동"
),
road_address = KakaoApiAddressResponse.RoadAddress(
road_name = "다산중앙로82번안길",
h_code = "4136011200"
)
)
)

every { kakaoApiWebClient.requestAddress(address) } returns response

val shopAddress = kakaoApiAddressResolver.resolve(address)

shopAddress.address.region1DepthName shouldBe "경기"
shopAddress.address.region2DepthName shouldBe "남양주시"
shopAddress.address.region3DepthName shouldBe "다산동"
shopAddress.address.roadName shouldBe "다산중앙로82번안길"
shopAddress.cityCode shouldBe "4136011200"
shopAddress.coordinates.longitude shouldBe 127.166069448936
shopAddress.coordinates.latitude shouldBe 37.6120947950094
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.mealkitary.addess

import com.fasterxml.jackson.databind.ObjectMapper
import com.mealkitary.address.KakaoApiWebClient
import com.mealkitary.address.payload.KakaoApiAddressResponse
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.inspectors.forAll
import io.kotest.matchers.shouldBe
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.client.WebClient
import java.lang.RuntimeException

class KakaoApiWebClientTest {

private lateinit var mockWebServer: MockWebServer
private lateinit var webClient: WebClient
private lateinit var kakaoApiWebClient: KakaoApiWebClient
private val objectMapper = ObjectMapper()

@BeforeEach
fun setUp() {
mockWebServer = MockWebServer()
mockWebServer.start()
webClient = WebClient.builder()
.baseUrl(mockWebServer.url("").toString())
.codecs { configurer ->
configurer.defaultCodecs().maxInMemorySize(5 * 1024 * 1024)
}
.defaultHeaders { headers ->
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
}
.build()

kakaoApiWebClient = KakaoApiWebClient(webClient, "serviceKey")
}

@AfterEach
fun teardown() {
mockWebServer.shutdown()
}

@Test
fun `200 OK를 받으면 아무 예외도 발생하지 않는다`() {
val address = "경기도 남양주시 다산중앙로82번안길 132-12"
val response = createResponse()

mockWebServer.enqueue(
MockResponse()
.setBody(objectMapper.writeValueAsString(response))
.setResponseCode(200)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
)

val actualResponse = kakaoApiWebClient.requestAddress(address)

val recordedRequest = mockWebServer.takeRequest()
recordedRequest.method shouldBe "GET"

actualResponse.document.road_address.h_code shouldBe response.document.road_address.h_code
actualResponse.document.x shouldBe response.document.x
actualResponse.document.y shouldBe response.document.y
actualResponse.document.address.region_1depth_name shouldBe response.document.address.region_1depth_name
actualResponse.document.address.region_2depth_name shouldBe response.document.address.region_2depth_name
actualResponse.document.address.region_3depth_name shouldBe response.document.address.region_3depth_name
actualResponse.document.road_address.road_name shouldBe response.document.road_address.road_name
}

@Test
fun `200이 아닌 코드는 RuntimeException으로 처리한다`() {
listOf(400, 401, 500).forAll {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(it)
.setBody(objectMapper.writeValueAsString(createResponse()))
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
)
shouldThrow<RuntimeException> {
kakaoApiWebClient.requestAddress("경기도남양주시다산중앙로82번안길132-12")
}
}
}

private fun createResponse() = KakaoApiAddressResponse(
document = KakaoApiAddressResponse.Document(
x = "127.166069448936",
y = "37.6120947950094",
address = KakaoApiAddressResponse.Address(
region_1depth_name = "경기",
region_2depth_name = "남양주시",
region_3depth_name = "다산동"
),
road_address = KakaoApiAddressResponse.RoadAddress(
road_name = "다산중앙로82번안길",
h_code = "4136011200"
)
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation(project(":mealkitary-domain"))
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mealkitary.address
package com.mealkitary

import com.mealkitary.common.model.Address
import com.mealkitary.common.model.Coordinates
Expand All @@ -11,8 +11,8 @@ private const val ADDRESS_MIN_LENGTH = 2
@Component
class SimpleAddressResolver : AddressResolver {

override fun resolveAddress(address: String): ShopAddress {
val value = address.split(" ")
override fun resolve(fullAddress: String): ShopAddress {
val value = fullAddress.split(" ")

if (value.size < ADDRESS_MIN_LENGTH) {
throw IllegalArgumentException("주소 형식이 올바르지 않습니다.")
Expand Down
Loading

0 comments on commit 194220c

Please sign in to comment.