diff --git a/src/main/java/side/onetime/controller/AdminController.java b/src/main/java/side/onetime/controller/AdminController.java index 25ac67b7..b0d7b82a 100644 --- a/src/main/java/side/onetime/controller/AdminController.java +++ b/src/main/java/side/onetime/controller/AdminController.java @@ -5,11 +5,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import side.onetime.dto.admin.request.*; +import side.onetime.dto.admin.request.LoginAdminUserRequest; +import side.onetime.dto.admin.request.RegisterAdminUserRequest; +import side.onetime.dto.admin.request.UpdateAdminUserStatusRequest; import side.onetime.dto.admin.response.*; import side.onetime.global.common.ApiResponse; import side.onetime.global.common.status.SuccessStatus; @@ -178,218 +178,4 @@ public ResponseEntity> getAllDashboard GetAllDashboardUsersResponse response = adminService.getAllDashboardUsers(authorizationHeader, pageable, keyword, sorting); return ApiResponse.onSuccess(SuccessStatus._GET_ALL_DASHBOARD_USERS, response); } - - /** - * 배너 등록 API. - * - * 요청으로 전달된 정보를 바탕으로 새로운 배너를 등록합니다. - * 기본적으로 비활성화 상태이며 삭제되지 않은 상태로 생성됩니다. - * - * @param authorizationHeader 액세스 토큰 - * @param request 배너 등록 요청 정보 - * @param imageFile 배너 등록 이미지 객체 - * @return 성공 응답 메시지 - */ - @PostMapping(value = "/banners/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> registerBanner( - @RequestHeader("Authorization") String authorizationHeader, - @Valid @RequestPart(value = "request") RegisterBannerRequest request, - @RequestPart(value = "image_file") MultipartFile imageFile) { - adminService.registerBanner(authorizationHeader, request, imageFile); - return ApiResponse.onSuccess(SuccessStatus._REGISTER_BANNER); - } - - /** - * 띠배너 등록 API. - * - * 요청으로 전달된 정보를 바탕으로 새로운 띠배너를 등록합니다. - * 기본적으로 비활성화 상태이며 삭제되지 않은 상태로 생성됩니다. - * - * @param authorizationHeader 액세스 토큰 - * @param request 띠배너 등록 요청 정보 - * @return 성공 응답 메시지 - */ - @PostMapping("/bar-banners/register") - public ResponseEntity> registerBarBanner( - @RequestHeader("Authorization") String authorizationHeader, - @Valid @RequestBody RegisterBarBannerRequest request) { - adminService.registerBarBanner(authorizationHeader, request); - return ApiResponse.onSuccess(SuccessStatus._REGISTER_BAR_BANNER); - } - - /** - * 배너 단건 조회 API. - * - * 삭제되지 않은 배너 중, ID에 해당하는 배너를 조회합니다. - * - * @param authorizationHeader 액세스 토큰 - * @param id 조회할 배너 ID - * @return 배너 응답 객체 - */ - @GetMapping("/banners/{id}") - public ResponseEntity> getBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id) { - GetBannerResponse response = adminService.getBanner(authorizationHeader, id); - return ApiResponse.onSuccess(SuccessStatus._GET_BANNER, response); - } - - /** - * 띠배너 단건 조회 API. - * - * 삭제되지 않은 띠배너 중, ID에 해당하는 띠배너를 조회합니다. - * - * @param authorizationHeader 액세스 토큰 - * @param id 조회할 띠배너 ID - * @return 띠배너 응답 객체 - */ - @GetMapping("/bar-banners/{id}") - public ResponseEntity> getBarBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id) { - GetBarBannerResponse response = adminService.getBarBanner(authorizationHeader, id); - return ApiResponse.onSuccess(SuccessStatus._GET_BAR_BANNER, response); - } - - /** - * 배너 전체 조회 API. - * - * 삭제되지 않은 모든 배너를 조회합니다. - * - * @param authorizationHeader 액세스 토큰 - * @return 배너 응답 객체 리스트 - */ - @GetMapping("/banners/all") - public ResponseEntity> getAllBanners( - @RequestHeader("Authorization") String authorizationHeader, - @RequestParam(value = "page", defaultValue = "1") @Min(1) int page - ) { - Pageable pageable = PageRequest.of(page - 1, 20); - GetAllBannersResponse response = adminService.getAllBanners(authorizationHeader, pageable); - return ApiResponse.onSuccess(SuccessStatus._GET_ALL_BANNERS, response); - } - - /** - * 띠배너 전체 조회 API. - * - * 삭제되지 않은 모든 띠배너를 조회합니다. - * - * @param authorizationHeader 액세스 토큰 - * @return 띠배너 응답 객체 리스트 - */ - @GetMapping("/bar-banners/all") - public ResponseEntity> getAllBarBanners( - @RequestHeader("Authorization") String authorizationHeader, - @RequestParam(value = "page", defaultValue = "1") @Min(1) int page - ) { - Pageable pageable = PageRequest.of(page - 1, 20); - GetAllBarBannersResponse response = adminService.getAllBarBanners(authorizationHeader, pageable); - return ApiResponse.onSuccess(SuccessStatus._GET_ALL_BAR_BANNERS, response); - } - - /** - * 현재 활성화된 배너 전체 조회 API. - * - * @return 활성화된 배너 응답 객체 리스트 - */ - @GetMapping("/banners/activated/all") - public ResponseEntity> getAllActivatedBanners() { - GetAllActivatedBannersResponse response = adminService.getAllActivatedBanners(); - return ApiResponse.onSuccess(SuccessStatus._GET_ALL_ACTIVATED_BANNERS, response); - } - - /** - * 현재 활성화된 띠배너 전체 조회 API. - * - * @return 활성화된 띠배너 응답 객체 리스트 - */ - @GetMapping("/bar-banners/activated/all") - public ResponseEntity> getAllActivatedBarBanners() { - GetAllActivatedBarBannersResponse response = adminService.getAllActivatedBarBanners(); - return ApiResponse.onSuccess(SuccessStatus._GET_ALL_ACTIVATED_BAR_BANNERS, response); - } - - /** - * 배너 수정 API. - * - * 일부 필드만 수정이 가능한 PATCH 방식의 API입니다. - * 전달받은 요청에서 null이 아닌 필드만 수정되며, - * 삭제된 배너는 수정할 수 없습니다. - * - * @param authorizationHeader 액세스 토큰 - * @param id 수정할 배너 ID - * @param request 수정 요청 DTO - * @param imageFile 배너 수정 이미지 객체 - * @return 성공 응답 메시지 - */ - @PatchMapping(value = "/banners/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> updateBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id, - @Valid @RequestPart(value = "request") UpdateBannerRequest request, - @RequestPart(value = "image_file", required = false) MultipartFile imageFile - ) { - adminService.updateBanner(authorizationHeader, id, request, imageFile); - return ApiResponse.onSuccess(SuccessStatus._UPDATE_BANNER); - } - - /** - * 띠배너 수정 API. - * - * 일부 필드만 수정이 가능한 PATCH 방식의 API입니다. - * 전달받은 요청에서 null이 아닌 필드만 수정되며, - * 삭제된 띠배너는 수정할 수 없습니다. - * - * @param authorizationHeader 액세스 토큰 - * @param id 수정할 띠배너 ID - * @param request 수정 요청 DTO - * @return 성공 응답 메시지 - */ - @PatchMapping("/bar-banners/{id}") - public ResponseEntity> updateBarBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id, - @Valid @RequestBody UpdateBarBannerRequest request - ) { - adminService.updateBarBanner(authorizationHeader, id, request); - return ApiResponse.onSuccess(SuccessStatus._UPDATE_BAR_BANNER); - } - - /** - * 배너 삭제 API. - * - * 배너를 DB에서 실제로 삭제하지 않고, isDeleted 플래그만 true로 변경합니다. - * 해당 배너는 이후 조회되지 않으며 비활성화 상태로 간주됩니다. - * - * @param authorizationHeader 액세스 토큰 - * @param id 삭제할 배너 ID - * @return 성공 응답 메시지 - */ - @DeleteMapping("/banners/{id}") - public ResponseEntity> deleteBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id - ) { - adminService.deleteBanner(authorizationHeader, id); - return ApiResponse.onSuccess(SuccessStatus._DELETE_BANNER); - } - - /** - * 띠배너 삭제 API. - * - * 띠배너를 DB에서 실제로 삭제하지 않고, isDeleted 플래그만 true로 변경합니다. - * 해당 띠배너는 이후 조회되지 않으며 비활성화 상태로 간주됩니다. - * - * @param authorizationHeader 액세스 토큰 - * @param id 삭제할 띠배너 ID - * @return 성공 응답 메시지 - */ - @DeleteMapping("/bar-banners/{id}") - public ResponseEntity> deleteBarBanner( - @RequestHeader("Authorization") String authorizationHeader, - @PathVariable Long id - ) { - adminService.deleteBarBanner(authorizationHeader, id); - return ApiResponse.onSuccess(SuccessStatus._DELETE_BAR_BANNER); - } } diff --git a/src/main/java/side/onetime/controller/BannerController.java b/src/main/java/side/onetime/controller/BannerController.java index 3f17816f..072af920 100644 --- a/src/main/java/side/onetime/controller/BannerController.java +++ b/src/main/java/side/onetime/controller/BannerController.java @@ -1,22 +1,244 @@ package side.onetime.controller; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import side.onetime.dto.banner.request.RegisterBannerRequest; +import side.onetime.dto.banner.request.RegisterBarBannerRequest; +import side.onetime.dto.banner.request.UpdateBannerRequest; +import side.onetime.dto.banner.request.UpdateBarBannerRequest; +import side.onetime.dto.banner.response.*; import side.onetime.global.common.ApiResponse; import side.onetime.global.common.status.SuccessStatus; import side.onetime.service.BannerService; @RestController -@RequestMapping("/api/v1/banners") +@RequestMapping("/api/v1") @RequiredArgsConstructor public class BannerController { private final BannerService bannerService; + /** + * 배너 등록 API. + * + * 요청으로 전달된 정보를 바탕으로 새로운 배너를 등록합니다. + * 기본적으로 비활성화 상태이며 삭제되지 않은 상태로 생성됩니다. + * + * @param authorizationHeader 액세스 토큰 + * @param request 배너 등록 요청 정보 + * @param imageFile 배너 등록 이미지 객체 + * @return 성공 응답 메시지 + */ + @PostMapping(value = "/banners/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> registerBanner( + @RequestHeader("Authorization") String authorizationHeader, + @Valid @RequestPart(value = "request") RegisterBannerRequest request, + @RequestPart(value = "image_file") MultipartFile imageFile) { + bannerService.registerBanner(authorizationHeader, request, imageFile); + return ApiResponse.onSuccess(SuccessStatus._REGISTER_BANNER); + } + + /** + * 띠배너 등록 API. + * + * 요청으로 전달된 정보를 바탕으로 새로운 띠배너를 등록합니다. + * 기본적으로 비활성화 상태이며 삭제되지 않은 상태로 생성됩니다. + * + * @param authorizationHeader 액세스 토큰 + * @param request 띠배너 등록 요청 정보 + * @return 성공 응답 메시지 + */ + @PostMapping("/bar-banners/register") + public ResponseEntity> registerBarBanner( + @RequestHeader("Authorization") String authorizationHeader, + @Valid @RequestBody RegisterBarBannerRequest request) { + bannerService.registerBarBanner(authorizationHeader, request); + return ApiResponse.onSuccess(SuccessStatus._REGISTER_BAR_BANNER); + } + + /** + * 배너 단건 조회 API. + * + * 삭제되지 않은 배너 중, ID에 해당하는 배너를 조회합니다. + * + * @param authorizationHeader 액세스 토큰 + * @param id 조회할 배너 ID + * @return 배너 응답 객체 + */ + @GetMapping("/banners/{id}") + public ResponseEntity> getBanner( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable Long id) { + GetBannerResponse response = bannerService.getBanner(authorizationHeader, id); + return ApiResponse.onSuccess(SuccessStatus._GET_BANNER, response); + } + + /** + * 띠배너 단건 조회 API. + * + * 삭제되지 않은 띠배너 중, ID에 해당하는 띠배너를 조회합니다. + * + * @param authorizationHeader 액세스 토큰 + * @param id 조회할 띠배너 ID + * @return 띠배너 응답 객체 + */ + @GetMapping("/bar-banners/{id}") + public ResponseEntity> getBarBanner( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable Long id) { + GetBarBannerResponse response = bannerService.getBarBanner(authorizationHeader, id); + return ApiResponse.onSuccess(SuccessStatus._GET_BAR_BANNER, response); + } + + /** + * 배너 전체 조회 API. + * + * 삭제되지 않은 모든 배너를 조회합니다. + * + * @param authorizationHeader 액세스 토큰 + * @return 배너 응답 객체 리스트 + */ + @GetMapping("/banners/all") + public ResponseEntity> getAllBanners( + @RequestHeader("Authorization") String authorizationHeader, + @RequestParam(value = "page", defaultValue = "1") @Min(1) int page + ) { + Pageable pageable = PageRequest.of(page - 1, 20); + GetAllBannersResponse response = bannerService.getAllBanners(authorizationHeader, pageable); + return ApiResponse.onSuccess(SuccessStatus._GET_ALL_BANNERS, response); + } + + /** + * 띠배너 전체 조회 API. + * + * 삭제되지 않은 모든 띠배너를 조회합니다. + * + * @param authorizationHeader 액세스 토큰 + * @return 띠배너 응답 객체 리스트 + */ + @GetMapping("/bar-banners/all") + public ResponseEntity> getAllBarBanners( + @RequestHeader("Authorization") String authorizationHeader, + @RequestParam(value = "page", defaultValue = "1") @Min(1) int page + ) { + Pageable pageable = PageRequest.of(page - 1, 20); + GetAllBarBannersResponse response = bannerService.getAllBarBanners(authorizationHeader, pageable); + return ApiResponse.onSuccess(SuccessStatus._GET_ALL_BAR_BANNERS, response); + } + + /** + * 현재 활성화된 배너 전체 조회 API. + * + * @return 활성화된 배너 응답 객체 리스트 + */ + @GetMapping("/banners/activated/all") + public ResponseEntity> getAllActivatedBanners() { + GetAllActivatedBannersResponse response = bannerService.getAllActivatedBanners(); + return ApiResponse.onSuccess(SuccessStatus._GET_ALL_ACTIVATED_BANNERS, response); + } + + /** + * 현재 활성화된 띠배너 전체 조회 API. + * + * @return 활성화된 띠배너 응답 객체 리스트 + */ + @GetMapping("/bar-banners/activated/all") + public ResponseEntity> getAllActivatedBarBanners() { + GetAllActivatedBarBannersResponse response = bannerService.getAllActivatedBarBanners(); + return ApiResponse.onSuccess(SuccessStatus._GET_ALL_ACTIVATED_BAR_BANNERS, response); + } + + /** + * 배너 수정 API. + * + * 일부 필드만 수정이 가능한 PATCH 방식의 API입니다. + * 전달받은 요청에서 null이 아닌 필드만 수정되며, + * 삭제된 배너는 수정할 수 없습니다. + * + * @param authorizationHeader 액세스 토큰 + * @param id 수정할 배너 ID + * @param request 수정 요청 DTO + * @param imageFile 배너 수정 이미지 객체 + * @return 성공 응답 메시지 + */ + @PatchMapping(value = "/banners/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> updateBanner( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable Long id, + @Valid @RequestPart(value = "request") UpdateBannerRequest request, + @RequestPart(value = "image_file", required = false) MultipartFile imageFile + ) { + bannerService.updateBanner(authorizationHeader, id, request, imageFile); + return ApiResponse.onSuccess(SuccessStatus._UPDATE_BANNER); + } + + /** + * 띠배너 수정 API. + * + * 일부 필드만 수정이 가능한 PATCH 방식의 API입니다. + * 전달받은 요청에서 null이 아닌 필드만 수정되며, + * 삭제된 띠배너는 수정할 수 없습니다. + * + * @param authorizationHeader 액세스 토큰 + * @param id 수정할 띠배너 ID + * @param request 수정 요청 DTO + * @return 성공 응답 메시지 + */ + @PatchMapping("/bar-banners/{id}") + public ResponseEntity> updateBarBanner( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable Long id, + @Valid @RequestBody UpdateBarBannerRequest request + ) { + bannerService.updateBarBanner(authorizationHeader, id, request); + return ApiResponse.onSuccess(SuccessStatus._UPDATE_BAR_BANNER); + } + + /** + * 배너 삭제 API. + * + * 배너를 DB에서 실제로 삭제하지 않고, isDeleted 플래그만 true로 변경합니다. + * 해당 배너는 이후 조회되지 않으며 비활성화 상태로 간주됩니다. + * + * @param authorizationHeader 액세스 토큰 + * @param id 삭제할 배너 ID + * @return 성공 응답 메시지 + */ + @DeleteMapping("/banners/{id}") + public ResponseEntity> deleteBanner( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable Long id + ) { + bannerService.deleteBanner(authorizationHeader, id); + return ApiResponse.onSuccess(SuccessStatus._DELETE_BANNER); + } + + /** + * 띠배너 삭제 API. + * + * 띠배너를 DB에서 실제로 삭제하지 않고, isDeleted 플래그만 true로 변경합니다. + * 해당 띠배너는 이후 조회되지 않으며 비활성화 상태로 간주됩니다. + * + * @param authorizationHeader 액세스 토큰 + * @param id 삭제할 띠배너 ID + * @return 성공 응답 메시지 + */ + @DeleteMapping("/bar-banners/{id}") + public ResponseEntity> deleteBarBanner( + @RequestHeader("Authorization") String authorizationHeader, + @PathVariable Long id + ) { + bannerService.deleteBarBanner(authorizationHeader, id); + return ApiResponse.onSuccess(SuccessStatus._DELETE_BAR_BANNER); + } + /** * 배너 클릭 수 증가 API. * @@ -25,7 +247,7 @@ public class BannerController { * @param id 클릭한 배너 ID * @return 성공 응답 메시지 */ - @PatchMapping("/{id}/clicks") + @PatchMapping("/banners/{id}/clicks") public ResponseEntity> increaseBannerClickCount(@PathVariable Long id) { bannerService.increaseBannerClickCount(id); return ApiResponse.onSuccess(SuccessStatus._INCREASE_BANNER_CLICK_COUNT); diff --git a/src/main/java/side/onetime/dto/admin/request/RegisterBannerRequest.java b/src/main/java/side/onetime/dto/banner/request/RegisterBannerRequest.java similarity index 97% rename from src/main/java/side/onetime/dto/admin/request/RegisterBannerRequest.java rename to src/main/java/side/onetime/dto/banner/request/RegisterBannerRequest.java index c24fa327..c74ce342 100644 --- a/src/main/java/side/onetime/dto/admin/request/RegisterBannerRequest.java +++ b/src/main/java/side/onetime/dto/banner/request/RegisterBannerRequest.java @@ -1,4 +1,4 @@ -package side.onetime.dto.admin.request; +package side.onetime.dto.banner.request; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/side/onetime/dto/admin/request/RegisterBarBannerRequest.java b/src/main/java/side/onetime/dto/banner/request/RegisterBarBannerRequest.java similarity index 97% rename from src/main/java/side/onetime/dto/admin/request/RegisterBarBannerRequest.java rename to src/main/java/side/onetime/dto/banner/request/RegisterBarBannerRequest.java index 9034a8f0..3f1d9255 100644 --- a/src/main/java/side/onetime/dto/admin/request/RegisterBarBannerRequest.java +++ b/src/main/java/side/onetime/dto/banner/request/RegisterBarBannerRequest.java @@ -1,4 +1,4 @@ -package side.onetime.dto.admin.request; +package side.onetime.dto.banner.request; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/side/onetime/dto/admin/request/UpdateBannerRequest.java b/src/main/java/side/onetime/dto/banner/request/UpdateBannerRequest.java similarity index 91% rename from src/main/java/side/onetime/dto/admin/request/UpdateBannerRequest.java rename to src/main/java/side/onetime/dto/banner/request/UpdateBannerRequest.java index c191a80a..0e09306b 100644 --- a/src/main/java/side/onetime/dto/admin/request/UpdateBannerRequest.java +++ b/src/main/java/side/onetime/dto/banner/request/UpdateBannerRequest.java @@ -1,8 +1,7 @@ -package side.onetime.dto.admin.request; +package side.onetime.dto.banner.request; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) diff --git a/src/main/java/side/onetime/dto/admin/request/UpdateBarBannerRequest.java b/src/main/java/side/onetime/dto/banner/request/UpdateBarBannerRequest.java similarity index 95% rename from src/main/java/side/onetime/dto/admin/request/UpdateBarBannerRequest.java rename to src/main/java/side/onetime/dto/banner/request/UpdateBarBannerRequest.java index 275f8b1e..f46f42a7 100644 --- a/src/main/java/side/onetime/dto/admin/request/UpdateBarBannerRequest.java +++ b/src/main/java/side/onetime/dto/banner/request/UpdateBarBannerRequest.java @@ -1,4 +1,4 @@ -package side.onetime.dto.admin.request; +package side.onetime.dto.banner.request; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/side/onetime/dto/admin/response/GetAllActivatedBannersResponse.java b/src/main/java/side/onetime/dto/banner/response/GetAllActivatedBannersResponse.java similarity index 91% rename from src/main/java/side/onetime/dto/admin/response/GetAllActivatedBannersResponse.java rename to src/main/java/side/onetime/dto/banner/response/GetAllActivatedBannersResponse.java index afe7e931..74d05cb0 100644 --- a/src/main/java/side/onetime/dto/admin/response/GetAllActivatedBannersResponse.java +++ b/src/main/java/side/onetime/dto/banner/response/GetAllActivatedBannersResponse.java @@ -1,4 +1,4 @@ -package side.onetime.dto.admin.response; +package side.onetime.dto.banner.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/side/onetime/dto/admin/response/GetAllActivatedBarBannersResponse.java b/src/main/java/side/onetime/dto/banner/response/GetAllActivatedBarBannersResponse.java similarity index 92% rename from src/main/java/side/onetime/dto/admin/response/GetAllActivatedBarBannersResponse.java rename to src/main/java/side/onetime/dto/banner/response/GetAllActivatedBarBannersResponse.java index 721d7d74..698044f6 100644 --- a/src/main/java/side/onetime/dto/admin/response/GetAllActivatedBarBannersResponse.java +++ b/src/main/java/side/onetime/dto/banner/response/GetAllActivatedBarBannersResponse.java @@ -1,4 +1,4 @@ -package side.onetime.dto.admin.response; +package side.onetime.dto.banner.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/side/onetime/dto/admin/response/GetAllBannersResponse.java b/src/main/java/side/onetime/dto/banner/response/GetAllBannersResponse.java similarity index 85% rename from src/main/java/side/onetime/dto/admin/response/GetAllBannersResponse.java rename to src/main/java/side/onetime/dto/banner/response/GetAllBannersResponse.java index d7748a4a..50b6c0e5 100644 --- a/src/main/java/side/onetime/dto/admin/response/GetAllBannersResponse.java +++ b/src/main/java/side/onetime/dto/banner/response/GetAllBannersResponse.java @@ -1,7 +1,8 @@ -package side.onetime.dto.admin.response; +package side.onetime.dto.banner.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import side.onetime.dto.admin.response.PageInfo; import java.util.List; diff --git a/src/main/java/side/onetime/dto/admin/response/GetAllBarBannersResponse.java b/src/main/java/side/onetime/dto/banner/response/GetAllBarBannersResponse.java similarity index 85% rename from src/main/java/side/onetime/dto/admin/response/GetAllBarBannersResponse.java rename to src/main/java/side/onetime/dto/banner/response/GetAllBarBannersResponse.java index 8abd0a71..85edcdf9 100644 --- a/src/main/java/side/onetime/dto/admin/response/GetAllBarBannersResponse.java +++ b/src/main/java/side/onetime/dto/banner/response/GetAllBarBannersResponse.java @@ -1,7 +1,8 @@ -package side.onetime.dto.admin.response; +package side.onetime.dto.banner.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import side.onetime.dto.admin.response.PageInfo; import java.util.List; diff --git a/src/main/java/side/onetime/dto/admin/response/GetBannerResponse.java b/src/main/java/side/onetime/dto/banner/response/GetBannerResponse.java similarity index 96% rename from src/main/java/side/onetime/dto/admin/response/GetBannerResponse.java rename to src/main/java/side/onetime/dto/banner/response/GetBannerResponse.java index 31714377..e0c1a73d 100644 --- a/src/main/java/side/onetime/dto/admin/response/GetBannerResponse.java +++ b/src/main/java/side/onetime/dto/banner/response/GetBannerResponse.java @@ -1,4 +1,4 @@ -package side.onetime.dto.admin.response; +package side.onetime.dto.banner.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/side/onetime/dto/admin/response/GetBarBannerResponse.java b/src/main/java/side/onetime/dto/banner/response/GetBarBannerResponse.java similarity index 96% rename from src/main/java/side/onetime/dto/admin/response/GetBarBannerResponse.java rename to src/main/java/side/onetime/dto/banner/response/GetBarBannerResponse.java index bff12192..3825e579 100644 --- a/src/main/java/side/onetime/dto/admin/response/GetBarBannerResponse.java +++ b/src/main/java/side/onetime/dto/banner/response/GetBarBannerResponse.java @@ -1,4 +1,4 @@ -package side.onetime.dto.admin.response; +package side.onetime.dto.banner.response; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; diff --git a/src/main/java/side/onetime/global/config/SecurityConfig.java b/src/main/java/side/onetime/global/config/SecurityConfig.java index ec9ebed0..c5d231d2 100644 --- a/src/main/java/side/onetime/global/config/SecurityConfig.java +++ b/src/main/java/side/onetime/global/config/SecurityConfig.java @@ -41,6 +41,7 @@ public class SecurityConfig { "/api/v1/tokens/**", "/api/v1/admin/**", "/api/v1/banners/**", + "/api/v1/bar-banners/**", "/api/v1/users/onboarding", "/api/v1/users/logout", "/actuator/health" diff --git a/src/main/java/side/onetime/global/filter/JwtFilter.java b/src/main/java/side/onetime/global/filter/JwtFilter.java index 336894d5..d71990ea 100644 --- a/src/main/java/side/onetime/global/filter/JwtFilter.java +++ b/src/main/java/side/onetime/global/filter/JwtFilter.java @@ -101,6 +101,7 @@ protected boolean shouldNotFilter(HttpServletRequest request) { path.startsWith("/api/v1/tokens") || path.startsWith("/api/v1/urls") || path.startsWith("/api/v1/banners") || + path.startsWith("/api/v1/bar-banners") || // 유저 관련 (isPost && path.equals("/api/v1/users/onboarding")) || (isPost && path.equals("/api/v1/users/logout")) || diff --git a/src/main/java/side/onetime/service/AdminService.java b/src/main/java/side/onetime/service/AdminService.java index 66b56001..ac472700 100644 --- a/src/main/java/side/onetime/service/AdminService.java +++ b/src/main/java/side/onetime/service/AdminService.java @@ -5,17 +5,17 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import side.onetime.domain.*; import side.onetime.domain.enums.AdminStatus; import side.onetime.domain.enums.EventStatus; -import side.onetime.dto.admin.request.*; +import side.onetime.dto.admin.request.LoginAdminUserRequest; +import side.onetime.dto.admin.request.RegisterAdminUserRequest; +import side.onetime.dto.admin.request.UpdateAdminUserStatusRequest; import side.onetime.dto.admin.response.*; import side.onetime.exception.CustomException; import side.onetime.exception.status.AdminErrorStatus; import side.onetime.repository.*; import side.onetime.util.JwtUtil; -import side.onetime.util.S3Util; import java.util.Comparator; import java.util.List; @@ -33,10 +33,7 @@ public class AdminService { private final ScheduleRepository scheduleRepository; private final MemberRepository memberRepository; private final UserRepository userRepository; - private final BannerRepository bannerRepository; - private final BarBannerRepository barBannerRepository; private final JwtUtil jwtUtil; - private final S3Util s3Util; /** * 관리자 계정 등록 메서드. @@ -267,293 +264,4 @@ public GetAllDashboardUsersResponse getAllDashboardUsers(String authorizationHea return GetAllDashboardUsersResponse.of(dashboardUsers, pageInfo); } - - /** - * 배너 등록 메서드. - * - * 요청 정보를 바탕으로 배너를 등록합니다. - * 기본적으로 비활성화 및 삭제되지 않은 상태로 저장됩니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @param request 배너 등록 요청 객체 - * @param imageFile 배너 등록 이미지 객체 - */ - @Transactional - public void registerBanner(String authorizationHeader, RegisterBannerRequest request, MultipartFile imageFile) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - Banner newBanner = bannerRepository.save(request.toEntity()); - - String imageUrl = uploadBannerImage(newBanner.getId(), imageFile); - newBanner.updateImageUrl(imageUrl); - } - - /** - * 띠배너 등록 메서드. - * - * 요청 정보를 바탕으로 배너를 등록합니다. - * 기본적으로 비활성화 및 삭제되지 않은 상태로 저장됩니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @param request 띠배너 등록 요청 객체 - */ - @Transactional - public void registerBarBanner(String authorizationHeader, RegisterBarBannerRequest request) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - BarBanner newBarBanner = request.toEntity(); - barBannerRepository.save(newBarBanner); - } - - /** - * 단일 배너 조회 메서드. - * - * 삭제되지 않은 상태의 배너를 ID 기준으로 조회합니다. - * 해당 배너가 존재하지 않을 경우 예외가 발생합니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @param id 조회할 배너 ID - * @return 배너 응답 객체 - */ - @Transactional(readOnly = true) - public GetBannerResponse getBanner(String authorizationHeader, Long id) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) - .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); - return GetBannerResponse.from(banner); - } - - /** - * 단일 띠배너 조회 메서드. - * - * 삭제되지 않은 상태의 배너를 ID 기준으로 조회합니다. - * 해당 띠배너가 존재하지 않을 경우 예외가 발생합니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @param id 조회할 띠배너 ID - * @return 띠배너 응답 객체 - */ - @Transactional(readOnly = true) - public GetBarBannerResponse getBarBanner(String authorizationHeader, Long id) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) - .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); - return GetBarBannerResponse.from(barBanner); - } - - /** - * 활성화된 배너 전체 조회 메서드. - * - * 현재 활성화 상태이며 삭제되지 않은 배너를 전체 조회합니다. - * - 없을 경우 빈 리스트를 반환합니다. - * - * @return 활성화된 배너 응답 객체 리스트 또는 빈 리스트 - */ - @Transactional(readOnly = true) - public GetAllActivatedBannersResponse getAllActivatedBanners() { - List banners = bannerRepository.findAllByIsActivatedTrueAndIsDeletedFalse().stream() - .map(GetBannerResponse::from) - .toList(); - return GetAllActivatedBannersResponse.from(banners); - } - - /** - * 활성화된 띠배너 전체 조회 메서드. - * - * 현재 활성화 상태이며 삭제되지 않은 띠배너를 전체 조회합니다. - * - 없을 경우 빈 리스트를 반환합니다. - * - * @return 활성화된 띠배너 응답 객체 리스트 또는 빈 리스트 - */ - @Transactional(readOnly = true) - public GetAllActivatedBarBannersResponse getAllActivatedBarBanners() { - List barBanners = barBannerRepository.findAllByIsActivatedTrueAndIsDeletedFalse().stream() - .map(GetBarBannerResponse::from) - .toList(); - return GetAllActivatedBarBannersResponse.from(barBanners); - } - - /** - * 전체 배너 조회 메서드. - * - * 삭제되지 않은 모든 배너를 조회하여 응답 객체로 반환합니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @return 배너 응답 객체 리스트 - */ - @Transactional(readOnly = true) - public GetAllBannersResponse getAllBanners(String authorizationHeader, Pageable pageable) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - - List banners = bannerRepository.findAllByIsDeletedFalseOrderByCreatedDateDesc(pageable).stream() - .map(GetBannerResponse::from) - .toList(); - - int totalElements = (int) bannerRepository.countByIsDeletedFalse(); - int totalPages = (int) Math.ceil((double) totalElements / pageable.getPageSize()); - - PageInfo pageInfo = PageInfo.of( - pageable.getPageNumber() + 1, - pageable.getPageSize(), - totalElements, - totalPages - ); - - return GetAllBannersResponse.of(banners, pageInfo); - } - - /** - * 전체 띠배너 조회 메서드. - * - * 삭제되지 않은 모든 띠배너를 조회하여 응답 객체로 반환합니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @return 띠배너 응답 객체 리스트 - */ - @Transactional(readOnly = true) - public GetAllBarBannersResponse getAllBarBanners(String authorizationHeader, Pageable pageable) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - - List barBanners = barBannerRepository.findAllByIsDeletedFalseOrderByCreatedDateDesc(pageable).stream() - .map(GetBarBannerResponse::from) - .toList(); - - int totalElements = (int) barBannerRepository.countByIsDeletedFalse(); - int totalPages = (int) Math.ceil((double) totalElements / pageable.getPageSize()); - - PageInfo pageInfo = PageInfo.of( - pageable.getPageNumber() + 1, - pageable.getPageSize(), - totalElements, - totalPages - ); - - return GetAllBarBannersResponse.of(barBanners, pageInfo); - } - - /** - * 배너 수정 메서드. - * - * 삭제되지 않은 배너를 ID 기준으로 조회합니다. - * 요청 객체에서 null이 아닌 필드만 선택적으로 수정합니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @param id 수정할 배너 ID - * @param request 수정 요청 객체 - * @param imageFile 배너 수정 이미지 객체 - */ - @Transactional - public void updateBanner(String authorizationHeader, Long id, UpdateBannerRequest request, MultipartFile imageFile) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) - .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); - - if (request.organization() != null) banner.updateOrganization(request.organization()); - if (request.title() != null) banner.updateTitle(request.title()); - if (request.subTitle() != null) banner.updateSubTitle(request.subTitle()); - if (request.buttonText() != null) banner.updateButtonText(request.buttonText()); - if (request.colorCode() != null) banner.updateColorCode(request.colorCode()); - if (request.linkUrl() != null) banner.updateLinkUrl(request.linkUrl()); - if (request.isActivated() != null) banner.updateIsActivated(request.isActivated()); - - if (imageFile != null) { - deleteExistingBannerImage(banner.getImageUrl()); - - String newImageUrl = uploadBannerImage(banner.getId(), imageFile); - banner.updateImageUrl(newImageUrl); - } - } - - /** - * 띠배너 수정 메서드. - * - * 삭제되지 않은 띠배너를 ID 기준으로 조회합니다. - * 요청 객체에서 null이 아닌 필드만 선택적으로 수정합니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @param id 수정할 띠배너 ID - * @param request 수정 요청 객체 - */ - @Transactional - public void updateBarBanner(String authorizationHeader, Long id, UpdateBarBannerRequest request) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) - .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); - - if (request.contentKor() != null) barBanner.updateContentKor(request.contentKor()); - if (request.contentEng() != null) barBanner.updateContentEng(request.contentEng()); - if (request.backgroundColorCode() != null) barBanner.updateBackgroundColorCode(request.backgroundColorCode()); - if (request.textColorCode() != null) barBanner.updateTextColorCode(request.textColorCode()); - if (request.linkUrl() != null) barBanner.updateLinkUrl(request.linkUrl()); - if (request.isActivated() != null) barBanner.updateIsActivated(request.isActivated()); - } - - /** - * 배너 삭제 메서드 (논리 삭제). - * - * 삭제되지 않은 배너를 ID 기준으로 조회합니다. - * 해당 배너의 삭제 상태를 true로 변경하고 S3에 저장된 이미지를 삭제합니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @param id 삭제할 배너 ID - */ - @Transactional - public void deleteBanner(String authorizationHeader, Long id) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) - .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); - banner.markAsDeleted(); - - String imageUrl = banner.getImageUrl(); - deleteExistingBannerImage(imageUrl); - } - - /** - * 띠배너 삭제 메서드 (논리 삭제). - * - * 삭제되지 않은 띠배너를 ID 기준으로 조회합니다. - * 해당 배너의 삭제 상태를 true로 변경합니다. - * - * @param authorizationHeader 요청자의 액세스 토큰 - * @param id 삭제할 띠배너 ID - */ - @Transactional - public void deleteBarBanner(String authorizationHeader, Long id) { - jwtUtil.getAdminUserFromHeader(authorizationHeader); - BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) - .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); - barBanner.markAsDeleted(); - } - - /** - * S3에 배너 이미지를 업로드하는 메서드. - * 주어진 이벤트 ID를 기반으로 QR 코드를 생성한 후, S3에 업로드합니다. - * - * @param bannerId 업로드할 배너의 ID - * @param imageFile 업로드할 이미지 파일 - * @return S3에 업로드된 이미지 Public URL - * @throws CustomException S3 업로드 실패 시 발생 - */ - private String uploadBannerImage(Long bannerId, MultipartFile imageFile) { - try { - String imageFileName = s3Util.uploadImage("banner/" + bannerId, imageFile); - return s3Util.getPublicUrl(imageFileName); - } catch (Exception e) { - throw new CustomException(AdminErrorStatus._FAILED_UPLOAD_BANNER_IMAGE); - } - } - - /** - * 기존 배너 이미지를 S3에서 삭제하는 메서드. - * - * @param imageUrl 삭제할 이미지 파일 - */ - private void deleteExistingBannerImage(String imageUrl) { - if (imageUrl != null && !imageUrl.isBlank()) { - try { - String imageFileKey = S3Util.extractKey(imageUrl); - s3Util.deleteFile(imageFileKey); - } catch (Exception e) { - log.warn("❌ 배너 이미지 삭제 예외 발생 - 요청 IMAGE_URI: {}", imageUrl, e); - } - } - } } diff --git a/src/main/java/side/onetime/service/BannerService.java b/src/main/java/side/onetime/service/BannerService.java index 308812c5..194b212b 100644 --- a/src/main/java/side/onetime/service/BannerService.java +++ b/src/main/java/side/onetime/service/BannerService.java @@ -1,15 +1,292 @@ package side.onetime.service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import side.onetime.domain.Banner; +import side.onetime.domain.BarBanner; +import side.onetime.dto.admin.response.PageInfo; +import side.onetime.dto.banner.request.RegisterBannerRequest; +import side.onetime.dto.banner.request.RegisterBarBannerRequest; +import side.onetime.dto.banner.request.UpdateBannerRequest; +import side.onetime.dto.banner.request.UpdateBarBannerRequest; +import side.onetime.dto.banner.response.*; +import side.onetime.exception.CustomException; +import side.onetime.exception.status.AdminErrorStatus; import side.onetime.repository.BannerRepository; +import side.onetime.repository.BarBannerRepository; +import side.onetime.util.JwtUtil; +import side.onetime.util.S3Util; +import java.util.List; + +@Slf4j @Service @RequiredArgsConstructor public class BannerService { private final BannerRepository bannerRepository; + private final BarBannerRepository barBannerRepository; + private final JwtUtil jwtUtil; + private final S3Util s3Util; + + /** + * 배너 등록 메서드. + * + * 요청 정보를 바탕으로 배너를 등록합니다. + * 기본적으로 비활성화 및 삭제되지 않은 상태로 저장됩니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @param request 배너 등록 요청 객체 + * @param imageFile 배너 등록 이미지 객체 + */ + @Transactional + public void registerBanner(String authorizationHeader, RegisterBannerRequest request, MultipartFile imageFile) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + Banner newBanner = bannerRepository.save(request.toEntity()); + + String imageUrl = uploadBannerImage(newBanner.getId(), imageFile); + newBanner.updateImageUrl(imageUrl); + } + + /** + * 띠배너 등록 메서드. + * + * 요청 정보를 바탕으로 배너를 등록합니다. + * 기본적으로 비활성화 및 삭제되지 않은 상태로 저장됩니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @param request 띠배너 등록 요청 객체 + */ + @Transactional + public void registerBarBanner(String authorizationHeader, RegisterBarBannerRequest request) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + BarBanner newBarBanner = request.toEntity(); + barBannerRepository.save(newBarBanner); + } + + /** + * 단일 배너 조회 메서드. + * + * 삭제되지 않은 상태의 배너를 ID 기준으로 조회합니다. + * 해당 배너가 존재하지 않을 경우 예외가 발생합니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @param id 조회할 배너 ID + * @return 배너 응답 객체 + */ + @Transactional(readOnly = true) + public GetBannerResponse getBanner(String authorizationHeader, Long id) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); + return GetBannerResponse.from(banner); + } + + /** + * 단일 띠배너 조회 메서드. + * + * 삭제되지 않은 상태의 배너를 ID 기준으로 조회합니다. + * 해당 띠배너가 존재하지 않을 경우 예외가 발생합니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @param id 조회할 띠배너 ID + * @return 띠배너 응답 객체 + */ + @Transactional(readOnly = true) + public GetBarBannerResponse getBarBanner(String authorizationHeader, Long id) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); + return GetBarBannerResponse.from(barBanner); + } + + /** + * 전체 배너 조회 메서드. + * + * 삭제되지 않은 모든 배너를 조회하여 응답 객체로 반환합니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @return 배너 응답 객체 리스트 + */ + @Transactional(readOnly = true) + public GetAllBannersResponse getAllBanners(String authorizationHeader, Pageable pageable) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + + List banners = bannerRepository.findAllByIsDeletedFalseOrderByCreatedDateDesc(pageable).stream() + .map(GetBannerResponse::from) + .toList(); + + int totalElements = (int) bannerRepository.countByIsDeletedFalse(); + int totalPages = (int) Math.ceil((double) totalElements / pageable.getPageSize()); + + PageInfo pageInfo = PageInfo.of( + pageable.getPageNumber() + 1, + pageable.getPageSize(), + totalElements, + totalPages + ); + + return GetAllBannersResponse.of(banners, pageInfo); + } + + /** + * 전체 띠배너 조회 메서드. + * + * 삭제되지 않은 모든 띠배너를 조회하여 응답 객체로 반환합니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @return 띠배너 응답 객체 리스트 + */ + @Transactional(readOnly = true) + public GetAllBarBannersResponse getAllBarBanners(String authorizationHeader, Pageable pageable) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + + List barBanners = barBannerRepository.findAllByIsDeletedFalseOrderByCreatedDateDesc(pageable).stream() + .map(GetBarBannerResponse::from) + .toList(); + + int totalElements = (int) barBannerRepository.countByIsDeletedFalse(); + int totalPages = (int) Math.ceil((double) totalElements / pageable.getPageSize()); + + PageInfo pageInfo = PageInfo.of( + pageable.getPageNumber() + 1, + pageable.getPageSize(), + totalElements, + totalPages + ); + + return GetAllBarBannersResponse.of(barBanners, pageInfo); + } + + /** + * 활성화된 배너 전체 조회 메서드. + * + * 현재 활성화 상태이며 삭제되지 않은 배너를 전체 조회합니다. + * - 없을 경우 빈 리스트를 반환합니다. + * + * @return 활성화된 배너 응답 객체 리스트 또는 빈 리스트 + */ + @Transactional(readOnly = true) + public GetAllActivatedBannersResponse getAllActivatedBanners() { + List banners = bannerRepository.findAllByIsActivatedTrueAndIsDeletedFalse().stream() + .map(GetBannerResponse::from) + .toList(); + return GetAllActivatedBannersResponse.from(banners); + } + + /** + * 활성화된 띠배너 전체 조회 메서드. + * + * 현재 활성화 상태이며 삭제되지 않은 띠배너를 전체 조회합니다. + * - 없을 경우 빈 리스트를 반환합니다. + * + * @return 활성화된 띠배너 응답 객체 리스트 또는 빈 리스트 + */ + @Transactional(readOnly = true) + public GetAllActivatedBarBannersResponse getAllActivatedBarBanners() { + List barBanners = barBannerRepository.findAllByIsActivatedTrueAndIsDeletedFalse().stream() + .map(GetBarBannerResponse::from) + .toList(); + return GetAllActivatedBarBannersResponse.from(barBanners); + } + + /** + * 배너 수정 메서드. + * + * 삭제되지 않은 배너를 ID 기준으로 조회합니다. + * 요청 객체에서 null이 아닌 필드만 선택적으로 수정합니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @param id 수정할 배너 ID + * @param request 수정 요청 객체 + * @param imageFile 배너 수정 이미지 객체 + */ + @Transactional + public void updateBanner(String authorizationHeader, Long id, UpdateBannerRequest request, MultipartFile imageFile) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); + + if (request.organization() != null) banner.updateOrganization(request.organization()); + if (request.title() != null) banner.updateTitle(request.title()); + if (request.subTitle() != null) banner.updateSubTitle(request.subTitle()); + if (request.buttonText() != null) banner.updateButtonText(request.buttonText()); + if (request.colorCode() != null) banner.updateColorCode(request.colorCode()); + if (request.linkUrl() != null) banner.updateLinkUrl(request.linkUrl()); + if (request.isActivated() != null) banner.updateIsActivated(request.isActivated()); + + if (imageFile != null) { + deleteExistingBannerImage(banner.getImageUrl()); + + String newImageUrl = uploadBannerImage(banner.getId(), imageFile); + banner.updateImageUrl(newImageUrl); + } + } + + /** + * 띠배너 수정 메서드. + * + * 삭제되지 않은 띠배너를 ID 기준으로 조회합니다. + * 요청 객체에서 null이 아닌 필드만 선택적으로 수정합니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @param id 수정할 띠배너 ID + * @param request 수정 요청 객체 + */ + @Transactional + public void updateBarBanner(String authorizationHeader, Long id, UpdateBarBannerRequest request) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); + + if (request.contentKor() != null) barBanner.updateContentKor(request.contentKor()); + if (request.contentEng() != null) barBanner.updateContentEng(request.contentEng()); + if (request.backgroundColorCode() != null) barBanner.updateBackgroundColorCode(request.backgroundColorCode()); + if (request.textColorCode() != null) barBanner.updateTextColorCode(request.textColorCode()); + if (request.linkUrl() != null) barBanner.updateLinkUrl(request.linkUrl()); + if (request.isActivated() != null) barBanner.updateIsActivated(request.isActivated()); + } + + /** + * 배너 삭제 메서드 (논리 삭제). + * + * 삭제되지 않은 배너를 ID 기준으로 조회합니다. + * 해당 배너의 삭제 상태를 true로 변경하고 S3에 저장된 이미지를 삭제합니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @param id 삭제할 배너 ID + */ + @Transactional + public void deleteBanner(String authorizationHeader, Long id) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + Banner banner = bannerRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BANNER)); + banner.markAsDeleted(); + + String imageUrl = banner.getImageUrl(); + deleteExistingBannerImage(imageUrl); + } + + /** + * 띠배너 삭제 메서드 (논리 삭제). + * + * 삭제되지 않은 띠배너를 ID 기준으로 조회합니다. + * 해당 배너의 삭제 상태를 true로 변경합니다. + * + * @param authorizationHeader 요청자의 액세스 토큰 + * @param id 삭제할 띠배너 ID + */ + @Transactional + public void deleteBarBanner(String authorizationHeader, Long id) { + jwtUtil.getAdminUserFromHeader(authorizationHeader); + BarBanner barBanner = barBannerRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_BAR_BANNER)); + barBanner.markAsDeleted(); + } /** * 배너 클릭 수 증가 메서드. @@ -22,4 +299,38 @@ public class BannerService { public void increaseBannerClickCount(Long id) { bannerRepository.increaseClickCount(id); } + + /** + * S3에 배너 이미지를 업로드하는 메서드. + * 주어진 이벤트 ID를 기반으로 QR 코드를 생성한 후, S3에 업로드합니다. + * + * @param bannerId 업로드할 배너의 ID + * @param imageFile 업로드할 이미지 파일 + * @return S3에 업로드된 이미지 Public URL + * @throws CustomException S3 업로드 실패 시 발생 + */ + private String uploadBannerImage(Long bannerId, MultipartFile imageFile) { + try { + String imageFileName = s3Util.uploadImage("banner/" + bannerId, imageFile); + return s3Util.getPublicUrl(imageFileName); + } catch (Exception e) { + throw new CustomException(AdminErrorStatus._FAILED_UPLOAD_BANNER_IMAGE); + } + } + + /** + * 기존 배너 이미지를 S3에서 삭제하는 메서드. + * + * @param imageUrl 삭제할 이미지 파일 + */ + private void deleteExistingBannerImage(String imageUrl) { + if (imageUrl != null && !imageUrl.isBlank()) { + try { + String imageFileKey = S3Util.extractKey(imageUrl); + s3Util.deleteFile(imageFileKey); + } catch (Exception e) { + log.warn("❌ 배너 이미지 삭제 예외 발생 - 요청 IMAGE_URI: {}", imageUrl, e); + } + } + } } diff --git a/src/test/java/side/onetime/admin/AdminControllerTest.java b/src/test/java/side/onetime/admin/AdminControllerTest.java index 183ad615..d94dce7c 100644 --- a/src/test/java/side/onetime/admin/AdminControllerTest.java +++ b/src/test/java/side/onetime/admin/AdminControllerTest.java @@ -10,27 +10,25 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.web.multipart.MultipartFile; import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.configuration.ControllerTestConfig; import side.onetime.controller.AdminController; import side.onetime.domain.enums.AdminStatus; import side.onetime.domain.enums.Category; import side.onetime.domain.enums.Language; -import side.onetime.dto.admin.request.*; +import side.onetime.dto.admin.request.LoginAdminUserRequest; +import side.onetime.dto.admin.request.RegisterAdminUserRequest; +import side.onetime.dto.admin.request.UpdateAdminUserStatusRequest; import side.onetime.dto.admin.response.*; import side.onetime.service.AdminService; import side.onetime.util.JwtUtil; import java.util.List; -import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -483,617 +481,4 @@ public void getAllDashboardUsers() throws Exception { ) )); } - - @Test - @DisplayName("배너를 등록한다.") - public void registerBanner() throws Exception { - // given - String accessToken = "Bearer test.jwt.token"; - - RegisterBannerRequest request = new RegisterBannerRequest( - "OneTime", - "OneTime's Title", - "OneTime's Sub Title", - "OneTime's Button Text", - "#FFFFFF", - "https://www.link.com" - ); - String requestContent = objectMapper.writeValueAsString(request); - - // when - Mockito.doNothing().when(adminService).registerBanner(any(String.class), any(RegisterBannerRequest.class), any(MultipartFile.class)); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.multipart("/api/v1/admin/banners/register") - .file(new MockMultipartFile("request", "", "application/json", requestContent.getBytes())) - .file(new MockMultipartFile("image_file", "banner.png", "image/png", "banner-image-content".getBytes())) - .header("Authorization", accessToken)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("201")) - .andExpect(jsonPath("$.message").value("배너 등록에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/banner-register", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("배너를 등록한다.") - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") - ) - .build() - ) - )); - } - - @Test - @DisplayName("띠배너를 등록한다.") - public void registerBarBanner() throws Exception { - // given - String accessToken = "Bearer test.jwt.token"; - RegisterBarBannerRequest request = new RegisterBarBannerRequest( - "최신 소식 안내", - "News", - "#FFFFFF", - "#FFFFFF", - "https://www.link.com" - ); - String requestContent = objectMapper.writeValueAsString(request); - - // when - Mockito.doNothing().when(adminService).registerBarBanner(any(String.class), any(RegisterBarBannerRequest.class)); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/admin/bar-banners/register") - .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", accessToken) - .content(requestContent)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("201")) - .andExpect(jsonPath("$.message").value("띠배너 등록에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/bar-banner-register", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("띠배너를 등록한다.") - .requestFields( - fieldWithPath("content_kor").type(JsonFieldType.STRING).description("한국어 내용"), - fieldWithPath("content_eng").type(JsonFieldType.STRING).description("영어 내용"), - fieldWithPath("background_color_code").type(JsonFieldType.STRING).description("배경 색상 코드"), - fieldWithPath("text_color_code").type(JsonFieldType.STRING).description("텍스트 색상 코드"), - fieldWithPath("link_url").type(JsonFieldType.STRING).optional().description("링크 URL") - ) - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") - ) - .requestSchema(Schema.schema("RegisterBarBannerRequest")) - .responseSchema(Schema.schema("CommonSuccessResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("배너를 단건 조회한다.") - public void getBanner() throws Exception { - // given - String accessToken = "Bearer test.jwt.token"; - Long bannerId = 1L; - GetBannerResponse response = new GetBannerResponse( - bannerId, "OneTime", "OneTime's Title", "OneTime's Sub Title", "OneTime's Button Text", "#FFFFFF", "https://www.image.com", true, "2025-08-26 12:00:00", "https://www.link.com", 1L - ); - - // when - Mockito.when(adminService.getBanner(any(String.class), any(Long.class))).thenReturn(response); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/banners/{id}", bannerId) - .header("Authorization", accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("배너 단건 조회에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/banner-get-one", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("배너를 단건 조회한다.") - .pathParameters( - parameterWithName("id").description("조회할 배너 ID") - ) - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), - fieldWithPath("payload").type(JsonFieldType.OBJECT).description("배너 정보"), - fieldWithPath("payload.id").type(JsonFieldType.NUMBER).description("배너 ID"), - fieldWithPath("payload.organization").type(JsonFieldType.STRING).description("조직명"), - fieldWithPath("payload.title").type(JsonFieldType.STRING).description("제목"), - fieldWithPath("payload.sub_title").type(JsonFieldType.STRING).description("부제목"), - fieldWithPath("payload.button_text").type(JsonFieldType.STRING).description("버튼 텍스트"), - fieldWithPath("payload.color_code").type(JsonFieldType.STRING).description("색상 코드"), - fieldWithPath("payload.image_url").type(JsonFieldType.STRING).description("배너 이미지 URL"), - fieldWithPath("payload.is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), - fieldWithPath("payload.created_date").type(JsonFieldType.STRING).description("생성일자"), - fieldWithPath("payload.link_url").type(JsonFieldType.STRING).description("링크 URL"), - fieldWithPath("payload.click_count").type(JsonFieldType.NUMBER).description("클릭 수") - ) - .responseSchema(Schema.schema("GetBannerResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("띠배너를 단건 조회한다.") - public void getBarBanner() throws Exception { - // given - String accessToken = "Bearer test.jwt.token"; - Long barBannerId = 1L; - GetBarBannerResponse response = new GetBarBannerResponse( - barBannerId, - "공지사항", "Notice", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com" - ); - - // when - Mockito.when(adminService.getBarBanner(any(String.class), any(Long.class))).thenReturn(response); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/bar-banners/{id}", barBannerId) - .header("Authorization", accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("띠배너 단건 조회에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/bar-banner-get-one", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("띠배너를 단건 조회한다.") - .pathParameters( - parameterWithName("id").description("조회할 띠배너 ID") - ) - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), - fieldWithPath("payload").type(JsonFieldType.OBJECT).description("띠배너 정보"), - fieldWithPath("payload.id").type(JsonFieldType.NUMBER).description("띠배너 ID"), - fieldWithPath("payload.content_kor").type(JsonFieldType.STRING).description("한국어 내용"), - fieldWithPath("payload.content_eng").type(JsonFieldType.STRING).description("영어 내용"), - fieldWithPath("payload.background_color_code").type(JsonFieldType.STRING).description("배경 색상 코드"), - fieldWithPath("payload.text_color_code").type(JsonFieldType.STRING).description("텍스트 색상 코드"), - fieldWithPath("payload.is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), - fieldWithPath("payload.created_date").type(JsonFieldType.STRING).description("생성일자"), - fieldWithPath("payload.link_url").type(JsonFieldType.STRING).description("링크 URL") - ) - .responseSchema(Schema.schema("GetBarBannerResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("배너를 전체 조회한다.") - public void getAllBanners() throws Exception { - // given - String accessToken = "Bearer test.jwt.token"; - int page = 1; - - List banners = List.of( - new GetBannerResponse(1L, "OneTime", "OneTime's Title", "OneTime's Sub Title", "OneTime's Button Text", "#FFFFFF", "https://www.image.com", true, "2025-08-26 12:00:00", "https://www.link.com", 1L), - new GetBannerResponse(2L, "OneTime2", "OneTime's Title2", "OneTime's Sub Title2", "OneTime's Button Text2", "#000000", "https://www.image.com", true, "2025-08-27 12:00:00", "https://www.link.com", 1L) - ); - - PageInfo pageInfo = PageInfo.of(1, 20, 2, 1); - GetAllBannersResponse response = GetAllBannersResponse.of(banners, pageInfo); - - // when - Mockito.when(adminService.getAllBanners(any(String.class), any(Pageable.class))) - .thenReturn(response); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/banners/all") - .header("Authorization", accessToken) - .param("page", String.valueOf(page))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("배너 전체 조회에 성공했습니다.")) - .andExpect(jsonPath("$.payload.banners[0].id").value(1L)) - .andExpect(jsonPath("$.payload.banners[1].id").value(2L)) - .andExpect(jsonPath("$.payload.page_info.page").value(1)) - .andExpect(jsonPath("$.payload.page_info.size").value(20)) - .andExpect(jsonPath("$.payload.page_info.total_elements").value(2)) - .andExpect(jsonPath("$.payload.page_info.total_pages").value(1)) - .andDo(MockMvcRestDocumentationWrapper.document("admin/banner-get-all", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("배너를 전체 조회한다.") - .queryParameters( - parameterWithName("page").description("조회할 페이지 번호 (1부터 시작)") - ) - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), - fieldWithPath("payload").type(JsonFieldType.OBJECT).description("페이로드 객체"), - fieldWithPath("payload.banners").type(JsonFieldType.ARRAY).description("배너 목록"), - fieldWithPath("payload.banners[].id").type(JsonFieldType.NUMBER).description("배너 ID"), - fieldWithPath("payload.banners[].organization").type(JsonFieldType.STRING).description("조직명"), - fieldWithPath("payload.banners[].title").type(JsonFieldType.STRING).description("제목"), - fieldWithPath("payload.banners[].sub_title").type(JsonFieldType.STRING).description("부제목"), - fieldWithPath("payload.banners[].button_text").type(JsonFieldType.STRING).description("버튼 텍스트"), - fieldWithPath("payload.banners[].color_code").type(JsonFieldType.STRING).description("색상 코드"), - fieldWithPath("payload.banners[].image_url").type(JsonFieldType.STRING).description("배너 이미지 URL"), - fieldWithPath("payload.banners[].is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), - fieldWithPath("payload.banners[].created_date").type(JsonFieldType.STRING).description("생성일자"), - fieldWithPath("payload.banners[].link_url").type(JsonFieldType.STRING).description("링크 URL"), - fieldWithPath("payload.banners[].click_count").type(JsonFieldType.NUMBER).description("클릭 수"), - fieldWithPath("payload.page_info.page").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), - fieldWithPath("payload.page_info.size").type(JsonFieldType.NUMBER).description("페이지당 항목 수"), - fieldWithPath("payload.page_info.total_elements").type(JsonFieldType.NUMBER).description("전체 항목 수"), - fieldWithPath("payload.page_info.total_pages").type(JsonFieldType.NUMBER).description("전체 페이지 수") - ) - .responseSchema(Schema.schema("GetAllBannersResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("띠배너를 전체 조회한다.") - public void getAllBarBanners() throws Exception { - // given - String accessToken = "Bearer test.jwt.token"; - int page = 1; - - List barBanners = List.of( - new GetBarBannerResponse(1L, "공지사항", "Notice", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com"), - new GetBarBannerResponse(2L, "공지사항2", "Notice2", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com") - ); - - PageInfo pageInfo = PageInfo.of(1, 20, 2, 1); - GetAllBarBannersResponse response = GetAllBarBannersResponse.of(barBanners, pageInfo); - - // when - Mockito.when(adminService.getAllBarBanners(any(String.class), any(Pageable.class))) - .thenReturn(response); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/bar-banners/all") - .header("Authorization", accessToken) - .param("page", String.valueOf(page))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("띠배너 전체 조회에 성공했습니다.")) - .andExpect(jsonPath("$.payload.bar_banners[0].id").value(1L)) - .andExpect(jsonPath("$.payload.bar_banners[1].id").value(2L)) - .andExpect(jsonPath("$.payload.page_info.page").value(1)) - .andExpect(jsonPath("$.payload.page_info.size").value(20)) - .andExpect(jsonPath("$.payload.page_info.total_elements").value(2)) - .andExpect(jsonPath("$.payload.page_info.total_pages").value(1)) - .andDo(MockMvcRestDocumentationWrapper.document("admin/bar-banner-get-all", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("띠배너를 전체 조회한다.") - .queryParameters( - parameterWithName("page").description("조회할 페이지 번호 (1부터 시작)") - ) - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), - fieldWithPath("payload").type(JsonFieldType.OBJECT).description("페이로드 객체"), - fieldWithPath("payload.bar_banners").type(JsonFieldType.ARRAY).description("띠배너 목록"), - fieldWithPath("payload.bar_banners[].id").type(JsonFieldType.NUMBER).description("띠배너 ID"), - fieldWithPath("payload.bar_banners[].content_kor").type(JsonFieldType.STRING).description("한국어 내용"), - fieldWithPath("payload.bar_banners[].content_eng").type(JsonFieldType.STRING).description("영어 내용"), - fieldWithPath("payload.bar_banners[].background_color_code").type(JsonFieldType.STRING).description("배경 색상 코드"), - fieldWithPath("payload.bar_banners[].text_color_code").type(JsonFieldType.STRING).description("텍스트 색상 코드"), - fieldWithPath("payload.bar_banners[].is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), - fieldWithPath("payload.bar_banners[].created_date").type(JsonFieldType.STRING).description("생성일자"), - fieldWithPath("payload.bar_banners[].link_url").type(JsonFieldType.STRING).description("링크 URL"), - fieldWithPath("payload.page_info.page").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), - fieldWithPath("payload.page_info.size").type(JsonFieldType.NUMBER).description("페이지당 항목 수"), - fieldWithPath("payload.page_info.total_elements").type(JsonFieldType.NUMBER).description("전체 항목 수"), - fieldWithPath("payload.page_info.total_pages").type(JsonFieldType.NUMBER).description("전체 페이지 수") - ) - .responseSchema(Schema.schema("GetAllBarBannersResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("활성화된 배너를 전체 조회한다.") - public void getAllActivatedBanners() throws Exception { - // given - List banners = List.of( - new GetBannerResponse(1L, "OneTime", "OneTime's Title", "OneTime's Sub Title", "OneTime's Button Text", "#FFFFFF", "https://www.image.com", true, "2025-08-26 12:00:00", "https://www.link.com", 1L), - new GetBannerResponse(2L, "OneTime2", "OneTime's Title2", "OneTime's Sub Title2", "OneTime's Button Text2", "#000000", "https://www.image.com", true, "2025-08-27 12:00:00", "https://www.link.com", 1L) - ); - GetAllActivatedBannersResponse response = GetAllActivatedBannersResponse.from(banners); - - // when - Mockito.when(adminService.getAllActivatedBanners()).thenReturn(response); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/banners/activated/all")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("활성화된 배너 전체 조회에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/activated-banner-get-all", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("활성화된 배너를 전체 조회한다.") - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), - fieldWithPath("payload").type(JsonFieldType.OBJECT).description("페이로드 객체"), - fieldWithPath("payload.banners").type(JsonFieldType.ARRAY).description("배너 목록"), - fieldWithPath("payload.banners[].id").type(JsonFieldType.NUMBER).description("배너 ID"), - fieldWithPath("payload.banners[].organization").type(JsonFieldType.STRING).description("조직명"), - fieldWithPath("payload.banners[].title").type(JsonFieldType.STRING).description("제목"), - fieldWithPath("payload.banners[].sub_title").type(JsonFieldType.STRING).description("부제목"), - fieldWithPath("payload.banners[].button_text").type(JsonFieldType.STRING).description("버튼 텍스트"), - fieldWithPath("payload.banners[].color_code").type(JsonFieldType.STRING).description("색상 코드"), - fieldWithPath("payload.banners[].image_url").type(JsonFieldType.STRING).description("배너 이미지 URL"), - fieldWithPath("payload.banners[].is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), - fieldWithPath("payload.banners[].created_date").type(JsonFieldType.STRING).description("생성일자"), - fieldWithPath("payload.banners[].link_url").type(JsonFieldType.STRING).description("링크 URL"), - fieldWithPath("payload.banners[].click_count").type(JsonFieldType.NUMBER).description("클릭 수") - ) - .responseSchema(Schema.schema("GetAllActivatedBannersResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("활성화된 띠배너를 전체 조회한다.") - public void getAllActivatedBarBanners() throws Exception { - // given - List barBanners = List.of( - new GetBarBannerResponse(1L, "공지사항", "Notice", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com"), - new GetBarBannerResponse(2L, "공지사항2", "Notice2", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com") - ); - GetAllActivatedBarBannersResponse response = GetAllActivatedBarBannersResponse.from(barBanners); - - // when - Mockito.when(adminService.getAllActivatedBarBanners()).thenReturn(response); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/admin/bar-banners/activated/all")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("활성화된 띠배너 전체 조회에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/activated-bar-banner-get-all", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("활성화된 띠배너를 전체 조회한다.") - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), - fieldWithPath("payload").type(JsonFieldType.OBJECT).description("페이로드 객체"), - fieldWithPath("payload.bar_banners").type(JsonFieldType.ARRAY).description("띠배너 목록"), - fieldWithPath("payload.bar_banners[].id").type(JsonFieldType.NUMBER).description("띠배너 ID"), - fieldWithPath("payload.bar_banners[].content_kor").type(JsonFieldType.STRING).description("한국어 내용"), - fieldWithPath("payload.bar_banners[].content_eng").type(JsonFieldType.STRING).description("영어 내용"), - fieldWithPath("payload.bar_banners[].background_color_code").type(JsonFieldType.STRING).description("배경 색상 코드"), - fieldWithPath("payload.bar_banners[].text_color_code").type(JsonFieldType.STRING).description("텍스트 색상 코드"), - fieldWithPath("payload.bar_banners[].is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), - fieldWithPath("payload.bar_banners[].created_date").type(JsonFieldType.STRING).description("생성일자"), - fieldWithPath("payload.bar_banners[].link_url").type(JsonFieldType.STRING).description("링크 URL") - ) - .responseSchema(Schema.schema("GetAllActivatedBarBannersResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("배너를 수정한다.") - public void updateBanner() throws Exception { - // given - Long bannerId = 1L; - String accessToken = "Bearer temp.jwt.access.token"; - - UpdateBannerRequest request = new UpdateBannerRequest( - "Modified OneTime", - "Modified OneTime's Title", - "Modified OneTime's Sub Title", - "Modified OneTime's Button Text", - "#000000", - true, - "https://www.link.com" - ); - String requestContent = objectMapper.writeValueAsString(request); - - // when - Mockito.doNothing().when(adminService).updateBanner(any(String.class), any(Long.class), any(UpdateBannerRequest.class), any(MultipartFile.class)); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.multipart("/api/v1/admin/banners/{id}", bannerId) - .file(new MockMultipartFile("request", "", "application/json", requestContent.getBytes())) - .file(new MockMultipartFile("image_file", "banner.png", "image/png", "banner-image-content".getBytes())) - .header("Authorization", accessToken) - .with(r -> { - r.setMethod("PATCH"); - return r; - })) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("배너 수정에 성공했습니다.")) - .andDo(document("admin/banner-update", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("배너를 수정한다.") - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") - ) - .build() - ) - )); - } - - @Test - @DisplayName("띠배너를 수정한다.") - public void updateBarBanner() throws Exception { - // given - Long barBannerId = 1L; - String accessToken = "Bearer temp.jwt.access.token"; - UpdateBarBannerRequest request = new UpdateBarBannerRequest( - "수정된 내용", - "modified content", - "#123456", - "#FFFFFF", - true, - "https://www.link.com" - ); - String requestContent = objectMapper.writeValueAsString(request); - - // when - Mockito.doNothing().when(adminService).updateBarBanner(any(String.class), eq(barBannerId), any(UpdateBarBannerRequest.class)); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/admin/bar-banners/{id}", barBannerId) - .header("Authorization", accessToken) - .contentType(MediaType.APPLICATION_JSON) - .content(requestContent)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("띠배너 수정에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/bar-banner-update", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("띠배너를 수정한다.") - .requestFields( - fieldWithPath("content_kor").type(JsonFieldType.STRING).optional().description("한국어 내용"), - fieldWithPath("content_eng").type(JsonFieldType.STRING).optional().description("영어 내용"), - fieldWithPath("background_color_code").type(JsonFieldType.STRING).optional().description("배경 색상 코드"), - fieldWithPath("text_color_code").type(JsonFieldType.STRING).optional().description("텍스트 색상 코드"), - fieldWithPath("is_activated").type(JsonFieldType.BOOLEAN).optional().description("활성화 여부"), - fieldWithPath("link_url").type(JsonFieldType.STRING).optional().description("링크 URL") - ) - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") - ) - .requestSchema(Schema.schema("UpdateBarBannerRequest")) - .responseSchema(Schema.schema("CommonSuccessResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("배너를 삭제한다.") - public void deleteBanner() throws Exception { - // given - String accessToken = "Bearer test.jwt.token"; - Long bannerId = 1L; - - // when - Mockito.doNothing().when(adminService).deleteBanner(any(String.class), eq(bannerId)); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/admin/banners/{id}", bannerId) - .header("Authorization", accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("배너 삭제에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/banner-delete", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("배너를 삭제한다.") - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") - ) - .responseSchema(Schema.schema("CommonSuccessResponse")) - .build() - ) - )); - } - - @Test - @DisplayName("띠배너를 삭제한다.") - public void deleteBarBanner() throws Exception { - // given - String accessToken = "Bearer test.jwt.token"; - Long barBannerId = 1L; - - // when - Mockito.doNothing().when(adminService).deleteBarBanner(any(String.class), eq(barBannerId)); - - // then - mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/admin/bar-banners/{id}", barBannerId) - .header("Authorization", accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.is_success").value(true)) - .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("띠배너 삭제에 성공했습니다.")) - .andDo(MockMvcRestDocumentationWrapper.document("admin/bar-banner-delete", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - resource( - ResourceSnippetParameters.builder() - .tag("Admin API") - .description("띠배너를 삭제한다.") - .responseFields( - fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), - fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") - ) - .responseSchema(Schema.schema("CommonSuccessResponse")) - .build() - ) - )); - } } diff --git a/src/test/java/side/onetime/banner/BannerControllerTest.java b/src/test/java/side/onetime/banner/BannerControllerTest.java index 6990f66b..f71dfd20 100644 --- a/src/test/java/side/onetime/banner/BannerControllerTest.java +++ b/src/test/java/side/onetime/banner/BannerControllerTest.java @@ -2,21 +2,36 @@ import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper; import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.web.multipart.MultipartFile; import side.onetime.auth.service.CustomUserDetailsService; import side.onetime.configuration.ControllerTestConfig; import side.onetime.controller.BannerController; +import side.onetime.dto.admin.response.PageInfo; +import side.onetime.dto.banner.request.RegisterBannerRequest; +import side.onetime.dto.banner.request.RegisterBarBannerRequest; +import side.onetime.dto.banner.request.UpdateBannerRequest; +import side.onetime.dto.banner.request.UpdateBarBannerRequest; +import side.onetime.dto.banner.response.*; import side.onetime.service.BannerService; import side.onetime.util.JwtUtil; +import java.util.List; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; @@ -35,6 +50,619 @@ public class BannerControllerTest extends ControllerTestConfig { @MockBean private CustomUserDetailsService customUserDetailsService; + @Test + @DisplayName("배너를 등록한다.") + public void registerBanner() throws Exception { + // given + String accessToken = "Bearer test.jwt.token"; + + RegisterBannerRequest request = new RegisterBannerRequest( + "OneTime", + "OneTime's Title", + "OneTime's Sub Title", + "OneTime's Button Text", + "#FFFFFF", + "https://www.link.com" + ); + String requestContent = objectMapper.writeValueAsString(request); + + // when + Mockito.doNothing().when(bannerService).registerBanner(any(String.class), any(RegisterBannerRequest.class), any(MultipartFile.class)); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.multipart("/api/v1/banners/register") + .file(new MockMultipartFile("request", "", "application/json", requestContent.getBytes())) + .file(new MockMultipartFile("image_file", "banner.png", "image/png", "banner-image-content".getBytes())) + .header("Authorization", accessToken)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("201")) + .andExpect(jsonPath("$.message").value("배너 등록에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/banner-register", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("배너를 등록한다.") + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") + ) + .build() + ) + )); + } + + @Test + @DisplayName("띠배너를 등록한다.") + public void registerBarBanner() throws Exception { + // given + String accessToken = "Bearer test.jwt.token"; + RegisterBarBannerRequest request = new RegisterBarBannerRequest( + "최신 소식 안내", + "News", + "#FFFFFF", + "#FFFFFF", + "https://www.link.com" + ); + String requestContent = objectMapper.writeValueAsString(request); + + // when + Mockito.doNothing().when(bannerService).registerBarBanner(any(String.class), any(RegisterBarBannerRequest.class)); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/bar-banners/register") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", accessToken) + .content(requestContent)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("201")) + .andExpect(jsonPath("$.message").value("띠배너 등록에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/bar-banner-register", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("띠배너를 등록한다.") + .requestFields( + fieldWithPath("content_kor").type(JsonFieldType.STRING).description("한국어 내용"), + fieldWithPath("content_eng").type(JsonFieldType.STRING).description("영어 내용"), + fieldWithPath("background_color_code").type(JsonFieldType.STRING).description("배경 색상 코드"), + fieldWithPath("text_color_code").type(JsonFieldType.STRING).description("텍스트 색상 코드"), + fieldWithPath("link_url").type(JsonFieldType.STRING).optional().description("링크 URL") + ) + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") + ) + .requestSchema(Schema.schema("RegisterBarBannerRequest")) + .responseSchema(Schema.schema("CommonSuccessResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("배너를 단건 조회한다.") + public void getBanner() throws Exception { + // given + String accessToken = "Bearer test.jwt.token"; + Long bannerId = 1L; + GetBannerResponse response = new GetBannerResponse( + bannerId, "OneTime", "OneTime's Title", "OneTime's Sub Title", "OneTime's Button Text", "#FFFFFF", "https://www.image.com", true, "2025-08-26 12:00:00", "https://www.link.com", 1L + ); + + // when + Mockito.when(bannerService.getBanner(any(String.class), any(Long.class))).thenReturn(response); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/banners/{id}", bannerId) + .header("Authorization", accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("배너 단건 조회에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/banner-get-one", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("배너를 단건 조회한다.") + .pathParameters( + parameterWithName("id").description("조회할 배너 ID") + ) + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("payload").type(JsonFieldType.OBJECT).description("배너 정보"), + fieldWithPath("payload.id").type(JsonFieldType.NUMBER).description("배너 ID"), + fieldWithPath("payload.organization").type(JsonFieldType.STRING).description("조직명"), + fieldWithPath("payload.title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("payload.sub_title").type(JsonFieldType.STRING).description("부제목"), + fieldWithPath("payload.button_text").type(JsonFieldType.STRING).description("버튼 텍스트"), + fieldWithPath("payload.color_code").type(JsonFieldType.STRING).description("색상 코드"), + fieldWithPath("payload.image_url").type(JsonFieldType.STRING).description("배너 이미지 URL"), + fieldWithPath("payload.is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), + fieldWithPath("payload.created_date").type(JsonFieldType.STRING).description("생성일자"), + fieldWithPath("payload.link_url").type(JsonFieldType.STRING).description("링크 URL"), + fieldWithPath("payload.click_count").type(JsonFieldType.NUMBER).description("클릭 수") + ) + .responseSchema(Schema.schema("GetBannerResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("띠배너를 단건 조회한다.") + public void getBarBanner() throws Exception { + // given + String accessToken = "Bearer test.jwt.token"; + Long barBannerId = 1L; + GetBarBannerResponse response = new GetBarBannerResponse( + barBannerId, + "공지사항", "Notice", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com" + ); + + // when + Mockito.when(bannerService.getBarBanner(any(String.class), any(Long.class))).thenReturn(response); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/bar-banners/{id}", barBannerId) + .header("Authorization", accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("띠배너 단건 조회에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/bar-banner-get-one", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("띠배너를 단건 조회한다.") + .pathParameters( + parameterWithName("id").description("조회할 띠배너 ID") + ) + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("payload").type(JsonFieldType.OBJECT).description("띠배너 정보"), + fieldWithPath("payload.id").type(JsonFieldType.NUMBER).description("띠배너 ID"), + fieldWithPath("payload.content_kor").type(JsonFieldType.STRING).description("한국어 내용"), + fieldWithPath("payload.content_eng").type(JsonFieldType.STRING).description("영어 내용"), + fieldWithPath("payload.background_color_code").type(JsonFieldType.STRING).description("배경 색상 코드"), + fieldWithPath("payload.text_color_code").type(JsonFieldType.STRING).description("텍스트 색상 코드"), + fieldWithPath("payload.is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), + fieldWithPath("payload.created_date").type(JsonFieldType.STRING).description("생성일자"), + fieldWithPath("payload.link_url").type(JsonFieldType.STRING).description("링크 URL") + ) + .responseSchema(Schema.schema("GetBarBannerResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("배너를 전체 조회한다.") + public void getAllBanners() throws Exception { + // given + String accessToken = "Bearer test.jwt.token"; + int page = 1; + + List banners = List.of( + new GetBannerResponse(1L, "OneTime", "OneTime's Title", "OneTime's Sub Title", "OneTime's Button Text", "#FFFFFF", "https://www.image.com", true, "2025-08-26 12:00:00", "https://www.link.com", 1L), + new GetBannerResponse(2L, "OneTime2", "OneTime's Title2", "OneTime's Sub Title2", "OneTime's Button Text2", "#000000", "https://www.image.com", true, "2025-08-27 12:00:00", "https://www.link.com", 1L) + ); + + PageInfo pageInfo = PageInfo.of(1, 20, 2, 1); + GetAllBannersResponse response = GetAllBannersResponse.of(banners, pageInfo); + + // when + Mockito.when(bannerService.getAllBanners(any(String.class), any(Pageable.class))) + .thenReturn(response); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/banners/all") + .header("Authorization", accessToken) + .param("page", String.valueOf(page))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("배너 전체 조회에 성공했습니다.")) + .andExpect(jsonPath("$.payload.banners[0].id").value(1L)) + .andExpect(jsonPath("$.payload.banners[1].id").value(2L)) + .andExpect(jsonPath("$.payload.page_info.page").value(1)) + .andExpect(jsonPath("$.payload.page_info.size").value(20)) + .andExpect(jsonPath("$.payload.page_info.total_elements").value(2)) + .andExpect(jsonPath("$.payload.page_info.total_pages").value(1)) + .andDo(MockMvcRestDocumentationWrapper.document("banner/banner-get-all", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("배너를 전체 조회한다.") + .queryParameters( + parameterWithName("page").description("조회할 페이지 번호 (1부터 시작)") + ) + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("payload").type(JsonFieldType.OBJECT).description("페이로드 객체"), + fieldWithPath("payload.banners").type(JsonFieldType.ARRAY).description("배너 목록"), + fieldWithPath("payload.banners[].id").type(JsonFieldType.NUMBER).description("배너 ID"), + fieldWithPath("payload.banners[].organization").type(JsonFieldType.STRING).description("조직명"), + fieldWithPath("payload.banners[].title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("payload.banners[].sub_title").type(JsonFieldType.STRING).description("부제목"), + fieldWithPath("payload.banners[].button_text").type(JsonFieldType.STRING).description("버튼 텍스트"), + fieldWithPath("payload.banners[].color_code").type(JsonFieldType.STRING).description("색상 코드"), + fieldWithPath("payload.banners[].image_url").type(JsonFieldType.STRING).description("배너 이미지 URL"), + fieldWithPath("payload.banners[].is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), + fieldWithPath("payload.banners[].created_date").type(JsonFieldType.STRING).description("생성일자"), + fieldWithPath("payload.banners[].link_url").type(JsonFieldType.STRING).description("링크 URL"), + fieldWithPath("payload.banners[].click_count").type(JsonFieldType.NUMBER).description("클릭 수"), + fieldWithPath("payload.page_info.page").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("payload.page_info.size").type(JsonFieldType.NUMBER).description("페이지당 항목 수"), + fieldWithPath("payload.page_info.total_elements").type(JsonFieldType.NUMBER).description("전체 항목 수"), + fieldWithPath("payload.page_info.total_pages").type(JsonFieldType.NUMBER).description("전체 페이지 수") + ) + .responseSchema(Schema.schema("GetAllBannersResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("띠배너를 전체 조회한다.") + public void getAllBarBanners() throws Exception { + // given + String accessToken = "Bearer test.jwt.token"; + int page = 1; + + List barBanners = List.of( + new GetBarBannerResponse(1L, "공지사항", "Notice", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com"), + new GetBarBannerResponse(2L, "공지사항2", "Notice2", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com") + ); + + PageInfo pageInfo = PageInfo.of(1, 20, 2, 1); + GetAllBarBannersResponse response = GetAllBarBannersResponse.of(barBanners, pageInfo); + + // when + Mockito.when(bannerService.getAllBarBanners(any(String.class), any(Pageable.class))) + .thenReturn(response); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/bar-banners/all") + .header("Authorization", accessToken) + .param("page", String.valueOf(page))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("띠배너 전체 조회에 성공했습니다.")) + .andExpect(jsonPath("$.payload.bar_banners[0].id").value(1L)) + .andExpect(jsonPath("$.payload.bar_banners[1].id").value(2L)) + .andExpect(jsonPath("$.payload.page_info.page").value(1)) + .andExpect(jsonPath("$.payload.page_info.size").value(20)) + .andExpect(jsonPath("$.payload.page_info.total_elements").value(2)) + .andExpect(jsonPath("$.payload.page_info.total_pages").value(1)) + .andDo(MockMvcRestDocumentationWrapper.document("banner/bar-banner-get-all", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("띠배너를 전체 조회한다.") + .queryParameters( + parameterWithName("page").description("조회할 페이지 번호 (1부터 시작)") + ) + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("payload").type(JsonFieldType.OBJECT).description("페이로드 객체"), + fieldWithPath("payload.bar_banners").type(JsonFieldType.ARRAY).description("띠배너 목록"), + fieldWithPath("payload.bar_banners[].id").type(JsonFieldType.NUMBER).description("띠배너 ID"), + fieldWithPath("payload.bar_banners[].content_kor").type(JsonFieldType.STRING).description("한국어 내용"), + fieldWithPath("payload.bar_banners[].content_eng").type(JsonFieldType.STRING).description("영어 내용"), + fieldWithPath("payload.bar_banners[].background_color_code").type(JsonFieldType.STRING).description("배경 색상 코드"), + fieldWithPath("payload.bar_banners[].text_color_code").type(JsonFieldType.STRING).description("텍스트 색상 코드"), + fieldWithPath("payload.bar_banners[].is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), + fieldWithPath("payload.bar_banners[].created_date").type(JsonFieldType.STRING).description("생성일자"), + fieldWithPath("payload.bar_banners[].link_url").type(JsonFieldType.STRING).description("링크 URL"), + fieldWithPath("payload.page_info.page").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("payload.page_info.size").type(JsonFieldType.NUMBER).description("페이지당 항목 수"), + fieldWithPath("payload.page_info.total_elements").type(JsonFieldType.NUMBER).description("전체 항목 수"), + fieldWithPath("payload.page_info.total_pages").type(JsonFieldType.NUMBER).description("전체 페이지 수") + ) + .responseSchema(Schema.schema("GetAllBarBannersResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("활성화된 배너를 전체 조회한다.") + public void getAllActivatedBanners() throws Exception { + // given + List banners = List.of( + new GetBannerResponse(1L, "OneTime", "OneTime's Title", "OneTime's Sub Title", "OneTime's Button Text", "#FFFFFF", "https://www.image.com", true, "2025-08-26 12:00:00", "https://www.link.com", 1L), + new GetBannerResponse(2L, "OneTime2", "OneTime's Title2", "OneTime's Sub Title2", "OneTime's Button Text2", "#000000", "https://www.image.com", true, "2025-08-27 12:00:00", "https://www.link.com", 1L) + ); + GetAllActivatedBannersResponse response = GetAllActivatedBannersResponse.from(banners); + + // when + Mockito.when(bannerService.getAllActivatedBanners()).thenReturn(response); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/banners/activated/all")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("활성화된 배너 전체 조회에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/activated-banner-get-all", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("활성화된 배너를 전체 조회한다.") + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("payload").type(JsonFieldType.OBJECT).description("페이로드 객체"), + fieldWithPath("payload.banners").type(JsonFieldType.ARRAY).description("배너 목록"), + fieldWithPath("payload.banners[].id").type(JsonFieldType.NUMBER).description("배너 ID"), + fieldWithPath("payload.banners[].organization").type(JsonFieldType.STRING).description("조직명"), + fieldWithPath("payload.banners[].title").type(JsonFieldType.STRING).description("제목"), + fieldWithPath("payload.banners[].sub_title").type(JsonFieldType.STRING).description("부제목"), + fieldWithPath("payload.banners[].button_text").type(JsonFieldType.STRING).description("버튼 텍스트"), + fieldWithPath("payload.banners[].color_code").type(JsonFieldType.STRING).description("색상 코드"), + fieldWithPath("payload.banners[].image_url").type(JsonFieldType.STRING).description("배너 이미지 URL"), + fieldWithPath("payload.banners[].is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), + fieldWithPath("payload.banners[].created_date").type(JsonFieldType.STRING).description("생성일자"), + fieldWithPath("payload.banners[].link_url").type(JsonFieldType.STRING).description("링크 URL"), + fieldWithPath("payload.banners[].click_count").type(JsonFieldType.NUMBER).description("클릭 수") + ) + .responseSchema(Schema.schema("GetAllActivatedBannersResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("활성화된 띠배너를 전체 조회한다.") + public void getAllActivatedBarBanners() throws Exception { + // given + List barBanners = List.of( + new GetBarBannerResponse(1L, "공지사항", "Notice", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com"), + new GetBarBannerResponse(2L, "공지사항2", "Notice2", "#FF5733", "#FFFFFF", true, "2025-04-01 12:00:00", "https://www.link.com") + ); + GetAllActivatedBarBannersResponse response = GetAllActivatedBarBannersResponse.from(barBanners); + + // when + Mockito.when(bannerService.getAllActivatedBarBanners()).thenReturn(response); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/bar-banners/activated/all")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("활성화된 띠배너 전체 조회에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/activated-bar-banner-get-all", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("활성화된 띠배너를 전체 조회한다.") + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"), + fieldWithPath("payload").type(JsonFieldType.OBJECT).description("페이로드 객체"), + fieldWithPath("payload.bar_banners").type(JsonFieldType.ARRAY).description("띠배너 목록"), + fieldWithPath("payload.bar_banners[].id").type(JsonFieldType.NUMBER).description("띠배너 ID"), + fieldWithPath("payload.bar_banners[].content_kor").type(JsonFieldType.STRING).description("한국어 내용"), + fieldWithPath("payload.bar_banners[].content_eng").type(JsonFieldType.STRING).description("영어 내용"), + fieldWithPath("payload.bar_banners[].background_color_code").type(JsonFieldType.STRING).description("배경 색상 코드"), + fieldWithPath("payload.bar_banners[].text_color_code").type(JsonFieldType.STRING).description("텍스트 색상 코드"), + fieldWithPath("payload.bar_banners[].is_activated").type(JsonFieldType.BOOLEAN).description("활성화 여부"), + fieldWithPath("payload.bar_banners[].created_date").type(JsonFieldType.STRING).description("생성일자"), + fieldWithPath("payload.bar_banners[].link_url").type(JsonFieldType.STRING).description("링크 URL") + ) + .responseSchema(Schema.schema("GetAllActivatedBarBannersResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("배너를 수정한다.") + public void updateBanner() throws Exception { + // given + Long bannerId = 1L; + String accessToken = "Bearer temp.jwt.access.token"; + + UpdateBannerRequest request = new UpdateBannerRequest( + "Modified OneTime", + "Modified OneTime's Title", + "Modified OneTime's Sub Title", + "Modified OneTime's Button Text", + "#000000", + true, + "https://www.link.com" + ); + String requestContent = objectMapper.writeValueAsString(request); + + // when + Mockito.doNothing().when(bannerService).updateBanner(any(String.class), any(Long.class), any(UpdateBannerRequest.class), any(MultipartFile.class)); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.multipart("/api/v1/banners/{id}", bannerId) + .file(new MockMultipartFile("request", "", "application/json", requestContent.getBytes())) + .file(new MockMultipartFile("image_file", "banner.png", "image/png", "banner-image-content".getBytes())) + .header("Authorization", accessToken) + .with(r -> { + r.setMethod("PATCH"); + return r; + })) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("배너 수정에 성공했습니다.")) + .andDo(document("banner/banner-update", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("배너를 수정한다.") + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") + ) + .build() + ) + )); + } + + @Test + @DisplayName("띠배너를 수정한다.") + public void updateBarBanner() throws Exception { + // given + Long barBannerId = 1L; + String accessToken = "Bearer temp.jwt.access.token"; + UpdateBarBannerRequest request = new UpdateBarBannerRequest( + "수정된 내용", + "modified content", + "#123456", + "#FFFFFF", + true, + "https://www.link.com" + ); + String requestContent = objectMapper.writeValueAsString(request); + + // when + Mockito.doNothing().when(bannerService).updateBarBanner(any(String.class), eq(barBannerId), any(UpdateBarBannerRequest.class)); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/bar-banners/{id}", barBannerId) + .header("Authorization", accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(requestContent)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("띠배너 수정에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/bar-banner-update", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("띠배너를 수정한다.") + .requestFields( + fieldWithPath("content_kor").type(JsonFieldType.STRING).optional().description("한국어 내용"), + fieldWithPath("content_eng").type(JsonFieldType.STRING).optional().description("영어 내용"), + fieldWithPath("background_color_code").type(JsonFieldType.STRING).optional().description("배경 색상 코드"), + fieldWithPath("text_color_code").type(JsonFieldType.STRING).optional().description("텍스트 색상 코드"), + fieldWithPath("is_activated").type(JsonFieldType.BOOLEAN).optional().description("활성화 여부"), + fieldWithPath("link_url").type(JsonFieldType.STRING).optional().description("링크 URL") + ) + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") + ) + .requestSchema(Schema.schema("UpdateBarBannerRequest")) + .responseSchema(Schema.schema("CommonSuccessResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("배너를 삭제한다.") + public void deleteBanner() throws Exception { + // given + String accessToken = "Bearer test.jwt.token"; + Long bannerId = 1L; + + // when + Mockito.doNothing().when(bannerService).deleteBanner(any(String.class), eq(bannerId)); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/banners/{id}", bannerId) + .header("Authorization", accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("배너 삭제에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/banner-delete", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("배너를 삭제한다.") + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") + ) + .responseSchema(Schema.schema("CommonSuccessResponse")) + .build() + ) + )); + } + + @Test + @DisplayName("띠배너를 삭제한다.") + public void deleteBarBanner() throws Exception { + // given + String accessToken = "Bearer test.jwt.token"; + Long barBannerId = 1L; + + // when + Mockito.doNothing().when(bannerService).deleteBarBanner(any(String.class), eq(barBannerId)); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.delete("/api/v1/bar-banners/{id}", barBannerId) + .header("Authorization", accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.is_success").value(true)) + .andExpect(jsonPath("$.code").value("200")) + .andExpect(jsonPath("$.message").value("띠배너 삭제에 성공했습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("banner/bar-banner-delete", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Banner API") + .description("띠배너를 삭제한다.") + .responseFields( + fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지") + ) + .responseSchema(Schema.schema("CommonSuccessResponse")) + .build() + ) + )); + } + @Test @DisplayName("배너 클릭 수가 1 증가한다.") public void increaseBannerClickCount() throws Exception {