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

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

View File

@ -34,5 +34,5 @@ export const fetchUserThemes = async (): Promise<ThemeInfoListResponse> => {
}; };
export const findThemesByIds = async (request: ThemeIdListResponse): Promise<ThemeInfoListResponse> => { export const findThemesByIds = async (request: ThemeIdListResponse): Promise<ThemeInfoListResponse> => {
return await apiClient.post<ThemeInfoListResponse>('/themes/retrieve', request); return await apiClient.post<ThemeInfoListResponse>('/themes/batch', request);
}; };

View File

@ -42,7 +42,7 @@ class ThemeService(
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemesByIds(request: ThemeIdListResponse): ThemeInfoListResponse { fun findThemesByIds(request: ThemeIdListRequest): ThemeInfoListResponse {
log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" } log.info { "[ThemeService.findThemesByIds] 예약 페이지에서의 테마 목록 조회 시작: themeIds=${request.themeIds}" }
val result: MutableList<ThemeEntity> = mutableListOf() val result: MutableList<ThemeEntity> = mutableListOf()
@ -86,7 +86,7 @@ class ThemeService(
} }
@Transactional @Transactional
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponseV2 { fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse {
log.info { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" } log.info { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" }
themeValidator.validateCanCreate(request) themeValidator.validateCanCreate(request)
@ -95,7 +95,7 @@ class ThemeService(
request.toEntity(tsidFactory.next()) request.toEntity(tsidFactory.next())
) )
return ThemeCreateResponseV2(theme.id).also { return ThemeCreateResponse(theme.id).also {
log.info { "[ThemeService.createTheme] 테마 생성 완료: id=${theme.id}, name=${theme.name}" } log.info { "[ThemeService.createTheme] 테마 생성 완료: id=${theme.id}, name=${theme.name}" }
} }
} }

View File

@ -30,7 +30,7 @@ interface HQAdminThemeAPI {
@AdminOnly(type = AdminType.HQ, privilege = Privilege.CREATE) @AdminOnly(type = AdminType.HQ, privilege = Privilege.CREATE)
@Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun createTheme(@Valid @RequestBody themeCreateRequest: ThemeCreateRequest): ResponseEntity<CommonApiResponse<ThemeCreateResponseV2>> fun createTheme(@Valid @RequestBody themeCreateRequest: ThemeCreateRequest): ResponseEntity<CommonApiResponse<ThemeCreateResponse>>
@AdminOnly(type = AdminType.HQ, privilege = Privilege.DELETE) @AdminOnly(type = AdminType.HQ, privilege = Privilege.DELETE)
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
@ -57,7 +57,7 @@ interface PublicThemeAPI {
@Public @Public
@Operation(summary = "입력된 모든 ID에 대한 테마 조회") @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: ThemeIdListRequest): ResponseEntity<CommonApiResponse<ThemeInfoListResponse>>
@Public @Public
@Operation(summary = "입력된 테마 ID에 대한 정보 조회") @Operation(summary = "입력된 테마 ID에 대한 정보 조회")

View File

@ -27,7 +27,7 @@ class HQAdminThemeController(
} }
@PostMapping("/admin/themes") @PostMapping("/admin/themes")
override fun createTheme(themeCreateRequest: ThemeCreateRequest): ResponseEntity<CommonApiResponse<ThemeCreateResponseV2>> { override fun createTheme(themeCreateRequest: ThemeCreateRequest): ResponseEntity<CommonApiResponse<ThemeCreateResponse>> {
val response = themeService.createTheme(themeCreateRequest) val response = themeService.createTheme(themeCreateRequest)
return ResponseEntity.created(URI.create("/admin/themes/${response.id}")) return ResponseEntity.created(URI.create("/admin/themes/${response.id}"))

View File

@ -31,7 +31,7 @@ data class ThemeCreateRequest(
val isActive: Boolean val isActive: Boolean
) )
data class ThemeCreateResponseV2( data class ThemeCreateResponse(
val id: Long val id: Long
) )
@ -145,38 +145,20 @@ fun ThemeEntity.toAdminThemeDetailResponse(createdBy: OperatorInfo, updatedBy: O
// Store Admin DTO // Store Admin DTO
// ======================================== // ========================================
data class ThemeInfoResponse( data class SimpleActiveThemeResponse(
val id: Long, val id: Long,
val name: String, 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.toSummaryResponse() = ThemeInfoResponse( fun ThemeEntity.toSimpleActiveThemeResponse() = SimpleActiveThemeResponse(
id = this.id, id = this.id,
name = this.name, 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( data class SimpleActiveThemeListResponse(
val themes: List<ThemeInfoResponse> val themes: List<SimpleActiveThemeResponse>
) )
fun List<ThemeEntity>.toListResponse() = ThemeInfoListResponse( fun List<ThemeEntity>.toSimpleActiveThemeResponse() = SimpleActiveThemeListResponse(
themes = this.map { it.toSummaryResponse() } themes = this.map { it.toSimpleActiveThemeResponse() }
) )

View File

@ -19,7 +19,7 @@ import roomescape.theme.business.MIN_PRICE
import roomescape.theme.exception.ThemeErrorCode import roomescape.theme.exception.ThemeErrorCode
import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.theme.infrastructure.persistence.ThemeRepository
import roomescape.theme.web.ThemeIdListResponse import roomescape.theme.web.ThemeIdListRequest
import roomescape.theme.web.ThemeUpdateRequest import roomescape.theme.web.ThemeUpdateRequest
import roomescape.supports.* import roomescape.supports.*
import roomescape.supports.ThemeFixture.createRequest import roomescape.supports.ThemeFixture.createRequest
@ -311,10 +311,10 @@ class ThemeApiTest(
runTest( runTest(
token = authUtil.defaultUserLogin(), token = authUtil.defaultUserLogin(),
using = { using = {
body(ThemeIdListResponse(themeIds)) body(ThemeIdListRequest(themeIds))
}, },
on = { on = {
post("/themes/retrieve") post("/themes/batch")
}, },
expect = { expect = {
statusCode(HttpStatus.OK.value()) statusCode(HttpStatus.OK.value())
@ -337,10 +337,10 @@ class ThemeApiTest(
runTest( runTest(
token = authUtil.defaultUserLogin(), token = authUtil.defaultUserLogin(),
using = { using = {
body(ThemeIdListResponse(themeIds)) body(ThemeIdListRequest(themeIds))
}, },
on = { on = {
post("/themes/retrieve") post("/themes/batch")
}, },
expect = { expect = {
statusCode(HttpStatus.OK.value()) statusCode(HttpStatus.OK.value())
@ -411,35 +411,38 @@ class ThemeApiTest(
} }
} }
context("예약 페이지에서 테마를 조회한다.") { context("ID로 테마 정보를 조회한다.") {
test("공개된 테마의 전체 정보가 조회된다.") { test("성공 응답") {
val token = authUtil.defaultHqAdminLogin() val createdTheme: ThemeEntity = dummyInitializer.createTheme(
listOf( adminToken = authUtil.defaultHqAdminLogin(),
createRequest.copy(name = "open", isOpen = true), request = createRequest
createRequest.copy(name = "close", isOpen = false) )
).forEach {
dummyInitializer.createTheme(token, it)
}
runTest( runTest(
token = authUtil.defaultUserLogin(),
on = { on = {
get("/themes") get("/themes/${createdTheme.id}")
}, },
expect = { expect = {
body("data.themes.size()", equalTo(1)) body("data.id", equalTo(createdTheme.id))
body("data.themes[0].name", equalTo("open")) body("data.name", equalTo(createdTheme.name))
assertProperties( assertProperties(
props = setOf( props = setOf(
"id", "name", "thumbnailUrl", "description", "difficulty", "price", "id", "name", "thumbnailUrl", "description", "difficulty", "price",
"minParticipants", "maxParticipants", "minParticipants", "maxParticipants",
"availableMinutes", "expectedMinutesFrom", "expectedMinutesTo" "availableMinutes", "expectedMinutesFrom", "expectedMinutesTo"
), ),
propsNameIfList = "themes",
) )
} }
) )
} }
test("테마가 없으면 실패한다.") {
runExceptionTest(
method = HttpMethod.GET,
endpoint = "/themes/$INVALID_PK",
expectedErrorCode = ThemeErrorCode.THEME_NOT_FOUND
)
}
} }
context("관리자 페이지에서 특정 테마의 상세 정보를 조회한다.") { context("관리자 페이지에서 특정 테마의 상세 정보를 조회한다.") {