feat: 새로운 테마와 관련된 Service / Validator / Repository 추가

This commit is contained in:
이상진 2025-09-03 10:46:45 +09:00
parent fb9aec62f1
commit b4896e243e
3 changed files with 243 additions and 0 deletions

View 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,
)
}
}

View 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())
}

View File

@ -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
}