refactor: 테마 API를 권한별로 분리

This commit is contained in:
이상진 2025-09-15 11:55:27 +09:00
parent 06549e8ac1
commit da88d66505
6 changed files with 167 additions and 46 deletions

View File

@ -16,6 +16,13 @@ import roomescape.theme.web.*
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
/**
* Structure:
* - Public: 모두가 접근 가능한 메서드
* - Store Admin: 매장 관리자가 사용하는 메서드
* - HQ Admin: 본사 관리자가 사용하는 메서드
* - Common: 공통 메서드
*/
@Service @Service
class ThemeService( class ThemeService(
private val themeRepository: ThemeRepository, private val themeRepository: ThemeRepository,
@ -23,6 +30,17 @@ class ThemeService(
private val tsidFactory: TsidFactory, private val tsidFactory: TsidFactory,
private val adminService: AdminService private val adminService: AdminService
) { ) {
// ========================================
// Public (인증 불필요)
// ========================================
@Transactional(readOnly = true)
fun findInfoById(id: Long): ThemeInfoResponse {
log.info { "[ThemeService.findById] 테마 조회 시작: id=$id" }
return findOrThrow(id).toInfoResponse()
.also { log.info { "[ThemeService.findById] 테마 조회 완료: id=$id" } }
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemesByIds(request: ThemeIdListResponse): ThemeInfoListResponse { fun findThemesByIds(request: ThemeIdListResponse): ThemeInfoListResponse {
log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" } log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" }
@ -37,20 +55,14 @@ class ThemeService(
result.add(theme) result.add(theme)
} }
return result.toListResponse().also { return result.toInfoListResponse().also {
log.info { "[ThemeService.findThemesByIds] ${it.themes.size} / ${request.themeIds.size} 개 테마 조회 완료" } log.info { "[ThemeService.findThemesByIds] ${it.themes.size} / ${request.themeIds.size} 개 테마 조회 완료" }
} }
} }
@Transactional(readOnly = true) // ========================================
fun findThemesForReservation(): ThemeInfoListResponse { // HQ Admin (본사)
log.info { "[ThemeService.findThemesForReservation] 예약 페이지에서의 테마 목록 조회 시작" } // ========================================
return themeRepository.findOpenedThemes()
.toListResponse()
.also { log.info { "[ThemeService.findThemesForReservation] ${it.themes.size}개 테마 조회 완료" } }
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findAdminThemes(): AdminThemeSummaryListResponse { fun findAdminThemes(): AdminThemeSummaryListResponse {
log.info { "[ThemeService.findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" } log.info { "[ThemeService.findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" }
@ -73,14 +85,6 @@ class ThemeService(
.also { log.info { "[ThemeService.findAdminThemeDetail] 테마 상세 조회 완료: id=$id, name=${theme.name}" } } .also { log.info { "[ThemeService.findAdminThemeDetail] 테마 상세 조회 완료: id=$id, name=${theme.name}" } }
} }
@Transactional(readOnly = true)
fun findSummaryById(id: Long): ThemeInfoResponse {
log.info { "[ThemeService.findById] 테마 조회 시작: id=$id" }
return findOrThrow(id).toSummaryResponse()
.also { log.info { "[ThemeService.findById] 테마 조회 완료: id=$id" } }
}
@Transactional @Transactional
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponseV2 { fun createTheme(request: ThemeCreateRequest): ThemeCreateResponseV2 {
log.info { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" } log.info { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" }
@ -137,6 +141,24 @@ class ThemeService(
} }
} }
// ========================================
// Store Admin (매장)
// ========================================
@Transactional(readOnly = true)
fun findActiveThemes(): SimpleActiveThemeListResponse {
log.info { "[ThemeService.findActiveThemes] open 상태인 모든 테마 조회 시작" }
return themeRepository.findActiveThemes()
.toSimpleActiveThemeResponse()
.also {
log.info { "[ThemeService.findActiveThemes] ${it.themes.size}개 테마 조회 완료" }
}
}
// ========================================
// Common (공통 메서드)
// ========================================
private fun findOrThrow(id: Long): ThemeEntity { private fun findOrThrow(id: Long): ThemeEntity {
log.info { "[ThemeService.findOrThrow] 테마 조회 시작: id=$id" } log.info { "[ThemeService.findOrThrow] 테마 조회 시작: id=$id" }

View File

@ -16,7 +16,7 @@ import roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.web.* import roomescape.theme.web.*
@Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.") @Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.")
interface ThemeAPI { interface HQAdminThemeAPI {
@AdminOnly(type = AdminType.HQ, privilege = Privilege.READ_SUMMARY) @AdminOnly(type = AdminType.HQ, privilege = Privilege.READ_SUMMARY)
@Operation(summary = "모든 테마 조회", description = "관리자 페이지에서 요약된 테마 목록을 조회합니다.", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "모든 테마 조회", description = "관리자 페이지에서 요약된 테마 목록을 조회합니다.", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
@ -44,14 +44,23 @@ interface ThemeAPI {
@PathVariable id: Long, @PathVariable id: Long,
@Valid @RequestBody themeUpdateRequest: ThemeUpdateRequest @Valid @RequestBody themeUpdateRequest: ThemeUpdateRequest
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
}
@Public interface StoreAdminThemeAPI {
@Operation(summary = "예약 페이지에서 모든 테마 조회", description = "모든 테마를 조회합니다.") @AdminOnly(type = AdminType.STORE, privilege = Privilege.READ_SUMMARY)
@Operation(summary = "테마 조회", description = "현재 open 상태인 모든 테마 ID + 이름 조회", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>> fun findActiveThemes(): ResponseEntity<CommonApiResponse<SimpleActiveThemeListResponse>>
}
interface PublicThemeAPI {
@Public @Public
@Operation(summary = "예약 페이지에서 입력한 날짜에 가능한 테마 조회", description = "입력한 날짜에 가능한 테마를 조회합니다.") @Operation(summary = "입력된 모든 ID에 대한 테마 조회")
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findThemesByIds(request: ThemeIdListResponse): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>> fun findThemesByIds(request: ThemeIdListResponse): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>>
@Public
@Operation(summary = "입력된 테마 ID에 대한 정보 조회")
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findThemeInfoById(@PathVariable id: Long): ResponseEntity<CommonApiResponse<ThemeInfoResponse>>
} }

View File

@ -4,30 +4,14 @@ 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.HQAdminThemeAPI
import roomescape.theme.docs.StoreAdminThemeAPI
import java.net.URI import java.net.URI
@RestController @RestController
class ThemeController( class HQAdminThemeController(
private val themeService: ThemeService, private val themeService: ThemeService,
) : ThemeAPI { ) : HQAdminThemeAPI {
@PostMapping("/themes/retrieve")
override fun findThemesByIds(
@RequestBody request: ThemeIdListResponse
): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>> {
val response = themeService.findThemesByIds(request)
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/themes")
override fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>> {
val response = themeService.findThemesForReservation()
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/admin/themes") @GetMapping("/admin/themes")
override fun findAdminThemes(): ResponseEntity<CommonApiResponse<AdminThemeSummaryListResponse>> { override fun findAdminThemes(): ResponseEntity<CommonApiResponse<AdminThemeSummaryListResponse>> {
val response = themeService.findAdminThemes() val response = themeService.findAdminThemes()
@ -67,3 +51,16 @@ class ThemeController(
return ResponseEntity.ok().build() return ResponseEntity.ok().build()
} }
} }
@RestController
class StoreAdminController(
private val themeService: ThemeService
) : StoreAdminThemeAPI {
@GetMapping("/admin/themes/active")
override fun findActiveThemes(): ResponseEntity<CommonApiResponse<SimpleActiveThemeListResponse>> {
val response = themeService.findActiveThemes()
return ResponseEntity.ok(CommonApiResponse(response))
}
}

View File

@ -5,6 +5,18 @@ import roomescape.theme.infrastructure.persistence.Difficulty
import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity
import java.time.LocalDateTime import java.time.LocalDateTime
/**
* Theme API DTO
*
* Structure:
* - HQ Admin DTO: 본사 관리자가 사용하는 테마 관련 DTO들
* - Store Admin DTO: 매장 관리자가 사용하는 테마 관련 DTO들
*/
// ========================================
// HQ Admin DTO (본사)
// ========================================
data class ThemeCreateRequest( data class ThemeCreateRequest(
val name: String, val name: String,
val description: String, val description: String,
@ -129,9 +141,9 @@ fun ThemeEntity.toAdminThemeDetailResponse(createdBy: OperatorInfo, updatedBy: O
updatedBy = updatedBy updatedBy = updatedBy
) )
data class ThemeIdListResponse( // ========================================
val themeIds: List<Long> // Store Admin DTO
) // ========================================
data class ThemeInfoResponse( data class ThemeInfoResponse(
val id: Long, val id: Long,

View File

@ -0,0 +1,37 @@
package roomescape.theme.web
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.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.business.ThemeService
import roomescape.theme.docs.PublicThemeAPI
@RestController
@RequestMapping("/themes")
class PublicThemeController(
private val themeService: ThemeService,
): PublicThemeAPI {
@PostMapping("/batch")
override fun findThemesByIds(
@RequestBody request: ThemeIdListRequest
): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>> {
val response = themeService.findThemesByIds(request)
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/{id}")
override fun findThemeInfoById(
@PathVariable id: Long
): ResponseEntity<CommonApiResponse<ThemeInfoResponse>> {
val response = themeService.findInfoById(id)
return ResponseEntity.ok(CommonApiResponse(response))
}
}

View File

@ -0,0 +1,44 @@
package roomescape.theme.web
import roomescape.theme.infrastructure.persistence.Difficulty
import roomescape.theme.infrastructure.persistence.ThemeEntity
data class ThemeIdListRequest(
val themeIds: List<Long>
)
data class ThemeInfoResponse(
val id: Long,
val name: String,
val thumbnailUrl: String,
val description: String,
val difficulty: Difficulty,
val price: Int,
val minParticipants: Short,
val maxParticipants: Short,
val availableMinutes: Short,
val expectedMinutesFrom: Short,
val expectedMinutesTo: Short
)
fun ThemeEntity.toInfoResponse() = ThemeInfoResponse(
id = this.id,
name = this.name,
thumbnailUrl = this.thumbnailUrl,
description = this.description,
difficulty = this.difficulty,
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(
themes = this.map { it.toInfoResponse() }
)