[#56] 예약 & 결제 프로세스 및 패키지 구조 재정의 #57

Merged
pricelees merged 45 commits from refactor/#56 into main 2025-10-09 09:33:29 +00:00
18 changed files with 344 additions and 313 deletions
Showing only changes of commit c4604ccdde - Show all commits

View File

@ -7,7 +7,7 @@ import com.sangdol.roomescape.reservation.exception.ReservationException
import com.sangdol.roomescape.reservation.web.PendingReservationCreateRequest import com.sangdol.roomescape.reservation.web.PendingReservationCreateRequest
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.schedule.web.ScheduleSummaryResponse import com.sangdol.roomescape.schedule.web.ScheduleSummaryResponse
import com.sangdol.roomescape.theme.web.ThemeInfoResponse import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component import org.springframework.stereotype.Component

View File

@ -0,0 +1,136 @@
package com.sangdol.roomescape.theme.business
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.admin.business.AdminService
import com.sangdol.roomescape.common.types.AuditingInfo
import com.sangdol.roomescape.theme.dto.ThemeDetailResponse
import com.sangdol.roomescape.theme.dto.ThemeSummaryListResponse
import com.sangdol.roomescape.theme.dto.ThemeNameListResponse
import com.sangdol.roomescape.theme.dto.ThemeCreateRequest
import com.sangdol.roomescape.theme.dto.ThemeCreateResponse
import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest
import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.exception.ThemeException
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.mapper.toDetailResponse
import com.sangdol.roomescape.theme.mapper.toSummaryListResponse
import com.sangdol.roomescape.theme.mapper.toEntity
import com.sangdol.roomescape.theme.mapper.toNameListResponse
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
private val log: KLogger = KotlinLogging.logger {}
@Service
class AdminThemeService(
private val themeRepository: ThemeRepository,
private val themeValidator: ThemeValidator,
private val idGenerator: IDGenerator,
private val adminService: AdminService
) {
@Transactional(readOnly = true)
fun findThemeSummaries(): ThemeSummaryListResponse {
log.info { "[findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" }
return themeRepository.findAll()
.toSummaryListResponse()
.also { log.info { "[findAdminThemes] ${it.themes.size}개 테마 조회 완료" } }
}
@Transactional(readOnly = true)
fun findThemeDetail(id: Long): ThemeDetailResponse {
log.info { "[findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" }
val theme: ThemeEntity = findOrThrow(id)
val createdBy = adminService.findOperatorOrUnknown(theme.createdBy)
val updatedBy = adminService.findOperatorOrUnknown(theme.updatedBy)
val audit = AuditingInfo(theme.createdAt, createdBy, theme.updatedAt, updatedBy)
return theme.toDetailResponse(audit)
.also { log.info { "[findAdminThemeDetail] 테마 상세 조회 완료: id=$id, name=${theme.name}" } }
}
@Transactional(readOnly = true)
fun findActiveThemes(): ThemeNameListResponse {
log.info { "[findActiveThemes] open 상태인 모든 테마 조회 시작" }
return themeRepository.findActiveThemes()
.toNameListResponse()
.also {
log.info { "[findActiveThemes] ${it.themes.size}개 테마 조회 완료" }
}
}
@Transactional
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse {
log.info { "[createTheme] 테마 생성 시작: name=${request.name}" }
themeValidator.validateCanCreate(request)
val theme: ThemeEntity = request.toEntity(id = idGenerator.create())
.also { themeRepository.save(it) }
return ThemeCreateResponse(theme.id).also {
log.info { "[createTheme] 테마 생성 완료: id=${theme.id}, name=${theme.name}" }
}
}
@Transactional
fun deleteTheme(id: Long) {
log.info { "[deleteTheme] 테마 삭제 시작: id=${id}" }
val theme: ThemeEntity = findOrThrow(id)
themeRepository.delete(theme).also {
log.info { "[deleteTheme] 테마 삭제 완료: id=$id, name=${theme.name}" }
}
}
@Transactional
fun updateTheme(id: Long, request: ThemeUpdateRequest) {
log.info { "[updateTheme] 테마 수정 시작: id=${id}, request=${request}" }
if (request.isAllParamsNull()) {
log.info { "[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.isActive,
).also {
log.info { "[updateTheme] 테마 수정 완료: id=$id, request=${request}" }
}
}
private fun findOrThrow(id: Long): ThemeEntity {
log.info { "[findOrThrow] 테마 조회 시작: id=$id" }
return themeRepository.findByIdOrNull(id)
?.also { log.info { "[findOrThrow] 테마 조회 완료: id=$id" } }
?: run {
log.warn { "[findOrThrow] 테마 조회 실패: id=$id" }
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
}
}
}

View File

@ -1,11 +0,0 @@
package com.sangdol.roomescape.theme.business
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.TemporalAdjusters
object DateUtils {
fun getSundayOfPreviousWeek(date: LocalDate): LocalDate = date
.minusWeeks(1)
.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY))
}

