[#44] 매장 기능 도입 #45

Merged
pricelees merged 116 commits from feat/#44 into main 2025-09-20 03:15:06 +00:00
6 changed files with 167 additions and 46 deletions
Showing only changes of commit da88d66505 - Show all commits

View File

@ -16,6 +16,13 @@ import roomescape.theme.web.*
private val log: KLogger = KotlinLogging.logger {}
/**
* Structure:
* - Public: 모두가 접근 가능한 메서드
* - Store Admin: 매장 관리자가 사용하는 메서드
* - HQ Admin: 본사 관리자가 사용하는 메서드
* - Common: 공통 메서드
*/
@Service
class ThemeService(
private val themeRepository: ThemeRepository,
@ -23,6 +30,17 @@ class ThemeService(
private val tsidFactory: TsidFactory,
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)
fun findThemesByIds(request: ThemeIdListResponse): ThemeInfoListResponse {
log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" }
@ -37,20 +55,14 @@ class ThemeService(
result.add(theme)
}
return result.toListResponse().also {
return result.toInfoListResponse().also {
log.info { "[ThemeService.findThemesByIds] ${it.themes.size} / ${request.themeIds.size} 개 테마 조회 완료" }
}
}
@Transactional(readOnly = true)
fun findThemesForReservation(): ThemeInfoListResponse {
log.info { "[ThemeService.findThemesForReservation] 예약 페이지에서의 테마 목록 조회 시작" }
return themeRepository.findOpenedThemes()
.toListResponse()
.also { log.info { "[ThemeService.findThemesForReservation] ${it.themes.size}개 테마 조회 완료" } }
}
// ========================================
// HQ Admin (본사)
// ========================================
@Transactional(readOnly = true)
fun findAdminThemes(): AdminThemeSummaryListResponse {
log.info { "[ThemeService.findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" }
@ -73,14 +85,6 @@ class ThemeService(
.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
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponseV2 {
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 {
log.info { "[ThemeService.findOrThrow] 테마 조회 시작: id=$id" }

View File

@ -16,7 +16,7 @@ import roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.web.*
@Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.")
interface ThemeAPI {
interface HQAdminThemeAPI {
@AdminOnly(type = AdminType.HQ, privilege = Privilege.READ_SUMMARY)
@Operation(summary = "모든 테마 조회", description = "관리자 페이지에서 요약된 테마 목록을 조회합니다.", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
@ -44,14 +44,23 @@ interface ThemeAPI {
@PathVariable id: Long,
@Valid @RequestBody themeUpdateRequest: ThemeUpdateRequest
): ResponseEntity<CommonApiResponse<Unit>>
}
@Public
@Operation(summary = "예약 페이지에서 모든 테마 조회", description = "모든 테마를 조회합니다.")
interface StoreAdminThemeAPI {
@AdminOnly(type = AdminType.STORE, privilege = Privilege.READ_SUMMARY)
@Operation(summary = "테마 조회", description = "현재 open 상태인 모든 테마 ID + 이름 조회", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findUserThemes(): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>>
fun findActiveThemes(): ResponseEntity<CommonApiResponse<SimpleActiveThemeListResponse>>
}
interface PublicThemeAPI {
@Public
@Operation(summary = "예약 페이지에서 입력한 날짜에 가능한 테마 조회", description = "입력한 날짜에 가능한 테마를 조회합니다.")
@Operation(summary = "입력된 모든 ID에 대한 테마 조회")
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
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 roomescape.common.dto.response.CommonApiResponse
import roomescape.theme.business.ThemeService
import roomescape.theme.docs.ThemeAPI
import roomescape.theme.docs.HQAdminThemeAPI
import roomescape.theme.docs.StoreAdminThemeAPI
import java.net.URI
@RestController
class ThemeController(
class HQAdminThemeController(
private val themeService: ThemeService,
) : ThemeAPI {
@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))
}
) : HQAdminThemeAPI {
@GetMapping("/admin/themes")
override fun findAdminThemes(): ResponseEntity<CommonApiResponse<AdminThemeSummaryListResponse>> {
val response = themeService.findAdminThemes()
@ -67,3 +51,16 @@ class ThemeController(
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 java.time.LocalDateTime
/**
* Theme API DTO
*
* Structure:
* - HQ Admin DTO: 본사 관리자가 사용하는 테마 관련 DTO들
* - Store Admin DTO: 매장 관리자가 사용하는 테마 관련 DTO들
*/
// ========================================
// HQ Admin DTO (본사)
// ========================================
data class ThemeCreateRequest(
val name: String,
val description: String,
@ -129,9 +141,9 @@ fun ThemeEntity.toAdminThemeDetailResponse(createdBy: OperatorInfo, updatedBy: O
updatedBy = updatedBy
)
data class ThemeIdListResponse(
val themeIds: List<Long>
)
// ========================================
// Store Admin DTO
// ========================================
data class ThemeInfoResponse(
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() }
)