11package com .iitp .domains .map .service .query ;
22
33import com .iitp .domains .map .dto .responseDto .MapListResponseDto ;
4+ import com .iitp .domains .map .dto .responseDto .MapListScrollResponseDto ;
45import com .iitp .domains .map .dto .responseDto .MapMarkerResponseDto ;
56import com .iitp .domains .map .dto .responseDto .MapSummaryResponseDto ;
67import 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}
0 commit comments