diff --git a/src/main/kotlin/roomescape/theme/business/ThemeService.kt b/src/main/kotlin/roomescape/theme/business/ThemeService.kt index 8f15ed41..a24be7cb 100644 --- a/src/main/kotlin/roomescape/theme/business/ThemeService.kt +++ b/src/main/kotlin/roomescape/theme/business/ThemeService.kt @@ -24,17 +24,26 @@ class ThemeService( private val themeValidator: ThemeValidator ) { @Transactional(readOnly = true) - fun findThemesByIds(request: ThemeListRetrieveRequest): ThemeRetrieveListResponse { + fun findThemesByIds(request: ThemeListRetrieveRequest): ThemeSummaryListResponse { log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" } + val result: MutableList = mutableListOf() - return request.themeIds - .map { findOrThrow(it) } - .toRetrieveListResponse() - .also { log.info { "[ThemeService.findThemesByIds] ${it.themes.size}개 테마 조회 완료" } } + for (id in request.themeIds) { + val theme: ThemeEntity? = themeRepository.findByIdOrNull(id) + if (theme == null) { + log.warn { "[ThemeService.findThemesByIds] id=${id} 인 테마 조회 실패" } + continue + } + result.add(theme) + } + + return result.toRetrieveListResponse().also { + log.info { "[ThemeService.findThemesByIds] ${it.themes.size} / ${request.themeIds.size} 개 테마 조회 완료" } + } } @Transactional(readOnly = true) - fun findThemesForReservation(): ThemeRetrieveListResponse { + fun findThemesForReservation(): ThemeSummaryListResponse { log.info { "[ThemeService.findThemesForReservation] 예약 페이지에서의 테마 목록 조회 시작" } return themeRepository.findOpenedThemes() @@ -65,10 +74,10 @@ class ThemeService( } @Transactional(readOnly = true) - fun findById(id: Long): ThemeRetrieveResponseV2 { + fun findSummaryById(id: Long): ThemeSummaryResponse { log.info { "[ThemeService.findById] 테마 조회 시작: id=$id" } - return findOrThrow(id).toRetrieveResponse() + return findOrThrow(id).toSummaryResponse() .also { log.info { "[ThemeService.findById] 테마 조회 완료: id=$id" } } } diff --git a/src/main/kotlin/roomescape/theme/docs/ThemeApi.kt b/src/main/kotlin/roomescape/theme/docs/ThemeApi.kt index fa270bc4..b3eaf563 100644 --- a/src/main/kotlin/roomescape/theme/docs/ThemeApi.kt +++ b/src/main/kotlin/roomescape/theme/docs/ThemeApi.kt @@ -17,7 +17,7 @@ import roomescape.theme.web.ThemeCreateRequest import roomescape.theme.web.ThemeCreateResponseV2 import roomescape.theme.web.ThemeListRetrieveRequest import roomescape.theme.web.ThemeUpdateRequest -import roomescape.theme.web.ThemeRetrieveListResponse +import roomescape.theme.web.ThemeSummaryListResponse @Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.") interface ThemeAPIV2 { @@ -53,10 +53,10 @@ interface ThemeAPIV2 { @LoginRequired @Operation(summary = "예약 페이지에서 모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun findUserThemes(): ResponseEntity> + fun findUserThemes(): ResponseEntity> @LoginRequired @Operation(summary = "예약 페이지에서 입력한 날짜에 가능한 테마 조회", description = "입력한 날짜에 가능한 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun findThemesByIds(request: ThemeListRetrieveRequest): ResponseEntity> + fun findThemesByIds(request: ThemeListRetrieveRequest): ResponseEntity> } diff --git a/src/main/kotlin/roomescape/theme/web/ThemeController.kt b/src/main/kotlin/roomescape/theme/web/ThemeController.kt index 732a6341..bc7321da 100644 --- a/src/main/kotlin/roomescape/theme/web/ThemeController.kt +++ b/src/main/kotlin/roomescape/theme/web/ThemeController.kt @@ -15,14 +15,14 @@ class ThemeController( @PostMapping("/themes/retrieve") override fun findThemesByIds( @RequestBody request: ThemeListRetrieveRequest - ): ResponseEntity> { + ): ResponseEntity> { val response = themeService.findThemesByIds(request) return ResponseEntity.ok(CommonApiResponse(response)) } @GetMapping("/v2/themes") - override fun findUserThemes(): ResponseEntity> { + override fun findUserThemes(): ResponseEntity> { val response = themeService.findThemesForReservation() return ResponseEntity.ok(CommonApiResponse(response)) diff --git a/src/main/kotlin/roomescape/theme/web/ThemeDto.kt b/src/main/kotlin/roomescape/theme/web/ThemeDto.kt index 617c71e3..8f5fb270 100644 --- a/src/main/kotlin/roomescape/theme/web/ThemeDto.kt +++ b/src/main/kotlin/roomescape/theme/web/ThemeDto.kt @@ -132,7 +132,7 @@ data class ThemeListRetrieveRequest( val themeIds: List ) -data class ThemeRetrieveResponseV2( +data class ThemeSummaryResponse( val id: Long, val name: String, val thumbnailUrl: String, @@ -146,7 +146,7 @@ data class ThemeRetrieveResponseV2( val expectedMinutesTo: Short ) -fun ThemeEntity.toRetrieveResponse() = ThemeRetrieveResponseV2( +fun ThemeEntity.toSummaryResponse() = ThemeSummaryResponse( id = this.id, name = this.name, thumbnailUrl = this.thumbnailUrl, @@ -160,10 +160,10 @@ fun ThemeEntity.toRetrieveResponse() = ThemeRetrieveResponseV2( expectedMinutesTo = this.expectedMinutesTo ) -data class ThemeRetrieveListResponse( - val themes: List +data class ThemeSummaryListResponse( + val themes: List ) -fun List.toRetrieveListResponse() = ThemeRetrieveListResponse( - themes = this.map { it.toRetrieveResponse() } +fun List.toRetrieveListResponse() = ThemeSummaryListResponse( + themes = this.map { it.toSummaryResponse() } ) diff --git a/src/test/kotlin/roomescape/theme/ThemeApiTest.kt b/src/test/kotlin/roomescape/theme/ThemeApiTest.kt index c2af464c..e6aee6d0 100644 --- a/src/test/kotlin/roomescape/theme/ThemeApiTest.kt +++ b/src/test/kotlin/roomescape/theme/ThemeApiTest.kt @@ -4,15 +4,11 @@ import io.kotest.matchers.date.shouldBeAfter import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe -import io.restassured.module.kotlin.extensions.Extract -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.When import io.restassured.response.ValidatableResponse import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.notNullValue import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpStatus -import org.springframework.http.MediaType import roomescape.auth.exception.AuthErrorCode import roomescape.member.infrastructure.persistence.Role import roomescape.theme.business.MIN_DURATION @@ -21,9 +17,10 @@ import roomescape.theme.business.MIN_PRICE import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeRepository -import roomescape.theme.web.ThemeCreateRequest +import roomescape.theme.web.ThemeListRetrieveRequest import roomescape.theme.web.ThemeUpdateRequest import roomescape.util.FunSpecSpringbootTest +import roomescape.util.INVALID_PK import roomescape.util.ThemeFixture.createRequest import roomescape.util.assertProperties import roomescape.util.runTest @@ -105,10 +102,15 @@ class ThemeApiTest( context("일반 회원도 접근할 수 있다.") { test("테마 조회: GET /v2/themes") { - createDummyTheme(createRequest.copy(name = "test123", isOpen = true)) + val token = loginUtil.loginAsUser() + + dummyInitializer.createTheme( + adminToken = loginUtil.loginAsAdmin(), + request = createRequest.copy(name = "test123", isOpen = true) + ) runTest( - token = loginUtil.loginAsUser(), + token = token, on = { get("/v2/themes") }, @@ -158,7 +160,10 @@ class ThemeApiTest( test("이미 동일한 이름의 테마가 있으면 실패한다.") { val commonName = "test123" - createDummyTheme(createRequest.copy(name = commonName)) + dummyInitializer.createTheme( + adminToken = token, + request = createRequest.copy(name = commonName) + ) runTest( token = token, @@ -329,15 +334,77 @@ class ThemeApiTest( } } - context("모든 테마를 조회한다.") { + context("입력된 모든 ID에 대한 테마를 조회한다.") { + val themeIds = mutableListOf() + beforeTest { - createDummyTheme(createRequest.copy(name = "open", isOpen = true)) - createDummyTheme(createRequest.copy(name = "close", isOpen = false)) + for (i in 1..3) { + dummyInitializer.createTheme( + adminToken = loginUtil.loginAsAdmin(), + request = createRequest.copy(name = "test$i") + ).also { + themeIds.add(it.id) + } + } + } + + afterTest { + themeIds.clear() + } + + test("정상 응답") { + runTest( + token = loginUtil.loginAsUser(), + using = { + body(ThemeListRetrieveRequest(themeIds)) + }, + on = { + post("/themes/retrieve") + }, + expect = { + statusCode(HttpStatus.OK.value()) + body("data.themes.size()", equalTo(3)) + } + ) + } + + test("없는 테마가 있으면 생략한다.") { + themeIds.add(INVALID_PK) + + runTest( + token = loginUtil.loginAsUser(), + using = { + body(ThemeListRetrieveRequest(themeIds)) + }, + on = { + post("/themes/retrieve") + }, + expect = { + statusCode(HttpStatus.OK.value()) + body("data.themes.size()", equalTo(3)) + } + ) + } + } + + context("모든 테마를 조회한다.") { + lateinit var token: String + + beforeTest { + token = loginUtil.loginAsAdmin() + dummyInitializer.createTheme( + adminToken = token, + request = createRequest.copy(name = "open", isOpen = true) + ) + dummyInitializer.createTheme( + adminToken = token, + request = createRequest.copy(name = "close", isOpen = false) + ) } test("관리자 페이지에서는 비공개 테마까지 포함하여 간단한 정보만 조회된다.") { runTest( - token = loginUtil.loginAsAdmin(), + token = token, on = { get("/admin/themes") }, @@ -375,10 +442,14 @@ class ThemeApiTest( context("관리자 페이지에서 특정 테마의 상세 정보를 조회한다.") { test("정상 응답") { - val createdTheme: ThemeEntity = createDummyTheme(createRequest) + val token = loginUtil.loginAsAdmin() + val createdTheme = dummyInitializer.createTheme( + adminToken = token, + request = createRequest + ) runTest( - token = loginUtil.loginAsAdmin(), + token = token, on = { get("/admin/themes/${createdTheme.id}") }, @@ -413,10 +484,14 @@ class ThemeApiTest( context("테마를 삭제한다.") { test("정상 삭제") { - val createdTheme = createDummyTheme(createRequest) + val token = loginUtil.loginAsAdmin() + val createdTheme = dummyInitializer.createTheme( + adminToken = token, + request = createRequest + ) runTest( - token = loginUtil.loginAsAdmin(), + token = token, on = { delete("/admin/themes/${createdTheme.id}") }, @@ -451,7 +526,10 @@ class ThemeApiTest( beforeTest { token = loginUtil.loginAsAdmin() - createdTheme = createDummyTheme(createRequest.copy(name = "theme-${Random.nextInt()}")) + createdTheme = dummyInitializer.createTheme( + adminToken = token, + request = createRequest.copy(name = "theme-${Random.nextInt()}") + ) apiPath = "/admin/themes/${createdTheme.id}" } @@ -669,19 +747,4 @@ class ThemeApiTest( } } } - - fun createDummyTheme(request: ThemeCreateRequest): ThemeEntity { - val createdThemeId: Long = Given { - contentType(MediaType.APPLICATION_JSON_VALUE) - header("Authorization", "Bearer ${loginUtil.loginAsAdmin()}") - body(request) - } When { - post("/admin/themes") - } Extract { - path("data.id") - } - - return themeRepository.findByIdOrNull(createdThemeId) - ?: throw RuntimeException("unreachable line") - } }