From e6684481da9f8e0b2ccffdb1d7755411f7fba1c0 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 11:20:38 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=ED=98=84=EC=9E=AC=20=EC=98=81=EC=97=85=20?= =?UTF-8?q?=EC=A4=91=EC=9D=B8=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 - 상점 목록 전체를 내려받지 않아도 현재 영업 중인 상점 수만 조회할 수 있도록 별도 API를 추가 - 기존 상점 영업 여부 기준과 어긋나지 않도록 현재 요일과 이전 요일의 심야 영업 케이스를 함께 고려 - 인수 테스트를 추가해 고정 시각 기준으로 오픈 상점 수 응답 계약을 검증 --- .../koin/domain/shop/controller/ShopApi.java | 13 +++++++ .../shop/controller/ShopController.java | 7 ++++ .../shop/response/OpenShopsCountResponse.java | 11 ++++++ .../shop/repository/shop/ShopRepository.java | 36 +++++++++++++++++++ .../koin/domain/shop/service/ShopService.java | 15 ++++++++ .../koin/acceptance/domain/ShopApiTest.java | 16 +++++++++ 6 files changed, 98 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/shop/dto/shop/response/OpenShopsCountResponse.java diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java index 3916581ac9..703eee8b8d 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java @@ -16,6 +16,7 @@ import in.koreatech.koin.domain.shop.dto.shop.ShopsFilterCriteriaV3; import in.koreatech.koin.domain.shop.dto.shop.ShopsSortCriteria; import in.koreatech.koin.domain.shop.dto.shop.ShopsSortCriteriaV3; +import in.koreatech.koin.domain.shop.dto.shop.response.OpenShopsCountResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopCategoriesResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopResponseV2; @@ -87,6 +88,18 @@ ResponseEntity getShopSummary( @GetMapping("/shops") ResponseEntity getShops(); + @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/open/count") + ResponseEntity getOpenShopsCount(); + @ApiResponses( value = { @ApiResponse(responseCode = "200"), diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java index 9a1a5fbda7..63af016029 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java @@ -18,6 +18,7 @@ import in.koreatech.koin.domain.shop.dto.shop.ShopsFilterCriteriaV3; import in.koreatech.koin.domain.shop.dto.shop.ShopsSortCriteria; import in.koreatech.koin.domain.shop.dto.shop.ShopsSortCriteriaV3; +import in.koreatech.koin.domain.shop.dto.shop.response.OpenShopsCountResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopCategoriesResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopResponseV2; @@ -68,6 +69,12 @@ public ResponseEntity getShops() { return ResponseEntity.ok(shopsResponse); } + @GetMapping("/shops/open/count") + public ResponseEntity getOpenShopsCount() { + OpenShopsCountResponse response = shopService.getOpenShopsCount(); + return ResponseEntity.ok(response); + } + @GetMapping("/shops/categories") public ResponseEntity getShopsCategories() { ShopCategoriesResponse shopCategoriesResponse = shopService.getShopsCategories(); diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/shop/response/OpenShopsCountResponse.java b/src/main/java/in/koreatech/koin/domain/shop/dto/shop/response/OpenShopsCountResponse.java new file mode 100644 index 0000000000..0b655c2dd6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/shop/response/OpenShopsCountResponse.java @@ -0,0 +1,11 @@ +package in.koreatech.koin.domain.shop.dto.shop.response; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record OpenShopsCountResponse( + @Schema(example = "24", 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..5521f2af03 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 @@ -4,6 +4,7 @@ import in.koreatech.koin.domain.shop.exception.ShopNotFoundException; import in.koreatech.koin.domain.shop.model.shop.Shop; import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; import java.util.Optional; @@ -36,6 +37,41 @@ default Shop getById(Integer shopId) { """) List findAll(); + @Query(""" + SELECT COUNT(DISTINCT s.id) + FROM Shop s + JOIN s.shopOpens so + WHERE s.isDeleted = false + AND so.isDeleted = false + AND so.closed = false + AND ( + ( + so.dayOfWeek = :currentDayOfWeek + AND ( + ( + so.closeTime > so.openTime + AND so.openTime <= :currentTime + AND so.closeTime >= :currentTime + ) + OR ( + so.closeTime <= so.openTime + AND so.openTime <= :currentTime + ) + ) + ) + OR ( + so.dayOfWeek = :previousDayOfWeek + AND so.closeTime <= so.openTime + AND so.closeTime >= :currentTime + ) + ) + """) + Long countOpenShops( + @Param("currentDayOfWeek") String currentDayOfWeek, + @Param("previousDayOfWeek") String previousDayOfWeek, + @Param("currentTime") LocalTime currentTime + ); + @Query(""" SELECT s FROM Shop s diff --git a/src/main/java/in/koreatech/koin/domain/shop/service/ShopService.java b/src/main/java/in/koreatech/koin/domain/shop/service/ShopService.java index 8b474c53a3..5a6f7fb656 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/service/ShopService.java +++ b/src/main/java/in/koreatech/koin/domain/shop/service/ShopService.java @@ -6,9 +6,11 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.time.format.TextStyle; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import org.springframework.data.domain.Sort; @@ -24,6 +26,7 @@ import in.koreatech.koin.domain.shop.dto.shop.ShopsFilterCriteriaV3; import in.koreatech.koin.domain.shop.dto.shop.ShopsSortCriteria; import in.koreatech.koin.domain.shop.dto.shop.ShopsSortCriteriaV3; +import in.koreatech.koin.domain.shop.dto.shop.response.OpenShopsCountResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopCategoriesResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopResponse; import in.koreatech.koin.domain.shop.dto.shop.response.ShopResponseV2; @@ -82,6 +85,14 @@ public ShopsResponse getShops() { return ShopsResponse.from(shops, eventDuration, now); } + public OpenShopsCountResponse getOpenShopsCount() { + LocalDateTime now = LocalDateTime.now(clock); + String currentDayOfWeek = getDayOfWeek(now); + String previousDayOfWeek = getDayOfWeek(now.minusDays(1)); + Long count = shopRepository.countOpenShops(currentDayOfWeek, previousDayOfWeek, now.toLocalTime()); + return new OpenShopsCountResponse(Math.toIntExact(count)); + } + public ShopCategoriesResponse getShopsCategories() { List shopCategories = shopCategoryRepository.findAll(Sort.by("orderIndex")); return ShopCategoriesResponse.from(shopCategories); @@ -178,4 +189,8 @@ public void publishCallNotification(Integer shopId, Integer studentId) { private boolean isSubscribeReviewNotification(Integer studentId) { return notificationSubscribeRepository.existsByUserIdAndSubscribeTypeAndDetailTypeIsNull(studentId, REVIEW_PROMPT); } + + private String getDayOfWeek(LocalDateTime dateTime) { + return dateTime.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.US).toUpperCase(); + } } 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..951c69de5c 100644 --- a/src/test/java/in/koreatech/koin/acceptance/domain/ShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/domain/ShopApiTest.java @@ -429,6 +429,22 @@ void setUp() { """, 마슬랜_영업여부, 신전_떡볶이_영업여부))); } + @Test + void 현재_영업중인_상점_개수를_조회한다() throws Exception { + shopFixture.영업중이_아닌_신전_떡볶이(owner); + + // 2024-01-15 12:00 월요일 기준 + mockMvc.perform( + get("/shops/open/count") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "count": 1 + } + """)); + } + @Test void 상점의_정렬된_모든_카테고리_조회() throws Exception { shopCategoryFixture.카테고리_일반음식(shopParentCategory_가게); // 카테고리_치킨이 먼저 생성됨 From a28e34368a8ae87e151b130b99fb090eca2445e5 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:08:48 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EB=B0=98=EC=98=81:=20=EC=83=81=EC=A0=90=20API=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?(#2271)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 신규 영업 중 상점 수 조회 API의 Swagger 응답 정의를 기존 @ApiResponses 대신 @ApiResponseCodes로 통일했습니다. - CallvanApi와 같은 정적 import 방식을 적용해 응답 코드 선언을 간결하게 유지했습니다. - 더 이상 사용하지 않는 직접 ApiResponseCode import를 제거해 리뷰에서 지적된 불필요한 코드가 남지 않도록 했습니다. --- .../koin/domain/shop/controller/ShopApi.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java index 703eee8b8d..434edc249d 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java @@ -1,6 +1,7 @@ package in.koreatech.koin.domain.shop.controller; import static in.koreatech.koin.domain.user.model.UserType.*; +import static in.koreatech.koin.global.code.ApiResponseCode.*; import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; import java.util.List; @@ -25,7 +26,6 @@ import in.koreatech.koin.domain.shop.dto.shop.response.ShopsResponseV2; import in.koreatech.koin.domain.shop.dto.shop.response.ShopsResponseV3; import in.koreatech.koin.global.auth.Auth; -import in.koreatech.koin.global.code.ApiResponseCode; import in.koreatech.koin.global.code.ApiResponseCodes; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -57,7 +57,7 @@ ResponseEntity getShopById( - openTime, closeTime 응답값 추가 """) @ApiResponseCodes({ - ApiResponseCode.OK + OK }) @GetMapping("/v2/shops/{id}") ResponseEntity getShopByIdV2( @@ -88,14 +88,9 @@ ResponseEntity getShopSummary( @GetMapping("/shops") ResponseEntity getShops(); - @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/open/count") ResponseEntity getOpenShopsCount();