remove: 기능 변경 완료로 인한 기존 테마 코드 제거

This commit is contained in:
이상진 2025-09-07 22:18:17 +09:00
parent e7f69aaee4
commit e4b9214d75
17 changed files with 230 additions and 649 deletions

View File

@ -20,7 +20,7 @@ import roomescape.schedule.business.ScheduleService
import roomescape.schedule.infrastructure.persistence.ScheduleStatus import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleSummaryResponse import roomescape.schedule.web.ScheduleSummaryResponse
import roomescape.schedule.web.ScheduleUpdateRequest import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.theme.business.ThemeServiceV2 import roomescape.theme.business.ThemeService
import roomescape.theme.web.ThemeRetrieveResponseV2 import roomescape.theme.web.ThemeRetrieveResponseV2
import java.time.LocalDateTime import java.time.LocalDateTime
@ -31,7 +31,7 @@ class ReservationService(
private val reservationRepository: ReservationRepository, private val reservationRepository: ReservationRepository,
private val scheduleService: ScheduleService, private val scheduleService: ScheduleService,
private val memberService: MemberService, private val memberService: MemberService,
private val themeService: ThemeServiceV2, private val themeService: ThemeService,
private val canceledReservationRepository: CanceledReservationRepository, private val canceledReservationRepository: CanceledReservationRepository,
private val tsidFactory: TsidFactory, private val tsidFactory: TsidFactory,
private val paymentService: PaymentService private val paymentService: PaymentService

View File

@ -1,67 +1,141 @@
package roomescape.theme.business package roomescape.theme.business
import com.github.f4b6a3.tsid.TsidFactory
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.theme.implement.ThemeFinder import roomescape.common.config.next
import roomescape.theme.implement.ThemeWriter import roomescape.member.business.MemberService
import roomescape.theme.exception.ThemeErrorCode
import roomescape.theme.exception.ThemeException
import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.theme.infrastructure.persistence.ThemeRepository
import roomescape.theme.web.* import roomescape.theme.web.*
import java.time.LocalDate
private val log = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@Service @Service
class ThemeService( class ThemeService(
private val themeFinder: ThemeFinder, private val themeRepository: ThemeRepository,
private val themeWriter: ThemeWriter, private val tsidFactory: TsidFactory,
private val memberService: MemberService,
private val themeValidator: ThemeValidator
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findById(id: Long): ThemeEntity { fun findThemesByIds(request: ThemeListRetrieveRequest): ThemeRetrieveListResponse {
log.debug { "[ThemeService.findById] 시작: themeId=$id" } log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" }
return themeFinder.findById(id) return request.themeIds
.also { log.info { "[ThemeService.findById] 완료: themeId=$id, name=${it.name}" } } .map { findOrThrow(it) }
.toRetrieveListResponse()
.also { log.info { "[ThemeService.findThemesByIds] ${it.themes.size}개 테마 조회 완료" } }
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemes(): ThemeRetrieveListResponse { fun findThemesForReservation(): ThemeRetrieveListResponse {
log.debug { "[ThemeService.findThemes] 시작" } log.info { "[ThemeService.findThemesForReservation] 예약 페이지에서의 테마 목록 조회 시작" }
return themeFinder.findAll() return themeRepository.findOpenedThemes()
.toRetrieveListResponse() .toRetrieveListResponse()
.also { log.info { "[ThemeService.findThemes] 완료. ${it.themes.size}개 반환" } } .also { log.info { "[ThemeService.findThemesForReservation] ${it.themes.size}개 테마 조회 완료" } }
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse { fun findAdminThemes(): AdminThemeSummaryRetrieveListResponse {
log.debug { "[ThemeService.findMostReservedThemes] 시작: count=$count" } log.info { "[ThemeService.findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" }
val today = LocalDate.now() return themeRepository.findAll()
val startFrom = today.minusDays(7) .toAdminThemeSummaryListResponse()
val endAt = today.minusDays(1) .also { log.info { "[ThemeService.findAdminThemes] ${it.themes.size}개 테마 조회 완료" } }
}
return themeFinder.findMostReservedThemes(count, startFrom, endAt) @Transactional(readOnly = true)
.toRetrieveListResponse() fun findAdminThemeDetail(id: Long): AdminThemeDetailRetrieveResponse {
.also { log.info { "[ThemeService.findMostReservedThemes] ${it.themes.size}개 반환" } } log.info { "[ThemeService.findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" }
val theme: ThemeEntity = findOrThrow(id)
val createdBy = memberService.findSummaryById(theme.createdBy).name
val updatedBy = memberService.findSummaryById(theme.updatedBy).name
return theme.toAdminThemeDetailResponse(createdBy, updatedBy)
.also { log.info { "[ThemeService.findAdminThemeDetail] 테마 상세 조회 완료: id=$id, name=${theme.name}" } }
}
@Transactional(readOnly = true)
fun findById(id: Long): ThemeRetrieveResponseV2 {
log.info { "[ThemeService.findById] 테마 조회 시작: id=$id" }
return findOrThrow(id).toRetrieveResponse()
.also { log.info { "[ThemeService.findById] 테마 조회 완료: id=$id" } }
} }
@Transactional @Transactional
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse { fun createTheme(request: ThemeCreateRequest): ThemeCreateResponseV2 {
log.debug { "[ThemeService.createTheme] 시작: name=${request.name}" } log.info { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" }
return themeWriter.create(request.name, request.description, request.thumbnail) themeValidator.validateCanCreate(request)
.toCreateResponse()
.also { log.info { "[ThemeService.createTheme] 테마 생성 완료: name=${it.name} themeId=${it.id}" } } val theme: ThemeEntity = themeRepository.save(
request.toEntity(tsidFactory.next())
)
return ThemeCreateResponseV2(theme.id).also {
log.info { "[ThemeService.createTheme] 테마 생성 완료: id=${theme.id}, name=${theme.name}" }
}
} }
@Transactional @Transactional
fun deleteTheme(id: Long) { fun deleteTheme(id: Long) {
log.debug { "[ThemeService.deleteTheme] 시작: themeId=$id" } log.info { "[ThemeService.deleteTheme] 테마 삭제 시작: id=${id}" }
val theme: ThemeEntity = themeFinder.findById(id) val theme: ThemeEntity = findOrThrow(id)
themeWriter.delete(theme) themeRepository.delete(theme).also {
.also { log.info { "[ThemeService.deleteTheme] 완료: themeId=$id, name=${theme.name}" } } log.info { "[ThemeService.deleteTheme] 테마 삭제 완료: id=$id, name=${theme.name}" }
}
}
@Transactional
fun updateTheme(id: Long, request: ThemeUpdateRequest) {
log.info { "[ThemeService.updateTheme] 테마 수정 시작: id=${id}, request=${request}" }
if (request.isAllParamsNull()) {
log.info { "[ThemeService.updateTheme] 테마 변경 사항 없음: id=${id}" }
return
}
themeValidator.validateCanUpdate(request)
val theme: ThemeEntity = findOrThrow(id)
theme.modifyIfNotNull(
request.name,
request.description,
request.thumbnailUrl,
request.difficulty,
request.price,
request.minParticipants,
request.maxParticipants,
request.availableMinutes,
request.expectedMinutesFrom,
request.expectedMinutesTo,
request.isOpen,
).also {
log.info { "[ThemeService.updateTheme] 테마 수정 완료: id=$id, request=${request}" }
}
}
private fun findOrThrow(id: Long): ThemeEntity {
log.info { "[ThemeService.findOrThrow] 테마 조회 시작: id=$id" }
return themeRepository.findByIdOrNull(id)
?.also { log.info { "[ThemeService.findOrThrow] 테마 조회 완료: id=$id" } }
?: run {
log.warn { "[ThemeService.updateTheme] 테마 조회 실패: id=$id" }
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
}
} }
} }

View File

@ -1,141 +0,0 @@
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 findThemesByIds(request: ThemeListRetrieveRequest): ThemeRetrieveListResponseV2 {
log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" }
return request.themeIds
.map { findOrThrow(it) }
.toRetrieveListResponse()
.also { log.info { "[ThemeService.findThemesByIds] ${it.themes.size}개 테마 조회 완료" } }
}
@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] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" }
val theme: ThemeEntityV2 = findOrThrow(id)
val createdBy = memberService.findSummaryById(theme.createdBy).name
val updatedBy = memberService.findSummaryById(theme.updatedBy).name
return theme.toAdminThemeDetailResponse(createdBy, updatedBy)
.also { log.info { "[ThemeService.findAdminThemeDetail] 테마 상세 조회 완료: id=$id, name=${theme.name}" } }
}
@Transactional(readOnly = true)
fun findById(id: Long): ThemeRetrieveResponseV2 {
log.info { "[ThemeService.findById] 테마 조회 시작: id=$id" }
return findOrThrow(id).toRetrieveResponse()
.also { log.info { "[ThemeService.findById] 테마 조회 완료: id=$id" } }
}
@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] 테마 삭제 시작: id=${id}" }
val theme: ThemeEntityV2 = findOrThrow(id)
themeRepository.delete(theme).also {
log.info { "[ThemeService.deleteTheme] 테마 삭제 완료: id=$id, name=${theme.name}" }
}
}
@Transactional
fun updateTheme(id: Long, request: ThemeUpdateRequest) {
log.info { "[ThemeService.updateTheme] 테마 수정 시작: id=${id}, request=${request}" }
if (request.isAllParamsNull()) {
log.info { "[ThemeService.updateTheme] 테마 변경 사항 없음: id=${id}" }
return
}
themeValidator.validateCanUpdate(request)
val theme: ThemeEntityV2 = findOrThrow(id)
theme.modifyIfNotNull(
request.name,
request.description,
request.thumbnailUrl,
request.difficulty,
request.price,
request.minParticipants,
request.maxParticipants,
request.availableMinutes,
request.expectedMinutesFrom,
request.expectedMinutesTo,
request.isOpen,
).also {
log.info { "[ThemeService.updateTheme] 테마 수정 완료: id=$id, request=${request}" }
}
}
private fun findOrThrow(id: Long): ThemeEntityV2 {
log.info { "[ThemeService.findOrThrow] 테마 조회 시작: id=$id" }
return themeRepository.findByIdOrNull(id)
?.also { log.info { "[ThemeService.findOrThrow] 테마 조회 완료: id=$id" } }
?: run {
log.warn { "[ThemeService.updateTheme] 테마 조회 실패: id=$id" }
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
}
}
}

View File

@ -5,8 +5,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.exception.ThemeErrorCode
import roomescape.theme.exception.ThemeException import roomescape.theme.exception.ThemeException
import roomescape.theme.infrastructure.persistence.v2.ThemeRepositoryV2 import roomescape.theme.infrastructure.persistence.ThemeRepository
import roomescape.theme.web.ThemeCreateRequestV2 import roomescape.theme.web.ThemeCreateRequest
import roomescape.theme.web.ThemeUpdateRequest import roomescape.theme.web.ThemeUpdateRequest
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -16,8 +16,8 @@ const val MIN_PARTICIPANTS = 1
const val MIN_DURATION = 1 const val MIN_DURATION = 1
@Component @Component
class ThemeValidatorV2( class ThemeValidator(
private val themeRepository: ThemeRepositoryV2, private val themeRepository: ThemeRepository,
) { ) {
fun validateCanUpdate(request: ThemeUpdateRequest) { fun validateCanUpdate(request: ThemeUpdateRequest) {
validateProperties( validateProperties(
@ -30,7 +30,7 @@ class ThemeValidatorV2(
) )
} }
fun validateCanCreate(request: ThemeCreateRequestV2) { fun validateCanCreate(request: ThemeCreateRequest) {
if (themeRepository.existsByName(request.name)) { if (themeRepository.existsByName(request.name)) {
log.info { "[ThemeValidator.validateCanCreate] 이름 중복으로 인한 실패: name=${request.name}" } log.info { "[ThemeValidator.validateCanCreate] 이름 중복으로 인한 실패: name=${request.name}" }
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED) throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)

View File

@ -1,52 +0,0 @@
package roomescape.theme.docs
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
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
@Tag(name = "5. 테마 API", description = "테마를 조회 / 추가 / 삭제할 때 사용합니다.")
interface ThemeAPI {
@LoginRequired
@Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findThemes(): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>>
@Operation(summary = "가장 많이 예약된 테마 조회")
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findMostReservedThemes(
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>>
@Admin
@Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(
ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true),
)
fun createTheme(
@Valid @RequestBody request: ThemeCreateRequest,
): ResponseEntity<CommonApiResponse<ThemeCreateResponse>>
@Admin
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(
ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true),
)
fun deleteTheme(
@PathVariable id: Long
): ResponseEntity<CommonApiResponse<Unit>>
}

View File

@ -13,11 +13,11 @@ import roomescape.auth.web.support.LoginRequired
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.web.AdminThemeDetailRetrieveResponse import roomescape.theme.web.AdminThemeDetailRetrieveResponse
import roomescape.theme.web.AdminThemeSummaryRetrieveListResponse import roomescape.theme.web.AdminThemeSummaryRetrieveListResponse
import roomescape.theme.web.ThemeCreateRequestV2 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.ThemeRetrieveListResponseV2 import roomescape.theme.web.ThemeRetrieveListResponse
@Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.") @Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.")
interface ThemeAPIV2 { interface ThemeAPIV2 {
@ -35,7 +35,7 @@ interface ThemeAPIV2 {
@Admin @Admin
@Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun createTheme(@Valid @RequestBody themeCreateRequestV2: ThemeCreateRequestV2): ResponseEntity<CommonApiResponse<ThemeCreateResponseV2>> fun createTheme(@Valid @RequestBody themeCreateRequest: ThemeCreateRequest): ResponseEntity<CommonApiResponse<ThemeCreateResponseV2>>
@Admin @Admin
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
@ -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<ThemeRetrieveListResponseV2>> fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>>
@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<ThemeRetrieveListResponseV2>> fun findThemesByIds(request: ThemeListRetrieveRequest): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>>
} }

View File

@ -1,47 +0,0 @@
package roomescape.theme.implement
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull
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
import java.time.LocalDate
private val log: KLogger = KotlinLogging.logger {}
@Component
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" }
return themeRepository.findByIdOrNull(id)
?.also { log.debug { "[ThemeFinder.findById] 조회 완료: id=$id, name=${it.name}" } }
?: run {
log.warn { "[ThemeFinder.findById] 조회 실패: id=$id" }
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}개 테마 조회 완료" } }
}
}

View File

@ -1,43 +0,0 @@
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" }
}
}

View File

@ -1,41 +0,0 @@
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}" } }
}
}

