refactor: 테마 반환 DTO 이름 수정 및 테스트 추가

This commit is contained in:
이상진 2025-09-09 09:05:31 +09:00
parent fc3c6e42b0
commit ed618e1699
5 changed files with 123 additions and 51 deletions

View File

@ -24,17 +24,26 @@ class ThemeService(
private val themeValidator: ThemeValidator private val themeValidator: ThemeValidator
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemesByIds(request: ThemeListRetrieveRequest): ThemeRetrieveListResponse { fun findThemesByIds(request: ThemeListRetrieveRequest): ThemeSummaryListResponse {
log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" } log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" }
val result: MutableList<ThemeEntity> = mutableListOf()
return request.themeIds for (id in request.themeIds) {
.map { findOrThrow(it) } val theme: ThemeEntity? = themeRepository.findByIdOrNull(id)
.toRetrieveListResponse() if (theme == null) {
.also { log.info { "[ThemeService.findThemesByIds] ${it.themes.size}개 테마 조회 완료" } } 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) @Transactional(readOnly = true)
fun findThemesForReservation(): ThemeRetrieveListResponse { fun findThemesForReservation(): ThemeSummaryListResponse {
log.info { "[ThemeService.findThemesForReservation] 예약 페이지에서의 테마 목록 조회 시작" } log.info { "[ThemeService.findThemesForReservation] 예약 페이지에서의 테마 목록 조회 시작" }
return themeRepository.findOpenedThemes() return themeRepository.findOpenedThemes()
@ -65,10 +74,10 @@ class ThemeService(
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findById(id: Long): ThemeRetrieveResponseV2 { fun findSummaryById(id: Long): ThemeSummaryResponse {
log.info { "[ThemeService.findById] 테마 조회 시작: id=$id" } log.info { "[ThemeService.findById] 테마 조회 시작: id=$id" }
return findOrThrow(id).toRetrieveResponse() return findOrThrow(id).toSummaryResponse()
.also { log.info { "[ThemeService.findById] 테마 조회 완료: id=$id" } } .also { log.info { "[ThemeService.findById] 테마 조회 완료: id=$id" } }
} }

View File

@ -17,7 +17,7 @@ import roomescape.theme.web.ThemeCreateRequest
import roomescape.theme.web.ThemeCreateResponseV2 import roomescape.theme.web.ThemeCreateResponseV2
import roomescape.theme.web.ThemeListRetrieveRequest import roomescape.theme.web.ThemeListRetrieveRequest
import roomescape.theme.web.ThemeUpdateRequest import roomescape.theme.web.ThemeUpdateRequest
import roomescape.theme.web.ThemeRetrieveListResponse import roomescape.theme.web.ThemeSummaryListResponse
@Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.") @Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.")
interface ThemeAPIV2 { interface ThemeAPIV2 {
@ -53,10 +53,10 @@ interface ThemeAPIV2 {
@LoginRequired @LoginRequired
@Operation(summary = "예약 페이지에서 모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) @Operation(summary = "예약 페이지에서 모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeSummaryListResponse>>
@LoginRequired @LoginRequired
@Operation(summary = "예약 페이지에서 입력한 날짜에 가능한 테마 조회", description = "입력한 날짜에 가능한 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) @Operation(summary = "예약 페이지에서 입력한 날짜에 가능한 테마 조회", description = "입력한 날짜에 가능한 테마를 조회합니다.", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findThemesByIds(request: ThemeListRetrieveRequest): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> fun findThemesByIds(request: ThemeListRetrieveRequest): ResponseEntity<CommonApiResponse<ThemeSummaryListResponse>>
} }

View File

@ -15,14 +15,14 @@ class ThemeController(
@PostMapping("/themes/retrieve") @PostMapping("/themes/retrieve")
override fun findThemesByIds( override fun findThemesByIds(
@RequestBody request: ThemeListRetrieveRequest @RequestBody request: ThemeListRetrieveRequest
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> { ): ResponseEntity<CommonApiResponse<ThemeSummaryListResponse>> {
val response = themeService.findThemesByIds(request) val response = themeService.findThemesByIds(request)
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@GetMapping("/v2/themes") @GetMapping("/v2/themes")
override fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> { override fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeSummaryListResponse>> {
val response = themeService.findThemesForReservation() val response = themeService.findThemesForReservation()
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))

View File

@ -132,7 +132,7 @@ data class ThemeListRetrieveRequest(
val themeIds: List<Long> val themeIds: List<Long>
) )
data class ThemeRetrieveResponseV2( data class ThemeSummaryResponse(
val id: Long, val id: Long,
val name: String, val name: String,
val thumbnailUrl: String, val thumbnailUrl: String,
@ -146,7 +146,7 @@ data class ThemeRetrieveResponseV2(
val expectedMinutesTo: Short val expectedMinutesTo: Short
) )
fun ThemeEntity.toRetrieveResponse() = ThemeRetrieveResponseV2( fun ThemeEntity.toSummaryResponse() = ThemeSummaryResponse(
id = this.id, id = this.id,
name = this.name, name = this.name,
thumbnailUrl = this.thumbnailUrl, thumbnailUrl = this.thumbnailUrl,
@ -160,10 +160,10 @@ fun ThemeEntity.toRetrieveResponse() = ThemeRetrieveResponseV2(
expectedMinutesTo = this.expectedMinutesTo expectedMinutesTo = this.expectedMinutesTo
) )
data class ThemeRetrieveListResponse( data class ThemeSummaryListResponse(
val themes: List<ThemeRetrieveResponseV2> val themes: List<ThemeSummaryResponse>
) )
fun List<ThemeEntity>.toRetrieveListResponse() = ThemeRetrieveListResponse( fun List<ThemeEntity>.toRetrieveListResponse() = ThemeSummaryListResponse(
themes = this.map { it.toRetrieveResponse() } themes = this.map { it.toSummaryResponse() }
) )

View File

@ -4,15 +4,11 @@ import io.kotest.matchers.date.shouldBeAfter
import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe 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 io.restassured.response.ValidatableResponse
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.notNullValue import org.hamcrest.CoreMatchers.notNullValue
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthErrorCode
import roomescape.member.infrastructure.persistence.Role import roomescape.member.infrastructure.persistence.Role
import roomescape.theme.business.MIN_DURATION import roomescape.theme.business.MIN_DURATION
@ -21,9 +17,10 @@ import roomescape.theme.business.MIN_PRICE
import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.exception.ThemeErrorCode
import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.theme.infrastructure.persistence.ThemeRepository
import roomescape.theme.web.ThemeCreateRequest import roomescape.theme.web.ThemeListRetrieveRequest
import roomescape.theme.web.ThemeUpdateRequest import roomescape.theme.web.ThemeUpdateRequest
import roomescape.util.FunSpecSpringbootTest import roomescape.util.FunSpecSpringbootTest
import roomescape.util.INVALID_PK
import roomescape.util.ThemeFixture.createRequest import roomescape.util.ThemeFixture.createRequest
import roomescape.util.assertProperties import roomescape.util.assertProperties
import roomescape.util.runTest import roomescape.util.runTest
@ -105,10 +102,15 @@ class ThemeApiTest(
context("일반 회원도 접근할 수 있다.") { context("일반 회원도 접근할 수 있다.") {
test("테마 조회: GET /v2/themes") { 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( runTest(
token = loginUtil.loginAsUser(), token = token,
on = { on = {
get("/v2/themes") get("/v2/themes")
}, },
@ -158,7 +160,10 @@ class ThemeApiTest(
test("이미 동일한 이름의 테마가 있으면 실패한다.") { test("이미 동일한 이름의 테마가 있으면 실패한다.") {
val commonName = "test123" val commonName = "test123"
createDummyTheme(createRequest.copy(name = commonName)) dummyInitializer.createTheme(
adminToken = token,
request = createRequest.copy(name = commonName)
)
runTest( runTest(
token = token, token = token,
@ -329,15 +334,77 @@ class ThemeApiTest(
} }
} }
context("모든 테마를 조회한다.") { context("입력된 모든 ID에 대한 테마를 조회한다.") {
val themeIds = mutableListOf<Long>()
beforeTest { beforeTest {
createDummyTheme(createRequest.copy(name = "open", isOpen = true)) for (i in 1..3) {
createDummyTheme(createRequest.copy(name = "close", isOpen = false)) 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("관리자 페이지에서는 비공개 테마까지 포함하여 간단한 정보만 조회된다.") { test("관리자 페이지에서는 비공개 테마까지 포함하여 간단한 정보만 조회된다.") {
runTest( runTest(
token = loginUtil.loginAsAdmin(), token = token,
on = { on = {
get("/admin/themes") get("/admin/themes")
}, },
@ -375,10 +442,14 @@ class ThemeApiTest(
context("관리자 페이지에서 특정 테마의 상세 정보를 조회한다.") { context("관리자 페이지에서 특정 테마의 상세 정보를 조회한다.") {
test("정상 응답") { test("정상 응답") {
val createdTheme: ThemeEntity = createDummyTheme(createRequest) val token = loginUtil.loginAsAdmin()
val createdTheme = dummyInitializer.createTheme(
adminToken = token,
request = createRequest
)
runTest( runTest(
token = loginUtil.loginAsAdmin(), token = token,
on = { on = {
get("/admin/themes/${createdTheme.id}") get("/admin/themes/${createdTheme.id}")
}, },
@ -413,10 +484,14 @@ class ThemeApiTest(
context("테마를 삭제한다.") { context("테마를 삭제한다.") {
test("정상 삭제") { test("정상 삭제") {
val createdTheme = createDummyTheme(createRequest) val token = loginUtil.loginAsAdmin()
val createdTheme = dummyInitializer.createTheme(
adminToken = token,
request = createRequest
)
runTest( runTest(
token = loginUtil.loginAsAdmin(), token = token,
on = { on = {
delete("/admin/themes/${createdTheme.id}") delete("/admin/themes/${createdTheme.id}")
}, },
@ -451,7 +526,10 @@ class ThemeApiTest(
beforeTest { beforeTest {
token = loginUtil.loginAsAdmin() 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}" 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")
}
} }