View File

@ -1,44 +1,38 @@
package com.sangdol.roomescape.theme.business package com.sangdol.roomescape.theme.business
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.common.utils.KoreaDate import com.sangdol.common.utils.KoreaDate
import com.sangdol.roomescape.admin.business.AdminService import com.sangdol.roomescape.theme.dto.ThemeInfoListResponse
import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.exception.ThemeException import com.sangdol.roomescape.theme.exception.ThemeException
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.web.* import com.sangdol.roomescape.theme.mapper.toInfoResponse
import com.sangdol.roomescape.theme.mapper.toListResponse
import io.github.oshai.kotlinlogging.KLogger 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.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 java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.TemporalAdjusters
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
private val themeValidator: ThemeValidator,
private val idGenerator: IDGenerator,
private val adminService: AdminService
) { ) {
// ========================================
// Public (인증 불필요)
// ========================================
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findInfoById(id: Long): ThemeInfoResponse { fun findInfoById(id: Long): ThemeInfoResponse {
log.info { "[findInfoById] 테마 조회 시작: id=$id" } log.info { "[findInfoById] 테마 조회 시작: id=$id" }
return findOrThrow(id).toInfoResponse() val theme = themeRepository.findByIdOrNull(id) ?: run {
log.warn { "[updateTheme] 테마 조회 실패: id=$id" }
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
}
return theme.toInfoResponse()
.also { log.info { "[findInfoById] 테마 조회 완료: id=$id" } } .also { log.info { "[findInfoById] 테마 조회 완료: id=$id" } }
} }
@ -54,115 +48,11 @@ class ThemeService(
.also { .also {
log.info { "[findMostReservedThemeLastWeek] ${it.themes.size} / $count 개의 인기 테마 조회 완료" } log.info { "[findMostReservedThemeLastWeek] ${it.themes.size} / $count 개의 인기 테마 조회 완료" }
} }
}
// ========================================
// HQ Admin (본사)
// ========================================
@Transactional(readOnly = true)
fun findAdminThemes(): AdminThemeSummaryListResponse {
log.info { "[findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" }
return themeRepository.findAll()
.toAdminThemeSummaryListResponse()
.also { log.info { "[findAdminThemes] ${it.themes.size}개 테마 조회 완료" } }
}
@Transactional(readOnly = true)
fun findAdminThemeDetail(id: Long): AdminThemeDetailResponse {
log.info { "[findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" }
val theme: ThemeEntity = findOrThrow(id)
val createdBy = adminService.findOperatorOrUnknown(theme.createdBy)
val updatedBy = adminService.findOperatorOrUnknown(theme.updatedBy)
val audit = AuditingInfo(theme.createdAt, createdBy, theme.updatedAt, updatedBy)
return theme.toAdminThemeDetailResponse(audit)
.also { log.info { "[findAdminThemeDetail] 테마 상세 조회 완료: id=$id, name=${theme.name}" } }
}
@Transactional
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse {
log.info { "[createTheme] 테마 생성 시작: name=${request.name}" }
themeValidator.validateCanCreate(request)
val theme: ThemeEntity = request.toEntity(id = idGenerator.create())
.also { themeRepository.save(it) }
return ThemeCreateResponse(theme.id).also {
log.info { "[createTheme] 테마 생성 완료: id=${theme.id}, name=${theme.name}" }
} }
} }
@Transactional object DateUtils {
fun deleteTheme(id: Long) { fun getSundayOfPreviousWeek(date: LocalDate): LocalDate = date
log.info { "[deleteTheme] 테마 삭제 시작: id=${id}" } .minusWeeks(1)
.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY))
val theme: ThemeEntity = findOrThrow(id)
themeRepository.delete(theme).also {
log.info { "[deleteTheme] 테마 삭제 완료: id=$id, name=${theme.name}" }
}
}
@Transactional
fun updateTheme(id: Long, request: ThemeUpdateRequest) {
log.info { "[updateTheme] 테마 수정 시작: id=${id}, request=${request}" }
if (request.isAllParamsNull()) {
log.info { "[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.isActive,
).also {
log.info { "[updateTheme] 테마 수정 완료: id=$id, request=${request}" }
}
}
// ========================================
// Store Admin (매장)
// ========================================
@Transactional(readOnly = true)
fun findActiveThemes(): SimpleActiveThemeListResponse {
log.info { "[findActiveThemes] open 상태인 모든 테마 조회 시작" }
return themeRepository.findActiveThemes()
.toSimpleActiveThemeResponse()
.also {
log.info { "[findActiveThemes] ${it.themes.size}개 테마 조회 완료" }
}
}
// ========================================
// Common (공통 메서드)
// ========================================
private fun findOrThrow(id: Long): ThemeEntity {
log.info { "[findOrThrow] 테마 조회 시작: id=$id" }
return themeRepository.findByIdOrNull(id)
?.also { log.info { "[findOrThrow] 테마 조회 완료: id=$id" } }
?: run {
log.warn { "[updateTheme] 테마 조회 실패: id=$id" }
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
}
}
} }

