generated from pricelees/issue-pr-template
feat: 지역 코드 조회 API 및 테스트 추가
This commit is contained in:
parent
5658f6c31f
commit
7db389ae49
75
src/main/kotlin/roomescape/region/business/RegionService.kt
Normal file
75
src/main/kotlin/roomescape/region/business/RegionService.kt
Normal file
@ -0,0 +1,75 @@
|
||||
package roomescape.region.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.region.exception.RegionErrorCode
|
||||
import roomescape.region.exception.RegionException
|
||||
import roomescape.region.infrastructure.persistence.RegionRepository
|
||||
import roomescape.region.infrastructure.web.*
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class RegionService(
|
||||
private val regionRepository: RegionRepository
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun readAllSido(): SidoListResponse {
|
||||
log.info { "[RegionService.readAllSido] 모든 시/도 조회 시작" }
|
||||
val result: List<Pair<String, String>> = regionRepository.readAllSido()
|
||||
|
||||
if (result.isEmpty()) {
|
||||
log.warn { "[RegionService.readAllSido] 시/도 조회 실패" }
|
||||
throw RegionException(RegionErrorCode.SIDO_CODE_NOT_FOUND)
|
||||
}
|
||||
|
||||
return SidoListResponse(result.map { SidoResponse(code = it.first, name = it.second) }).also {
|
||||
log.info { "[RegionService.readAllSido] ${it.sidoList.size}개의 시/도 조회 완료" }
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findSigunguBySido(sidoCode: String): SigunguListResponse {
|
||||
log.info { "[RegionService.findSigunguBySido] 시/군/구 조회 시작: sidoCode=${sidoCode}" }
|
||||
val result: List<Pair<String, String>> = regionRepository.findAllSigunguBySido(sidoCode)
|
||||
|
||||
if (result.isEmpty()) {
|
||||
log.warn { "[RegionService.findSigunguBySido] 시/군/구 조회 실패: sidoCode=${sidoCode}" }
|
||||
throw RegionException(RegionErrorCode.SIGUNGU_CODE_NOT_FOUND)
|
||||
}
|
||||
|
||||
return SigunguListResponse(result.map { SigunguResponse(code = it.first, name = it.second) }).also {
|
||||
log.info { "[RegionService.findSigunguBySido] sidoCode=${sidoCode}인 ${it.sigunguList.size}개의 시/군/구 조회 완료" }
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findDongBySidoAndSigungu(sidoCode: String, sigunguCode: String): DongListResponse {
|
||||
log.info { "[RegionService.findDongBySidoAndSigungu] 행정동 조회 시작: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" }
|
||||
val result: List<Pair<String, String>> = regionRepository.findAllDongBySidoAndSigungu(sidoCode, sigunguCode)
|
||||
|
||||
if (result.isEmpty()) {
|
||||
log.warn { "[RegionService.findDongBySidoAndSigungu] 행정동 조회 실패: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" }
|
||||
throw RegionException(RegionErrorCode.DONG_CODE_NOT_FOUND)
|
||||
}
|
||||
|
||||
return DongListResponse(result.map { DongResponse(code = it.first, name = it.second) }).also {
|
||||
log.info { "[RegionService.findDongBySidoAndSigungu] sidoCode=${sidoCode}, sigunguCode=${sigunguCode}인 ${it.dongList.size}개의 행정동 조회 완료" }
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findRegionCode(sidoCode: String, sigunguCode: String, dongCode: String): RegionCodeResponse {
|
||||
log.info { "[RegionService.findRegionCode] 지역 코드 조회 시작: sidoCode=${sidoCode} / sigunguCode=${sigunguCode} / dongCode=${dongCode}" }
|
||||
|
||||
return regionRepository.findRegionCode(sidoCode, sigunguCode, dongCode)?.let {
|
||||
log.info { "[RegionService.findRegionCode] 지역 코드 조회 완료: code=${it} sidoCode=${sidoCode} / sigunguCode=${sigunguCode} / dongCode=${dongCode}" }
|
||||
RegionCodeResponse(it)
|
||||
} ?: run {
|
||||
log.warn { "[RegionService.findRegionCode] 지역 코드 조회 실패: sidoCode=${sidoCode} / sigunguCode=${sigunguCode} / dongCode=${dongCode}" }
|
||||
throw RegionException(RegionErrorCode.REGION_CODE_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/main/kotlin/roomescape/region/docs/RegionAPI.kt
Normal file
45
src/main/kotlin/roomescape/region/docs/RegionAPI.kt
Normal file
@ -0,0 +1,45 @@
|
||||
package roomescape.region.docs
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import roomescape.auth.web.support.Public
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.region.infrastructure.web.DongListResponse
|
||||
import roomescape.region.infrastructure.web.RegionCodeResponse
|
||||
import roomescape.region.infrastructure.web.SidoListResponse
|
||||
import roomescape.region.infrastructure.web.SigunguListResponse
|
||||
|
||||
interface RegionAPI {
|
||||
|
||||
@Public
|
||||
@Operation(summary = "지역 코드 조회")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun findRegionCode(
|
||||
@RequestParam(name = "sidoCode", required = true) sidoCode: String,
|
||||
@RequestParam(name = "sigunguCode", required = true) sigunguCode: String,
|
||||
@RequestParam(name = "dongCode", required = true) dongCode: String,
|
||||
): ResponseEntity<CommonApiResponse<RegionCodeResponse>>
|
||||
|
||||
@Public
|
||||
@Operation(summary = "모든 시 / 도 목록 조회")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun readAllSido(): ResponseEntity<CommonApiResponse<SidoListResponse>>
|
||||
|
||||
@Public
|
||||
@Operation(summary = "모든 시 / 군 / 구 목록 조회")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun findAllSigunguBySido(
|
||||
@RequestParam(required = true) sidoCode: String
|
||||
): ResponseEntity<CommonApiResponse<SigunguListResponse>>
|
||||
|
||||
@Public
|
||||
@Operation(summary = "모든 행정동 목록 조회")
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun findAllDongBySigungu(
|
||||
@RequestParam(name = "sidoCode", required = true) sidoCode: String,
|
||||
@RequestParam(name = "sigunguCode", required = true) sigunguCode: String
|
||||
): ResponseEntity<CommonApiResponse<DongListResponse>>
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package roomescape.region.exception
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorCode
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
|
||||
class RegionException(
|
||||
override val errorCode: RegionErrorCode,
|
||||
override val message: String = errorCode.message
|
||||
) : RoomescapeException(errorCode, message)
|
||||
|
||||
enum class RegionErrorCode(
|
||||
override val httpStatus: HttpStatus,
|
||||
override val errorCode: String,
|
||||
override val message: String
|
||||
) : ErrorCode {
|
||||
REGION_CODE_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "지역 코드를 찾을 수 없어요."),
|
||||
SIDO_CODE_NOT_FOUND(HttpStatus.NOT_FOUND, "R002", "시/도 를 찾을 수 없어요."),
|
||||
SIGUNGU_CODE_NOT_FOUND(HttpStatus.NOT_FOUND, "R003", "시/군/구 를 찾을 수 없어요."),
|
||||
DONG_CODE_NOT_FOUND(HttpStatus.NOT_FOUND, "R004", "행정동을 찾을 수 없어요."),
|
||||
}
|
||||
@ -1,5 +1,68 @@
|
||||
package roomescape.region.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
|
||||
interface RegionRepository : JpaRepository<RegionEntity, String>
|
||||
interface RegionRepository : JpaRepository<RegionEntity, String> {
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
new kotlin.Pair(r.sidoCode, r.sidoName)
|
||||
FROM
|
||||
RegionEntity r
|
||||
GROUP BY
|
||||
r.sidoCode
|
||||
ORDER BY
|
||||
r.sidoName
|
||||
""")
|
||||
fun readAllSido(): List<Pair<String, String>>
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
new kotlin.Pair(r.sigunguCode, r.sigunguName)
|
||||
FROM
|
||||
RegionEntity r
|
||||
WHERE
|
||||
r.sidoCode = :sidoCode
|
||||
GROUP BY
|
||||
r.sigunguCode
|
||||
ORDER BY
|
||||
r.sigunguName
|
||||
""")
|
||||
fun findAllSigunguBySido(
|
||||
@Param("sidoCode") sidoCode: String
|
||||
): List<Pair<String, String>>
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
new kotlin.Pair(r.dongCode, r.dongName)
|
||||
FROM
|
||||
RegionEntity r
|
||||
WHERE
|
||||
r.sidoCode = :sidoCode
|
||||
AND r.sigunguCode = :sigunguCode
|
||||
ORDER BY
|
||||
r.dongName
|
||||
""")
|
||||
fun findAllDongBySidoAndSigungu(
|
||||
@Param("sidoCode") sidoCode: String,
|
||||
@Param("sigunguCode") sigunguCode: String
|
||||
): List<Pair<String, String>>
|
||||
|
||||
@Query("""
|
||||
SELECT
|
||||
r.code
|
||||
FROM
|
||||
RegionEntity r
|
||||
WHERE
|
||||
r.sidoCode = :sidoCode
|
||||
AND r.sigunguCode = :sigunguCode
|
||||
AND r.dongCode = :dongCode
|
||||
""")
|
||||
fun findRegionCode(
|
||||
@Param("sidoCode") sidoCode: String,
|
||||
@Param("sigunguCode") sigunguCode: String,
|
||||
@Param("dongCode") dongCode: String,
|
||||
): String?
|
||||
}
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
package roomescape.region.infrastructure.web
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.region.business.RegionService
|
||||
import roomescape.region.docs.RegionAPI
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/regions")
|
||||
class RegionController(
|
||||
private val regionService: RegionService
|
||||
) : RegionAPI {
|
||||
@GetMapping("/code")
|
||||
override fun findRegionCode(
|
||||
@RequestParam(name = "sidoCode", required = true) sidoCode: String,
|
||||
@RequestParam(name = "sigunguCode", required = true) sigunguCode: String,
|
||||
@RequestParam(name = "dongCode", required = true) dongCode: String,
|
||||
): ResponseEntity<CommonApiResponse<RegionCodeResponse>> {
|
||||
val response = regionService.findRegionCode(sidoCode, sigunguCode, dongCode)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/sido")
|
||||
override fun readAllSido(): ResponseEntity<CommonApiResponse<SidoListResponse>> {
|
||||
val response = regionService.readAllSido()
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/sigungu")
|
||||
override fun findAllSigunguBySido(
|
||||
@RequestParam(required = true) sidoCode: String
|
||||
): ResponseEntity<CommonApiResponse<SigunguListResponse>> {
|
||||
val response = regionService.findSigunguBySido(sidoCode)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/dong")
|
||||
override fun findAllDongBySigungu(
|
||||
@RequestParam(name = "sidoCode", required = true) sidoCode: String,
|
||||
@RequestParam(name = "sigunguCode", required = true) sigunguCode: String
|
||||
): ResponseEntity<CommonApiResponse<DongListResponse>> {
|
||||
val response = regionService.findDongBySidoAndSigungu(sidoCode, sigunguCode)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package roomescape.region.infrastructure.web
|
||||
|
||||
data class SidoResponse(
|
||||
val code: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
data class SidoListResponse(
|
||||
val sidoList: List<SidoResponse>
|
||||
)
|
||||
|
||||
data class SigunguResponse(
|
||||
val code: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
data class SigunguListResponse(
|
||||
val sigunguList: List<SigunguResponse>
|
||||
)
|
||||
|
||||
data class DongResponse(
|
||||
val code: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
data class DongListResponse(
|
||||
val dongList: List<DongResponse>
|
||||
)
|
||||
|
||||
data class RegionCodeResponse(
|
||||
val code: String
|
||||
)
|
||||
65
src/test/kotlin/roomescape/region/RegionApiFailTest.kt
Normal file
65
src/test/kotlin/roomescape/region/RegionApiFailTest.kt
Normal file
@ -0,0 +1,65 @@
|
||||
package roomescape.region
|
||||
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import io.mockk.every
|
||||
import org.springframework.http.HttpMethod
|
||||
import roomescape.region.exception.RegionErrorCode
|
||||
import roomescape.region.infrastructure.persistence.RegionRepository
|
||||
import roomescape.supports.FunSpecSpringbootTest
|
||||
import roomescape.supports.runExceptionTest
|
||||
|
||||
class RegionApiFailTest(
|
||||
@MockkBean private val regionRepository: RegionRepository
|
||||
): FunSpecSpringbootTest() {
|
||||
init {
|
||||
context("조회 실패") {
|
||||
test("시/도") {
|
||||
every {
|
||||
regionRepository.readAllSido()
|
||||
} returns emptyList()
|
||||
|
||||
runExceptionTest(
|
||||
method = HttpMethod.GET,
|
||||
endpoint = "/regions/sido",
|
||||
expectedErrorCode = RegionErrorCode.SIDO_CODE_NOT_FOUND,
|
||||
)
|
||||
}
|
||||
|
||||
test("시/군/구") {
|
||||
every {
|
||||
regionRepository.findAllSigunguBySido(any())
|
||||
} returns emptyList()
|
||||
|
||||
runExceptionTest(
|
||||
method = HttpMethod.GET,
|
||||
endpoint = "/regions/sigungu?sidoCode=11",
|
||||
expectedErrorCode = RegionErrorCode.SIGUNGU_CODE_NOT_FOUND,
|
||||
)
|
||||
}
|
||||
|
||||
test("행정동") {
|
||||
every {
|
||||
regionRepository.findAllDongBySidoAndSigungu(any(), any())
|
||||
} returns emptyList()
|
||||
|
||||
runExceptionTest(
|
||||
method = HttpMethod.GET,
|
||||
endpoint = "/regions/dong?sidoCode=11&sigunguCode=110",
|
||||
expectedErrorCode = RegionErrorCode.DONG_CODE_NOT_FOUND,
|
||||
)
|
||||
}
|
||||
|
||||
test("지역 코드") {
|
||||
every {
|
||||
regionRepository.findRegionCode(any(), any(), any())
|
||||
} returns null
|
||||
|
||||
runExceptionTest(
|
||||
method = HttpMethod.GET,
|
||||
endpoint = "/regions/code?sidoCode=11&sigunguCode=110&dongCode=10100",
|
||||
expectedErrorCode = RegionErrorCode.REGION_CODE_NOT_FOUND,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/test/kotlin/roomescape/region/RegionApiSuccessTest.kt
Normal file
58
src/test/kotlin/roomescape/region/RegionApiSuccessTest.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package roomescape.region
|
||||
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.region.exception.RegionErrorCode
|
||||
import roomescape.region.infrastructure.persistence.RegionRepository
|
||||
import roomescape.supports.FunSpecSpringbootTest
|
||||
import roomescape.supports.runExceptionTest
|
||||
import roomescape.supports.runTest
|
||||
|
||||
class RegionApiSuccessTest: FunSpecSpringbootTest() {
|
||||
init {
|
||||
context("시/도 -> 시/군/구 -> 행정동 -> 지역 코드 순으로 조회한다.") {
|
||||
test("정상 응답") {
|
||||
val sidoCode: String = runTest(
|
||||
on = {
|
||||
get("/regions/sido")
|
||||
},
|
||||
expect = {
|
||||
statusCode(HttpStatus.OK.value())
|
||||
}
|
||||
).extract().path("data.sidoList[0].code")
|
||||
|
||||
val sigunguCode: String = runTest(
|
||||
on = {
|
||||
get("/regions/sigungu?sidoCode=$sidoCode")
|
||||
},
|
||||
expect = {
|
||||
statusCode(HttpStatus.OK.value())
|
||||
}
|
||||
).extract().path("data.sigunguList[0].code")
|
||||
|
||||
val dongCode: String = runTest(
|
||||
on = {
|
||||
get("/regions/dong?sidoCode=$sidoCode&sigunguCode=$sigunguCode")
|
||||
},
|
||||
expect = {
|
||||
statusCode(HttpStatus.OK.value())
|
||||
}
|
||||
).extract().path("data.dongList[0].code")
|
||||
|
||||
val regionCode: String = runTest(
|
||||
on = {
|
||||
get("/regions/code?sidoCode=$sidoCode&sigunguCode=$sigunguCode&dongCode=${dongCode}")
|
||||
},
|
||||
expect = {
|
||||
statusCode(HttpStatus.OK.value())
|
||||
}
|
||||
).extract().path("data.code")
|
||||
|
||||
regionCode shouldBe "$sidoCode$sigunguCode$dongCode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user