Skip to content

Commit a8a2960

Browse files
authored
Merge pull request #179 from YAPP-Github/feat/PRODUCT-262
[Feat] '최근 응원한 가게 조회 API'에 검색 조건 추가
2 parents 5abbd60 + 2c37531 commit a8a2960

File tree

12 files changed

+317
-127
lines changed

12 files changed

+317
-127
lines changed

src/main/java/eatda/controller/store/StoreController.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package eatda.controller.store;
22

33
import eatda.controller.web.auth.LoginMember;
4+
import eatda.domain.cheer.CheerTagName;
5+
import eatda.domain.store.SearchDistrict;
6+
import eatda.domain.store.StoreCategory;
47
import eatda.domain.store.StoreSearchResult;
58
import eatda.service.store.StoreSearchService;
69
import eatda.service.store.StoreService;
@@ -32,8 +35,11 @@ public ResponseEntity<StoreResponse> getStore(@PathVariable long storeId) {
3235
@GetMapping("/api/shops")
3336
public ResponseEntity<StoresResponse> getStores(@RequestParam(defaultValue = "0") @Min(0) int page,
3437
@RequestParam(defaultValue = "5") @Min(1) @Max(50) int size,
35-
@RequestParam(required = false) String category) {
36-
StoresResponse response = storeService.getStores(page, size, category);
38+
@RequestParam(required = false) StoreCategory category,
39+
@RequestParam(required = false) List<CheerTagName> tag,
40+
@RequestParam(required = false) List<SearchDistrict> location) {
41+
StoreSearchParameters parameters = new StoreSearchParameters(page, size, category, tag, location);
42+
StoresResponse response = storeService.getStores(parameters);
3743
return ResponseEntity.ok(response);
3844
}
3945

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package eatda.controller.store;
2+
3+
import eatda.domain.cheer.CheerTagName;
4+
import eatda.domain.store.District;
5+
import eatda.domain.store.SearchDistrict;
6+
import eatda.domain.store.StoreCategory;
7+
import java.util.Collections;
8+
import java.util.List;
9+
import lombok.Getter;
10+
import org.springframework.lang.Nullable;
11+
12+
public class StoreSearchParameters {
13+
14+
@Getter
15+
private final int page;
16+
@Getter
17+
private final int size;
18+
@Nullable
19+
private final StoreCategory category;
20+
private final List<CheerTagName> tag;
21+
private final List<SearchDistrict> location;
22+
23+
public StoreSearchParameters(int page,
24+
int size,
25+
@Nullable StoreCategory category,
26+
@Nullable List<CheerTagName> tag,
27+
@Nullable List<SearchDistrict> location) {
28+
this.page = page;
29+
this.size = size;
30+
this.category = category;
31+
this.tag = tag != null ? tag : Collections.emptyList();
32+
this.location = location != null ? location : Collections.emptyList();
33+
}
34+
35+
@Nullable
36+
public StoreCategory getCategory() {
37+
return category;
38+
}
39+
40+
public List<CheerTagName> getCheerTagNames() {
41+
return tag;
42+
}
43+
44+
public List<District> getDistricts() {
45+
return location.stream()
46+
.flatMap(district -> district.getDistricts().stream())
47+
.distinct()
48+
.toList();
49+
}
50+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package eatda.domain.store;
2+
3+
import java.util.List;
4+
import lombok.Getter;
5+
6+
@Getter
7+
public enum SearchDistrict {
8+
9+
GANGNAM("강남/역삼/선릉", List.of(District.GANGNAM)),
10+
KONDAE("건대/성수/서울숲/왕십리", List.of(District.SEONGDONG, District.GWANGJIN)),
11+
GEUMHO("금호/옥수/신당", List.of(District.JUNG)),
12+
HAPJEONG("합정/망원/홍대", List.of(District.MAPO)),
13+
SINCHON("신촌/이대", List.of(District.SEODAEMUN)),
14+
MYEONGDONG("명동/을지로/충무로", List.of(District.DONGDAEMUN, District.SEONGBUK)),
15+
SEOCHON("서촌/북촌/삼청", List.of(District.JONGNO)),
16+
DAECHI("대치/논현/서초", List.of(District.SEOCHO)),
17+
YONGSAN("용산/이태원/한남", List.of(District.YONGSAN, District.DONGJAK)),
18+
GEUMCHEON("금천/도봉/노원", List.of(District.GEUMCHEON, District.DOBONG, District.NOWON)),
19+
YEONGDEUNGPO("영등포/여의도", List.of(District.YEONGDEUNGPO)),
20+
JAMSIL("잠실/송파", List.of(District.SONGPA)),
21+
JONGRO("종로/광화문", List.of(District.JONGNO)),
22+
MAGOK("마곡/목동/강서", List.of(District.GANGSEO, District.YANGCHEON)),
23+
GURO("구로/서울대입구", List.of(District.GURO, District.GWANAK)),
24+
;
25+
26+
private final String displayName;
27+
private final List<District> districts;
28+
29+
SearchDistrict(String displayName, List<District> districts) {
30+
this.displayName = displayName;
31+
this.districts = districts;
32+
}
33+
}

src/main/java/eatda/domain/store/Store.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ public class Store extends AuditingEntity {
5454
@Enumerated(EnumType.STRING)
5555
@Column(name = "district", nullable = false, length = 31)
5656
private District district;
57-
5857
@Embedded
5958
private Coordinates coordinates;
6059

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package eatda.repository.store;
22

3+
import eatda.domain.cheer.CheerTagName;
4+
import eatda.domain.store.District;
35
import eatda.domain.store.Store;
46
import eatda.domain.store.StoreCategory;
57
import eatda.exception.BusinessErrorCode;
68
import eatda.exception.BusinessException;
79
import java.util.List;
810
import java.util.Optional;
911
import org.springframework.data.domain.Pageable;
12+
import org.springframework.data.jpa.domain.Specification;
1013
import org.springframework.data.jpa.repository.EntityGraph;
1114
import org.springframework.data.jpa.repository.JpaRepository;
1215
import org.springframework.data.jpa.repository.Query;
16+
import org.springframework.lang.Nullable;
1317

1418
public interface StoreRepository extends JpaRepository<Store, Long> {
1519

@@ -21,17 +25,39 @@ default Store getById(Long id) {
2125

2226
Optional<Store> findByKakaoId(String kakaoId);
2327

24-
@EntityGraph(attributePaths = {"cheers"})
25-
List<Store> findAllByOrderByCreatedAtDesc(Pageable pageable);
26-
27-
@EntityGraph(attributePaths = {"cheers"})
28-
List<Store> findAllByCategoryOrderByCreatedAtDesc(StoreCategory category, Pageable pageable);
29-
3028
@Query("""
3129
SELECT s FROM Store s
3230
JOIN Cheer c ON s.id = c.store.id
3331
WHERE c.member.id = :memberId
3432
ORDER BY c.createdAt DESC
3533
""")
3634
List<Store> findAllByCheeredMemberId(long memberId);
35+
36+
default List<Store> findAllByConditions(@Nullable StoreCategory category,
37+
List<CheerTagName> cheerTagNames,
38+
List<District> districts,
39+
Pageable pageable) {
40+
Specification<Store> spec = createSpecification(category, cheerTagNames, districts);
41+
return findAll(spec, pageable);
42+
}
43+
44+
@EntityGraph(attributePaths = {"cheers"})
45+
List<Store> findAll(Specification<Store> spec, Pageable pageable);
46+
47+
private Specification<Store> createSpecification(@Nullable StoreCategory category,
48+
List<CheerTagName> cheerTagNames,
49+
List<District> districts) {
50+
Specification<Store> spec = Specification.allOf();
51+
if (category != null) {
52+
spec = spec.and((root, query, cb) -> cb.equal(root.get("category"), category));
53+
}
54+
if (!cheerTagNames.isEmpty()) {
55+
spec = spec.and(((root, query, cb) ->
56+
root.join("cheers").get("cheerTags").get("values").get("name").in(cheerTagNames)));
57+
}
58+
if (!districts.isEmpty()) {
59+
spec = spec.and((root, query, cb) -> root.get("district").in(districts));
60+
}
61+
return spec;
62+
}
3763
}

src/main/java/eatda/service/store/StoreService.java

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
package eatda.service.store;
22

3-
import static java.util.stream.Collectors.collectingAndThen;
4-
import static java.util.stream.Collectors.toList;
5-
63
import eatda.controller.store.ImagesResponse;
74
import eatda.controller.store.StoreInMemberResponse;
85
import eatda.controller.store.StorePreviewResponse;
96
import eatda.controller.store.StoreResponse;
7+
import eatda.controller.store.StoreSearchParameters;
108
import eatda.controller.store.StoresInMemberResponse;
119
import eatda.controller.store.StoresResponse;
1210
import eatda.controller.store.TagsResponse;
1311
import eatda.domain.cheer.CheerImage;
1412
import eatda.domain.cheer.CheerTag;
1513
import eatda.domain.store.Store;
16-
import eatda.domain.store.StoreCategory;
1714
import eatda.repository.cheer.CheerImageRepository;
1815
import eatda.repository.cheer.CheerRepository;
1916
import eatda.repository.cheer.CheerTagRepository;
@@ -23,7 +20,8 @@
2320
import lombok.RequiredArgsConstructor;
2421
import org.springframework.beans.factory.annotation.Value;
2522
import org.springframework.data.domain.PageRequest;
26-
import org.springframework.lang.Nullable;
23+
import org.springframework.data.domain.Sort;
24+
import org.springframework.data.domain.Sort.Direction;
2725
import org.springframework.stereotype.Service;
2826
import org.springframework.transaction.annotation.Transactional;
2927

@@ -46,19 +44,18 @@ public StoreResponse getStore(long storeId) {
4644

4745
// TODO : N+1 문제 해결
4846
@Transactional(readOnly = true)
49-
public StoresResponse getStores(int page, int size, @Nullable String category) {
50-
return findStores(page, size, category)
51-
.stream()
52-
.map(store -> new StorePreviewResponse(store, getStoreImageUrl(store).orElse(null)))
53-
.collect(collectingAndThen(toList(), StoresResponse::new));
54-
}
47+
public StoresResponse getStores(StoreSearchParameters parameters) {
48+
List<Store> stores = storeRepository.findAllByConditions(
49+
parameters.getCategory(),
50+
parameters.getCheerTagNames(),
51+
parameters.getDistricts(),
52+
PageRequest.of(parameters.getPage(), parameters.getSize(), Sort.by(Direction.DESC, "createdAt"))
53+
);
5554

56-
private List<Store> findStores(int page, int size, @Nullable String category) {
57-
if (category == null || category.isBlank()) {
58-
return storeRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(page, size));
59-
}
60-
return storeRepository.findAllByCategoryOrderByCreatedAtDesc(
61-
StoreCategory.from(category), PageRequest.of(page, size));
55+
List<StorePreviewResponse> responses = stores.stream()
56+
.map(store -> new StorePreviewResponse(store, getStoreImageUrl(store).orElse(null)))
57+
.toList();
58+
return new StoresResponse(responses);
6259
}
6360

6461
@Transactional(readOnly = true)

src/test/java/eatda/controller/store/StoreControllerTest.java

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,54 +60,52 @@ class GetStores {
6060
cheerGenerator.generateCommon(member, store2);
6161
cheerGenerator.generateCommon(member, store3);
6262

63-
int page = 0;
64-
int size = 2;
65-
6663
StoresResponse response = given()
67-
.queryParam("page", page)
68-
.queryParam("size", size)
64+
.queryParam("page", 0)
65+
.queryParam("size", 2)
6966
.when()
7067
.get("/api/shops")
7168
.then()
7269
.statusCode(200)
7370
.extract().as(StoresResponse.class);
7471

7572
assertAll(
76-
() -> assertThat(response.stores()).hasSize(size),
73+
() -> assertThat(response.stores()).hasSize(2),
7774
() -> assertThat(response.stores().get(0).id()).isEqualTo(store3.getId()),
7875
() -> assertThat(response.stores().get(1).id()).isEqualTo(store2.getId())
7976
);
8077
}
8178

8279
@Test
83-
void 특정_카테고리의_음식점_목록을_최신순으로_조회한다() {
80+
void 음식점_목록을_필터링하여_최신순으로_조회한다() {
8481
Member member = memberGenerator.generate("111");
8582
LocalDateTime startAt = LocalDateTime.of(2025, 7, 26, 1, 0, 0);
86-
Store store1 = storeGenerator.generate("112", "서울 강남구 대치동 896-33", StoreCategory.CAFE, startAt);
83+
Store store1 = storeGenerator.generate("112", "서울 강남구 대치동 896-33", StoreCategory.CAFE,
84+
startAt);
8785
Store store2 = storeGenerator.generate("113", "서울 성북구 석관동 123-45", StoreCategory.OTHER,
8886
startAt.plusHours(1));
8987
Store store3 = storeGenerator.generate("114", "서울 강남구 역삼동 678-90", StoreCategory.CAFE,
9088
startAt.plusHours(2));
91-
cheerGenerator.generateCommon(member, store1);
92-
cheerGenerator.generateCommon(member, store2);
93-
cheerGenerator.generateCommon(member, store3);
94-
95-
int page = 0;
96-
int size = 2;
97-
StoreCategory category = StoreCategory.CAFE;
89+
Cheer cheer1 = cheerGenerator.generateCommon(member, store1);
90+
Cheer cheer2 = cheerGenerator.generateCommon(member, store2);
91+
Cheer cheer3 = cheerGenerator.generateCommon(member, store3);
92+
cheerTagGenerator.generate(cheer1, List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.ENERGETIC));
93+
cheerTagGenerator.generate(cheer3, List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM));
9894

9995
StoresResponse response = given()
100-
.queryParam("page", page)
101-
.queryParam("size", size)
102-
.queryParam("category", category.getCategoryName())
96+
.queryParam("page", 0)
97+
.queryParam("size", 2)
98+
.queryParam("category", StoreCategory.CAFE)
99+
.queryParam("tag", CheerTagName.INSTAGRAMMABLE)
100+
.queryParam("location", "")
103101
.when()
104102
.get("/api/shops")
105103
.then()
106104
.statusCode(200)
107105
.extract().as(StoresResponse.class);
108106

109107
assertAll(
110-
() -> assertThat(response.stores()).hasSize(size),
108+
() -> assertThat(response.stores()).hasSize(2),
111109
() -> assertThat(response.stores().get(0).id()).isEqualTo(store3.getId()),
112110
() -> assertThat(response.stores().get(1).id()).isEqualTo(store1.getId())
113111
);

0 commit comments

Comments
 (0)