generated from pricelees/issue-pr-template
feat: 새로운 테마와 관련된 Service / Validator / Repository 추가
This commit is contained in:
parent
fb9aec62f1
commit
b4896e243e
117
src/main/kotlin/roomescape/theme/business/ThemeServiceV2.kt
Normal file
117
src/main/kotlin/roomescape/theme/business/ThemeServiceV2.kt
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package roomescape.theme.business
|
||||||
|
|
||||||
|
import com.github.f4b6a3.tsid.TsidFactory
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
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.member.business.MemberService
|
||||||
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.infrastructure.persistence.v2.ThemeEntityV2
|
||||||
|
import roomescape.theme.infrastructure.persistence.v2.ThemeRepositoryV2
|
||||||
|
import roomescape.theme.web.*
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class ThemeServiceV2(
|
||||||
|
private val themeRepository: ThemeRepositoryV2,
|
||||||
|
private val tsidFactory: TsidFactory,
|
||||||
|
private val memberService: MemberService,
|
||||||
|
private val themeValidator: ThemeValidatorV2
|
||||||
|
) {
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findThemesForReservation(): ThemeRetrieveListResponseV2 {
|
||||||
|
log.info { "[ThemeService.findThemesForReservation] 예약 페이지에서의 테마 목록 조회 시작" }
|
||||||
|
|
||||||
|
return themeRepository.findOpenedThemes()
|
||||||
|
.toRetrieveListResponse()
|
||||||
|
.also { log.info { "[ThemeService.findThemesForReservation] ${it.themes.size}개 테마 조회 완료" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findAdminThemes(): AdminThemeSummaryRetrieveListResponse {
|
||||||
|
log.info { "[ThemeService.findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" }
|
||||||
|
|
||||||
|
return themeRepository.findAll()
|
||||||
|
.toAdminThemeSummaryListResponse()
|
||||||
|
.also { log.info { "[ThemeService.findAdminThemes] ${it.themes.size}개 테마 조회 완료" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findAdminThemeDetail(id: Long): AdminThemeDetailRetrieveResponse {
|
||||||
|
log.info { "[ThemeService.findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작" }
|
||||||
|
|
||||||
|
val theme = themeRepository.findByIdOrNull(id)
|
||||||
|
?: run {
|
||||||
|
log.warn { "[ThemeService.findAdminThemeDetail] 테마 조회 실패. id=$id" }
|
||||||
|
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
val createdBy = memberService.findById(theme.createdBy).name
|
||||||
|
val updatedBy = memberService.findById(theme.updatedBy).name
|
||||||
|
|
||||||
|
return theme.toAdminThemeDetailResponse(createdBy, updatedBy)
|
||||||
|
.also { log.info { "[ThemeService.findAdminThemeDetail] 테마 상세 조회 완료. id=$id, name=${theme.name}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun createTheme(request: ThemeCreateRequestV2): ThemeCreateResponseV2 {
|
||||||
|
log.info { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" }
|
||||||
|
|
||||||
|
themeValidator.validateCanCreate(request)
|
||||||
|
|
||||||
|
val theme: ThemeEntityV2 = themeRepository.save(
|
||||||
|
request.toEntity(tsidFactory.next())
|
||||||
|
)
|
||||||
|
|
||||||
|
return ThemeCreateResponseV2(theme.id).also {
|
||||||
|
log.info { "[ThemeService.createTheme] 테마 생성 완료. id=${theme.id}, name=${theme.name}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun deleteTheme(id: Long) {
|
||||||
|
log.info { "[ThemeService.deleteTheme] 테마 삭제 시작" }
|
||||||
|
|
||||||
|
val theme = themeRepository.findByIdOrNull(id)
|
||||||
|
?: run {
|
||||||
|
log.warn { "[ThemeService.deleteTheme] 테마 조회 실패. id=$id" }
|
||||||
|
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
themeRepository.delete(theme).also {
|
||||||
|
log.info { "[ThemeService.deleteTheme] 테마 삭제 완료. id=$id, name=${theme.name}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun updateTheme(id: Long, request: ThemeUpdateRequest) {
|
||||||
|
log.info { "[ThemeService.updateTheme] 테마 수정 시작" }
|
||||||
|
|
||||||
|
themeValidator.validateCanUpdate(request)
|
||||||
|
|
||||||
|
val theme: ThemeEntityV2 = themeRepository.findByIdOrNull(id)
|
||||||
|
?: run {
|
||||||
|
log.warn { "[ThemeService.updateTheme] 테마 조회 실패. id=$id" }
|
||||||
|
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.modifyIfNotNull(
|
||||||
|
request.name,
|
||||||
|
request.description,
|
||||||
|
request.thumbnailUrl,
|
||||||
|
request.difficulty,
|
||||||
|
request.price,
|
||||||
|
request.minParticipants,
|
||||||
|
request.maxParticipants,
|
||||||
|
request.availableMinutes,
|
||||||
|
request.expectedMinutesFrom,
|
||||||
|
request.expectedMinutesTo,
|
||||||
|
request.isOpen,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/main/kotlin/roomescape/theme/business/ThemeValidatorV2.kt
Normal file
114
src/main/kotlin/roomescape/theme/business/ThemeValidatorV2.kt
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package roomescape.theme.business
|
||||||
|
|
||||||
|
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.v2.ThemeRepositoryV2
|
||||||
|
import roomescape.theme.web.ThemeCreateRequestV2
|
||||||
|
import roomescape.theme.web.ThemeUpdateRequest
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
const val MIN_PRICE = 0
|
||||||
|
const val MIN_PARTICIPANTS = 1
|
||||||
|
const val MIN_DURATION = 1
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ThemeValidatorV2(
|
||||||
|
private val themeRepository: ThemeRepositoryV2,
|
||||||
|
) {
|
||||||
|
fun validateCanUpdate(request: ThemeUpdateRequest) {
|
||||||
|
validateProperties(
|
||||||
|
request.price,
|
||||||
|
request.availableMinutes,
|
||||||
|
request.expectedMinutesFrom,
|
||||||
|
request.expectedMinutesTo,
|
||||||
|
request.minParticipants,
|
||||||
|
request.maxParticipants
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateCanCreate(request: ThemeCreateRequestV2) {
|
||||||
|
if (themeRepository.existsByName(request.name)) {
|
||||||
|
log.info { "[ThemeValidator.validateCanCreate] 이름 중복: name=${request.name}" }
|
||||||
|
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
validateProperties(
|
||||||
|
request.price,
|
||||||
|
request.availableMinutes,
|
||||||
|
request.expectedMinutesFrom,
|
||||||
|
request.expectedMinutesTo,
|
||||||
|
request.minParticipants,
|
||||||
|
request.maxParticipants
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateProperties(
|
||||||
|
price: Int?,
|
||||||
|
availableMinutes: Short?,
|
||||||
|
expectedMinutesFrom: Short?,
|
||||||
|
expectedMinutesTo: Short?,
|
||||||
|
minParticipants: Short?,
|
||||||
|
maxParticipants: Short?,
|
||||||
|
) {
|
||||||
|
if (isNotNullAndBelowThan(price, MIN_PRICE)) {
|
||||||
|
log.info { "[ThemeValidator.validateCanCreate] 최소 가격 미달: price=${price}" }
|
||||||
|
throw ThemeException(ThemeErrorCode.PRICE_BELOW_MINIMUM)
|
||||||
|
}
|
||||||
|
validateTimes(availableMinutes, expectedMinutesFrom, expectedMinutesTo)
|
||||||
|
validateParticipants(minParticipants, maxParticipants)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateTimes(
|
||||||
|
availableMinutes: Short?,
|
||||||
|
expectedMinutesFrom: Short?,
|
||||||
|
expectedMinutesTo: Short?
|
||||||
|
) {
|
||||||
|
if (isNotNullAndBelowThan(availableMinutes, MIN_DURATION)
|
||||||
|
|| isNotNullAndBelowThan(expectedMinutesFrom, MIN_DURATION)
|
||||||
|
|| isNotNullAndBelowThan(expectedMinutesTo, MIN_DURATION)
|
||||||
|
) {
|
||||||
|
log.info {
|
||||||
|
"[ThemeValidator.validateTimes] 최소 시간 미달: availableMinutes=$availableMinutes" +
|
||||||
|
", expectedMinutesFrom=$expectedMinutesFrom, expectedMinutesTo=$expectedMinutesTo"
|
||||||
|
}
|
||||||
|
throw ThemeException(ThemeErrorCode.DURATION_BELOW_MINIMUM)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedMinutesFrom.isNotNullAndGraterThan(expectedMinutesTo)) {
|
||||||
|
log.info { "[ThemeValidator.validateTimes] 최소 예상 시간의 최대 예상 시간 초과: expectedMinutesFrom=$expectedMinutesFrom, expectedMinutesTo=$expectedMinutesTo" }
|
||||||
|
throw ThemeException(ThemeErrorCode.MIN_EXPECTED_TIME_EXCEEDS_MAX_EXPECTED_TIME)
|
||||||
|
}
|
||||||
|
if (expectedMinutesTo.isNotNullAndGraterThan(availableMinutes)) {
|
||||||
|
log.info { "[ThemeValidator.validateTimes] 예상 시간의 이용 가능 시간 초과: availableMinutes=$expectedMinutesFrom, expectedMinutesFrom=$expectedMinutesFrom, expectedMinutesTo=$expectedMinutesTo" }
|
||||||
|
throw ThemeException(ThemeErrorCode.EXPECTED_TIME_EXCEEDS_AVAILABLE_TIME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateParticipants(
|
||||||
|
minParticipants: Short?,
|
||||||
|
maxParticipants: Short?
|
||||||
|
) {
|
||||||
|
if (isNotNullAndBelowThan(minParticipants, MIN_PARTICIPANTS)
|
||||||
|
|| isNotNullAndBelowThan(maxParticipants, MIN_PARTICIPANTS)
|
||||||
|
) {
|
||||||
|
log.info { "[ThemeValidator.validateParticipants] 최소 인원 미달: minParticipants=$minParticipants, maxParticipants=$maxParticipants" }
|
||||||
|
throw ThemeException(ThemeErrorCode.PARTICIPANT_BELOW_MINIMUM)
|
||||||
|
}
|
||||||
|
if (minParticipants.isNotNullAndGraterThan(maxParticipants)) {
|
||||||
|
log.info { "[ThemeValidator.validateParticipants] 최소 인원의 최대 인원 초과: minParticipants=$minParticipants, maxParticipants=$maxParticipants" }
|
||||||
|
throw ThemeException(ThemeErrorCode.MIN_PARTICIPANT_EXCEEDS_MAX_PARTICIPANT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNotNullAndBelowThan(value: Number?, threshold: Int): Boolean {
|
||||||
|
return value != null && value.toInt() < threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Number?.isNotNullAndGraterThan(value: Number?): Boolean {
|
||||||
|
return this != null && value != null && (this.toInt() > value.toInt())
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package roomescape.theme.infrastructure.persistence.v2
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
|
||||||
|
interface ThemeRepositoryV2: JpaRepository<ThemeEntityV2, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT t FROM ThemeEntityV2 t WHERE t.isOpen = true")
|
||||||
|
fun findOpenedThemes(): List<ThemeEntityV2>
|
||||||
|
|
||||||
|
fun existsByName(name: String): Boolean
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user