generated from pricelees/issue-pr-template
feat: theme 도메인에 Finder, Writer, Validator 추가 및 서비스 로직 수정
This commit is contained in:
parent
c5664e8aef
commit
8a56dc7841
@ -7,7 +7,7 @@ import roomescape.member.web.toRetrieveResponse
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.theme.web.ThemeRetrieveResponse
|
||||
import roomescape.theme.web.toResponse
|
||||
import roomescape.theme.web.toRetrieveResponse
|
||||
import roomescape.time.web.TimeCreateResponse
|
||||
import roomescape.time.web.toCreateResponse
|
||||
import java.time.LocalDate
|
||||
@ -53,7 +53,7 @@ fun ReservationEntity.toRetrieveResponse(): ReservationRetrieveResponse = Reserv
|
||||
date = this.date,
|
||||
member = this.member.toRetrieveResponse(),
|
||||
time = this.time.toCreateResponse(),
|
||||
theme = this.theme.toResponse(),
|
||||
theme = this.theme.toRetrieveResponse(),
|
||||
status = this.status
|
||||
)
|
||||
|
||||
|
||||
@ -1,92 +1,67 @@
|
||||
package roomescape.theme.business
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.config.next
|
||||
import roomescape.theme.exception.ThemeErrorCode
|
||||
import roomescape.theme.exception.ThemeException
|
||||
import roomescape.theme.implement.ThemeFinder
|
||||
import roomescape.theme.implement.ThemeWriter
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
import roomescape.theme.web.ThemeCreateRequest
|
||||
import roomescape.theme.web.ThemeRetrieveListResponse
|
||||
import roomescape.theme.web.ThemeRetrieveResponse
|
||||
import roomescape.theme.web.toResponse
|
||||
import roomescape.theme.web.*
|
||||
import java.time.LocalDate
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class ThemeService(
|
||||
private val tsidFactory: TsidFactory,
|
||||
private val themeRepository: ThemeRepository,
|
||||
private val themeFinder: ThemeFinder,
|
||||
private val themeWriter: ThemeWriter,
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun findById(id: Long): ThemeEntity {
|
||||
log.debug { "[ThemeService.findById] 테마 조회 시작: themeId=$id" }
|
||||
log.info { "[ThemeService.findById] 시작: themeId=$id" }
|
||||
|
||||
return themeRepository.findByIdOrNull(id)
|
||||
?.also { log.info { "[ThemeService.findById] 테마 조회 완료: themeId=$id" } }
|
||||
?: run {
|
||||
log.warn { "[ThemeService.findById] 테마 조회 실패: themeId=$id" }
|
||||
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||
}
|
||||
return themeFinder.findById(id)
|
||||
.also { log.info { "[ThemeService.findById] 완료: themeId=$id, name=${it.name}" } }
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findThemes(): ThemeRetrieveListResponse {
|
||||
log.debug { "[ThemeService.findThemes] 모든 테마 조회 시작" }
|
||||
log.info { "[ThemeService.findThemes] 시작" }
|
||||
|
||||
return themeRepository.findAll()
|
||||
.also { log.info { "[ThemeService.findThemes] ${it.size}개의 테마 조회 완료" } }
|
||||
.toResponse()
|
||||
return themeFinder.findAll()
|
||||
.toRetrieveListResponse()
|
||||
.also { log.info { "[ThemeService.findThemes] 완료. ${it.themes.size}개 반환" } }
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse {
|
||||
log.debug { "[ThemeService.findMostReservedThemes] 인기 테마 조회 시작: count=$count" }
|
||||
log.info { "[ThemeService.findMostReservedThemes] 시작: count=$count" }
|
||||
|
||||
val today = LocalDate.now()
|
||||
val startDate = today.minusDays(7)
|
||||
val endDate = today.minusDays(1)
|
||||
val startFrom = today.minusDays(7)
|
||||
val endAt = today.minusDays(1)
|
||||
|
||||
return themeRepository.findPopularThemes(startDate, endDate, count)
|
||||
.also { log.info { "[ThemeService.findMostReservedThemes] ${it.size} 개의 인기 테마 조회 완료" } }
|
||||
.toResponse()
|
||||
return themeFinder.findMostReservedThemes(count, startFrom, endAt)
|
||||
.toRetrieveListResponse()
|
||||
.also { log.info { "[ThemeService.findMostReservedThemes] ${it.themes.size}개 반환" } }
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createTheme(request: ThemeCreateRequest): ThemeRetrieveResponse {
|
||||
log.debug { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" }
|
||||
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse {
|
||||
log.info { "[ThemeService.createTheme] 시작: name=${request.name}" }
|
||||
|
||||
if (themeRepository.existsByName(request.name)) {
|
||||
log.info { "[ThemeService.createTheme] 테마 생성 실패(이름 중복): name=${request.name}" }
|
||||
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||
}
|
||||
|
||||
val theme = ThemeEntity(
|
||||
_id = tsidFactory.next(),
|
||||
name = request.name,
|
||||
description = request.description,
|
||||
thumbnail = request.thumbnail
|
||||
)
|
||||
return themeRepository.save(theme)
|
||||
.also { log.info { "[ThemeService.createTheme] 테마 생성 완료: themeId=${it.id}" } }
|
||||
.toResponse()
|
||||
return themeWriter.create(request.name, request.description, request.thumbnail)
|
||||
.toCreateResponse()
|
||||
.also { log.info { "[ThemeService.createTheme] 테마 생성 완료: name=${it.name} themeId=${it.id}" } }
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteTheme(id: Long) {
|
||||
log.debug { "[ThemeService.deleteTheme] 테마 삭제 시작: themeId=$id" }
|
||||
log.info { "[ThemeService.deleteTheme] 시작: themeId=$id" }
|
||||
|
||||
if (themeRepository.isReservedTheme(id)) {
|
||||
log.info { "[ThemeService.deleteTheme] 테마 삭제 실패(예약이 있는 테마): themeId=$id" }
|
||||
throw ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
||||
}
|
||||
val theme: ThemeEntity = themeFinder.findById(id)
|
||||
|
||||
themeRepository.deleteById(id)
|
||||
.also { log.info { "[ThemeService.deleteTheme] 테마 삭제 완료: themeId=$id" } }
|
||||
themeWriter.delete(theme)
|
||||
.also { log.info { "[ThemeService.deleteTheme] 완료: themeId=$id, name=${theme.name}" } }
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import roomescape.auth.web.support.Admin
|
||||
import roomescape.auth.web.support.LoginRequired
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.theme.web.ThemeCreateRequest
|
||||
import roomescape.theme.web.ThemeCreateResponse
|
||||
import roomescape.theme.web.ThemeRetrieveListResponse
|
||||
import roomescape.theme.web.ThemeRetrieveResponse
|
||||
|
||||
@ -38,7 +39,7 @@ interface ThemeAPI {
|
||||
)
|
||||
fun createTheme(
|
||||
@Valid @RequestBody request: ThemeCreateRequest,
|
||||
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>>
|
||||
): ResponseEntity<CommonApiResponse<ThemeCreateResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
|
||||
|
||||
@ -10,5 +10,6 @@ enum class ThemeErrorCode(
|
||||
) : ErrorCode {
|
||||
THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "TH001", "테마를 찾을 수 없어요."),
|
||||
THEME_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "TH002", "이미 같은 이름의 테마가 있어요."),
|
||||
THEME_ALREADY_RESERVED(HttpStatus.CONFLICT, "TH003", "예약된 테마라 삭제할 수 없어요.")
|
||||
THEME_ALREADY_RESERVED(HttpStatus.CONFLICT, "TH003", "예약된 테마라 삭제할 수 없어요."),
|
||||
INVALID_REQUEST_VALUE(HttpStatus.BAD_REQUEST, "TH004", "입력 값이 잘못되었어요."),
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import roomescape.theme.exception.ThemeErrorCode
|
||||
import roomescape.theme.exception.ThemeException
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
import java.time.LocalDate
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@ -15,6 +16,13 @@ private val log: KLogger = KotlinLogging.logger {}
|
||||
class ThemeFinder(
|
||||
private val themeRepository: ThemeRepository
|
||||
) {
|
||||
fun findAll(): List<ThemeEntity> {
|
||||
log.debug { "[ThemeFinder.findAll] 시작" }
|
||||
|
||||
return themeRepository.findAll()
|
||||
.also { log.debug { "[TimeFinder.findAll] ${it.size}개 테마 조회 완료" } }
|
||||
}
|
||||
|
||||
fun findById(id: Long): ThemeEntity {
|
||||
log.debug { "[ThemeFinder.findById] 조회 시작: memberId=$id" }
|
||||
|
||||
@ -25,4 +33,15 @@ class ThemeFinder(
|
||||
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
fun findMostReservedThemes(
|
||||
count: Int,
|
||||
startFrom: LocalDate,
|
||||
endAt: LocalDate
|
||||
): List<ThemeEntity> {
|
||||
log.debug { "[ThemeFinder.findMostReservedThemes] 시작. count=$count, startFrom=$startFrom, endAt=$endAt" }
|
||||
|
||||
return themeRepository.findPopularThemes(startFrom, endAt, count)
|
||||
.also { log.debug { "[ThemeFinder.findMostReservedThemes] ${it.size} / ${count}개 테마 조회 완료" } }
|
||||
}
|
||||
}
|
||||
|
||||
43
src/main/kotlin/roomescape/theme/implement/ThemeValidator.kt
Normal file
43
src/main/kotlin/roomescape/theme/implement/ThemeValidator.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package roomescape.theme.implement
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Component
|
||||
import roomescape.theme.exception.ThemeErrorCode
|
||||
import roomescape.theme.exception.ThemeException
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Component
|
||||
class ThemeValidator(
|
||||
private val themeRepository: ThemeRepository
|
||||
) {
|
||||
fun validateNameAlreadyExists(name: String) {
|
||||
log.debug { "[ThemeValidator.validateNameAlreadyExists] 시작: name=$name" }
|
||||
|
||||
if (themeRepository.existsByName(name)) {
|
||||
log.info { "[ThemeService.createTheme] 이름 중복: name=${name}" }
|
||||
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||
}
|
||||
|
||||
log.debug { "[ThemeValidator.validateNameAlreadyExists] 완료: name=$name" }
|
||||
}
|
||||
|
||||
fun validateIsReserved(theme: ThemeEntity) {
|
||||
val themeId: Long = theme.id ?: run {
|
||||
log.warn { "[ThemeValidator.validateIsReserved] ID를 찾을 수 없음: name:${theme.name}" }
|
||||
throw ThemeException(ThemeErrorCode.INVALID_REQUEST_VALUE)
|
||||
}
|
||||
|
||||
log.debug { "[ThemeValidator.validateIsReserved] 시작: themeId=${themeId}" }
|
||||
|
||||
if (themeRepository.isReservedTheme(themeId)) {
|
||||
log.info { "[ThemeService.deleteTheme] 예약이 있는 테마: themeId=$themeId" }
|
||||
throw ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
||||
}
|
||||
|
||||
log.debug { "[ThemeValidator.validateIsReserved] 완료: themeId=$themeId" }
|
||||
}
|
||||
}
|
||||
41
src/main/kotlin/roomescape/theme/implement/ThemeWriter.kt
Normal file
41
src/main/kotlin/roomescape/theme/implement/ThemeWriter.kt
Normal file
@ -0,0 +1,41 @@
|
||||
package roomescape.theme.implement
|
||||
|
||||
import com.github.f4b6a3.tsid.TsidFactory
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Component
|
||||
import roomescape.common.config.next
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Component
|
||||
class ThemeWriter(
|
||||
private val themeValidator: ThemeValidator,
|
||||
private val themeRepository: ThemeRepository,
|
||||
private val tsidFactory: TsidFactory
|
||||
) {
|
||||
fun create(name: String, description: String, thumbnail: String): ThemeEntity {
|
||||
log.debug { "[ThemeWriter.create] 시작: name=$name" }
|
||||
themeValidator.validateNameAlreadyExists(name)
|
||||
|
||||
val theme = ThemeEntity(
|
||||
_id = tsidFactory.next(),
|
||||
name = name,
|
||||
description = description,
|
||||
thumbnail = thumbnail
|
||||
)
|
||||
|
||||
return themeRepository.save(theme)
|
||||
.also { log.debug { "[ThemeWriter.create] 완료: name=$name, id=${it.id}" } }
|
||||
}
|
||||
|
||||
fun delete(theme: ThemeEntity) {
|
||||
log.debug { "[ThemeWriter.delete] 시작: id=${theme.id}" }
|
||||
themeValidator.validateIsReserved(theme)
|
||||
|
||||
themeRepository.delete(theme)
|
||||
.also { log.debug { "[ThemeWriter.delete] 완료: id=${theme.id}, name=${theme.name}" } }
|
||||
}
|
||||
}
|
||||
@ -6,27 +6,25 @@ import java.time.LocalDate
|
||||
|
||||
interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
@Query(value = """
|
||||
SELECT t
|
||||
FROM ThemeEntity t
|
||||
RIGHT JOIN ReservationEntity r ON t.id = r.theme.id
|
||||
WHERE r.date BETWEEN :startDate AND :endDate
|
||||
GROUP BY r.theme.id
|
||||
ORDER BY COUNT(r.theme.id) DESC, t.id ASC
|
||||
LIMIT :limit
|
||||
RIGHT JOIN ReservationEntity r ON t._id = r.theme._id
|
||||
WHERE r.date BETWEEN :startFrom AND :endAt
|
||||
GROUP BY r.theme._id
|
||||
ORDER BY COUNT(r.theme._id) DESC, t._id ASC
|
||||
LIMIT :count
|
||||
"""
|
||||
)
|
||||
fun findPopularThemes(startDate: LocalDate, endDate: LocalDate, limit: Int): List<ThemeEntity>
|
||||
fun findPopularThemes(startFrom: LocalDate, endAt: LocalDate, count: Int): List<ThemeEntity>
|
||||
|
||||
fun existsByName(name: String): Boolean
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
@Query(value = """
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM ReservationEntity r
|
||||
WHERE r.theme.id = :id
|
||||
WHERE r.theme._id = :id
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
@ -33,8 +33,8 @@ class ThemeController(
|
||||
@PostMapping("/themes")
|
||||
override fun createTheme(
|
||||
@RequestBody @Valid request: ThemeCreateRequest
|
||||
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>> {
|
||||
val themeResponse: ThemeRetrieveResponse = themeService.createTheme(request)
|
||||
): ResponseEntity<CommonApiResponse<ThemeCreateResponse>> {
|
||||
val themeResponse: ThemeCreateResponse = themeService.createTheme(request)
|
||||
|
||||
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
||||
.body(CommonApiResponse(themeResponse))
|
||||
|
||||
@ -21,6 +21,21 @@ data class ThemeCreateRequest(
|
||||
val thumbnail: String
|
||||
)
|
||||
|
||||
data class ThemeCreateResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val description: String,
|
||||
@Schema(description = "썸네일 이미지 주소(URL).")
|
||||
val thumbnail: String
|
||||
)
|
||||
|
||||
fun ThemeEntity.toCreateResponse(): ThemeCreateResponse = ThemeCreateResponse(
|
||||
id = this.id!!,
|
||||
name = this.name,
|
||||
description = this.description,
|
||||
thumbnail = this.thumbnail
|
||||
)
|
||||
|
||||
data class ThemeRetrieveResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
@ -29,7 +44,7 @@ data class ThemeRetrieveResponse(
|
||||
val thumbnail: String
|
||||
)
|
||||
|
||||
fun ThemeEntity.toResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse(
|
||||
fun ThemeEntity.toRetrieveResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse(
|
||||
id = this.id!!,
|
||||
name = this.name,
|
||||
description = this.description,
|
||||
@ -40,6 +55,6 @@ data class ThemeRetrieveListResponse(
|
||||
val themes: List<ThemeRetrieveResponse>
|
||||
)
|
||||
|
||||
fun List<ThemeEntity>.toResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse(
|
||||
themes = this.map { it.toResponse() }
|
||||
fun List<ThemeEntity>.toRetrieveListResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse(
|
||||
themes = this.map { it.toRetrieveResponse() }
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user