diff --git a/src/main/java/eatda/controller/store/StoreController.java b/src/main/java/eatda/controller/store/StoreController.java index f77e0593..bfeb07af 100644 --- a/src/main/java/eatda/controller/store/StoreController.java +++ b/src/main/java/eatda/controller/store/StoreController.java @@ -23,9 +23,10 @@ public class StoreController { private final StoreService storeService; private final StoreSearchService storeSearchService; - @GetMapping("/api/shops/{storeId}/images") - public ResponseEntity getStoreImages(@PathVariable long storeId) { - return ResponseEntity.ok(storeService.getStoreImages(storeId)); + @GetMapping("/api/shops/{storeId}") + public ResponseEntity getStore(@PathVariable long storeId) { + StoreResponse response = storeService.getStore(storeId); + return ResponseEntity.ok(response); } @GetMapping("/api/shops") @@ -36,9 +37,14 @@ public ResponseEntity getStores(@RequestParam(defaultValue = "0" return ResponseEntity.ok(response); } - @GetMapping("/api/shops/{storeId}") - public ResponseEntity getStore(@PathVariable long storeId) { - StoreResponse response = storeService.getStore(storeId); + @GetMapping("/api/shops/{storeId}/images") + public ResponseEntity getStoreImages(@PathVariable long storeId) { + return ResponseEntity.ok(storeService.getStoreImages(storeId)); + } + + @GetMapping("/api/shops/{storeId}/tags") + public ResponseEntity getStoreTags(@PathVariable long storeId) { + TagsResponse response = storeService.getStoreTags(storeId); return ResponseEntity.ok(response); } diff --git a/src/main/java/eatda/controller/store/TagsResponse.java b/src/main/java/eatda/controller/store/TagsResponse.java new file mode 100644 index 00000000..b8ab45e5 --- /dev/null +++ b/src/main/java/eatda/controller/store/TagsResponse.java @@ -0,0 +1,16 @@ +package eatda.controller.store; + +import eatda.domain.cheer.CheerTag; +import eatda.domain.cheer.CheerTagName; +import java.util.List; + +public record TagsResponse(List tags) { + + public static TagsResponse from(List cheerTags) { + List cheerTagNames = cheerTags.stream() + .map(CheerTag::getName) + .distinct() + .toList(); + return new TagsResponse(cheerTagNames); + } +} diff --git a/src/main/java/eatda/repository/cheer/CheerTagRepository.java b/src/main/java/eatda/repository/cheer/CheerTagRepository.java index 3f52b3de..08ac65a4 100644 --- a/src/main/java/eatda/repository/cheer/CheerTagRepository.java +++ b/src/main/java/eatda/repository/cheer/CheerTagRepository.java @@ -1,8 +1,11 @@ package eatda.repository.cheer; import eatda.domain.cheer.CheerTag; +import eatda.domain.store.Store; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface CheerTagRepository extends JpaRepository { + List findAllByCheerStore(Store storeId); } diff --git a/src/main/java/eatda/service/cheer/CheerService.java b/src/main/java/eatda/service/cheer/CheerService.java index 08ba5681..0bfd33e0 100644 --- a/src/main/java/eatda/service/cheer/CheerService.java +++ b/src/main/java/eatda/service/cheer/CheerService.java @@ -16,9 +16,7 @@ import eatda.domain.store.StoreSearchResult; import eatda.exception.BusinessErrorCode; import eatda.exception.BusinessException; -import eatda.repository.cheer.CheerImageRepository; import eatda.repository.cheer.CheerRepository; -import eatda.repository.cheer.CheerTagRepository; import eatda.repository.member.MemberRepository; import eatda.repository.store.StoreRepository; import java.util.Comparator; @@ -39,8 +37,6 @@ public class CheerService { private final MemberRepository memberRepository; private final StoreRepository storeRepository; private final CheerRepository cheerRepository; - private final CheerTagRepository cheerTagRepository; - private final CheerImageRepository cheerImageRepository; private final FileClient fileClient; @Value("${cdn.base-url}") @@ -79,7 +75,8 @@ private void validateRegisterCheer(Member member, String storeKakaoId) { } } - private List sortImages(List images) { + private List sortImages( + List images) { return images.stream() .sorted(Comparator.comparingLong(CheerRegisterRequest.UploadedImageDetail::orderIndex)) .toList(); diff --git a/src/main/java/eatda/service/store/StoreService.java b/src/main/java/eatda/service/store/StoreService.java index e59ee2b1..1a3b079b 100644 --- a/src/main/java/eatda/service/store/StoreService.java +++ b/src/main/java/eatda/service/store/StoreService.java @@ -9,11 +9,14 @@ import eatda.controller.store.StoreResponse; import eatda.controller.store.StoresInMemberResponse; import eatda.controller.store.StoresResponse; +import eatda.controller.store.TagsResponse; import eatda.domain.cheer.CheerImage; +import eatda.domain.cheer.CheerTag; import eatda.domain.store.Store; import eatda.domain.store.StoreCategory; import eatda.repository.cheer.CheerImageRepository; import eatda.repository.cheer.CheerRepository; +import eatda.repository.cheer.CheerTagRepository; import eatda.repository.store.StoreRepository; import java.util.List; import java.util.Optional; @@ -30,6 +33,7 @@ public class StoreService { private final StoreRepository storeRepository; private final CheerRepository cheerRepository; + private final CheerTagRepository cheerTagRepository; private final CheerImageRepository cheerImageRepository; @Value("${cdn.base-url}") @@ -57,6 +61,13 @@ private List findStores(int page, int size, @Nullable String category) { StoreCategory.from(category), PageRequest.of(page, size)); } + @Transactional(readOnly = true) + public TagsResponse getStoreTags(long storeId) { + Store store = storeRepository.getById(storeId); + List cheerTags = cheerTagRepository.findAllByCheerStore(store); + return TagsResponse.from(cheerTags); + } + @Transactional(readOnly = true) public ImagesResponse getStoreImages(long storeId) { Store store = storeRepository.getById(storeId); diff --git a/src/test/java/eatda/controller/BaseControllerTest.java b/src/test/java/eatda/controller/BaseControllerTest.java index 9773ee24..0e502100 100644 --- a/src/test/java/eatda/controller/BaseControllerTest.java +++ b/src/test/java/eatda/controller/BaseControllerTest.java @@ -15,6 +15,7 @@ import eatda.domain.member.Member; import eatda.fixture.CheerGenerator; import eatda.fixture.CheerImageGenerator; +import eatda.fixture.CheerTagGenerator; import eatda.fixture.MemberGenerator; import eatda.fixture.StoreGenerator; import eatda.fixture.StoryGenerator; @@ -60,6 +61,9 @@ public class BaseControllerTest { @Autowired protected CheerGenerator cheerGenerator; + @Autowired + protected CheerTagGenerator cheerTagGenerator; + @Autowired protected StoryGenerator storyGenerator; diff --git a/src/test/java/eatda/controller/store/StoreControllerTest.java b/src/test/java/eatda/controller/store/StoreControllerTest.java index 87f33c4a..131b5e9c 100644 --- a/src/test/java/eatda/controller/store/StoreControllerTest.java +++ b/src/test/java/eatda/controller/store/StoreControllerTest.java @@ -5,10 +5,13 @@ import eatda.controller.BaseControllerTest; import eatda.domain.cheer.Cheer; +import eatda.domain.cheer.CheerTagName; import eatda.domain.member.Member; import eatda.domain.store.Store; import eatda.domain.store.StoreCategory; +import io.restassured.http.ContentType; import java.time.LocalDateTime; +import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -149,6 +152,30 @@ class GetStoreImages { } } + + @Nested + class GetStoreTags { + + @Test + void 음식점_태그들을_조회한다() { + Member member = memberGenerator.generate("111"); + Store store = storeGenerator.generate("농민백암순대", "서울 강남구 대치동 896-33"); + Cheer cheer = cheerGenerator.generateCommon(member, store); + cheerTagGenerator.generate(cheer, List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM)); + + TagsResponse response = given() + .when() + .contentType(ContentType.JSON) + .get("/api/shops/{storeId}/tags", store.getId()) + .then() + .statusCode(200) + .extract().as(TagsResponse.class); + + assertThat(response.tags()) + .containsExactlyInAnyOrder(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM); + } + } + @Nested class GetStoresByCheeredMember { diff --git a/src/test/java/eatda/document/store/StoreDocumentTest.java b/src/test/java/eatda/document/store/StoreDocumentTest.java index 868baefd..90ecec47 100644 --- a/src/test/java/eatda/document/store/StoreDocumentTest.java +++ b/src/test/java/eatda/document/store/StoreDocumentTest.java @@ -17,10 +17,12 @@ import eatda.controller.store.StoreResponse; import eatda.controller.store.StoresInMemberResponse; import eatda.controller.store.StoresResponse; +import eatda.controller.store.TagsResponse; import eatda.document.BaseDocumentTest; import eatda.document.RestDocsRequest; import eatda.document.RestDocsResponse; import eatda.document.Tag; +import eatda.domain.cheer.CheerTagName; import eatda.domain.store.District; import eatda.domain.store.StoreCategory; import eatda.domain.store.StoreSearchResult; @@ -228,6 +230,57 @@ class GetStoreImages { } + @Nested + class GetStoreTags { + + RestDocsRequest requestDocument = request() + .tag(Tag.STORE_API) + .summary("음식점 태그 조회") + .pathParameter( + parameterWithName("storeId").description("음식점 ID") + ); + + RestDocsResponse responseDocument = response() + .responseBodyField( + fieldWithPath("tags").type(ARRAY).description("음식점 태그 목록") + ); + + @Test + void 음식점_태그_조회_성공() { + long storeId = 7L; + TagsResponse response = new TagsResponse(List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM)); + doReturn(response).when(storeService).getStoreTags(storeId); + + var document = document("store/get-tags", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .when().get("/api/shops/{storeId}/tags", storeId) + .then().statusCode(200); + } + + @EnumSource(value = BusinessErrorCode.class, names = {"STORE_NOT_FOUND"}) + @ParameterizedTest + void 음식점_태그_조회_실패(BusinessErrorCode errorCode) { + long storeId = 1L; + doThrow(new BusinessException(errorCode)).when(storeService).getStoreTags(storeId); + + var document = document("store/get-tags", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .pathParam("storeId", storeId) + .when().get("/api/shops/{storeId}/tags") + .then().statusCode(errorCode.getStatus().value()); + } + } + @Nested class GetStoresByCheeredMember { diff --git a/src/test/java/eatda/fixture/CheerTagGenerator.java b/src/test/java/eatda/fixture/CheerTagGenerator.java new file mode 100644 index 00000000..f0c7ba01 --- /dev/null +++ b/src/test/java/eatda/fixture/CheerTagGenerator.java @@ -0,0 +1,24 @@ +package eatda.fixture; + +import eatda.domain.cheer.Cheer; +import eatda.domain.cheer.CheerTag; +import eatda.domain.cheer.CheerTagName; +import eatda.repository.cheer.CheerTagRepository; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class CheerTagGenerator { + + private final CheerTagRepository cheerTagRepository; + + public CheerTagGenerator(CheerTagRepository cheerTagRepository) { + this.cheerTagRepository = cheerTagRepository; + } + + public List generate(Cheer cheer, List tagNames) { + return tagNames.stream() + .map(name -> cheerTagRepository.save(new CheerTag(cheer, name))) + .toList(); + } +} diff --git a/src/test/java/eatda/service/BaseServiceTest.java b/src/test/java/eatda/service/BaseServiceTest.java index eb681176..d7c85446 100644 --- a/src/test/java/eatda/service/BaseServiceTest.java +++ b/src/test/java/eatda/service/BaseServiceTest.java @@ -6,6 +6,7 @@ import eatda.client.oauth.OauthClient; import eatda.fixture.CheerGenerator; import eatda.fixture.CheerImageGenerator; +import eatda.fixture.CheerTagGenerator; import eatda.fixture.MemberGenerator; import eatda.fixture.StoreGenerator; import eatda.fixture.StoryGenerator; @@ -54,6 +55,9 @@ public abstract class BaseServiceTest { @Autowired protected CheerGenerator cheerGenerator; + @Autowired + protected CheerTagGenerator cheerTagGenerator; + @Autowired protected StoryGenerator storyGenerator; diff --git a/src/test/java/eatda/service/store/StoreServiceTest.java b/src/test/java/eatda/service/store/StoreServiceTest.java index c48f868b..d2d70833 100644 --- a/src/test/java/eatda/service/store/StoreServiceTest.java +++ b/src/test/java/eatda/service/store/StoreServiceTest.java @@ -9,6 +9,9 @@ import eatda.controller.store.StoresInMemberResponse; import eatda.controller.store.StoresResponse; import eatda.domain.cheer.Cheer; +import eatda.controller.store.TagsResponse; +import eatda.domain.cheer.Cheer; +import eatda.domain.cheer.CheerTagName; import eatda.domain.member.Member; import eatda.domain.store.District; import eatda.domain.store.Store; @@ -17,6 +20,7 @@ import eatda.exception.BusinessException; import eatda.service.BaseServiceTest; import java.time.LocalDateTime; +import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -164,6 +168,49 @@ class GetStores { } } + @Nested + class GetStoreTags { + + @Test + void 음식점의_태그들을_조회한다() { + Member member1 = memberGenerator.generate("111", "a@kakao.com", "nickname1"); + Member member2 = memberGenerator.generate("112", "b@kakao.com", "nickname2"); + Store store = storeGenerator.generate("농민백암순대", "서울 강남구 대치동 896-33"); + Cheer cheer1 = cheerGenerator.generateCommon(member1, store); + Cheer cheer2 = cheerGenerator.generateCommon(member2, store); + cheerTagGenerator.generate(cheer1, List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.ENERGETIC)); + cheerTagGenerator.generate(cheer2, List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM)); + + TagsResponse response = storeService.getStoreTags(store.getId()); + + assertThat(response.tags()).containsExactlyInAnyOrder( + CheerTagName.INSTAGRAMMABLE, CheerTagName.ENERGETIC, CheerTagName.CLEAN_RESTROOM); + } + + @Test + void 음식점의_태그가_없다면_빈_리스트를_반환한다() { + Member member1 = memberGenerator.generate("111", "a@kakao.com", "nickname1"); + Member member2 = memberGenerator.generate("112", "b@kakao.com", "nickname2"); + Store store = storeGenerator.generate("농민백암순대", "서울 강남구 대치동 896-33"); + cheerGenerator.generateCommon(member1, store); + cheerGenerator.generateCommon(member2, store); + + TagsResponse response = storeService.getStoreTags(store.getId()); + + assertThat(response.tags()).isEmpty(); + } + + @Test + void 음식점이_존재하지_않으면_예외를_발생시킨다() { + long nonExistentStoreId = 999L; + + BusinessException exception = assertThrows(BusinessException.class, + () -> storeService.getStoreTags(nonExistentStoreId)); + + assertThat(exception.getErrorCode()).isEqualTo(BusinessErrorCode.STORE_NOT_FOUND); + } + } + @Nested class GetStoreImages {