Skip to content

Commit 545a239

Browse files
committed
주변 코스 조회 API에 페이징 적용 (#452)
* feat: 주변 코스 조회 API에 페이징 기능 추가 * fix: Course가 Spring Data MongoDB에 의해 제대로 매핑되지 않는 문제를 Converter로 해결 - MongoTemplate을 사용하는 방식도 고려했으나, 이보단 Converter만 구현하는 것이 편하다고 느낌 - 추후에 schemaVersion이 추가되었을 때에도 유연하게 대응 가능 * refactor: 생성되면 안되는 Converter 클래스를 abstract로 변경 * test: RepositoryTest를 가독성 좋게 setUp 절로 분리 * fix: PageNumber가 null 이거나 음수인 경우에 대응 * fix: Id가 세팅되어 있는 경우(업데이트)에는 DB 저장시 Id를 정상적으로 세팅 * chore: 불필요한 Converter 등록 제거 * refactor: 등록되지 않는 converter들은 내부 클래스로 갖도록 리팩터링 * test: 페이징 테스트를 다양하게 추가
1 parent c3e1e68 commit 545a239

File tree

12 files changed

+140
-99
lines changed

12 files changed

+140
-99
lines changed

backend/src/main/java/coursepick/coursepick/application/CourseApplicationService.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import coursepick.coursepick.domain.Meter;
88
import lombok.RequiredArgsConstructor;
99
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.data.domain.PageRequest;
11+
import org.springframework.data.domain.Pageable;
12+
import org.springframework.data.domain.Slice;
1013
import org.springframework.stereotype.Service;
1114
import org.springframework.transaction.annotation.Transactional;
1215

@@ -23,11 +26,12 @@ public class CourseApplicationService {
2326
private final WalkingRouteService walkingRouteService;
2427

2528
@Transactional(readOnly = true)
26-
public List<CourseResponse> findNearbyCourses(double mapLatitude, double mapLongitude, Double userLatitude, Double userLongitude, int scope) {
29+
public List<CourseResponse> findNearbyCourses(double mapLatitude, double mapLongitude, Double userLatitude, Double userLongitude, int scope, Integer pageNumber) {
2730
final Coordinate mapPosition = new Coordinate(mapLatitude, mapLongitude);
28-
Meter meter = new Meter(scope).clamp(1000, 3000);
31+
final Meter meter = new Meter(scope).clamp(1000, 3000);
32+
Pageable pageable = createPageable(pageNumber);
2933

30-
List<Course> coursesWithinScope = courseRepository.findAllHasDistanceWithin(mapPosition, meter);
34+
Slice<Course> coursesWithinScope = courseRepository.findAllHasDistanceWithin(mapPosition, meter, pageable);
3135

3236
if (userLatitude == null || userLongitude == null) {
3337
return coursesWithinScope
@@ -43,6 +47,11 @@ public List<CourseResponse> findNearbyCourses(double mapLatitude, double mapLong
4347
.toList();
4448
}
4549

50+
private static Pageable createPageable(Integer pageNumber) {
51+
if (pageNumber == null || pageNumber < 0) return PageRequest.of(0, 10);
52+
else return PageRequest.of(pageNumber, 10);
53+
}
54+
4655
@Transactional(readOnly = true)
4756
public Coordinate findClosestCoordinate(String id, double latitude, double longitude) {
4857
Course course = courseRepository.findById(id)

backend/src/main/java/coursepick/coursepick/domain/Course.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import java.util.List;
1515

1616
@Document
17-
@AllArgsConstructor(access = AccessLevel.PROTECTED, onConstructor_ = @PersistenceCreator)
17+
@AllArgsConstructor(access = AccessLevel.PUBLIC, onConstructor_ = @PersistenceCreator)
1818
@Getter
1919
@Accessors(fluent = true)
2020
public class Course {

backend/src/main/java/coursepick/coursepick/domain/CourseRepository.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package coursepick.coursepick.domain;
22

3+
import org.springframework.data.domain.Pageable;
4+
import org.springframework.data.domain.Slice;
5+
36
import java.util.List;
47
import java.util.Optional;
58

69
public interface CourseRepository {
710

811
void saveAll(Iterable<? extends Course> courses);
912

10-
List<Course> findAllHasDistanceWithin(Coordinate target, Meter distance);
13+
Slice<Course> findAllHasDistanceWithin(Coordinate target, Meter distance, Pageable pageable);
1114

1215
List<Course> findByIdIn(List<String> ids);
1316

backend/src/main/java/coursepick/coursepick/infrastructure/CourseRepositoryMongoTemplateImpl.java

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import coursepick.coursepick.domain.*;
44
import lombok.RequiredArgsConstructor;
5-
import org.springframework.data.geo.Point;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.domain.Slice;
8+
import org.springframework.data.domain.SliceImpl;
69
import org.springframework.data.mongodb.core.MongoTemplate;
710
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
811
import org.springframework.data.mongodb.core.query.Criteria;
@@ -30,23 +33,41 @@ public void saveAll(Iterable<? extends Course> courses) {
3033
*/
3134
List<? extends Course> listCourses = StreamSupport.stream(courses.spliterator(), false)
3235
.toList();
36+
3337
mongoTemplate.insertAll(listCourses);
3438
}
3539

3640
@Override
37-
public List<Course> findAllHasDistanceWithin(Coordinate target, Meter distance) {
38-
if (target == null || distance == null) return List.of();
41+
public Slice<Course> findAllHasDistanceWithin(Coordinate target, Meter distance, Pageable pageable) {
42+
if (target == null || distance == null) return Page.empty(pageable);
3943

40-
Point center = new GeoJsonPoint(target.longitude(), target.latitude());
41-
Query query = Query.query(Criteria.where("segments").near(center).maxDistance(distance.value()));
42-
return mongoTemplate.find(query, Course.class);
44+
Criteria criteria = Criteria.where("segments")
45+
.near(new GeoJsonPoint(target.longitude(), target.latitude()))
46+
.maxDistance(distance.value());
47+
48+
if (pageable == null) {
49+
Query query = Query.query(criteria);
50+
51+
return new SliceImpl<>(mongoTemplate.find(query, Course.class));
52+
}
53+
54+
Query query = Query.query(criteria)
55+
.with(pageable)
56+
.limit(pageable.getPageSize() + 1);
57+
58+
List<Course> result = mongoTemplate.find(query, Course.class);
59+
60+
boolean hasNext = result.size() > pageable.getPageSize();
61+
if (hasNext) result.removeLast();
62+
return new SliceImpl<>(result, pageable, hasNext);
4363
}
4464

4565
@Override
4666
public List<Course> findByIdIn(List<String> ids) {
4767
if (ids == null || ids.isEmpty()) return List.of();
4868

4969
Query query = Query.query(Criteria.where("_id").in(ids));
70+
5071
return mongoTemplate.find(query, Course.class);
5172
}
5273

@@ -62,6 +83,7 @@ public boolean existsByName(CourseName courseName) {
6283
if (courseName == null) return false;
6384

6485
Query query = Query.query(Criteria.where("name").is(courseName.value()));
86+
6587
return mongoTemplate.exists(query, Course.class);
6688
}
6789
}

backend/src/main/java/coursepick/coursepick/infrastructure/converter/SegmentListConverter.java renamed to backend/src/main/java/coursepick/coursepick/infrastructure/converter/CourseConverter.java

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package coursepick.coursepick.infrastructure.converter;
22

3-
import coursepick.coursepick.domain.Coordinate;
4-
import coursepick.coursepick.domain.GeoLine;
5-
import coursepick.coursepick.domain.GeoLineBuilder;
6-
import coursepick.coursepick.domain.Segment;
3+
import coursepick.coursepick.domain.*;
74
import coursepick.coursepick.logging.LogContent;
85
import lombok.extern.slf4j.Slf4j;
96
import org.bson.Document;
7+
import org.bson.types.ObjectId;
108
import org.springframework.core.convert.converter.Converter;
119
import org.springframework.data.convert.ReadingConverter;
1210
import org.springframework.data.convert.WritingConverter;
@@ -15,16 +13,52 @@
1513
import java.util.Collections;
1614
import java.util.List;
1715

18-
@Slf4j
19-
public class SegmentListConverter {
16+
public abstract class CourseConverter {
17+
18+
private static final SegmentListReader SEGMENTS_READER = new SegmentListReader();
19+
private static final SegmentListWriter SEGMENTS_WRITER = new SegmentListWriter();
20+
21+
@WritingConverter
22+
public static class Writer implements Converter<Course, Document> {
23+
@Override
24+
public Document convert(Course source) {
25+
Document document = new Document();
26+
if (source.id() != null && !source.id().isBlank()) {
27+
document.put("_id", new ObjectId(source.id()));
28+
}
29+
document.put("name", source.name().value());
30+
document.put("road_type", source.roadType().name());
31+
document.put("incline_summary", source.inclineSummary().name());
32+
document.put("segments", SEGMENTS_WRITER.convert(source.segments()));
33+
document.put("length", source.length().value());
34+
document.put("difficulty", source.difficulty().name());
35+
return document;
36+
}
37+
}
38+
39+
@ReadingConverter
40+
public static class Reader implements Converter<Document, Course> {
41+
@Override
42+
public Course convert(Document source) {
43+
return new Course(
44+
source.getObjectId("_id").toHexString(),
45+
new CourseName(source.getString("name")),
46+
RoadType.valueOf(source.getString("road_type")),
47+
InclineSummary.valueOf(source.getString("incline_summary")),
48+
SEGMENTS_READER.convert(source.get("segments", Document.class)),
49+
new Meter(source.getDouble("length")),
50+
Difficulty.valueOf(source.getString("difficulty"))
51+
);
52+
}
53+
}
2054

2155
@WritingConverter
22-
public static class Writer implements Converter<List<Segment>, Document> {
56+
private static class SegmentListWriter implements Converter<List<Segment>, Document> {
2357
@Override
2458
public Document convert(List<Segment> source) {
2559
if (source == null) return null;
2660
List<List<List<Double>>> segmentsData = source.stream()
27-
.map(Writer::parseSegment)
61+
.map(SegmentListWriter::parseSegment)
2862
.toList();
2963

3064
Document document = new Document();
@@ -47,16 +81,17 @@ private static List<List<Double>> parseSegment(Segment segment) {
4781
}
4882
}
4983

84+
@Slf4j
5085
@ReadingConverter
51-
public static class Reader implements Converter<Document, List<Segment>> {
86+
private static class SegmentListReader implements Converter<Document, List<Segment>> {
5287
@Override
5388
public List<Segment> convert(Document source) {
5489
try {
5590
if (source == null) return null;
5691
List<List<List<Double>>> segmentsData = (List<List<List<Double>>>) source.get("coordinates");
5792

5893
return segmentsData.stream()
59-
.map(Reader::parseSegment)
94+
.map(SegmentListReader::parseSegment)
6095
.toList();
6196
} catch (Exception e) {
6297
log.warn("[EXCEPTION] 세그먼트 파싱 중 예외 발생", LogContent.exception(source, e));

backend/src/main/java/coursepick/coursepick/infrastructure/converter/CourseNameConverter.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

backend/src/main/java/coursepick/coursepick/infrastructure/converter/MeterConverter.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

backend/src/main/java/coursepick/coursepick/infrastructure/converter/MongoConfig.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,8 @@ public class MongoConfig {
1212
@Bean
1313
public MongoCustomConversions mongoCustomConversions() {
1414
return new MongoCustomConversions(List.of(
15-
new SegmentListConverter.Reader(),
16-
new SegmentListConverter.Writer(),
17-
new CourseNameConverter.Reader(),
18-
new CourseNameConverter.Writer(),
19-
new MeterConverter.Reader(),
20-
new MeterConverter.Writer()
15+
new CourseConverter.Reader(),
16+
new CourseConverter.Writer()
2117
));
2218
}
2319
}

backend/src/main/java/coursepick/coursepick/presentation/CourseWebController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ public List<CourseWebResponse> findNearbyCourses(
4040
@RequestParam("mapLng") double mapLongitude,
4141
@RequestParam(value = "userLat", required = false) Double userLatitude,
4242
@RequestParam(value = "userLng", required = false) Double userLongitude,
43-
@RequestParam("scope") int scope
43+
@RequestParam("scope") int scope,
44+
@RequestParam(value = "page", required = false) Integer page
4445
) {
45-
List<CourseResponse> responses = courseApplicationService.findNearbyCourses(mapLatitude, mapLongitude, userLatitude, userLongitude, scope);
46+
List<CourseResponse> responses = courseApplicationService.findNearbyCourses(mapLatitude, mapLongitude, userLatitude, userLongitude, scope, page);
4647
return CourseWebResponse.from(responses);
4748
}
4849

backend/src/main/java/coursepick/coursepick/presentation/api/CourseWebApi.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ List<CourseWebResponse> findNearbyCourses(
3838
@Parameter(example = "127.1040109", required = true) double mapLongitude,
3939
@Parameter(example = "38.5165004") Double userLatitude,
4040
@Parameter(example = "126.1040109") Double userLongitude,
41-
@Parameter(example = "1000", required = true) int scope
41+
@Parameter(example = "1000", required = true) int scope,
42+
@Parameter(example = "1") Integer page
4243
);
4344

4445
@Operation(summary = "좌표에서 가장 가까운 코스 위 좌표 조회")
@@ -92,7 +93,7 @@ List<CoordinateWebResponse> routeToCourse(
9293
@Parameter(example = "37.5165004", required = true) double latitude,
9394
@Parameter(example = "127.1040109", required = true) double longitude
9495
);
95-
96+
9697
@Operation(summary = "즐겨찾기 코스 조회")
9798
@ApiResponse(responseCode = "200")
9899
List<CourseWebResponse> findFavoriteCourses(

0 commit comments

Comments
 (0)