Skip to content

Commit

Permalink
Merge pull request #77 from le2sky/feat/71
Browse files Browse the repository at this point in the history
[기능 구현] 신규 가게 등록 기능 구현(issue#71)
  • Loading branch information
le2sky authored Sep 27, 2023
2 parents 2e182bb + a321979 commit 0cad251
Show file tree
Hide file tree
Showing 40 changed files with 694 additions and 54 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/mealkitary-main-develop-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
./mealkitary-infrastructure/adapter-persistence-spring-data-jpa/build/test-results/**/*.xml
./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
- name: Jacoco Coverage 리포트 전송
uses: codecov/codecov-action@v3
Expand All @@ -65,7 +66,8 @@ jobs:
./mealkitary-application/build/reports/jacoco/test/jacocoTestReport.xml,
./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/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
name: mealkitary-codecov
verbose: true

Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/mealkitary-test-coverage-automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
./mealkitary-infrastructure/adapter-persistence-spring-data-jpa/build/test-results/**/*.xml
./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
- name: Jacoco Coverage 리포트 전송
uses: codecov/codecov-action@v3
Expand All @@ -53,6 +54,7 @@ jobs:
./mealkitary-application/build/reports/jacoco/test/jacocoTestReport.xml,
./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/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
name: mealkitary-codecov
verbose: true
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ktlintVersion=11.0.0
springBootVersion=2.7.11
springDependencyManagementVersion=1.0.15.RELEASE
# project
applicationVersion=0.4.0
applicationVersion=0.5.0
projectGroup=com.mealkitary
# test
kotestVersion=4.4.3
Expand Down
10 changes: 10 additions & 0 deletions mealkitary-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ 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:business-registration-number-validator:adapter-open-api-brn-validator",
)
)
implementation(
project(
":mealkitary-infrastructure:business-registration-number-validator:adapter-simple-brn-validator",
)
)
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor")
}
Expand Down
38 changes: 38 additions & 0 deletions mealkitary-api/src/docs/asciidoc/shop.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

가게의 상태를 변경하거나, 가게의 속한 요소(상품, 예약 가능 시간)들을 조회할 수 있습니다.

==== 신규 가게 등록

신규 가게를 등록합니다.

[NOTE]
====
신규 리소스 생성 시, 반환 헤더의 Location 값은 신규 리소스의 URI입니다.
====

===== 요청

include::{snippets}/shop-post/curl-request.adoc[]
include::{snippets}/shop-post/http-request.adoc[]
include::{snippets}/shop-post/request-fields.adoc[]

===== 응답

include::{snippets}/shop-post/http-response.adoc[]

===== 응답 헤더

include::{snippets}/shop-post/response-headers.adoc[]

==== 가게 조회

가게 목록을 조회합니다.
Expand Down Expand Up @@ -61,3 +84,18 @@ include::{snippets}/shop-get-reservable-time/path-parameters.adoc[]

include::{snippets}/shop-get-reservable-time/http-response.adoc[]
include::{snippets}/shop-get-reservable-time/response-fields.adoc[]


==== 가게 상태 변경

가게의 상태(VALID, INVALID)를 변경합니다. 이미 가게의 상태가 VALID인 경우에는 INVALID로, INVALID인 경우에는 VALID로 변경됩니다.

===== 요청

include::{snippets}/shop-post-update/curl-request.adoc[]
include::{snippets}/shop-post-update/http-request.adoc[]
include::{snippets}/shop-post-update/path-parameters.adoc[]

===== 응답

