generated from pricelees/issue-pr-template
[#44] 매장 기능 도입 #45
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
|
package roomescape.region.infrastructure.persistence
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
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