Skip to content

Commit 69cf9ab

Browse files
authored
feat: 지도 목록 조회 단방향 무한스크롤 (#76)
1 parent 27ddafe commit 69cf9ab

4 files changed

Lines changed: 139 additions & 5 deletions

File tree

src/main/java/com/iitp/domains/map/controller/query/MapQueryController.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.iitp.domains.map.controller.query;
22

33
import com.iitp.domains.map.dto.responseDto.MapListResponseDto;
4+
import com.iitp.domains.map.dto.responseDto.MapListScrollResponseDto;
45
import com.iitp.domains.map.dto.responseDto.MapMarkerResponseDto;
56
import com.iitp.domains.map.dto.responseDto.MapSummaryResponseDto;
67
import com.iitp.domains.map.service.query.MapQueryService;
@@ -49,14 +50,16 @@ public ApiResponse<MapSummaryResponseDto> getStoreSummary(
4950
@Operation(summary = "가게 지도 목록 조회")
5051
@GetMapping("/lists")
5152
@PreAuthorize("isAuthenticated()")
52-
public ApiResponse<List<MapListResponseDto>> getNearbyStoreList(
53+
public ApiResponse<MapListScrollResponseDto> getNearbyStoreList(
5354
@RequestParam Double latitude,
5455
@RequestParam Double longitude,
5556
@RequestParam(defaultValue = "5.0") Double radiusKm,
56-
@RequestParam(defaultValue = "거리순") String sort) {
57+
@RequestParam(defaultValue = "거리순") String sort,
58+
@RequestParam(required = false) Long cursorId,
59+
@RequestParam(defaultValue = "10") Integer limit) {
5760

58-
List<MapListResponseDto> stores = mapQueryService.getNearbyStoreList(
59-
latitude, longitude, radiusKm, sort);
61+
MapListScrollResponseDto stores = mapQueryService.getNearbyStoreListWithScroll(
62+
latitude, longitude, radiusKm, sort, cursorId, limit);
6063
return ApiResponse.ok(200, stores, "근처 가게 목록 조회 성공");
6164
}
6265
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.iitp.domains.map.dto.responseDto;
2+
3+
import lombok.Builder;
4+
5+
import java.util.List;
6+
7+
@Builder
8+
public record MapListScrollResponseDto(
9+
List<MapListResponseDto> stores,
10+
Long prevCursor,
11+
Long nextCursor,
12+
Boolean hasNext
13+
) {
14+
public static MapListScrollResponseDto of(List<MapListResponseDto> stores,
15+
Long prevCursor, Long nextCursor, Boolean hasNext) {
16+
return MapListScrollResponseDto.builder()
17+
.stores(stores)
18+
.prevCursor(prevCursor)
19+
.nextCursor(nextCursor)
20+
.hasNext(hasNext)
21+
.build();
22+
}
23+
24+
public static MapListScrollResponseDto empty() {
25+
return MapListScrollResponseDto.builder()
26+
.stores(List.of())
27+
.prevCursor(null)
28+
.nextCursor(null)
29+
.hasNext(false)
30+
.build();
31+
}
32+
}

src/main/java/com/iitp/domains/map/service/query/MapQueryService.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.iitp.domains.map.service.query;
22

33
import com.iitp.domains.map.dto.responseDto.MapListResponseDto;
4+
import com.iitp.domains.map.dto.responseDto.MapListScrollResponseDto;
45
import com.iitp.domains.map.dto.responseDto.MapMarkerResponseDto;
56
import com.iitp.domains.map.dto.responseDto.MapSummaryResponseDto;
67
import com.iitp.domains.map.repository.MapRepository;
@@ -157,6 +158,65 @@ public List<MapListResponseDto> getNearbyStoreList(Double latitude, Double longi
157158
return applySorting(storeList, sort);
158159
}
159160

161+
/**
162+
* 근처 가게 목록 조회 - 무한스크롤
163+
*/
164+
public MapListScrollResponseDto getNearbyStoreListWithScroll(Double latitude, Double longitude,
165+
Double radiusKm, String sort,
166+
Long cursorId, Integer limit) {
167+
log.info("근처 가게 목록 무한스크롤 조회 - lat: {}, lng: {}, radius: {}km, sort: {}, cursor: {}, limit: {}",
168+
latitude, longitude, radiusKm, sort, cursorId, limit);
169+
170+
// Redis GEO에서 근처 가게 ID 조회
171+
List<String> nearbyStoreIds = redisGeoService.findNearbyStores(latitude, longitude, radiusKm);
172+
173+
if (nearbyStoreIds.isEmpty()) {
174+
return MapListScrollResponseDto.empty();
175+
}
176+
177+
// 가게 ID 리스트를 Long으로 변환
178+
List<Long> storeIds = nearbyStoreIds.stream()
179+
.map(Long::valueOf)
180+
.collect(Collectors.toList());
181+
182+
// DB에서 가게 정보 조회
183+
List<Store> stores = mapRepository.findStoreListByIds(storeIds);
184+
185+
// 응답 DTO 생성
186+
List<MapListResponseDto> storeList = stores.stream()
187+
.map(store -> {
188+
String imageUrl = getStoreImageUrl(store);
189+
Double distance = distanceCalculator.calculateDistance(
190+
latitude, longitude,
191+
store.getLatitude(), store.getLongitude());
192+
193+
// 리뷰 조회
194+
List<ReviewResponse> reviews = reviewQueryService.getStoreReviews(
195+
store.getId(), 0L, Integer.MAX_VALUE);
196+
197+
Double rating = 0.0;
198+
Integer reviewCount = 0;
199+
200+
if (!reviews.isEmpty()) {
201+
rating = reviews.stream()
202+
.mapToInt(ReviewResponse::rating)
203+
.average()
204+
.orElse(0.0);
205+
rating = Math.round(rating * 10.0) / 10.0;
206+
reviewCount = reviews.size();
207+
}
208+
209+
return MapListResponseDto.from(store, imageUrl, distance, rating, reviewCount);
210+
})
211+
.collect(Collectors.toList());
212+
213+
// 정렬 적용
214+
List<MapListResponseDto> sortedList = applySorting(storeList, sort);
215+
216+
// 커서 기반 페이징 적용
217+
return applyCursorPagination(sortedList, cursorId, limit);
218+
}
219+
160220
/**
161221
* 가게 이미지 URL 조회
162222
*/
@@ -199,4 +259,42 @@ private List<MapListResponseDto> applySorting(List<MapListResponseDto> storeList
199259
.collect(Collectors.toList());
200260
};
201261
}
262+
263+
/**
264+
* 커서 기반 페이징 적용
265+
*/
266+
private MapListScrollResponseDto applyCursorPagination(List<MapListResponseDto> storeList,
267+
Long cursorId, Integer limit) {
268+
if (storeList.isEmpty()) {
269+
return MapListScrollResponseDto.empty();
270+
}
271+
272+
int startIndex = 0;
273+
274+
// cursorId가 있는 경우 해당 위치 찾기
275+
if (cursorId != null) {
276+
for (int i = 0; i < storeList.size(); i++) {
277+
if (storeList.get(i).id().equals(cursorId)) {
278+
startIndex = i + 1; // 커서 다음부터 시작
279+
break;
280+
}
281+
}
282+
}
283+
284+
// 시작 인덱스가 리스트 크기를 넘으면 빈 결과 반환
285+
if (startIndex >= storeList.size()) {
286+
return MapListScrollResponseDto.empty();
287+
}
288+
289+
// limit만큼 데이터 가져오기
290+
int endIndex = Math.min(startIndex + limit, storeList.size());
291+
List<MapListResponseDto> pagedList = storeList.subList(startIndex, endIndex);
292+
293+
// 커서 정보 계산
294+
Long prevCursor = (startIndex > 0) ? storeList.get(startIndex - 1).id() : null;
295+
Long nextCursor = (endIndex < storeList.size()) ? pagedList.get(pagedList.size() - 1).id() : null;
296+
Boolean hasNext = endIndex < storeList.size();
297+
298+
return MapListScrollResponseDto.of(pagedList, prevCursor, nextCursor, hasNext);
299+
}
202300
}

src/main/java/com/iitp/global/common/constants/BusinessLogicConstants.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ public class BusinessLogicConstants {
6969
* 지도 관련 상수
7070
*/
7171
public static final int MAP_SEARCHING_RANGE_KM = 5;
72-
72+
public static final int MAP_LIST_DEFAULT_LIMIT = 10; // 무한스크롤 기본 한 페이지 크기
73+
public static final int MAP_LIST_MAX_LIMIT = 50; // 무한스크롤 최대 한 페이지 크기
7374
/**
7475
* Store
7576
*/

0 commit comments

Comments
 (0)