[#5]: 공통 기능 코틀린 마이그레이션 및 패키지 분리 #6

Merged
pricelees merged 20 commits from refactor/#5 into main 2025-07-14 05:05:48 +00:00
3 changed files with 204 additions and 0 deletions
Showing only changes of commit 808c6675c1 - Show all commits

View File

@ -0,0 +1,33 @@
package roomescape.common.dto.response
import com.fasterxml.jackson.annotation.JsonInclude
import roomescape.common.exception.ErrorType
@JsonInclude(JsonInclude.Include.NON_NULL)
data class RoomescapeApiResponseKT<T>(
val success: Boolean,
val data: T? = null,
val errorType: ErrorType? = null,
val message: String? = null,
) {
companion object {
@JvmStatic
fun <T> success(data: T? = null): RoomescapeApiResponseKT<T> {
return RoomescapeApiResponseKT(
success = true,
data = data,
)
}
@JvmStatic
fun <T> fail(errorType: ErrorType, message: String? = null): RoomescapeApiResponseKT<T> {
return RoomescapeApiResponseKT(
success = false,
errorType = errorType,
message = message ?: errorType.description
)
}
}
}

View File

@ -1,5 +1,10 @@
package roomescape.common.exception; package roomescape.common.exception;
import java.util.Arrays;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public enum ErrorType { public enum ErrorType {
// 400 Bad Request // 400 Bad Request
@ -58,4 +63,12 @@ public enum ErrorType {
public String getDescription() { public String getDescription() {
return description; return description;
} }
@JsonCreator
public static ErrorType from(@JsonProperty("errorType") String errorType) {
return Arrays.stream(ErrorType.values())
.filter(type -> type.name().equalsIgnoreCase(errorType))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid error type: " + errorType));
}
} }

View File

@ -0,0 +1,158 @@
package roomescape.common.dto.response
import com.fasterxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockkBean
import com.ninjasquad.springmockk.SpykBean
import io.kotest.core.spec.style.BehaviorSpec
import org.hamcrest.CoreMatchers.equalTo
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.web.bind.annotation.*
import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.auth.web.support.AdminInterceptor
import roomescape.auth.web.support.LoginInterceptor
import roomescape.auth.web.support.MemberIdResolver
import roomescape.common.exception.ErrorType
import roomescape.member.business.MemberService
import roomescape.member.infrastructure.persistence.MemberRepository
@WebMvcTest(ApiResponseTestController::class)
class RoomescapeApiResponseKTTest(
@Autowired private val mockMvc: MockMvc
) : BehaviorSpec() {
@Autowired
private lateinit var AdminInterceptor: AdminInterceptor
@Autowired
private lateinit var loginInterceptor: LoginInterceptor
@Autowired
private lateinit var memberIdResolver: MemberIdResolver
@SpykBean
private lateinit var memberService: MemberService
@MockkBean
private lateinit var memberRepository: MemberRepository
@MockkBean
private lateinit var jwtHandler: JwtHandler
init {
Given("성공 응답에") {
val endpoint = "/success"
When("객체 데이터를 담으면") {
val id: Long = 1L
val name = "name"
Then("success=true, data={객체} 형태로 응답한다.") {
mockMvc.post("$endpoint/$id/$name") {
contentType = MediaType.APPLICATION_JSON
}.andDo {
print()
}.andExpect {
status { isOk() }
jsonPath("$.success", equalTo(true))
jsonPath("$.data.id", equalTo(id.toInt()))
jsonPath("$.data.name", equalTo(name))
}
}
}
When("문자열 데이터를 담으면") {
val message: String = "Hello, World!"
Then("success=true, data={문자열} 형태로 응답한다.") {
mockMvc.get("/success/$message") {
contentType = MediaType.APPLICATION_JSON
}.andDo {
print()
}.andExpect {
status { isOk() }
jsonPath("$.success", equalTo(true))
jsonPath("$.data", equalTo(message))
}
}
}
}
Given("실패 응답에") {
val endpoint = "/fail"
val objectMapper = ObjectMapper()
When("errorType만 담으면") {
Then("success=false, errorType={errorType}, message={errorType.description} 형태로 응답한다.") {
mockMvc.post(endpoint) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(FailRequest(errorType = ErrorType.INTERNAL_SERVER_ERROR))
}.andDo {
print()
}.andExpect {
status { isOk() }
jsonPath("$.success", equalTo(false))
jsonPath("$.errorType", equalTo(ErrorType.INTERNAL_SERVER_ERROR.name))
jsonPath("$.message", equalTo(ErrorType.INTERNAL_SERVER_ERROR.description))
}
}
}
When("errorType과 message를 담으면") {
val message: String = "An error occurred"
Then("success=false, errorType={errorType}, message={message} 형태로 응답한다.") {
mockMvc.post(endpoint) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(FailRequest(errorType = ErrorType.INTERNAL_SERVER_ERROR, message = message))
}.andDo {
print()
}.andExpect {
status { isOk() }
jsonPath("$.success", equalTo(false))
jsonPath("$.errorType", equalTo(ErrorType.INTERNAL_SERVER_ERROR.name))
jsonPath("$.message", equalTo(message))
}
}
}
}
}
}
data class SuccessResponse(
val id: Long,
val name: String
)
data class FailRequest(
val errorType: ErrorType,
val message: String? = null
)
@RestController
class ApiResponseTestController {
@GetMapping("/success/{message}")
fun succeedToGet(
@PathVariable message: String,
): RoomescapeApiResponseKT<String> =
RoomescapeApiResponseKT.success(message)
@PostMapping("/success/{id}/{name}")
fun succeedToPost(
@PathVariable id: Long,
@PathVariable name: String,
): RoomescapeApiResponseKT<SuccessResponse> =
RoomescapeApiResponseKT.success(SuccessResponse(id, name))
@PostMapping("/fail")
fun fail(
@RequestBody request: FailRequest
): RoomescapeApiResponseKT<Unit> =
request.message?.let {
RoomescapeApiResponseKT.fail(request.errorType, it)
} ?: RoomescapeApiResponseKT.fail(request.errorType)
}