include::{snippets}/shop-post-update/http-response.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ class HttpResponseUtils {

companion object {

fun createResourceUri(resourceId: UUID): URI {
fun createResourceUri(resourceId: UUID): URI = URI.create("${createBaseUri()}/$resourceId")

fun createResourceUri(resourceId: Long): URI = URI.create("${createBaseUri()}/$resourceId")

private fun createBaseUri(): String {
val uriComponents = ServletUriComponentsBuilder.fromCurrentRequest().build()
val scheme = removeSlash(uriComponents.scheme)
val host = removeSlash(uriComponents.host)
val path = removeSlash(uriComponents.path)

return URI.create("$scheme://$host/$path/$resourceId")
return "$scheme://$host/$path"
}

fun createResourceUri(path: String, resourceId: UUID): URI {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.mealkitary.shop.web

import com.mealkitary.common.utils.HttpResponseUtils
import com.mealkitary.shop.application.port.input.RegisterShopUseCase
import com.mealkitary.shop.web.request.RegisterShopWebRequest
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import javax.validation.Valid

@RestController
@RequestMapping("/shops")
class RegisterShopController(
private val registerShopUseCase: RegisterShopUseCase
) {

@PostMapping
fun registerShop(@Valid @RequestBody registerShopWebRequest: RegisterShopWebRequest): ResponseEntity<Unit> {
val resourceId = registerShopUseCase.register(registerShopWebRequest.mapToServiceRequest())
val location = HttpResponseUtils.createResourceUri(resourceId)

return ResponseEntity.created(location).build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.mealkitary.shop.web.request

import com.mealkitary.shop.application.port.input.RegisterShopRequest
import javax.validation.constraints.NotBlank

data class RegisterShopWebRequest(
@field:NotBlank(message = "등록 대상 가게의 이름은 필수입니다.")
val title: String? = null,

@field:NotBlank(message = "사업자 번호는 필수입니다.")
val brn: String? = null
) {

fun mapToServiceRequest() = RegisterShopRequest(title!!, brn!!)
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class GetReservationControllerDocsTest : RestDocsSupport() {
}

@Test
fun `api integration test - getAllReservation`() {
fun `api docs test - getAllReservation`() {
val reservationId = UUID.randomUUID()
val reserveAt = LocalDateTime.of(
LocalDate.of(2023, 6, 23), LocalTime.of(6, 30)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.docs.shop

import com.docs.RestDocsSupport
import com.mealkitary.shop.application.port.input.RegisterShopUseCase
import com.mealkitary.shop.web.RegisterShopController
import com.mealkitary.shop.web.request.RegisterShopWebRequest
import io.mockk.every
import io.mockk.mockk
import org.springframework.http.MediaType
import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName
import org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders
import org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest
import org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse
import org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint
import org.springframework.restdocs.payload.JsonFieldType
import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath
import org.springframework.restdocs.payload.PayloadDocumentation.requestFields
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

class RegisterShopControllerDocsTest : RestDocsSupport() {

private val registerShopUseCase = mockk<RegisterShopUseCase>()

@Test
fun `api docs test - registerShop`() {
every { registerShopUseCase.register(any()) } answers { 1L }

val registerShopWebRequest = RegisterShopWebRequest("집밥뚝딱 안양점", "123-23-12345")

mvc.perform(
RestDocumentationRequestBuilders.post("/shops")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerShopWebRequest))
)
.andExpect(status().isCreated)
.andExpect(header().string("Location", "http://localhost/shops/1"))
.andDo(
MockMvcRestDocumentation.document(
"shop-post",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("title").type(JsonFieldType.STRING).description("등록 대상 가게 이름"),
fieldWithPath("brn").type(JsonFieldType.STRING).description("사업자 번호"),
),
responseHeaders(headerWithName("Location").description("생성된 가게 리소스 URI")),
)
)
}

override fun initController() = RegisterShopController(registerShopUseCase)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import com.mealkitary.reservation.web.ReserveProductController
import com.mealkitary.shop.application.port.input.GetProductQuery
import com.mealkitary.shop.application.port.input.GetReservableTimeQuery
import com.mealkitary.shop.application.port.input.GetShopQuery
import com.mealkitary.shop.application.port.input.RegisterShopUseCase
import com.mealkitary.shop.application.port.input.UpdateShopStatusUseCase
import com.mealkitary.shop.web.GetProductController
import com.mealkitary.shop.web.GetReservableTimeController
import com.mealkitary.shop.web.GetShopController
import com.mealkitary.shop.web.RegisterShopController
import com.mealkitary.shop.web.UpdateShopStatusController
import com.ninjasquad.springmockk.MockkBean
import io.kotest.core.spec.style.AnnotationSpec
Expand All @@ -32,6 +34,7 @@ import org.springframework.test.web.servlet.MockMvc
PayReservationController::class,
AcceptReservationController::class,
RejectReservationController::class,
RegisterShopController::class,
GetReservationController::class,
GetShopController::class,
GetReservableTimeController::class,
Expand Down Expand Up @@ -61,6 +64,9 @@ abstract class WebIntegrationTestSupport : AnnotationSpec() {
@MockkBean
protected lateinit var rejectReservationUseCase: RejectReservationUseCase

@MockkBean
protected lateinit var registerShopUseCase: RegisterShopUseCase

@MockkBean
protected lateinit var getReservationQuery: GetReservationQuery

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.mealkitary.shop.web

import com.mealkitary.WebIntegrationTestSupport
import com.mealkitary.shop.web.request.RegisterShopWebRequest
import io.mockk.every
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

class RegisterShopControllerTest : WebIntegrationTestSupport() {

@Test
fun `api integration test - registerShop`() {
every { registerShopUseCase.register(any()) } answers { 1L }

val registerShopWebRequest = RegisterShopWebRequest("집밥뚝딱 안양점", "123-23-12345")

mvc.perform(
MockMvcRequestBuilders.post("/shops")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerShopWebRequest))
)
.andExpect(status().isCreated)
.andExpect(header().string("Location", "http://localhost/shops/1"))
}

@Test
fun `api integration test - 가게 이름이 누락된 경우 400 에러를 발생한다`() {
val registerShopWebRequest = RegisterShopWebRequest(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("title"))
.andExpect(jsonPath("$..errors[0].reason").value("등록 대상 가게의 이름은 필수입니다."))
}

@Test
fun `api integration test - 사업자 번호가 누락된 경우 400 에러를 발생한다`() {
val registerShopWebRequest = RegisterShopWebRequest(title = "집밥뚝딱 안양점")

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("brn"))
.andExpect(jsonPath("$..errors[0].reason").value("사업자 번호는 필수입니다."))
}

@Test
fun `api integration test - JSON 형식이 아닌 경우 400 에러가 발생한다`() {
mvc.perform(
MockMvcRequestBuilders.post("/shops")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString("}{"))
)
.andExpect(status().isBadRequest)
.andExpect(jsonPath("$.status").value("400"))
.andExpect(jsonPath("$.message").value("JSON 형식이 잘못되었습니다."))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mealkitary.shop.application.port.input

data class RegisterShopRequest(
val title: String,
val brn: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mealkitary.shop.application.port.input

interface RegisterShopUseCase {

fun register(registerShopRequest: RegisterShopRequest): Long
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mealkitary.shop.application.port.output

import com.mealkitary.shop.domain.shop.Shop

interface SaveShopPort {

fun saveOne(shop: Shop): Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ class GetShopService(
private val loadShopPort: LoadShopPort
) : GetShopQuery {

override fun loadAllShop() = loadShopPort.loadAllShop().map { ShopResponse(it.id, it.title) }
override fun loadAllShop() = loadShopPort.loadAllShop().map { ShopResponse(it.id, it.title.value) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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)
class RegisterShopService(
private val saveShopPort: SaveShopPort,
shopBusinessNumberValidator: ShopBusinessNumberValidator
) : RegisterShopUseCase {

private val shopFactory = ShopFactory(shopBusinessNumberValidator)

@Transactional
override fun register(registerShopRequest: RegisterShopRequest): Long {
val shop = shopFactory.createOne(registerShopRequest.title, registerShopRequest.brn)

return saveShopPort.saveOne(shop)
}
}
Loading

0 comments on commit 0cad251

Please sign in to comment.