Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[기능 구현] 카카오 주소 조회 api 연동 (issue#84) #86

Merged
merged 14 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
le2sky marked this conversation as resolved.
Show resolved Hide resolved
}
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