View File

@ -2,25 +2,65 @@ package roomescape.theme.infrastructure.persistence
import jakarta.persistence.Column import jakarta.persistence.Column
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.Id import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Table import jakarta.persistence.Table
import roomescape.common.entity.BaseEntity import roomescape.common.entity.AuditingBaseEntity
@Entity @Entity
@Table(name = "themes") @Table(name = "theme")
class ThemeEntity( class ThemeEntity(
@Id id: Long,
@Column(name = "theme_id")
private var _id: Long?,
@Column(name = "name", nullable = false)
var name: String, var name: String,
@Column(name = "description", nullable = false)
var description: String, var description: String,
var thumbnailUrl: String,
@Column(name = "thumbnail", nullable = false) @Enumerated(value = EnumType.STRING)
var thumbnail: String, var difficulty: Difficulty,
): BaseEntity() {
override fun getId(): Long? = _id var price: Int,
var minParticipants: Short,
var maxParticipants: Short,
var availableMinutes: Short,
var expectedMinutesFrom: Short,
var expectedMinutesTo: Short,
@Column(columnDefinition = "TINYINT", length = 1)
var isOpen: Boolean
) : AuditingBaseEntity(id) {
fun modifyIfNotNull(
name: String?,
description: String?,
thumbnailUrl: String?,
difficulty: Difficulty?,
price: Int?,
minParticipants: Short?,
maxParticipants: Short?,
availableMinutes: Short?,
expectedMinutesFrom: Short?,
expectedMinutesTo: Short?,
isOpen: Boolean?
) {
name?.let { this.name = it }
description?.let { this.description = it }
thumbnailUrl?.let { this.thumbnailUrl = it }
difficulty?.let { this.difficulty = it }
price?.let { this.price = it }
minParticipants?.let { this.minParticipants = it }
maxParticipants?.let { this.maxParticipants = it }
availableMinutes?.let { this.availableMinutes = it }
expectedMinutesFrom?.let { this.expectedMinutesFrom = it }
expectedMinutesTo?.let { this.expectedMinutesTo = it }
isOpen?.let { this.isOpen = it }
}
}
enum class Difficulty {
VERY_EASY,
EASY,
NORMAL,
HARD,
VERY_HARD
} }

