[#46] 더미 데이터 생성 및 1개의 슬로우쿼리 개선 #47

Merged
pricelees merged 15 commits from feat/#46 into main 2025-09-27 06:38:44 +00:00
5 changed files with 77 additions and 26 deletions
Showing only changes of commit e552636aec - Show all commits

View File

@ -9,11 +9,13 @@ import org.springframework.transaction.annotation.Transactional
import roomescape.admin.business.AdminService
import roomescape.common.config.next
import roomescape.common.dto.AuditInfo
import roomescape.common.util.DateUtils
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: KLogger = KotlinLogging.logger {}
@ -43,13 +45,18 @@ class ThemeService(
}
@Transactional(readOnly = true)
fun findAllInfosByIds(request: ThemeIdListRequest): ThemeInfoListResponse {
log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" }
val result: List<ThemeEntity> = themeRepository.findAllByIdIn(request.themeIds)
fun findMostReservedThemeLastWeek(count: Int): ThemeInfoListResponse {
log.info { "[ThemeService.findMostReservedThemeLastWeek] 인기 테마 조회 시작: count=$count" }
return result.toInfoListResponse().also {
log.info { "[ThemeService.findThemesByIds] ${it.themes.size} / ${request.themeIds.size} 개 테마 조회 완료" }
val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(LocalDate.now())
val previousWeekSaturday = previousWeekSunday.plusDays(6)
return themeRepository.findMostReservedThemeByDateAndCount(previousWeekSunday, previousWeekSaturday, count)
.toListResponse()
.also {
log.info { "[ThemeService.findMostReservedThemeLastWeek] ${it.themes.size} / $count 개의 인기 테마 조회 완료" }
}
}
// ========================================

View File

@ -5,8 +5,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import roomescape.admin.infrastructure.persistence.AdminType
import roomescape.admin.infrastructure.persistence.Privilege
import roomescape.auth.web.support.AdminOnly
@ -50,13 +52,13 @@ interface AdminThemeAPI {
}
interface PublicThemeAPI {
@Public
@Operation(summary = "입력된 모든 ID에 대한 테마 정보 조회")
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
fun findThemeInfosByIds(request: ThemeIdListRequest): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>>
@Public
@Operation(summary = "입력된 테마 ID에 대한 정보 조회")
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
fun findThemeInfoById(@PathVariable id: Long): ResponseEntity<CommonApiResponse<ThemeInfoResponse>>
@Public
@Operation(summary = "지난 주에 가장 많이 예약된 count 개의 테마 조회")
@GetMapping("/most-reserved")
fun findMostReservedThemeLastWeek(@RequestParam count: Int): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>>
}

View File

@ -2,6 +2,8 @@ package roomescape.theme.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import roomescape.theme.business.domain.ThemeInfo
import java.time.LocalDate
interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
@ -10,5 +12,32 @@ interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
fun existsByName(name: String): Boolean
fun findAllByIdIn(themeIds: List<Long>): List<ThemeEntity>
@Query(
value = """
SELECT
t.id, t.name, t.description, t.difficulty, t.thumbnail_url, t.price,
t.min_participants, t.max_participants,
t.available_minutes, t.expected_minutes_from, t.expected_minutes_to
FROM
theme t
JOIN (
SELECT
s.theme_id, count(*) as reservation_count
FROM
schedule s
JOIN
reservation r ON s.id = r.schedule_id AND r.status = 'CONFIRMED'
WHERE
s.status = 'RESERVED'
AND (s.date BETWEEN :startFrom AND :endAt)
GROUP BY
s.theme_id
ORDER BY
reservation_count desc
LIMIT :count
) ranked_themes ON t.id = ranked_themes.theme_id
""",
nativeQuery = true
)
fun findMostReservedThemeByDateAndCount(startFrom: LocalDate, endAt: LocalDate, count: Int): List<ThemeInfo>
}

View File

@ -1,6 +1,5 @@
package roomescape.theme.web
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import roomescape.common.dto.response.CommonApiResponse
@ -12,15 +11,6 @@ import roomescape.theme.docs.PublicThemeAPI
class ThemeController(
private val themeService: ThemeService,
) : PublicThemeAPI {
@PostMapping("/batch")
override fun findThemeInfosByIds(
@Valid @RequestBody request: ThemeIdListRequest
): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>> {
val response = themeService.findAllInfosByIds(request)
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/{id}")
override fun findThemeInfoById(
@PathVariable id: Long
@ -29,4 +19,13 @@ class ThemeController(
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/most-reserved")
override fun findMostReservedThemeLastWeek(
@RequestParam count: Int
): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>> {
val response = themeService.findMostReservedThemeLastWeek(count)
return ResponseEntity.ok(CommonApiResponse(response))
}
}

View File

@ -1,6 +1,6 @@
package roomescape.theme.web
import roomescape.theme.infrastructure.persistence.Difficulty
import roomescape.theme.business.domain.ThemeInfo
import roomescape.theme.infrastructure.persistence.ThemeEntity
data class ThemeIdListRequest(
@ -12,7 +12,7 @@ data class ThemeInfoResponse(
val name: String,
val thumbnailUrl: String,
val description: String,
val difficulty: Difficulty,
val difficulty: String,
val price: Int,
val minParticipants: Short,
val maxParticipants: Short,
@ -21,7 +21,7 @@ data class ThemeInfoResponse(
val expectedMinutesTo: Short
)
fun ThemeEntity.toInfoResponse() = ThemeInfoResponse(
fun ThemeInfo.toInfoResponse() = ThemeInfoResponse(
id = this.id,
name = this.name,
thumbnailUrl = this.thumbnailUrl,
@ -35,10 +35,24 @@ fun ThemeEntity.toInfoResponse() = ThemeInfoResponse(
expectedMinutesTo = this.expectedMinutesTo
)
fun ThemeEntity.toInfoResponse() = ThemeInfoResponse(
id = this.id,
name = this.name,
thumbnailUrl = this.thumbnailUrl,
description = this.description,
difficulty = this.difficulty.name,
price = this.price,
minParticipants = this.minParticipants,
maxParticipants = this.maxParticipants,
availableMinutes = this.availableMinutes,
expectedMinutesFrom = this.expectedMinutesFrom,
expectedMinutesTo = this.expectedMinutesTo
)
data class ThemeInfoListResponse(
val themes: List<ThemeInfoResponse>
)
fun List<ThemeEntity>.toInfoListResponse() = ThemeInfoListResponse(
fun List<ThemeInfo>.toListResponse() = ThemeInfoListResponse(
themes = this.map { it.toInfoResponse() }
)