Skip to content

Commit 6ae2a7d

Browse files
authored
Merge pull request #99 from sohyu-na/feature/#2-Bookstore
feat(#49): 서점리스트 별점순/찜한순 추가
2 parents 7c12e68 + cca1341 commit 6ae2a7d

4 files changed

Lines changed: 124 additions & 9 deletions

File tree

src/main/java/com/capstone/bszip/Bookstore/controller/BookstoreController.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,43 @@ public class BookstoreController {
4646
private final HashtagService hashtagService;
4747
private final IndepBookService indepBookService;
4848

49-
@Operation(summary = "서점 검색", description = "검색창에서 서점을 이름,주소로 검색합니다.")
49+
@Operation(
50+
summary = "서점 검색",
51+
description = "키워드, 서점 ID 목록, 지역, 정렬 방식, 위치 정보(위도/경도)를 기반으로 서점을 검색합니다."
52+
)
53+
@Parameters({
54+
@Parameter(name = "searchK", description = "검색 키워드(서점명,주소)",
55+
required = false, schema = @Schema(type = "string")),
56+
@Parameter(name = "bookstoreK", description = "필터 - 조건(서적종류,북스테이,북카페..)",
57+
required = false, schema = @Schema(type = "array", implementation = String.class)),
58+
@Parameter(name = "region", description = "필터 - 지역명",
59+
required = false, schema = @Schema(type = "string")),
60+
@Parameter(name = "sortField", description = "정렬 기준 (distance: 거리순, rating: 평점순 ,likes : 찜한순)",
61+
required = false, schema = @Schema(type = "string", allowableValues = {"distance", "rating","likes"})),
62+
@Parameter(name = "lat", description = "검색 기준 위도",
63+
required = true, schema = @Schema(type = "number", format = "double")),
64+
@Parameter(name = "lng", description = "검색 기준 경도",
65+
required = true, schema = @Schema(type = "number", format = "double"))
66+
})
5067
@ApiResponses(value = {
51-
@ApiResponse(responseCode = "200", description = "검색 성공",
52-
content = @Content(schema = @Schema(implementation = Bookstore.class))),
53-
@ApiResponse(responseCode = "404", description = "검색 결과 없음",
68+
@ApiResponse(responseCode = "200", description = "서점 검색 성공"),
69+
@ApiResponse(responseCode = "400", description = "잘못된 요청 파라미터",
5470
content = @Content(schema = @Schema(implementation = String.class))),
55-
@ApiResponse(responseCode = "400", description = "잘못된 요청",
71+
@ApiResponse(
72+
responseCode = "404", description = "검색 결과 없음",
5673
content = @Content(schema = @Schema(implementation = String.class))),
57-
@ApiResponse(responseCode = "500", description = "서버 오류",
74+
@ApiResponse(responseCode = "500", description = "서버 오류 발생",
5875
content = @Content(schema = @Schema(implementation = String.class)))
5976
})
6077
@GetMapping("/search")
6178
public ResponseEntity<?> searchBookstores(@RequestParam(required = false) String searchK,
6279
@RequestParam(required = false) List<String> bookstoreK,
6380
@RequestParam(required = false) String region,
81+
@RequestParam(required = false, defaultValue = "distance") String sortField,
6482
@AuthenticationPrincipal Member member,
6583
@RequestParam double lat, @RequestParam double lng) {
6684
try {
67-
List<BookstoreResponse> bookstores = bookstoreService.searchBookstores(searchK,bookstoreK,region, member, lat, lng);
85+
List<BookstoreResponse> bookstores = bookstoreService.searchBookstores(searchK,bookstoreK,region,sortField, member, lat, lng);
6886
/*if (bookstores.isEmpty()) {
6987
return ResponseEntity.status(HttpStatus.NOT_FOUND)
7088
.body(ErrorResponse.builder()

src/main/java/com/capstone/bszip/Bookstore/repository/BookstoreRepositoryCustom.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ public interface BookstoreRepositoryCustom {
99
List<Bookstore> findWithFiltersOrderByDistance(Specification<Bookstore> spec,
1010
double userLat,
1111
double userLng);
12+
List<Bookstore> findWithFiltersOrderByRating(Specification<Bookstore> spec);
13+
14+
List<Bookstore> findWithFiltersOrderByLikes(Specification<Bookstore> spec);
1215
}

src/main/java/com/capstone/bszip/Bookstore/repository/BookstoreRepositoryImpl.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
import jakarta.persistence.criteria.Root;
99
import lombok.RequiredArgsConstructor;
1010
import org.springframework.data.jpa.domain.Specification;
11+
import org.springframework.data.redis.core.RedisTemplate;
1112

1213
import java.util.Collections;
14+
import java.util.HashMap;
1315
import java.util.List;
16+
import java.util.Map;
1417

1518
@RequiredArgsConstructor
1619
public class BookstoreRepositoryImpl implements BookstoreRepositoryCustom {
1720

1821
private final EntityManager em;
22+
private final RedisTemplate<String,String> redisTemplate;
1923

2024
@Override
2125
public List<Bookstore> findWithFiltersOrderByDistance(Specification<Bookstore> spec,
@@ -55,4 +59,82 @@ public List<Bookstore> findWithFiltersOrderByDistance(Specification<Bookstore> s
5559

5660
return (List<Bookstore>) hibernateQuery.getResultList();
5761
}
62+
@Override
63+
public List<Bookstore> findWithFiltersOrderByRating(Specification<Bookstore> spec) {
64+
// spec 조건으로 1차 필터링
65+
CriteriaBuilder builder = em.getCriteriaBuilder();
66+
CriteriaQuery<Long> query = builder.createQuery(Long.class);
67+
Root<Bookstore> root = query.from(Bookstore.class);
68+
69+
if(spec != null) {
70+
query.where(spec.toPredicate(root,query,builder));
71+
}
72+
73+
query.select(root.get("bookstoreId"));
74+
List<Long> filteredIds = em.createQuery(query).getResultList();
75+
76+
// native query로 별점순 정렬
77+
if(filteredIds.isEmpty()) {
78+
return Collections.emptyList();
79+
}
80+
81+
String nativeQuery = "SELECT b.* FROM bookstores b " +
82+
"WHERE b.bookstore_id IN (:filteredIds) " +
83+
"ORDER BY b.rating DESC";
84+
85+
org.hibernate.query.NativeQuery<?> hibernateQuery =
86+
em.createNativeQuery(nativeQuery, Bookstore.class)
87+
.unwrap(org.hibernate.query.NativeQuery.class);
88+
89+
hibernateQuery.setParameterList("filteredIds", filteredIds);
90+
91+
return (List<Bookstore>) hibernateQuery.getResultList();
92+
}
93+
94+
@Override
95+
public List<Bookstore> findWithFiltersOrderByLikes(Specification<Bookstore> spec) {
96+
// spec 조건으로 1차 필터링
97+
CriteriaBuilder builder = em.getCriteriaBuilder();
98+
CriteriaQuery<Long> query = builder.createQuery(Long.class);
99+
Root<Bookstore> root = query.from(Bookstore.class);
100+
101+
if (spec != null) {
102+
query.where(spec.toPredicate(root, query, builder));
103+
}
104+
105+
query.select(root.get("bookstoreId"));
106+
List<Long> filteredIds = em.createQuery(query).getResultList();
107+
108+
if(filteredIds.isEmpty()) {
109+
return Collections.emptyList();
110+
}
111+
// 필터링된 서점들의 좋아요 수 조회
112+
Map<Long, Integer> likeCountMap = new HashMap<>();
113+
for(Long bookstoreId : filteredIds) {
114+
// 각 서점의 좋아요 수 조회 (bookstore:{id}:likes 형태로 저장된다고 가정)
115+
String bookstoreKey = "bookstore:" + bookstoreId + ":likes";
116+
String likeCount = redisTemplate.opsForValue().get(bookstoreKey);
117+
if(likeCount != null) {
118+
likeCountMap.put(bookstoreId, Integer.parseInt(likeCount));
119+
} else {
120+
likeCountMap.put(bookstoreId, 0); // 좋아요 없는 경우
121+
}
122+
}
123+
124+
// DB에서 서점 정보 조회
125+
List<Bookstore> bookstores = em.createQuery(
126+
"SELECT b FROM Bookstore b WHERE b.bookstoreId IN :ids",
127+
Bookstore.class)
128+
.setParameter("ids", filteredIds)
129+
.getResultList();
130+
131+
// 좋아요 수 기준으로 정렬
132+
bookstores.sort((b1, b2) ->
133+
Integer.compare(
134+
likeCountMap.getOrDefault(b2.getBookstoreId(), 0),
135+
likeCountMap.getOrDefault(b1.getBookstoreId(), 0)
136+
)
137+
);
138+
139+
return bookstores; }
58140
}

src/main/java/com/capstone/bszip/Bookstore/service/BookstoreService.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,24 @@ public class BookstoreService {
2929
private final RedisTemplate<String,String> redisTemplate;
3030

3131
@Transactional
32-
public List<BookstoreResponse> searchBookstores(String searchK, List<String> bookstoreK, String region, Member member,double lat, double lng) {
32+
public List<BookstoreResponse> searchBookstores(String searchK, List<String> bookstoreK, String region, String sortField, Member member,double lat, double lng) {
3333
Specification<Bookstore> spec = Specification.where(BookstoreSpecs.nameOrAddressContains(searchK))
3434
.and(BookstoreSpecs.keywordIn(bookstoreK))
3535
.and(BookstoreSpecs.regionContains(region));
3636

37-
List<Bookstore> bookstores = bookstoreRepository.findWithFiltersOrderByDistance(spec,lat,lng);
37+
List<Bookstore> bookstores;
38+
switch(sortField){
39+
case "rating":
40+
bookstores = bookstoreRepository.findWithFiltersOrderByRating(spec);
41+
break;
42+
case "likes":
43+
bookstores = bookstoreRepository.findWithFiltersOrderByLikes(spec);
44+
break;
45+
case "distance":
46+
default:
47+
bookstores = bookstoreRepository.findWithFiltersOrderByDistance(spec,lat,lng);
48+
break;
49+
}
3850
return bookstores.stream()
3951
.map(Bookstore -> convertToBookstoreResponse(Bookstore, member))
4052
.collect(Collectors.toList());

0 commit comments

Comments
 (0)