View File

@ -2,31 +2,11 @@ package roomescape.theme.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import java.time.LocalDate
interface ThemeRepository : JpaRepository<ThemeEntity, Long> { interface ThemeRepository: JpaRepository<ThemeEntity, Long> {
@Query(value = """ @Query("SELECT t FROM ThemeEntity t WHERE t.isOpen = true")
SELECT t fun findOpenedThemes(): List<ThemeEntity>
FROM ThemeEntity t
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(startFrom: LocalDate, endAt: LocalDate, count: Int): List<ThemeEntity>
fun existsByName(name: String): Boolean fun existsByName(name: String): Boolean
}
@Query(value = """
SELECT EXISTS(
SELECT 1
FROM ReservationEntity r
WHERE r.theme._id = :id
)
"""
)
fun isReservedTheme(id: Long): Boolean
}

View File

@ -1,66 +0,0 @@
package roomescape.theme.infrastructure.persistence.v2
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Table
import roomescape.common.entity.AuditingBaseEntity
@Entity
@Table(name = "theme")
class ThemeEntityV2(
id: Long,
var name: String,
var description: String,
var thumbnailUrl: String,
@Enumerated(value = EnumType.STRING)
var difficulty: Difficulty,
var price: Int,
var minParticipants: Short,
var maxParticipants: Short,
var availableMinutes: Short,
var expectedMinutesFrom: Short,
var expectedMinutesTo: Short,
@Column(columnDefinition = "TINYINT", length = 1)
var isOpen: Boolean
) : AuditingBaseEntity(id) {
fun modifyIfNotNull(
name: String?,
description: String?,
thumbnailUrl: String?,
difficulty: Difficulty?,
price: Int?,
minParticipants: Short?,
maxParticipants: Short?,
availableMinutes: Short?,
expectedMinutesFrom: Short?,
expectedMinutesTo: Short?,
isOpen: Boolean?
) {
name?.let { this.name = it }
description?.let { this.description = it }
thumbnailUrl?.let { this.thumbnailUrl = it }
difficulty?.let { this.difficulty = it }
price?.let { this.price = it }
minParticipants?.let { this.minParticipants = it }
maxParticipants?.let { this.maxParticipants = it }
availableMinutes?.let { this.availableMinutes = it }
expectedMinutesFrom?.let { this.expectedMinutesFrom = it }
expectedMinutesTo?.let { this.expectedMinutesTo = it }
isOpen?.let { this.isOpen = it }
}
}
enum class Difficulty {
VERY_EASY,
EASY,
NORMAL,
HARD,
VERY_HARD
}

View File

@ -1,12 +0,0 @@
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
}

View File

@ -1,51 +1,69 @@
package roomescape.theme.web package roomescape.theme.web
import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.business.ThemeService import roomescape.theme.business.ThemeService
import roomescape.theme.docs.ThemeAPI import roomescape.theme.docs.ThemeAPIV2
import java.net.URI import java.net.URI
@RestController @RestController
class ThemeController( class ThemeController(
private val themeService: ThemeService private val themeService: ThemeService,
) : ThemeAPI { ) : ThemeAPIV2 {
@GetMapping("/themes") @PostMapping("/themes/retrieve")
override fun findThemes(): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> { override fun findThemesByIds(
val response: ThemeRetrieveListResponse = themeService.findThemes() @RequestBody request: ThemeListRetrieveRequest
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/themes/most-reserved-last-week")
override fun findMostReservedThemes(
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> { ): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> {
val response: ThemeRetrieveListResponse = themeService.findMostReservedThemes(count) val response = themeService.findThemesByIds(request)
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@PostMapping("/themes") @GetMapping("/v2/themes")
override fun createTheme( override fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> {
@RequestBody @Valid request: ThemeCreateRequest val response = themeService.findThemesForReservation()
): ResponseEntity<CommonApiResponse<ThemeCreateResponse>> {
val themeResponse: ThemeCreateResponse = themeService.createTheme(request)
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}")) return ResponseEntity.ok(CommonApiResponse(response))
.body(CommonApiResponse(themeResponse))
} }
@DeleteMapping("/themes/{id}") @GetMapping("/admin/themes")
override fun deleteTheme( override fun findAdminThemes(): ResponseEntity<CommonApiResponse<AdminThemeSummaryRetrieveListResponse>> {
@PathVariable id: Long val response = themeService.findAdminThemes()
): ResponseEntity<CommonApiResponse<Unit>> {
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/admin/themes/{id}")
override fun findAdminThemeDetail(@PathVariable id: Long): ResponseEntity<CommonApiResponse<AdminThemeDetailRetrieveResponse>> {
val response = themeService.findAdminThemeDetail(id)
return ResponseEntity.ok(CommonApiResponse(response))
}
@PostMapping("/admin/themes")
override fun createTheme(themeCreateRequest: ThemeCreateRequest): ResponseEntity<CommonApiResponse<ThemeCreateResponseV2>> {
val response = themeService.createTheme(themeCreateRequest)
return ResponseEntity.created(URI.create("/admin/themes/${response.id}"))
.body(CommonApiResponse(response))
}
@DeleteMapping("/admin/themes/{id}")
override fun deleteTheme(@PathVariable id: Long): ResponseEntity<CommonApiResponse<Unit>> {
themeService.deleteTheme(id) themeService.deleteTheme(id)
return ResponseEntity.noContent().build() return ResponseEntity.noContent().build()
} }
@PatchMapping("/admin/themes/{id}")
override fun updateTheme(
@PathVariable id: Long,
themeUpdateRequest: ThemeUpdateRequest
): ResponseEntity<CommonApiResponse<Unit>> {
themeService.updateTheme(id, themeUpdateRequest)
return ResponseEntity.ok().build()
}
} }

View File

@ -1,69 +0,0 @@
package roomescape.theme.web
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.business.ThemeServiceV2
import roomescape.theme.docs.ThemeAPIV2
import java.net.URI
@RestController
class ThemeControllerV2(
private val themeService: ThemeServiceV2,
) : ThemeAPIV2 {
@PostMapping("/themes/retrieve")
override fun findThemesByIds(
@RequestBody request: ThemeListRetrieveRequest
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponseV2>> {
val response = themeService.findThemesByIds(request)
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/v2/themes")
override fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponseV2>> {
val response = themeService.findThemesForReservation()
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/admin/themes")
override fun findAdminThemes(): ResponseEntity<CommonApiResponse<AdminThemeSummaryRetrieveListResponse>> {
val response = themeService.findAdminThemes()
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/admin/themes/{id}")
override fun findAdminThemeDetail(@PathVariable id: Long): ResponseEntity<CommonApiResponse<AdminThemeDetailRetrieveResponse>> {
val response = themeService.findAdminThemeDetail(id)
return ResponseEntity.ok(CommonApiResponse(response))
}
@PostMapping("/admin/themes")
override fun createTheme(themeCreateRequestV2: ThemeCreateRequestV2): ResponseEntity<CommonApiResponse<ThemeCreateResponseV2>> {
val response = themeService.createTheme(themeCreateRequestV2)
return ResponseEntity.created(URI.create("/admin/themes/${response.id}"))
.body(CommonApiResponse(response))
}
@DeleteMapping("/admin/themes/{id}")
override fun deleteTheme(@PathVariable id: Long): ResponseEntity<CommonApiResponse<Unit>> {
themeService.deleteTheme(id)
return ResponseEntity.noContent().build()
}
@PatchMapping("/admin/themes/{id}")
override fun updateTheme(
@PathVariable id: Long,
themeUpdateRequest: ThemeUpdateRequest
): ResponseEntity<CommonApiResponse<Unit>> {
themeService.updateTheme(id, themeUpdateRequest)
return ResponseEntity.ok().build()
}
}

View File

@ -1,60 +0,0 @@
package roomescape.theme.web
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import org.hibernate.validator.constraints.URL
import roomescape.theme.infrastructure.persistence.ThemeEntity
data class ThemeCreateRequest(
@NotBlank
@Size(max = 20)
val name: String,
@NotBlank
@Size(max = 100)
val description: String,
@URL
@NotBlank
@Schema(description = "썸네일 이미지 주소(URL).")
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,
val description: String,
@Schema(description = "썸네일 이미지 주소(URL).")
val thumbnail: String
)
fun ThemeEntity.toRetrieveResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse(
id = this.id!!,
name = this.name,
description = this.description,
thumbnail = this.thumbnail
)
data class ThemeRetrieveListResponse(
val themes: List<ThemeRetrieveResponse>
)
fun List<ThemeEntity>.toRetrieveListResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse(
themes = this.map { it.toRetrieveResponse() }
)

View File

@ -1,10 +1,10 @@
package roomescape.theme.web package roomescape.theme.web
import roomescape.theme.infrastructure.persistence.v2.Difficulty import roomescape.theme.infrastructure.persistence.Difficulty
import roomescape.theme.infrastructure.persistence.v2.ThemeEntityV2 import roomescape.theme.infrastructure.persistence.ThemeEntity
import java.time.LocalDateTime import java.time.LocalDateTime
data class ThemeCreateRequestV2( data class ThemeCreateRequest(
val name: String, val name: String,
val description: String, val description: String,
val thumbnailUrl: String, val thumbnailUrl: String,
@ -22,7 +22,7 @@ data class ThemeCreateResponseV2(
val id: Long val id: Long
) )
fun ThemeCreateRequestV2.toEntity(id: Long) = ThemeEntityV2( fun ThemeCreateRequest.toEntity(id: Long) = ThemeEntity(
id = id, id = id,
name = this.name, name = this.name,
description = this.description, description = this.description,
@ -73,7 +73,7 @@ data class AdminThemeSummaryRetrieveResponse(
val isOpen: Boolean val isOpen: Boolean
) )
fun ThemeEntityV2.toAdminThemeSummaryResponse() = AdminThemeSummaryRetrieveResponse( fun ThemeEntity.toAdminThemeSummaryResponse() = AdminThemeSummaryRetrieveResponse(
id = this.id, id = this.id,
name = this.name, name = this.name,
difficulty = this.difficulty, difficulty = this.difficulty,
@ -85,7 +85,7 @@ data class AdminThemeSummaryRetrieveListResponse(
val themes: List<AdminThemeSummaryRetrieveResponse> val themes: List<AdminThemeSummaryRetrieveResponse>
) )
fun List<ThemeEntityV2>.toAdminThemeSummaryListResponse() = AdminThemeSummaryRetrieveListResponse( fun List<ThemeEntity>.toAdminThemeSummaryListResponse() = AdminThemeSummaryRetrieveListResponse(
themes = this.map { it.toAdminThemeSummaryResponse() } themes = this.map { it.toAdminThemeSummaryResponse() }
) )
@ -108,7 +108,7 @@ data class AdminThemeDetailRetrieveResponse(
val updatedBy: String, val updatedBy: String,
) )
fun ThemeEntityV2.toAdminThemeDetailResponse(createUserName: String, updateUserName: String) = fun ThemeEntity.toAdminThemeDetailResponse(createUserName: String, updateUserName: String) =
AdminThemeDetailRetrieveResponse( AdminThemeDetailRetrieveResponse(
id = this.id, id = this.id,
name = this.name, name = this.name,
@ -146,7 +146,7 @@ data class ThemeRetrieveResponseV2(
val expectedMinutesTo: Short val expectedMinutesTo: Short
) )
fun ThemeEntityV2.toRetrieveResponse() = ThemeRetrieveResponseV2( fun ThemeEntity.toRetrieveResponse() = ThemeRetrieveResponseV2(
id = this.id, id = this.id,
name = this.name, name = this.name,
thumbnailUrl = this.thumbnailUrl, thumbnailUrl = this.thumbnailUrl,
@ -160,10 +160,10 @@ fun ThemeEntityV2.toRetrieveResponse() = ThemeRetrieveResponseV2(
expectedMinutesTo = this.expectedMinutesTo expectedMinutesTo = this.expectedMinutesTo
) )
data class ThemeRetrieveListResponseV2( data class ThemeRetrieveListResponse(
val themes: List<ThemeRetrieveResponseV2> val themes: List<ThemeRetrieveResponseV2>
) )
fun List<ThemeEntityV2>.toRetrieveListResponse() = ThemeRetrieveListResponseV2( fun List<ThemeEntity>.toRetrieveListResponse() = ThemeRetrieveListResponse(
themes = this.map { it.toRetrieveResponse() } themes = this.map { it.toRetrieveResponse() }
) )