View File

@ -3,8 +3,8 @@ package com.sangdol.roomescape.theme.business
import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.exception.ThemeException import com.sangdol.roomescape.theme.exception.ThemeException
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.web.ThemeCreateRequest import com.sangdol.roomescape.theme.dto.ThemeCreateRequest
import com.sangdol.roomescape.theme.web.ThemeUpdateRequest import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component import org.springframework.stereotype.Component

View File

@ -5,7 +5,14 @@ import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import com.sangdol.roomescape.admin.infrastructure.persistence.Privilege import com.sangdol.roomescape.admin.infrastructure.persistence.Privilege
import com.sangdol.roomescape.auth.web.support.AdminOnly import com.sangdol.roomescape.auth.web.support.AdminOnly
import com.sangdol.roomescape.auth.web.support.Public import com.sangdol.roomescape.auth.web.support.Public
import com.sangdol.roomescape.theme.web.* import com.sangdol.roomescape.theme.dto.ThemeDetailResponse
import com.sangdol.roomescape.theme.dto.ThemeSummaryListResponse
import com.sangdol.roomescape.theme.dto.ThemeNameListResponse
import com.sangdol.roomescape.theme.dto.ThemeCreateRequest
import com.sangdol.roomescape.theme.dto.ThemeCreateResponse
import com.sangdol.roomescape.theme.dto.ThemeInfoListResponse
import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
@ -20,12 +27,12 @@ interface AdminThemeAPI {
@AdminOnly(type = AdminType.HQ, privilege = Privilege.READ_SUMMARY) @AdminOnly(type = AdminType.HQ, privilege = Privilege.READ_SUMMARY)
@Operation(summary = "모든 테마 조회") @Operation(summary = "모든 테마 조회")
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
fun getAdminThemeSummaries(): ResponseEntity<CommonApiResponse<AdminThemeSummaryListResponse>> fun getAdminThemeSummaries(): ResponseEntity<CommonApiResponse<ThemeSummaryListResponse>>
@AdminOnly(type = AdminType.HQ, privilege = Privilege.READ_DETAIL) @AdminOnly(type = AdminType.HQ, privilege = Privilege.READ_DETAIL)
@Operation(summary = "테마 상세 조회") @Operation(summary = "테마 상세 조회")
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
fun findAdminThemeDetail(@PathVariable("id") id: Long): ResponseEntity<CommonApiResponse<AdminThemeDetailResponse>> fun findAdminThemeDetail(@PathVariable("id") id: Long): ResponseEntity<CommonApiResponse<ThemeDetailResponse>>
@AdminOnly(type = AdminType.HQ, privilege = Privilege.CREATE) @AdminOnly(type = AdminType.HQ, privilege = Privilege.CREATE)
@Operation(summary = "테마 추가") @Operation(summary = "테마 추가")
@ -48,7 +55,7 @@ interface AdminThemeAPI {
@AdminOnly(privilege = Privilege.READ_SUMMARY) @AdminOnly(privilege = Privilege.READ_SUMMARY)
@Operation(summary = "현재 활성화 상태인 테마 ID + 이름 목록 조회") @Operation(summary = "현재 활성화 상태인 테마 ID + 이름 목록 조회")
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
fun getActiveThemes(): ResponseEntity<CommonApiResponse<SimpleActiveThemeListResponse>> fun getActiveThemes(): ResponseEntity<CommonApiResponse<ThemeNameListResponse>>
} }
interface PublicThemeAPI { interface PublicThemeAPI {

View File

@ -0,0 +1,31 @@
package com.sangdol.roomescape.theme.dto
import com.sangdol.roomescape.common.types.AuditingInfo
import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty
data class ThemeSummaryResponse(
val id: Long,
val name: String,
val difficulty: Difficulty,
val price: Int,
val isActive: Boolean
)
data class ThemeSummaryListResponse(
val themes: List<ThemeSummaryResponse>
)
data class ThemeDetailResponse(
val theme: ThemeInfoResponse,
val isActive: Boolean,
val audit: AuditingInfo
)
data class ThemeNameResponse(
val id: Long,
val name: String
)
data class ThemeNameListResponse(
val themes: List<ThemeNameResponse>
)

View File

@ -0,0 +1,49 @@
package com.sangdol.roomescape.theme.dto
import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty
data class ThemeCreateRequest(
val name: String,
val description: String,
val thumbnailUrl: String,
val difficulty: Difficulty,
val price: Int,
val minParticipants: Short,
val maxParticipants: Short,
val availableMinutes: Short,
val expectedMinutesFrom: Short,
val expectedMinutesTo: Short,
val isActive: Boolean
)
data class ThemeCreateResponse(
val id: Long
)
data class ThemeUpdateRequest(
val name: String? = null,
val description: String? = null,
val thumbnailUrl: String? = null,
val difficulty: Difficulty? = null,
val price: Int? = null,
val minParticipants: Short? = null,
val maxParticipants: Short? = null,
val availableMinutes: Short? = null,
val expectedMinutesFrom: Short? = null,
val expectedMinutesTo: Short? = null,
val isActive: Boolean? = null,
) {
fun isAllParamsNull(): Boolean {
return name == null &&
description == null &&
thumbnailUrl == null &&
difficulty == null &&
price == null &&
minParticipants == null &&
maxParticipants == null &&
availableMinutes == null &&
expectedMinutesFrom == null &&
expectedMinutesTo == null &&
isActive == null
}
}

View File

@ -0,0 +1,19 @@
package com.sangdol.roomescape.theme.dto
data class ThemeInfoResponse(
val id: Long,
val name: String,
val thumbnailUrl: String,
val description: String,
val difficulty: String,
val price: Int,
val minParticipants: Short,
val maxParticipants: Short,
val availableMinutes: Short,
val expectedMinutesFrom: Short,
val expectedMinutesTo: Short
)
data class ThemeInfoListResponse(
val themes: List<ThemeInfoResponse>
)

View File

@ -0,0 +1,48 @@
package com.sangdol.roomescape.theme.mapper
import com.sangdol.roomescape.common.types.AuditingInfo
import com.sangdol.roomescape.theme.dto.*
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
fun ThemeCreateRequest.toEntity(id: Long) = ThemeEntity(
id = id,
name = this.name,
description = this.description,
thumbnailUrl = this.thumbnailUrl,
difficulty = this.difficulty,
price = this.price,
minParticipants = this.minParticipants,
maxParticipants = this.maxParticipants,
availableMinutes = this.availableMinutes,
expectedMinutesFrom = this.expectedMinutesFrom,
expectedMinutesTo = this.expectedMinutesTo,
isActive = this.isActive
)
fun ThemeEntity.toSummaryResponse() = ThemeSummaryResponse(
id = this.id,
name = this.name,
difficulty = this.difficulty,
price = this.price,
isActive = this.isActive
)
fun ThemeEntity.toDetailResponse(audit: AuditingInfo) =
ThemeDetailResponse(
theme = this.toInfoResponse(),
isActive = this.isActive,
audit = audit
)
fun ThemeEntity.toNameResponse() = ThemeNameResponse(
id = this.id,
name = this.name
)
fun List<ThemeEntity>.toSummaryListResponse() = ThemeSummaryListResponse(
themes = this.map { it.toSummaryResponse() }
)
fun List<ThemeEntity>.toNameListResponse() = ThemeNameListResponse(
themes = this.map { it.toNameResponse() }
)

View File

@ -1,23 +1,11 @@
package com.sangdol.roomescape.theme.web package com.sangdol.roomescape.theme.mapper
import com.sangdol.roomescape.theme.business.domain.ThemeInfo import com.sangdol.roomescape.theme.business.domain.ThemeInfo
import com.sangdol.roomescape.theme.dto.ThemeInfoListResponse
import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
data class ThemeInfoResponse( fun ThemeInfo.toResponse() = ThemeInfoResponse(
val id: Long,
val name: String,
val thumbnailUrl: String,
val description: String,
val difficulty: String,
val price: Int,
val minParticipants: Short,
val maxParticipants: Short,
val availableMinutes: Short,
val expectedMinutesFrom: Short,
val expectedMinutesTo: Short
)
fun ThemeInfo.toInfoResponse() = ThemeInfoResponse(
id = this.id, id = this.id,
name = this.name, name = this.name,
thumbnailUrl = this.thumbnailUrl, thumbnailUrl = this.thumbnailUrl,
@ -45,10 +33,8 @@ fun ThemeEntity.toInfoResponse() = ThemeInfoResponse(
expectedMinutesTo = this.expectedMinutesTo expectedMinutesTo = this.expectedMinutesTo
) )
data class ThemeInfoListResponse( fun List<ThemeInfo>.toListResponse() = ThemeInfoListResponse(
val themes: List<ThemeInfoResponse> themes = this.map { it.toResponse() }
) )
fun List<ThemeInfo>.toListResponse() = ThemeInfoListResponse(
themes = this.map { it.toInfoResponse() }
)

View File

@ -1,8 +1,9 @@
package com.sangdol.roomescape.theme.web package com.sangdol.roomescape.theme.web
import com.sangdol.common.types.web.CommonApiResponse import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.theme.business.ThemeService import com.sangdol.roomescape.theme.business.AdminThemeService
import com.sangdol.roomescape.theme.docs.AdminThemeAPI import com.sangdol.roomescape.theme.docs.AdminThemeAPI
import com.sangdol.roomescape.theme.dto.*
import jakarta.validation.Valid 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.*
@ -10,19 +11,19 @@ import java.net.URI
@RestController @RestController
class AdminThemeController( class AdminThemeController(
private val themeService: ThemeService, private val adminThemeService: AdminThemeService,
) : AdminThemeAPI { ) : AdminThemeAPI {
@GetMapping("/admin/themes") @GetMapping("/admin/themes")
override fun getAdminThemeSummaries(): ResponseEntity<CommonApiResponse<AdminThemeSummaryListResponse>> { override fun getAdminThemeSummaries(): ResponseEntity<CommonApiResponse<ThemeSummaryListResponse>> {
val response = themeService.findAdminThemes() val response = adminThemeService.findThemeSummaries()
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@GetMapping("/admin/themes/{id}") @GetMapping("/admin/themes/{id}")
override fun findAdminThemeDetail(@PathVariable id: Long): ResponseEntity<CommonApiResponse<AdminThemeDetailResponse>> { override fun findAdminThemeDetail(@PathVariable id: Long): ResponseEntity<CommonApiResponse<ThemeDetailResponse>> {
val response = themeService.findAdminThemeDetail(id) val response = adminThemeService.findThemeDetail(id)
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@ -31,7 +32,7 @@ class AdminThemeController(
override fun createTheme( override fun createTheme(
@Valid @RequestBody themeCreateRequest: ThemeCreateRequest @Valid @RequestBody themeCreateRequest: ThemeCreateRequest
): ResponseEntity<CommonApiResponse<ThemeCreateResponse>> { ): ResponseEntity<CommonApiResponse<ThemeCreateResponse>> {
val response = themeService.createTheme(themeCreateRequest) val response = adminThemeService.createTheme(themeCreateRequest)
return ResponseEntity.created(URI.create("/admin/themes/${response.id}")) return ResponseEntity.created(URI.create("/admin/themes/${response.id}"))
.body(CommonApiResponse(response)) .body(CommonApiResponse(response))
@ -39,7 +40,7 @@ class AdminThemeController(
@DeleteMapping("/admin/themes/{id}") @DeleteMapping("/admin/themes/{id}")
override fun deleteTheme(@PathVariable id: Long): ResponseEntity<CommonApiResponse<Unit>> { override fun deleteTheme(@PathVariable id: Long): ResponseEntity<CommonApiResponse<Unit>> {
themeService.deleteTheme(id) adminThemeService.deleteTheme(id)
return ResponseEntity.noContent().build() return ResponseEntity.noContent().build()
} }
@ -49,14 +50,14 @@ class AdminThemeController(
@PathVariable id: Long, @PathVariable id: Long,
@Valid @RequestBody request: ThemeUpdateRequest @Valid @RequestBody request: ThemeUpdateRequest
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
themeService.updateTheme(id, request) adminThemeService.updateTheme(id, request)
return ResponseEntity.ok().build() return ResponseEntity.ok().build()
} }
@GetMapping("/admin/themes/active") @GetMapping("/admin/themes/active")
override fun getActiveThemes(): ResponseEntity<CommonApiResponse<SimpleActiveThemeListResponse>> { override fun getActiveThemes(): ResponseEntity<CommonApiResponse<ThemeNameListResponse>> {
val response = themeService.findActiveThemes() val response = adminThemeService.findActiveThemes()
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }

View File

@ -1,127 +0,0 @@
package com.sangdol.roomescape.theme.web
import com.sangdol.roomescape.common.types.AuditingInfo
import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
// ========================================
// HQ Admin DTO (본사)
// ========================================
data class ThemeCreateRequest(
val name: String,
val description: String,
val thumbnailUrl: String,
val difficulty: Difficulty,
val price: Int,
val minParticipants: Short,
val maxParticipants: Short,
val availableMinutes: Short,
val expectedMinutesFrom: Short,
val expectedMinutesTo: Short,
val isActive: Boolean
)
data class ThemeCreateResponse(
val id: Long
)
fun ThemeCreateRequest.toEntity(id: Long) = ThemeEntity(
id = id,
name = this.name,
description = this.description,
thumbnailUrl = this.thumbnailUrl,
difficulty = this.difficulty,
price = this.price,
minParticipants = this.minParticipants,
maxParticipants = this.maxParticipants,
availableMinutes = this.availableMinutes,
expectedMinutesFrom = this.expectedMinutesFrom,
expectedMinutesTo = this.expectedMinutesTo,
isActive = this.isActive
)
data class ThemeUpdateRequest(
val name: String? = null,
val description: String? = null,
val thumbnailUrl: String? = null,
val difficulty: Difficulty? = null,
val price: Int? = null,
val minParticipants: Short? = null,
val maxParticipants: Short? = null,
val availableMinutes: Short? = null,
val expectedMinutesFrom: Short? = null,
val expectedMinutesTo: Short? = null,
val isActive: Boolean? = null,
) {
fun isAllParamsNull(): Boolean {
return name == null &&
description == null &&
thumbnailUrl == null &&
difficulty == null &&
price == null &&
minParticipants == null &&
maxParticipants == null &&
availableMinutes == null &&
expectedMinutesFrom == null &&
expectedMinutesTo == null &&
isActive == null
}
}
data class AdminThemeSummaryResponse(
val id: Long,
val name: String,
val difficulty: Difficulty,
val price: Int,
val isActive: Boolean
)
fun ThemeEntity.toAdminThemeSummaryResponse() = AdminThemeSummaryResponse(
id = this.id,
name = this.name,
difficulty = this.difficulty,
price = this.price,
isActive = this.isActive
)
data class AdminThemeSummaryListResponse(
val themes: List<AdminThemeSummaryResponse>
)
fun List<ThemeEntity>.toAdminThemeSummaryListResponse() = AdminThemeSummaryListResponse(
themes = this.map { it.toAdminThemeSummaryResponse() }
)
data class AdminThemeDetailResponse(
val theme: ThemeInfoResponse,
val isActive: Boolean,
val audit: AuditingInfo
)
fun ThemeEntity.toAdminThemeDetailResponse(audit: AuditingInfo) =
AdminThemeDetailResponse(
theme = this.toInfoResponse(),
isActive = this.isActive,
audit = audit
)
// ========================================
// Store Admin DTO
// ========================================
data class SimpleActiveThemeResponse(
val id: Long,
val name: String
)
fun ThemeEntity.toSimpleActiveThemeResponse() = SimpleActiveThemeResponse(
id = this.id,
name = this.name
)
data class SimpleActiveThemeListResponse(
val themes: List<SimpleActiveThemeResponse>
)
fun List<ThemeEntity>.toSimpleActiveThemeResponse() = SimpleActiveThemeListResponse(
themes = this.map { it.toSimpleActiveThemeResponse() }
)

View File

@ -3,6 +3,8 @@ package com.sangdol.roomescape.theme.web
import com.sangdol.common.types.web.CommonApiResponse import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.theme.business.ThemeService import com.sangdol.roomescape.theme.business.ThemeService
import com.sangdol.roomescape.theme.docs.PublicThemeAPI import com.sangdol.roomescape.theme.docs.PublicThemeAPI
import com.sangdol.roomescape.theme.dto.ThemeInfoListResponse
import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*

View File

@ -26,8 +26,8 @@ import com.sangdol.roomescape.store.infrastructure.persistence.StoreRepository
import com.sangdol.roomescape.store.infrastructure.persistence.StoreStatus import com.sangdol.roomescape.store.infrastructure.persistence.StoreStatus
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.web.ThemeCreateRequest import com.sangdol.roomescape.theme.dto.ThemeCreateRequest
import com.sangdol.roomescape.theme.web.toEntity import com.sangdol.roomescape.theme.mapper.toEntity
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import java.time.Instant import java.time.Instant

View File

@ -19,7 +19,7 @@ import com.sangdol.roomescape.store.infrastructure.persistence.StoreStatus
import com.sangdol.roomescape.store.web.StoreRegisterRequest import com.sangdol.roomescape.store.web.StoreRegisterRequest
import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.web.ThemeCreateRequest import com.sangdol.roomescape.theme.dto.ThemeCreateRequest
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus
import com.sangdol.roomescape.user.dto.MIN_PASSWORD_LENGTH import com.sangdol.roomescape.user.dto.MIN_PASSWORD_LENGTH

View File

@ -12,7 +12,7 @@ import com.sangdol.roomescape.theme.business.MIN_PRICE
import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.web.ThemeUpdateRequest import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest
import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo

View File

@ -7,8 +7,8 @@ import com.sangdol.roomescape.theme.business.DateUtils
import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.web.ThemeInfoResponse import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import com.sangdol.roomescape.theme.web.toEntity import com.sangdol.roomescape.theme.mapper.toEntity
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
import io.kotest.matchers.collections.shouldContainInOrder import io.kotest.matchers.collections.shouldContainInOrder
import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.collections.shouldHaveSize