diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt index 9dbae3e0..f722db2b 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt @@ -20,7 +20,7 @@ import roomescape.schedule.business.ScheduleService import roomescape.schedule.infrastructure.persistence.ScheduleStatus import roomescape.schedule.web.ScheduleSummaryResponse import roomescape.schedule.web.ScheduleUpdateRequest -import roomescape.theme.business.ThemeServiceV2 +import roomescape.theme.business.ThemeService import roomescape.theme.web.ThemeRetrieveResponseV2 import java.time.LocalDateTime @@ -31,7 +31,7 @@ class ReservationService( private val reservationRepository: ReservationRepository, private val scheduleService: ScheduleService, private val memberService: MemberService, - private val themeService: ThemeServiceV2, + private val themeService: ThemeService, private val canceledReservationRepository: CanceledReservationRepository, private val tsidFactory: TsidFactory, private val paymentService: PaymentService diff --git a/src/main/kotlin/roomescape/theme/business/ThemeService.kt b/src/main/kotlin/roomescape/theme/business/ThemeService.kt index 741195c3..8f15ed41 100644 --- a/src/main/kotlin/roomescape/theme/business/ThemeService.kt +++ b/src/main/kotlin/roomescape/theme/business/ThemeService.kt @@ -1,67 +1,141 @@ 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.theme.implement.ThemeFinder -import roomescape.theme.implement.ThemeWriter +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.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.theme.web.* -import java.time.LocalDate -private val log = KotlinLogging.logger {} +private val log: KLogger = KotlinLogging.logger {} @Service class ThemeService( - private val themeFinder: ThemeFinder, - private val themeWriter: ThemeWriter, + private val themeRepository: ThemeRepository, + private val tsidFactory: TsidFactory, + private val memberService: MemberService, + private val themeValidator: ThemeValidator ) { @Transactional(readOnly = true) - fun findById(id: Long): ThemeEntity { - log.debug { "[ThemeService.findById] 시작: themeId=$id" } + fun findThemesByIds(request: ThemeListRetrieveRequest): ThemeRetrieveListResponse { + log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" } - return themeFinder.findById(id) - .also { log.info { "[ThemeService.findById] 완료: themeId=$id, name=${it.name}" } } + return request.themeIds + .map { findOrThrow(it) } + .toRetrieveListResponse() + .also { log.info { "[ThemeService.findThemesByIds] ${it.themes.size}개 테마 조회 완료" } } } @Transactional(readOnly = true) - fun findThemes(): ThemeRetrieveListResponse { - log.debug { "[ThemeService.findThemes] 시작" } + fun findThemesForReservation(): ThemeRetrieveListResponse { + log.info { "[ThemeService.findThemesForReservation] 예약 페이지에서의 테마 목록 조회 시작" } - return themeFinder.findAll() + return themeRepository.findOpenedThemes() .toRetrieveListResponse() - .also { log.info { "[ThemeService.findThemes] 완료. ${it.themes.size}개 반환" } } + .also { log.info { "[ThemeService.findThemesForReservation] ${it.themes.size}개 테마 조회 완료" } } } @Transactional(readOnly = true) - fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse { - log.debug { "[ThemeService.findMostReservedThemes] 시작: count=$count" } + fun findAdminThemes(): AdminThemeSummaryRetrieveListResponse { + log.info { "[ThemeService.findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" } - val today = LocalDate.now() - val startFrom = today.minusDays(7) - val endAt = today.minusDays(1) + return themeRepository.findAll() + .toAdminThemeSummaryListResponse() + .also { log.info { "[ThemeService.findAdminThemes] ${it.themes.size}개 테마 조회 완료" } } + } - return themeFinder.findMostReservedThemes(count, startFrom, endAt) - .toRetrieveListResponse() - .also { log.info { "[ThemeService.findMostReservedThemes] ${it.themes.size}개 반환" } } + @Transactional(readOnly = true) + fun findAdminThemeDetail(id: Long): AdminThemeDetailRetrieveResponse { + 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 - fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse { - log.debug { "[ThemeService.createTheme] 시작: name=${request.name}" } + fun createTheme(request: ThemeCreateRequest): ThemeCreateResponseV2 { + log.info { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" } - return themeWriter.create(request.name, request.description, request.thumbnail) - .toCreateResponse() - .also { log.info { "[ThemeService.createTheme] 테마 생성 완료: name=${it.name} themeId=${it.id}" } } + themeValidator.validateCanCreate(request) + + 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 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) - .also { log.info { "[ThemeService.deleteTheme] 완료: themeId=$id, name=${theme.name}" } } + 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: 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) + } } } diff --git a/src/main/kotlin/roomescape/theme/business/ThemeServiceV2.kt b/src/main/kotlin/roomescape/theme/business/ThemeServiceV2.kt deleted file mode 100644 index dead4c10..00000000 --- a/src/main/kotlin/roomescape/theme/business/ThemeServiceV2.kt +++ /dev/null @@ -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) - } - } -} diff --git a/src/main/kotlin/roomescape/theme/business/ThemeValidatorV2.kt b/src/main/kotlin/roomescape/theme/business/ThemeValidator.kt similarity index 94% rename from src/main/kotlin/roomescape/theme/business/ThemeValidatorV2.kt rename to src/main/kotlin/roomescape/theme/business/ThemeValidator.kt index 522b7187..6de7e205 100644 --- a/src/main/kotlin/roomescape/theme/business/ThemeValidatorV2.kt +++ b/src/main/kotlin/roomescape/theme/business/ThemeValidator.kt @@ -5,8 +5,8 @@ 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.infrastructure.persistence.ThemeRepository +import roomescape.theme.web.ThemeCreateRequest import roomescape.theme.web.ThemeUpdateRequest private val log: KLogger = KotlinLogging.logger {} @@ -16,8 +16,8 @@ const val MIN_PARTICIPANTS = 1 const val MIN_DURATION = 1 @Component -class ThemeValidatorV2( - private val themeRepository: ThemeRepositoryV2, +class ThemeValidator( + private val themeRepository: ThemeRepository, ) { fun validateCanUpdate(request: ThemeUpdateRequest) { validateProperties( @@ -30,7 +30,7 @@ class ThemeValidatorV2( ) } - fun validateCanCreate(request: ThemeCreateRequestV2) { + fun validateCanCreate(request: ThemeCreateRequest) { if (themeRepository.existsByName(request.name)) { log.info { "[ThemeValidator.validateCanCreate] 이름 중복으로 인한 실패: name=${request.name}" } throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED) diff --git a/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt b/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt deleted file mode 100644 index 0a9db7dc..00000000 --- a/src/main/kotlin/roomescape/theme/docs/ThemeAPI.kt +++ /dev/null @@ -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> - - @Operation(summary = "가장 많이 예약된 테마 조회") - @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun findMostReservedThemes( - @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int - ): ResponseEntity> - - @Admin - @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) - @ApiResponses( - ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true), - ) - fun createTheme( - @Valid @RequestBody request: ThemeCreateRequest, - ): ResponseEntity> - - @Admin - @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) - @ApiResponses( - ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true), - ) - fun deleteTheme( - @PathVariable id: Long - ): ResponseEntity> -} diff --git a/src/main/kotlin/roomescape/theme/docs/ThemeApiV2.kt b/src/main/kotlin/roomescape/theme/docs/ThemeApi.kt similarity index 91% rename from src/main/kotlin/roomescape/theme/docs/ThemeApiV2.kt rename to src/main/kotlin/roomescape/theme/docs/ThemeApi.kt index 61e94dd1..fa270bc4 100644 --- a/src/main/kotlin/roomescape/theme/docs/ThemeApiV2.kt +++ b/src/main/kotlin/roomescape/theme/docs/ThemeApi.kt @@ -13,11 +13,11 @@ import roomescape.auth.web.support.LoginRequired import roomescape.common.dto.response.CommonApiResponse import roomescape.theme.web.AdminThemeDetailRetrieveResponse import roomescape.theme.web.AdminThemeSummaryRetrieveListResponse -import roomescape.theme.web.ThemeCreateRequestV2 +import roomescape.theme.web.ThemeCreateRequest import roomescape.theme.web.ThemeCreateResponseV2 import roomescape.theme.web.ThemeListRetrieveRequest import roomescape.theme.web.ThemeUpdateRequest -import roomescape.theme.web.ThemeRetrieveListResponseV2 +import roomescape.theme.web.ThemeRetrieveListResponse @Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.") interface ThemeAPIV2 { @@ -35,7 +35,7 @@ interface ThemeAPIV2 { @Admin @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun createTheme(@Valid @RequestBody themeCreateRequestV2: ThemeCreateRequestV2): ResponseEntity> + fun createTheme(@Valid @RequestBody themeCreateRequest: ThemeCreateRequest): ResponseEntity> @Admin @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) @@ -53,10 +53,10 @@ interface ThemeAPIV2 { @LoginRequired @Operation(summary = "예약 페이지에서 모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun findUserThemes(): ResponseEntity> + fun findUserThemes(): ResponseEntity> @LoginRequired @Operation(summary = "예약 페이지에서 입력한 날짜에 가능한 테마 조회", description = "입력한 날짜에 가능한 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) - fun findThemesByIds(request: ThemeListRetrieveRequest): ResponseEntity> + fun findThemesByIds(request: ThemeListRetrieveRequest): ResponseEntity> } diff --git a/src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt b/src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt deleted file mode 100644 index 42a86062..00000000 --- a/src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt +++ /dev/null @@ -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 { - 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 { - log.debug { "[ThemeFinder.findMostReservedThemes] 시작. count=$count, startFrom=$startFrom, endAt=$endAt" } - - return themeRepository.findPopularThemes(startFrom, endAt, count) - .also { log.debug { "[ThemeFinder.findMostReservedThemes] ${it.size} / ${count}개 테마 조회 완료" } } - } -} diff --git a/src/main/kotlin/roomescape/theme/implement/ThemeValidator.kt b/src/main/kotlin/roomescape/theme/implement/ThemeValidator.kt deleted file mode 100644 index 6887d894..00000000 --- a/src/main/kotlin/roomescape/theme/implement/ThemeValidator.kt +++ /dev/null @@ -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" } - } -} diff --git a/src/main/kotlin/roomescape/theme/implement/ThemeWriter.kt b/src/main/kotlin/roomescape/theme/implement/ThemeWriter.kt deleted file mode 100644 index 3f07b335..00000000 --- a/src/main/kotlin/roomescape/theme/implement/ThemeWriter.kt +++ /dev/null @@ -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}" } } - } -} diff --git a/src/main/kotlin/roomescape/theme/infrastructure/persistence/ThemeEntity.kt b/src/main/kotlin/roomescape/theme/infrastructure/persistence/ThemeEntity.kt index 4abd4383..efd0bd7f 100644 --- a/src/main/kotlin/roomescape/theme/infrastructure/persistence/ThemeEntity.kt +++ b/src/main/kotlin/roomescape/theme/infrastructure/persistence/ThemeEntity.kt @@ -2,25 +2,65 @@ package roomescape.theme.infrastructure.persistence import jakarta.persistence.Column import jakarta.persistence.Entity -import jakarta.persistence.Id +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated import jakarta.persistence.Table -import roomescape.common.entity.BaseEntity +import roomescape.common.entity.AuditingBaseEntity @Entity -@Table(name = "themes") +@Table(name = "theme") class ThemeEntity( - @Id - @Column(name = "theme_id") - private var _id: Long?, + id: Long, - @Column(name = "name", nullable = false) var name: String, - - @Column(name = "description", nullable = false) var description: String, + var thumbnailUrl: String, - @Column(name = "thumbnail", nullable = false) - var thumbnail: String, -): BaseEntity() { - override fun getId(): Long? = _id + @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 } diff --git a/src/main/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepository.kt b/src/main/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepository.kt index c7129a93..e173196b 100644 --- a/src/main/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepository.kt +++ b/src/main/kotlin/roomescape/theme/infrastructure/persistence/ThemeRepository.kt @@ -2,31 +2,11 @@ package roomescape.theme.infrastructure.persistence import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query -import java.time.LocalDate -interface ThemeRepository : JpaRepository { +interface ThemeRepository: JpaRepository { - @Query(value = """ - SELECT t - 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 + @Query("SELECT t FROM ThemeEntity t WHERE t.isOpen = true") + fun findOpenedThemes(): List fun existsByName(name: String): Boolean - - @Query(value = """ - SELECT EXISTS( - SELECT 1 - FROM ReservationEntity r - WHERE r.theme._id = :id - ) - """ - ) - fun isReservedTheme(id: Long): Boolean -} +} \ No newline at end of file diff --git a/src/main/kotlin/roomescape/theme/infrastructure/persistence/v2/ThemeEntityV2.kt b/src/main/kotlin/roomescape/theme/infrastructure/persistence/v2/ThemeEntityV2.kt deleted file mode 100644 index fd462878..00000000 --- a/src/main/kotlin/roomescape/theme/infrastructure/persistence/v2/ThemeEntityV2.kt +++ /dev/null @@ -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 -} diff --git a/src/main/kotlin/roomescape/theme/infrastructure/persistence/v2/ThemeRepositoryV2.kt b/src/main/kotlin/roomescape/theme/infrastructure/persistence/v2/ThemeRepositoryV2.kt deleted file mode 100644 index 64f12e7a..00000000 --- a/src/main/kotlin/roomescape/theme/infrastructure/persistence/v2/ThemeRepositoryV2.kt +++ /dev/null @@ -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 { - - @Query("SELECT t FROM ThemeEntityV2 t WHERE t.isOpen = true") - fun findOpenedThemes(): List - - fun existsByName(name: String): Boolean -} \ No newline at end of file diff --git a/src/main/kotlin/roomescape/theme/web/ThemeController.kt b/src/main/kotlin/roomescape/theme/web/ThemeController.kt index 3b12fb3c..732a6341 100644 --- a/src/main/kotlin/roomescape/theme/web/ThemeController.kt +++ b/src/main/kotlin/roomescape/theme/web/ThemeController.kt @@ -1,51 +1,69 @@ package roomescape.theme.web -import io.swagger.v3.oas.annotations.Parameter -import jakarta.validation.Valid import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import roomescape.common.dto.response.CommonApiResponse import roomescape.theme.business.ThemeService -import roomescape.theme.docs.ThemeAPI +import roomescape.theme.docs.ThemeAPIV2 import java.net.URI @RestController class ThemeController( - private val themeService: ThemeService -) : ThemeAPI { + private val themeService: ThemeService, +) : ThemeAPIV2 { - @GetMapping("/themes") - override fun findThemes(): ResponseEntity> { - val response: ThemeRetrieveListResponse = themeService.findThemes() - - return ResponseEntity.ok(CommonApiResponse(response)) - } - - @GetMapping("/themes/most-reserved-last-week") - override fun findMostReservedThemes( - @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int + @PostMapping("/themes/retrieve") + override fun findThemesByIds( + @RequestBody request: ThemeListRetrieveRequest ): ResponseEntity> { - val response: ThemeRetrieveListResponse = themeService.findMostReservedThemes(count) + val response = themeService.findThemesByIds(request) return ResponseEntity.ok(CommonApiResponse(response)) } - @PostMapping("/themes") - override fun createTheme( - @RequestBody @Valid request: ThemeCreateRequest - ): ResponseEntity> { - val themeResponse: ThemeCreateResponse = themeService.createTheme(request) + @GetMapping("/v2/themes") + override fun findUserThemes(): ResponseEntity> { + val response = themeService.findThemesForReservation() - return ResponseEntity.created(URI.create("/themes/${themeResponse.id}")) - .body(CommonApiResponse(themeResponse)) + return ResponseEntity.ok(CommonApiResponse(response)) } - @DeleteMapping("/themes/{id}") - override fun deleteTheme( - @PathVariable id: Long - ): ResponseEntity> { + @GetMapping("/admin/themes") + override fun findAdminThemes(): ResponseEntity> { + val response = themeService.findAdminThemes() + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @GetMapping("/admin/themes/{id}") + override fun findAdminThemeDetail(@PathVariable id: Long): ResponseEntity> { + val response = themeService.findAdminThemeDetail(id) + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @PostMapping("/admin/themes") + override fun createTheme(themeCreateRequest: ThemeCreateRequest): ResponseEntity> { + 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> { themeService.deleteTheme(id) return ResponseEntity.noContent().build() } + + @PatchMapping("/admin/themes/{id}") + override fun updateTheme( + @PathVariable id: Long, + themeUpdateRequest: ThemeUpdateRequest + ): ResponseEntity> { + themeService.updateTheme(id, themeUpdateRequest) + + return ResponseEntity.ok().build() + } } diff --git a/src/main/kotlin/roomescape/theme/web/ThemeControllerV2.kt b/src/main/kotlin/roomescape/theme/web/ThemeControllerV2.kt deleted file mode 100644 index bdf454a0..00000000 --- a/src/main/kotlin/roomescape/theme/web/ThemeControllerV2.kt +++ /dev/null @@ -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> { - val response = themeService.findThemesByIds(request) - - return ResponseEntity.ok(CommonApiResponse(response)) - } - - @GetMapping("/v2/themes") - override fun findUserThemes(): ResponseEntity> { - val response = themeService.findThemesForReservation() - - return ResponseEntity.ok(CommonApiResponse(response)) - } - - @GetMapping("/admin/themes") - override fun findAdminThemes(): ResponseEntity> { - val response = themeService.findAdminThemes() - - return ResponseEntity.ok(CommonApiResponse(response)) - } - - @GetMapping("/admin/themes/{id}") - override fun findAdminThemeDetail(@PathVariable id: Long): ResponseEntity> { - val response = themeService.findAdminThemeDetail(id) - - return ResponseEntity.ok(CommonApiResponse(response)) - } - - @PostMapping("/admin/themes") - override fun createTheme(themeCreateRequestV2: ThemeCreateRequestV2): ResponseEntity> { - 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> { - themeService.deleteTheme(id) - - return ResponseEntity.noContent().build() - } - - @PatchMapping("/admin/themes/{id}") - override fun updateTheme( - @PathVariable id: Long, - themeUpdateRequest: ThemeUpdateRequest - ): ResponseEntity> { - themeService.updateTheme(id, themeUpdateRequest) - - return ResponseEntity.ok().build() - } -} diff --git a/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt b/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt deleted file mode 100644 index 32b14485..00000000 --- a/src/main/kotlin/roomescape/theme/web/ThemeDTO.kt +++ /dev/null @@ -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 -) - -fun List.toRetrieveListResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse( - themes = this.map { it.toRetrieveResponse() } -) diff --git a/src/main/kotlin/roomescape/theme/web/ThemeDtoV2.kt b/src/main/kotlin/roomescape/theme/web/ThemeDto.kt similarity index 86% rename from src/main/kotlin/roomescape/theme/web/ThemeDtoV2.kt rename to src/main/kotlin/roomescape/theme/web/ThemeDto.kt index d5b6461a..617c71e3 100644 --- a/src/main/kotlin/roomescape/theme/web/ThemeDtoV2.kt +++ b/src/main/kotlin/roomescape/theme/web/ThemeDto.kt @@ -1,10 +1,10 @@ package roomescape.theme.web -import roomescape.theme.infrastructure.persistence.v2.Difficulty -import roomescape.theme.infrastructure.persistence.v2.ThemeEntityV2 +import roomescape.theme.infrastructure.persistence.Difficulty +import roomescape.theme.infrastructure.persistence.ThemeEntity import java.time.LocalDateTime -data class ThemeCreateRequestV2( +data class ThemeCreateRequest( val name: String, val description: String, val thumbnailUrl: String, @@ -22,7 +22,7 @@ data class ThemeCreateResponseV2( val id: Long ) -fun ThemeCreateRequestV2.toEntity(id: Long) = ThemeEntityV2( +fun ThemeCreateRequest.toEntity(id: Long) = ThemeEntity( id = id, name = this.name, description = this.description, @@ -73,7 +73,7 @@ data class AdminThemeSummaryRetrieveResponse( val isOpen: Boolean ) -fun ThemeEntityV2.toAdminThemeSummaryResponse() = AdminThemeSummaryRetrieveResponse( +fun ThemeEntity.toAdminThemeSummaryResponse() = AdminThemeSummaryRetrieveResponse( id = this.id, name = this.name, difficulty = this.difficulty, @@ -85,7 +85,7 @@ data class AdminThemeSummaryRetrieveListResponse( val themes: List ) -fun List.toAdminThemeSummaryListResponse() = AdminThemeSummaryRetrieveListResponse( +fun List.toAdminThemeSummaryListResponse() = AdminThemeSummaryRetrieveListResponse( themes = this.map { it.toAdminThemeSummaryResponse() } ) @@ -108,7 +108,7 @@ data class AdminThemeDetailRetrieveResponse( val updatedBy: String, ) -fun ThemeEntityV2.toAdminThemeDetailResponse(createUserName: String, updateUserName: String) = +fun ThemeEntity.toAdminThemeDetailResponse(createUserName: String, updateUserName: String) = AdminThemeDetailRetrieveResponse( id = this.id, name = this.name, @@ -146,7 +146,7 @@ data class ThemeRetrieveResponseV2( val expectedMinutesTo: Short ) -fun ThemeEntityV2.toRetrieveResponse() = ThemeRetrieveResponseV2( +fun ThemeEntity.toRetrieveResponse() = ThemeRetrieveResponseV2( id = this.id, name = this.name, thumbnailUrl = this.thumbnailUrl, @@ -160,10 +160,10 @@ fun ThemeEntityV2.toRetrieveResponse() = ThemeRetrieveResponseV2( expectedMinutesTo = this.expectedMinutesTo ) -data class ThemeRetrieveListResponseV2( +data class ThemeRetrieveListResponse( val themes: List ) -fun List.toRetrieveListResponse() = ThemeRetrieveListResponseV2( +fun List.toRetrieveListResponse() = ThemeRetrieveListResponse( themes = this.map { it.toRetrieveResponse() } -) \ No newline at end of file +)