From 7fd6b030dd1f6bb9a5e0bf090dcea719c58930e8 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 3 Sep 2025 10:45:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=ED=85=8C?= =?UTF-8?q?=EB=A7=88=20API=20=EC=A0=95=EC=9D=98=20=EB=B0=8F=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roomescape/theme/docs/ThemeApiV2.kt | 56 +++++++ .../roomescape/theme/web/ThemeControllerV2.kt | 60 +++++++ .../kotlin/roomescape/theme/web/ThemeDtoV2.kt | 151 ++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 src/main/kotlin/roomescape/theme/docs/ThemeApiV2.kt create mode 100644 src/main/kotlin/roomescape/theme/web/ThemeControllerV2.kt create mode 100644 src/main/kotlin/roomescape/theme/web/ThemeDtoV2.kt diff --git a/src/main/kotlin/roomescape/theme/docs/ThemeApiV2.kt b/src/main/kotlin/roomescape/theme/docs/ThemeApiV2.kt new file mode 100644 index 00000000..d64ecc25 --- /dev/null +++ b/src/main/kotlin/roomescape/theme/docs/ThemeApiV2.kt @@ -0,0 +1,56 @@ +package roomescape.theme.docs + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestBody +import roomescape.auth.web.support.Admin +import roomescape.auth.web.support.LoginRequired +import roomescape.common.dto.response.CommonApiResponse +import roomescape.theme.web.AdminThemeDetailRetrieveResponse +import roomescape.theme.web.AdminThemeSummaryRetrieveListResponse +import roomescape.theme.web.ThemeCreateRequestV2 +import roomescape.theme.web.ThemeCreateResponseV2 +import roomescape.theme.web.ThemeUpdateRequest +import roomescape.theme.web.ThemeRetrieveListResponseV2 + +@Tag(name = "5. 관리자 테마 API", description = "관리자 페이지에서 테마를 조회 / 추가 / 삭제할 때 사용합니다.") +interface ThemeAPIV2 { + + @Admin + @Operation(summary = "모든 테마 조회", description = "관리자 페이지에서 요약된 테마 목록을 조회합니다.", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun findAdminThemes(): ResponseEntity> + + @Admin + @Operation(summary = "테마 상세 조회", description = "해당 테마의 상세 정보를 조회합니다.", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun findAdminThemeDetail(@PathVariable("id") id: Long): ResponseEntity> + + @Admin + @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true)) + fun createTheme(@Valid @RequestBody themeCreateRequestV2: ThemeCreateRequestV2): ResponseEntity> + + @Admin + @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true)) + fun deleteTheme(@PathVariable id: Long): ResponseEntity> + + @Admin + @Operation(summary = "테마 수정", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun updateTheme( + @PathVariable id: Long, + @Valid @RequestBody themeUpdateRequest: ThemeUpdateRequest + ): ResponseEntity> + + @LoginRequired + @Operation(summary = "예약 페이지에서 모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun findUserThemes(): ResponseEntity> +} diff --git a/src/main/kotlin/roomescape/theme/web/ThemeControllerV2.kt b/src/main/kotlin/roomescape/theme/web/ThemeControllerV2.kt new file mode 100644 index 00000000..b9925e5b --- /dev/null +++ b/src/main/kotlin/roomescape/theme/web/ThemeControllerV2.kt @@ -0,0 +1,60 @@ +package roomescape.theme.web + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import roomescape.common.dto.response.CommonApiResponse +import roomescape.theme.business.ThemeServiceV2 +import roomescape.theme.docs.ThemeAPIV2 +import java.net.URI + +@RestController +class ThemeControllerV2( + private val themeService: ThemeServiceV2, +) : ThemeAPIV2 { + + @GetMapping("/v2/themes") + override fun findUserThemes(): ResponseEntity> { + val response = themeService.findThemesForReservation() + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @GetMapping("/admin/themes") + override fun findAdminThemes(): ResponseEntity> { + val response = themeService.findAdminThemes() + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @GetMapping("/admin/themes/{id}") + override fun findAdminThemeDetail(@PathVariable id: Long): ResponseEntity> { + val response = themeService.findAdminThemeDetail(id) + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @PostMapping("/admin/themes") + override fun createTheme(themeCreateRequestV2: ThemeCreateRequestV2): ResponseEntity> { + val response = themeService.createTheme(themeCreateRequestV2) + + return ResponseEntity.created(URI.create("/admin/themes/${response.id}")) + .body(CommonApiResponse(response)) + } + + @DeleteMapping("/admin/themes/{id}") + override fun deleteTheme(@PathVariable id: Long): ResponseEntity> { + themeService.deleteTheme(id) + + return ResponseEntity.noContent().build() + } + + @PatchMapping("/admin/themes/{id}") + override fun updateTheme( + @PathVariable id: Long, + themeUpdateRequest: ThemeUpdateRequest + ): ResponseEntity> { + themeService.updateTheme(id, themeUpdateRequest) + + return ResponseEntity.ok().build() + } +} diff --git a/src/main/kotlin/roomescape/theme/web/ThemeDtoV2.kt b/src/main/kotlin/roomescape/theme/web/ThemeDtoV2.kt new file mode 100644 index 00000000..6185cf2b --- /dev/null +++ b/src/main/kotlin/roomescape/theme/web/ThemeDtoV2.kt @@ -0,0 +1,151 @@ +package roomescape.theme.web + +import roomescape.theme.infrastructure.persistence.v2.Difficulty +import roomescape.theme.infrastructure.persistence.v2.ThemeEntityV2 +import java.time.LocalDateTime + +data class ThemeCreateRequestV2( + 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 isOpen: Boolean +) + +data class ThemeCreateResponseV2( + val id: Long +) + +fun ThemeCreateRequestV2.toEntity(id: Long) = ThemeEntityV2( + 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, + isOpen = this.isOpen +) + +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 isOpen: Boolean? = null, +) + +data class AdminThemeSummaryRetrieveResponse( + val id: Long, + val name: String, + val difficulty: Difficulty, + val price: Int, + val isOpen: Boolean +) + +fun ThemeEntityV2.toAdminThemeSummaryResponse() = AdminThemeSummaryRetrieveResponse( + id = this.id, + name = this.name, + difficulty = this.difficulty, + price = this.price, + isOpen = this.isOpen +) + +data class AdminThemeSummaryRetrieveListResponse( + val themes: List +) + +fun List.toAdminThemeSummaryListResponse() = AdminThemeSummaryRetrieveListResponse( + themes = this.map { it.toAdminThemeSummaryResponse() } +) + +data class AdminThemeDetailRetrieveResponse( + val id: Long, + 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 isOpen: Boolean, + val createdAt: LocalDateTime, + val createdBy: String, + val updatedAt: LocalDateTime, + val updatedBy: String, +) + +fun ThemeEntityV2.toAdminThemeDetailResponse(createUserName: String, updateUserName: String) = + AdminThemeDetailRetrieveResponse( + id = this.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, + isOpen = this.isOpen, + createdAt = this.createdAt, + createdBy = createUserName, + updatedAt = this.updatedAt, + updatedBy = updateUserName + ) + +data class ThemeRetrieveResponseV2( + 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 ThemeEntityV2.toRetrieveResponse() = ThemeRetrieveResponseV2( + 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 ThemeRetrieveListResponseV2( + val themes: List +) + +fun List.toRetrieveListResponse() = ThemeRetrieveListResponseV2( + themes = this.map { it.toRetrieveResponse() } +) \ No newline at end of file