From 80412a50ecbfb5f46b0c47b3649d620ff8c4677b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 27 May 2026 12:25:08 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EC=83=81=EC=A0=90=20=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 주변상점 이벤트 영역에서 진행 중인 이벤트를 가진 상점 수를 별도로 조회할 수 있도록 엔드포인트를 추가 - 같은 상점에 진행 중인 이벤트가 여러 개 있어도 상점 기준으로 한 번만 집계하도록 DISTINCT count 쿼리를 사용 - 현재 날짜 기준 진행 중 이벤트만 포함되도록 수락 테스트로 중복 집계와 기간 제외 동작을 검증 --- .../domain/shop/controller/ShopEventApi.java | 13 ++++++++++++ .../shop/controller/ShopEventController.java | 8 +++++++- .../response/ShopEventCountResponse.java | 11 ++++++++++ .../shop/repository/shop/ShopRepository.java | 11 ++++++++++ .../domain/shop/service/ShopEventService.java | 8 ++++++++ .../koin/acceptance/domain/ShopApiTest.java | 20 +++++++++++++++++++ 6 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/main/java/in/koreatech/koin/domain/shop/dto/event/response/ShopEventCountResponse.java diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventApi.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventApi.java index ccbe2d44dc..df26de5a1f 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventApi.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventApi.java @@ -1,5 +1,6 @@ package in.koreatech.koin.domain.shop.controller; +import in.koreatech.koin.domain.shop.dto.event.response.ShopEventCountResponse; import in.koreatech.koin.domain.shop.dto.event.response.ShopEventsWithBannerUrlResponse; import in.koreatech.koin.domain.shop.dto.event.response.ShopEventsWithThumbnailUrlResponse; import io.swagger.v3.oas.annotations.Operation; @@ -40,4 +41,16 @@ ResponseEntity getShopEvents( @Operation(summary = "모든 상점의 모든 이벤트 조회") @GetMapping("/shops/events") ResponseEntity getShopAllEvent(); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "이벤트 진행 중인 상점 개수 조회") + @GetMapping("/shops/events/count") + ResponseEntity getEventShopCount(); } diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventController.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventController.java index b4b6f3371e..e7d96eb2e0 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventController.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventController.java @@ -1,9 +1,9 @@ package in.koreatech.koin.domain.shop.controller; +import in.koreatech.koin.domain.shop.dto.event.response.ShopEventCountResponse; import in.koreatech.koin.domain.shop.dto.event.response.ShopEventsWithBannerUrlResponse; import in.koreatech.koin.domain.shop.dto.event.response.ShopEventsWithThumbnailUrlResponse; import in.koreatech.koin.domain.shop.service.ShopEventService; -import in.koreatech.koin.domain.shop.service.ShopService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -29,4 +29,10 @@ public ResponseEntity getShopAllEvent() { var response = shopEventService.getAllEvents(); return ResponseEntity.ok(response); } + + @GetMapping("/shops/events/count") + public ResponseEntity getEventShopCount() { + var response = shopEventService.getEventShopCount(); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/event/response/ShopEventCountResponse.java b/src/main/java/in/koreatech/koin/domain/shop/dto/event/response/ShopEventCountResponse.java new file mode 100644 index 0000000000..b187d06fd6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/event/response/ShopEventCountResponse.java @@ -0,0 +1,11 @@ +package in.koreatech.koin.domain.shop.dto.event.response; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record ShopEventCountResponse( + @Schema(example = "5", description = "이벤트 진행 중인 상점 개수", requiredMode = REQUIRED) + Integer count +) { +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopRepository.java b/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopRepository.java index 9868921741..27fe0a2280 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopRepository.java +++ b/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopRepository.java @@ -46,6 +46,17 @@ default Shop getById(Integer shopId) { """) List findAllWithEventArticles(); + @Query(""" + SELECT COUNT(DISTINCT s.id) + FROM Shop s + JOIN s.eventArticles e + WHERE s.isDeleted = false + AND e.isDeleted = false + AND e.startDate <= :now + AND e.endDate >= :now + """) + Long countShopsWithOngoingEvent(@Param("now") LocalDate now); + @Query(""" SELECT new in.koreatech.koin.domain.shop.dto.shop.ShopNotificationQueryResponse( s.id, diff --git a/src/main/java/in/koreatech/koin/domain/shop/service/ShopEventService.java b/src/main/java/in/koreatech/koin/domain/shop/service/ShopEventService.java index 96e2cffb5c..ce6a2f9c24 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/service/ShopEventService.java +++ b/src/main/java/in/koreatech/koin/domain/shop/service/ShopEventService.java @@ -1,10 +1,12 @@ package in.koreatech.koin.domain.shop.service; +import in.koreatech.koin.domain.shop.dto.event.response.ShopEventCountResponse; import in.koreatech.koin.domain.shop.dto.event.response.ShopEventsWithBannerUrlResponse; import in.koreatech.koin.domain.shop.dto.event.response.ShopEventsWithThumbnailUrlResponse; import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.domain.shop.repository.shop.ShopRepository; import java.time.Clock; +import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -27,4 +29,10 @@ public ShopEventsWithBannerUrlResponse getAllEvents() { List shops = shopRepository.findAllWithEventArticles(); return ShopEventsWithBannerUrlResponse.of(shops, clock); } + + public ShopEventCountResponse getEventShopCount() { + LocalDate now = LocalDate.now(clock); + Long count = shopRepository.countShopsWithOngoingEvent(now); + return new ShopEventCountResponse(Math.toIntExact(count)); + } } diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/ShopApiTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/ShopApiTest.java index 361fb2423d..f74026619b 100644 --- a/src/test/java/in/koreatech/koin/acceptance/domain/ShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/domain/ShopApiTest.java @@ -560,6 +560,26 @@ void setUp() { """)); } + @Test + void 이벤트_진행중인_상점_개수를_조회한다() throws Exception { + Shop 영업중인_티바 = shopFixture.영업중인_티바(owner); + Shop 영업중이_아닌_신전떡볶이 = shopFixture.영업중이_아닌_신전_떡볶이(owner); + eventArticleFixture.할인_이벤트(마슬랜, LocalDate.now(clock).minusDays(3), LocalDate.now(clock).plusDays(3)); + eventArticleFixture.참여_이벤트(마슬랜, LocalDate.now(clock).minusDays(3), LocalDate.now(clock).plusDays(3)); + eventArticleFixture.참여_이벤트(영업중인_티바, LocalDate.now(clock), LocalDate.now(clock).plusDays(10)); + eventArticleFixture.할인_이벤트(영업중이_아닌_신전떡볶이, LocalDate.now(clock).minusDays(10), LocalDate.now(clock).minusDays(1)); + + mockMvc.perform( + get("/shops/events/count") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "count": 2 + } + """)); + } + @Test void 리뷰_평점순으로_정렬하여_모든_상점을_조회한다() throws Exception { Shop 영업중인_티바 = shopFixture.영업중인_티바(owner); From 4b0bb5c4ee28a73ca25a4f3d52ddb6cce944c77d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 27 May 2026 14:14:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=83=81=EC=A0=90=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=9C=EC=88=98=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 신규 이벤트 상점 개수 조회 API의 Swagger 응답 정의를 공통 ApiResponseCodes 형식으로 맞췄습니다. - 실제 구현이 성공 응답만 반환하므로 불필요한 401/403/404 응답 선언을 제거했습니다. --- .../koin/domain/shop/controller/ShopEventApi.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventApi.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventApi.java index df26de5a1f..70a0aa589b 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventApi.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopEventApi.java @@ -1,8 +1,11 @@ package in.koreatech.koin.domain.shop.controller; +import static in.koreatech.koin.global.code.ApiResponseCode.OK; + import in.koreatech.koin.domain.shop.dto.event.response.ShopEventCountResponse; import in.koreatech.koin.domain.shop.dto.event.response.ShopEventsWithBannerUrlResponse; import in.koreatech.koin.domain.shop.dto.event.response.ShopEventsWithThumbnailUrlResponse; +import in.koreatech.koin.global.code.ApiResponseCodes; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -42,14 +45,9 @@ ResponseEntity getShopEvents( @GetMapping("/shops/events") ResponseEntity getShopAllEvent(); - @ApiResponses( - value = { - @ApiResponse(responseCode = "200"), - @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), - @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), - } - ) + @ApiResponseCodes({ + OK + }) @Operation(summary = "이벤트 진행 중인 상점 개수 조회") @GetMapping("/shops/events/count") ResponseEntity getEventShopCount();