diff --git a/src/main/java/eatda/controller/store/StoreController.java b/src/main/java/eatda/controller/store/StoreController.java index 3f967ce5..5afa657c 100644 --- a/src/main/java/eatda/controller/store/StoreController.java +++ b/src/main/java/eatda/controller/store/StoreController.java @@ -23,8 +23,9 @@ public ResponseEntity getStoreImages(@PathVariable long storeId) } @GetMapping("/api/shops") - public ResponseEntity getStores(@RequestParam @Min(1) @Max(50) int size) { - return ResponseEntity.ok(storeService.getStores(size)); + public ResponseEntity getStores(@RequestParam @Min(1) @Max(50) int size, + @RequestParam(required = false) String category) { + return ResponseEntity.ok(storeService.getStores(size, category)); } @GetMapping("/api/shops/{storeId}") diff --git a/src/main/java/eatda/domain/store/StoreCategory.java b/src/main/java/eatda/domain/store/StoreCategory.java index 60a8d08b..9d2d91f3 100644 --- a/src/main/java/eatda/domain/store/StoreCategory.java +++ b/src/main/java/eatda/domain/store/StoreCategory.java @@ -12,7 +12,7 @@ public enum StoreCategory { CHINESE("중식"), JAPANESE("일식"), WESTERN("양식"), - CAFE("카페"), + CAFE("카페/디저트"), OTHER("기타"); private final String categoryName; diff --git a/src/main/java/eatda/repository/store/StoreRepository.java b/src/main/java/eatda/repository/store/StoreRepository.java index 508d3b37..9572c716 100644 --- a/src/main/java/eatda/repository/store/StoreRepository.java +++ b/src/main/java/eatda/repository/store/StoreRepository.java @@ -1,6 +1,7 @@ package eatda.repository.store; import eatda.domain.store.Store; +import eatda.domain.store.StoreCategory; import eatda.exception.BusinessErrorCode; import eatda.exception.BusinessException; import java.util.List; @@ -19,4 +20,6 @@ default Store getById(Long id) { Optional findByKakaoId(String kakaoId); List findAllByOrderByCreatedAtDesc(Pageable pageable); + + List findAllByCategoryOrderByCreatedAtDesc(StoreCategory category, Pageable pageable); } diff --git a/src/main/java/eatda/service/store/StoreService.java b/src/main/java/eatda/service/store/StoreService.java index 02e3ffdb..b1ad2062 100644 --- a/src/main/java/eatda/service/store/StoreService.java +++ b/src/main/java/eatda/service/store/StoreService.java @@ -11,6 +11,7 @@ import eatda.controller.store.StoreSearchResponses; import eatda.controller.store.StoresResponse; import eatda.domain.store.Store; +import eatda.domain.store.StoreCategory; import eatda.repository.store.CheerRepository; import eatda.repository.store.StoreRepository; import eatda.storage.image.ImageStorage; @@ -18,6 +19,7 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; @Service @@ -36,13 +38,21 @@ public StoreResponse getStore(long storeId) { } // TODO : N+1 문제 해결 - public StoresResponse getStores(int size) { - return storeRepository.findAllByOrderByCreatedAtDesc(Pageable.ofSize(size)) + public StoresResponse getStores(int size, @Nullable String category) { + return findStores(size, category) .stream() .map(store -> new StorePreviewResponse(store, getStoreImageUrl(store).orElse(null))) .collect(collectingAndThen(toList(), StoresResponse::new)); } + private List findStores(int size, @Nullable String category) { + if (category == null || category.isBlank()) { + return storeRepository.findAllByOrderByCreatedAtDesc(Pageable.ofSize(size)); + } + return storeRepository.findAllByCategoryOrderByCreatedAtDesc( + StoreCategory.from(category), Pageable.ofSize(size)); + } + public ImagesResponse getStoreImages(long storeId) { Store store = storeRepository.getById(storeId); List imageUrls = cheerRepository.findAllImageKey(store) diff --git a/src/test/java/eatda/controller/store/StoreControllerTest.java b/src/test/java/eatda/controller/store/StoreControllerTest.java index ef826bd3..aa81bc70 100644 --- a/src/test/java/eatda/controller/store/StoreControllerTest.java +++ b/src/test/java/eatda/controller/store/StoreControllerTest.java @@ -6,6 +6,7 @@ import eatda.controller.BaseControllerTest; import eatda.domain.member.Member; import eatda.domain.store.Store; +import eatda.domain.store.StoreCategory; import java.time.LocalDateTime; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -43,12 +44,14 @@ class GetStore { class GetStores { @Test - void 음식점_목록을_최신순으로_조회한다() { + void 모든_카테고리의_음식점_목록을_최신순으로_조회한다() { Member member = memberGenerator.generate("111"); LocalDateTime startAt = LocalDateTime.of(2025, 7, 26, 1, 0, 0); - Store store1 = storeGenerator.generate("111", "서울 강남구 대치동 896-33", startAt); - Store store2 = storeGenerator.generate("222", "서울 강남구 대치동 896-34", startAt.plusHours(1)); - Store store3 = storeGenerator.generate("333", "서울 강남구 대치동 896-35", startAt.plusHours(2)); + Store store1 = storeGenerator.generate("112", "서울 강남구 대치동 896-33", StoreCategory.KOREAN, startAt); + Store store2 = storeGenerator.generate("113", "서울 성북구 석관동 123-45", StoreCategory.OTHER, + startAt.plusHours(1)); + Store store3 = storeGenerator.generate("114", "서울 강남구 역삼동 678-90", StoreCategory.KOREAN, + startAt.plusHours(2)); cheerGenerator.generateCommon(member, store1, "image-key-1"); cheerGenerator.generateCommon(member, store2, "image-key-2"); cheerGenerator.generateCommon(member, store3, "image-key-3"); @@ -69,6 +72,38 @@ class GetStores { () -> assertThat(response.stores().get(1).id()).isEqualTo(store2.getId()) ); } + + @Test + void 특정_카테고리의_음식점_목록을_최신순으로_조회한다() { + Member member = memberGenerator.generate("111"); + LocalDateTime startAt = LocalDateTime.of(2025, 7, 26, 1, 0, 0); + Store store1 = storeGenerator.generate("112", "서울 강남구 대치동 896-33", StoreCategory.CAFE, startAt); + Store store2 = storeGenerator.generate("113", "서울 성북구 석관동 123-45", StoreCategory.OTHER, + startAt.plusHours(1)); + Store store3 = storeGenerator.generate("114", "서울 강남구 역삼동 678-90", StoreCategory.CAFE, + startAt.plusHours(2)); + cheerGenerator.generateCommon(member, store1, "image-key-1"); + cheerGenerator.generateCommon(member, store2, "image-key-2"); + cheerGenerator.generateCommon(member, store3, "image-key-3"); + + int size = 2; + StoreCategory category = StoreCategory.CAFE; + + StoresResponse response = given() + .queryParam("size", size) + .queryParam("category", category.getCategoryName()) + .when() + .get("/api/shops") + .then() + .statusCode(200) + .extract().as(StoresResponse.class); + + assertAll( + () -> assertThat(response.stores()).hasSize(size), + () -> assertThat(response.stores().get(0).id()).isEqualTo(store3.getId()), + () -> assertThat(response.stores().get(1).id()).isEqualTo(store1.getId()) + ); + } } @Nested diff --git a/src/test/java/eatda/document/store/StoreDocumentTest.java b/src/test/java/eatda/document/store/StoreDocumentTest.java index 1845b77b..46c70a8e 100644 --- a/src/test/java/eatda/document/store/StoreDocumentTest.java +++ b/src/test/java/eatda/document/store/StoreDocumentTest.java @@ -1,7 +1,6 @@ package eatda.document.store; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; @@ -23,6 +22,7 @@ import eatda.document.RestDocsRequest; import eatda.document.RestDocsResponse; import eatda.document.Tag; +import eatda.domain.store.StoreCategory; import eatda.exception.BusinessErrorCode; import eatda.exception.BusinessException; import io.restassured.http.ContentType; @@ -101,7 +101,9 @@ class GetStores { .tag(Tag.STORE_API) .summary("음식점 목록 조회") .queryParameter( - parameterWithName("size").description("조회할 음식점 개수 (최소 1, 최대 50)") + parameterWithName("size").description("조회할 음식점 개수 (최소 1, 최대 50)"), + parameterWithName("category") + .description("음식점 카테고리(기본값: 전체) (한식,중식,일식,양식,디저트/카페,기타)").optional() ); RestDocsResponse responseDocument = response() @@ -117,13 +119,14 @@ class GetStores { @Test void 음식점_목록_최신순으로_조회() { + int size = 2; + StoreCategory category = StoreCategory.CAFE; StoresResponse response = new StoresResponse(List.of( new StorePreviewResponse(2L, "https://example.image", "농민백암순대", "강남구", "대치동", "한식"), new StorePreviewResponse(1L, "https://example.image", "석관동떡볶이", "성북구", "석관동", "한식") )); - doReturn(response).when(storeService).getStores(anyInt()); + doReturn(response).when(storeService).getStores(size, category.getCategoryName()); - int size = 2; var document = document("store/get", 200) .request(requestDocument) .response(responseDocument) @@ -132,6 +135,7 @@ class GetStores { given(document) .contentType(ContentType.JSON) .queryParam("size", size) + .queryParam("category", category.getCategoryName()) .when().get("/api/shops") .then().statusCode(200); } @@ -139,9 +143,10 @@ class GetStores { @EnumSource(value = BusinessErrorCode.class, names = {"PRESIGNED_URL_GENERATION_FAILED"}) @ParameterizedTest void 음식점_목록_조회_실패(BusinessErrorCode errorCode) { - doThrow(new BusinessException(errorCode)).when(storeService).getStores(anyInt()); - int size = 2; + StoreCategory category = StoreCategory.CAFE; + doThrow(new BusinessException(errorCode)).when(storeService).getStores(size, category.getCategoryName()); + var document = document("store/get", errorCode) .request(requestDocument) .response(ERROR_RESPONSE) @@ -150,6 +155,7 @@ class GetStores { given(document) .contentType(ContentType.JSON) .queryParam("size", size) + .queryParam("category", category.getCategoryName()) .when().get("/api/shops") .then().statusCode(errorCode.getStatus().value()); } diff --git a/src/test/java/eatda/fixture/StoreGenerator.java b/src/test/java/eatda/fixture/StoreGenerator.java index 445dae41..2372e501 100644 --- a/src/test/java/eatda/fixture/StoreGenerator.java +++ b/src/test/java/eatda/fixture/StoreGenerator.java @@ -26,9 +26,26 @@ public StoreGenerator(StoreRepository storeRepository) { } public Store generate(String kakaoId, String lotNumberAddress) { - Store store = Store.builder() + Store store = create(kakaoId, lotNumberAddress, DEFAULT_CATEGORY); + return storeRepository.save(store); + } + + public Store generate(String kakaoId, String lotNumberAddress, LocalDateTime createdAt) { + Store store = create(kakaoId, lotNumberAddress, DEFAULT_CATEGORY); + DomainUtils.setCreatedAt(store, createdAt); + return storeRepository.save(store); + } + + public Store generate(String kakaoId, String lotNumberAddress, StoreCategory category, LocalDateTime createdAt) { + Store store = create(kakaoId, lotNumberAddress, category); + DomainUtils.setCreatedAt(store, createdAt); + return storeRepository.save(store); + } + + private Store create(String kakaoId, String lotNumberAddress, StoreCategory category) { + return Store.builder() .kakaoId(kakaoId) - .category(DEFAULT_CATEGORY) + .category(category) .phoneNumber(DEFAULT_PHONE_NUMBER) .name(DEFAULT_NAME) .placeUrl(DEFAULT_PLACE_URL) @@ -37,12 +54,5 @@ public Store generate(String kakaoId, String lotNumberAddress) { .latitude(DEFAULT_LATITUDE) .longitude(DEFAULT_LONGITUDE) .build(); - return storeRepository.save(store); - } - - public Store generate(String kakaoId, String lotNumberAddress, LocalDateTime createdAt) { - Store store = generate(kakaoId, lotNumberAddress); - DomainUtils.setCreatedAt(store, createdAt); - return storeRepository.save(store); } } diff --git a/src/test/java/eatda/service/store/StoreServiceTest.java b/src/test/java/eatda/service/store/StoreServiceTest.java index f5ab894a..86497589 100644 --- a/src/test/java/eatda/service/store/StoreServiceTest.java +++ b/src/test/java/eatda/service/store/StoreServiceTest.java @@ -11,6 +11,7 @@ import eatda.controller.store.StoreResponse; import eatda.domain.member.Member; import eatda.domain.store.Store; +import eatda.domain.store.StoreCategory; import eatda.exception.BusinessErrorCode; import eatda.exception.BusinessException; import eatda.service.BaseServiceTest; @@ -59,19 +60,20 @@ class GetStore { class GetStores { @Test - void 음식점_목록을_최신순으로_조회한다() { + void 모든_카테고리의_음식점_목록을_최신순으로_조회한다() { Member member = memberGenerator.generate("111"); LocalDateTime startAt = LocalDateTime.of(2025, 7, 26, 1, 0, 0); - Store store1 = storeGenerator.generate("농민백암순대", "서울 강남구 대치동 896-33", startAt); - Store store2 = storeGenerator.generate("석관동떡볶이", "서울 성북구 석관동 123-45", startAt.plusHours(1)); - Store store3 = storeGenerator.generate("강남순대국", "서울 강남구 역삼동 678-90", startAt.plusHours(2)); + Store store1 = storeGenerator.generate("농민백암순대", "서울 강남구 대치동 896-33", StoreCategory.KOREAN, startAt); + Store store2 = storeGenerator.generate("석관동떡볶이", "서울 성북구 석관동 123-45", StoreCategory.OTHER, + startAt.plusHours(1)); + Store store3 = storeGenerator.generate("강남순대국", "서울 강남구 역삼동 678-90", StoreCategory.KOREAN, + startAt.plusHours(2)); cheerGenerator.generateCommon(member, store1, "image-key-1"); cheerGenerator.generateCommon(member, store2, "image-key-2"); cheerGenerator.generateCommon(member, store3, "image-key-3"); - int size = 2; - var response = storeService.getStores(size); + var response = storeService.getStores(size, null); assertAll( () -> assertThat(response.stores()).hasSize(size), @@ -79,6 +81,30 @@ class GetStores { () -> assertThat(response.stores().get(1).id()).isEqualTo(store2.getId()) ); } + + @Test + void 특정_카테고리의_음식점_목록을_최신순으로_조회한다() { + Member member = memberGenerator.generate("111"); + LocalDateTime startAt = LocalDateTime.of(2025, 7, 26, 1, 0, 0); + Store store1 = storeGenerator.generate("112", "서울 강남구 대치동 896-33", StoreCategory.KOREAN, startAt); + Store store2 = storeGenerator.generate("113", "서울 성북구 석관동 123-45", StoreCategory.OTHER, + startAt.plusHours(1)); + Store store3 = storeGenerator.generate("114", "서울 강남구 역삼동 678-90", StoreCategory.KOREAN, + startAt.plusHours(2)); + cheerGenerator.generateCommon(member, store1, "image-key-1"); + cheerGenerator.generateCommon(member, store2, "image-key-2"); + cheerGenerator.generateCommon(member, store3, "image-key-3"); + int size = 2; + StoreCategory category = StoreCategory.KOREAN; + + var response = storeService.getStores(size, category.getCategoryName()); + + assertAll( + () -> assertThat(response.stores()).hasSize(size), + () -> assertThat(response.stores().get(0).id()).isEqualTo(store3.getId()), + () -> assertThat(response.stores().get(1).id()).isEqualTo(store1.getId()) + ); + } } @Nested