diff --git a/src/main/java/com/gamsa/activity/controller/ActivityController.java b/src/main/java/com/gamsa/activity/controller/ActivityController.java index af9bbfb..2a80f63 100644 --- a/src/main/java/com/gamsa/activity/controller/ActivityController.java +++ b/src/main/java/com/gamsa/activity/controller/ActivityController.java @@ -3,12 +3,18 @@ import com.gamsa.activity.constant.Category; import com.gamsa.activity.dto.ActivityDetailResponse; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import com.gamsa.activity.dto.ActivityFindSliceResponse; import com.gamsa.activity.service.ActivityService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController @@ -25,9 +31,10 @@ public Slice findSlice( @RequestParam(defaultValue = "false") boolean teenPossibleOnly, @RequestParam(defaultValue = "false") boolean beforeDeadlineOnly, @RequestParam(required = false) String keyword, + @RequestBody(required = false) ActivityFindDistanceOrderRequest distanceOrderRequest, Pageable pageable) { - ActivityFilterRequest request = ActivityFilterRequest.builder() + ActivityFilterRequest filterRequest = ActivityFilterRequest.builder() .category(Category.fromValuesForSlice(category)) .sidoGunguCode(sidoGunguCode) .sidoCode(sidoCode) @@ -36,7 +43,7 @@ public Slice findSlice( .keyword(keyword) .build(); - return activityService.findSlice(request, pageable); + return activityService.findSlice(filterRequest, distanceOrderRequest, pageable); } @GetMapping("{activity-id}") diff --git a/src/main/java/com/gamsa/activity/dto/ActivityFindDistanceOrderRequest.java b/src/main/java/com/gamsa/activity/dto/ActivityFindDistanceOrderRequest.java new file mode 100644 index 0000000..f0d4958 --- /dev/null +++ b/src/main/java/com/gamsa/activity/dto/ActivityFindDistanceOrderRequest.java @@ -0,0 +1,17 @@ +package com.gamsa.activity.dto; + +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class ActivityFindDistanceOrderRequest { + + private final BigDecimal latitude; + private final BigDecimal longitude; + private final int distanceKm; + +} diff --git a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java index 0fef1c2..1b01a7f 100644 --- a/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java +++ b/src/main/java/com/gamsa/activity/entity/ActivityJpaEntity.java @@ -4,11 +4,23 @@ import com.gamsa.activity.constant.CategoryConverter; import com.gamsa.activity.domain.Activity; import com.gamsa.common.entity.BaseEntity; -import jakarta.persistence.*; -import lombok.*; - +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Temporal; +import jakarta.persistence.TemporalType; import java.math.BigDecimal; import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder diff --git a/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java b/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java index 92b5171..c2503a1 100644 --- a/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java +++ b/src/main/java/com/gamsa/activity/entity/DistrictJpaEntity.java @@ -6,9 +6,12 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; -import lombok.*; - import java.math.BigDecimal; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Builder diff --git a/src/main/java/com/gamsa/activity/repository/ActivityCustomRepository.java b/src/main/java/com/gamsa/activity/repository/ActivityCustomRepository.java index 862b484..ac0f9b7 100644 --- a/src/main/java/com/gamsa/activity/repository/ActivityCustomRepository.java +++ b/src/main/java/com/gamsa/activity/repository/ActivityCustomRepository.java @@ -1,6 +1,7 @@ package com.gamsa.activity.repository; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import com.gamsa.activity.entity.ActivityJpaEntity; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -9,4 +10,8 @@ public interface ActivityCustomRepository { Slice findSlice(ActivityFilterRequest request, Pageable pageable); + Slice findSliceDistanceOrder( + ActivityFilterRequest filterRequest, + ActivityFindDistanceOrderRequest distanceOrderRequest, + Pageable pageable); } diff --git a/src/main/java/com/gamsa/activity/repository/ActivityCustomRepositoryImpl.java b/src/main/java/com/gamsa/activity/repository/ActivityCustomRepositoryImpl.java index 003d7fd..1242d85 100644 --- a/src/main/java/com/gamsa/activity/repository/ActivityCustomRepositoryImpl.java +++ b/src/main/java/com/gamsa/activity/repository/ActivityCustomRepositoryImpl.java @@ -3,14 +3,19 @@ import static com.gamsa.activity.entity.QActivityJpaEntity.activityJpaEntity; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import com.gamsa.activity.entity.ActivityJpaEntity; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -37,6 +42,45 @@ public Slice findSlice(ActivityFilterRequest request, Pageabl return checkLastPage(pageable, results); } + @Override + public Slice findSliceDistanceOrder( + ActivityFilterRequest filterRequest, + ActivityFindDistanceOrderRequest distanceOrderRequest, + Pageable pageable) { + + final double EARTH_RADIUS_KM = 6371.0; // 지구 반지름 (km) + final BigDecimal latitude = distanceOrderRequest.getLatitude(); + final BigDecimal longitude = distanceOrderRequest.getLongitude(); + final int distanceKm = distanceOrderRequest.getDistanceKm(); + + BooleanBuilder filterBuilder = ActivityFilterBuilder.createFilter(filterRequest); + + // 하버사인 공식 구현 + NumberTemplate distanceExpression = Expressions.numberTemplate(Double.class, + "({0} * acos(cos(radians({1})) * cos(radians({2})) * cos(radians({3}) - radians({4})) + sin(radians({1})) * sin(radians({2}))))", + EARTH_RADIUS_KM, + latitude, + activityJpaEntity.latitude, + longitude, + activityJpaEntity.longitude + ); + + List results = jpaQueryFactory + .select(activityJpaEntity, distanceExpression.as("distance")) + .from(activityJpaEntity) + .where(filterBuilder.and(distanceExpression.loe(distanceKm))) + .orderBy(distanceExpression.asc()) + .orderBy(activityJpaEntity.actId.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch() + .stream() + .map(v -> v.get(activityJpaEntity)) + .collect(Collectors.toList()); + + return checkLastPage(pageable, results); + } + // 무한스크롤 처리 private Slice checkLastPage(Pageable pageable, List results) { diff --git a/src/main/java/com/gamsa/activity/repository/ActivityRepository.java b/src/main/java/com/gamsa/activity/repository/ActivityRepository.java index ad9e60e..e722e24 100644 --- a/src/main/java/com/gamsa/activity/repository/ActivityRepository.java +++ b/src/main/java/com/gamsa/activity/repository/ActivityRepository.java @@ -2,6 +2,7 @@ import com.gamsa.activity.domain.Activity; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -10,7 +11,12 @@ public interface ActivityRepository { void save(Activity activity); - Slice findSlice(ActivityFilterRequest request, Pageable pageable); + Slice findSlice(ActivityFilterRequest filterRequest, Pageable pageable); + + Slice findSliceDistanceOrder( + ActivityFilterRequest filterRequest, + ActivityFindDistanceOrderRequest distanceOrderRequest, + Pageable pageable); Optional findById(Long activityId); } diff --git a/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java b/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java index b2ec5f1..0121f7e 100644 --- a/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java +++ b/src/main/java/com/gamsa/activity/repository/ActivityRepositoryImpl.java @@ -2,6 +2,7 @@ import com.gamsa.activity.domain.Activity; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import com.gamsa.activity.entity.ActivityJpaEntity; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -26,6 +27,16 @@ public Slice findSlice(ActivityFilterRequest request, Pageable pageabl .map(ActivityJpaEntity::toModel); } + @Override + public Slice findSliceDistanceOrder( + ActivityFilterRequest filterRequest, + ActivityFindDistanceOrderRequest distanceOrderRequest, + Pageable pageable) { + return activityJpaRepository.findSliceDistanceOrder(filterRequest, distanceOrderRequest, + pageable) + .map(ActivityJpaEntity::toModel); + } + @Override public Optional findById(Long activityId) { return activityJpaRepository.findById(activityId) diff --git a/src/main/java/com/gamsa/activity/service/ActivityService.java b/src/main/java/com/gamsa/activity/service/ActivityService.java index c3b4134..050ca97 100644 --- a/src/main/java/com/gamsa/activity/service/ActivityService.java +++ b/src/main/java/com/gamsa/activity/service/ActivityService.java @@ -6,22 +6,23 @@ import com.gamsa.activity.domain.Institute; import com.gamsa.activity.dto.ActivityDetailResponse; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import com.gamsa.activity.dto.ActivityFindSliceResponse; import com.gamsa.activity.dto.ActivitySaveRequest; import com.gamsa.activity.exception.ActivityException; import com.gamsa.activity.repository.ActivityRepository; import com.gamsa.review.domain.Question; +import com.gamsa.review.dto.QuestionResponse; import com.gamsa.review.service.QuestionService; import com.gamsa.review.service.ReviewService; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; -import java.math.BigDecimal; -import java.util.HashMap; -import java.util.Map; - @RequiredArgsConstructor @Service public class ActivityService { @@ -34,9 +35,19 @@ public void save(ActivitySaveRequest saveRequest, Institute institute, District activityRepository.save(saveRequest.toModel(institute, district)); } - public Slice findSlice(ActivityFilterRequest request, - Pageable pageable) { - Slice activities = activityRepository.findSlice(request, pageable); + public Slice findSlice(ActivityFilterRequest filterRequest, + ActivityFindDistanceOrderRequest distanceOrderRequest, + Pageable pageable) { + + Slice activities; + // 가까운순 정렬 + if (pageable.getSort().toString().contains("distance")) { + activities = activityRepository + .findSliceDistanceOrder(filterRequest, distanceOrderRequest, pageable); + } else { + activities = activityRepository.findSlice(filterRequest, pageable); + } + return activities.map(ActivityFindSliceResponse::from); } diff --git a/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java b/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java index a7db449..ba8de98 100644 --- a/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java +++ b/src/test/java/com/gamsa/activity/repository/ActivityJpaRepositoryTest.java @@ -4,8 +4,10 @@ import com.gamsa.activity.constant.Category; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import com.gamsa.activity.entity.ActivityJpaEntity; import com.gamsa.common.config.TestConfig; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -24,7 +26,7 @@ class ActivityJpaRepositoryTest { @Autowired private ActivityJpaRepository activityJpaRepository; - private final ActivityJpaEntity jpaEntity = ActivityJpaEntity.builder() + private final ActivityJpaEntity jpaEntity1 = ActivityJpaEntity.builder() .actId(1L) .actTitle("어린이놀이안전관리 및 놀잇감 청결유지 및 정리") .actLocation("아이사랑꿈터 서구 5호점") @@ -39,6 +41,8 @@ class ActivityJpaRepositoryTest { .adultPossible(true) .teenPossible(false) .groupPossible(false) + .latitude(new BigDecimal("126.111111")) + .longitude(new BigDecimal("37.111111")) .actWeek(0111110) .actManager("윤순영") .actPhone("032-577-3026") @@ -61,6 +65,8 @@ class ActivityJpaRepositoryTest { .adultPossible(true) .teenPossible(true) .groupPossible(false) + .latitude(new BigDecimal("127.666666")) + .longitude(new BigDecimal("38.666666")) .actWeek(0111110) .actManager("홀란드") .actPhone("032-111-2222") @@ -83,6 +89,8 @@ class ActivityJpaRepositoryTest { .adultPossible(true) .teenPossible(true) .groupPossible(false) + .latitude(new BigDecimal("128.999999")) + .longitude(new BigDecimal("39.999999")) .actWeek(0111110) .actManager("사서쌤") .actPhone("032-111-2222") @@ -103,20 +111,24 @@ class ActivityJpaRepositoryTest { private final ActivityFilterRequest beforeDeadlineFilterReq = new ActivityFilterRequest( null, null, null, false, true, null); + // distance sorting + private final ActivityFindDistanceOrderRequest distanceOrderReq = new ActivityFindDistanceOrderRequest( + new BigDecimal("126.111111"), new BigDecimal("37.111111"), 9999999); + @Test void 새_활동_저장() { // when - activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity1); // then assertThat(activityJpaRepository.findById(1L).get().getActTitle()) - .isEqualTo(jpaEntity.getActTitle()); + .isEqualTo(jpaEntity1.getActTitle()); } @Test void 모든_활동_리스트_반환() { // given - activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity1); // when List content = activityJpaRepository .findSlice(noFilterReq, PageRequest.of(0, 10)) @@ -128,17 +140,17 @@ class ActivityJpaRepositoryTest { @Test void 활동_상세정보_조회() { // given - activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity1); // when Optional result = activityJpaRepository.findById(1L); // then - assertThat(result.get().getActTitle()).isEqualTo(jpaEntity.getActTitle()); + assertThat(result.get().getActTitle()).isEqualTo(jpaEntity1.getActTitle()); } @Test void id로_정렬된_조회() { // given - activityJpaRepository.save(jpaEntity); // id = 1L + activityJpaRepository.save(jpaEntity1); // id = 1L activityJpaRepository.save(jpaEntity2); // id = 2L Pageable pageable = PageRequest.of(0, 2, Direction.DESC, "actId"); @@ -155,7 +167,7 @@ class ActivityJpaRepositoryTest { @Test void 마감_날짜_오름차순_정렬_조회() { // given - activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity1); activityJpaRepository.save(jpaEntity2); activityJpaRepository.save(jpaEntity3); Pageable pageable = PageRequest @@ -177,7 +189,7 @@ class ActivityJpaRepositoryTest { void 마감된_공고중_마감_날짜_가까운순_정렬_조회() { // given LocalDateTime date = LocalDateTime.of(2024, 10, 1, 0, 0); - activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity1); activityJpaRepository.save(jpaEntity2); activityJpaRepository.save(jpaEntity3); Pageable pageable = PageRequest @@ -200,7 +212,7 @@ class ActivityJpaRepositoryTest { @Test void 카테고리로_필터링_조회() { // given - activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity1); activityJpaRepository.save(jpaEntity2); Pageable pageable = PageRequest.of(0, 2, Direction.ASC, "actId"); @@ -216,7 +228,7 @@ class ActivityJpaRepositoryTest { @Test void 청소년_가능여부_필터링_조회() { // given - activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity1); activityJpaRepository.save(jpaEntity2); Pageable pageable = PageRequest.of(0, 2, Direction.ASC, "actId"); @@ -233,7 +245,7 @@ class ActivityJpaRepositoryTest { void 마감되지_않은_활동만_필터링_조회() { // given LocalDateTime date = LocalDateTime.of(2024, 10, 1, 0, 0); - activityJpaRepository.save(jpaEntity); + activityJpaRepository.save(jpaEntity1); activityJpaRepository.save(jpaEntity2); Pageable pageable = PageRequest.of(0, 2, Direction.ASC, "actId"); @@ -245,4 +257,23 @@ class ActivityJpaRepositoryTest { assertThat(content.size()).isEqualTo(1); assertThat(content.getFirst().getNoticeEndDate().isAfter(date)).isTrue(); } + + @Test + void 가까운_거리순_정렬_조회() { + // given + activityJpaRepository.save(jpaEntity1); + activityJpaRepository.save(jpaEntity2); + activityJpaRepository.save(jpaEntity3); + Pageable pageable = PageRequest.of(0, 3, Direction.ASC, "distance"); + + // when + List content = activityJpaRepository.findSliceDistanceOrder( + noFilterReq, distanceOrderReq, pageable) + .getContent(); + + // then + assertThat(content.size()).isEqualTo(3); + assertThat(content.getFirst().getActId()).isEqualTo(jpaEntity1.getActId()); + assertThat(content.getLast().getActId()).isEqualTo(jpaEntity3.getActId()); + } } \ No newline at end of file diff --git a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java index ce15081..24e3639 100644 --- a/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java +++ b/src/test/java/com/gamsa/activity/service/ActivityServiceTest.java @@ -41,7 +41,7 @@ class ActivityServiceTest { ); // when - Slice result = activityService.findSlice(null, + Slice result = activityService.findSlice(null, null, PageRequest.of(0, 10)); // then diff --git a/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java b/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java index 4b58491..8f634f3 100644 --- a/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java +++ b/src/test/java/com/gamsa/activity/stub/StubEmptyActivityRepository.java @@ -2,6 +2,7 @@ import com.gamsa.activity.domain.Activity; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import com.gamsa.activity.repository.ActivityRepository; import java.util.List; import java.util.Optional; @@ -21,6 +22,12 @@ public Slice findSlice(ActivityFilterRequest request, Pageable pageabl return new SliceImpl<>(List.of()); } + @Override + public Slice findSliceDistanceOrder(ActivityFilterRequest filterRequest, + ActivityFindDistanceOrderRequest distanceOrderRequest, Pageable pageable) { + return new SliceImpl<>(List.of()); + } + @Override public Optional findById(Long activityId) { return Optional.empty(); diff --git a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java index d4161d2..d0a11cc 100644 --- a/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java +++ b/src/test/java/com/gamsa/activity/stub/StubExistsActivityRepository.java @@ -4,6 +4,7 @@ import com.gamsa.activity.domain.District; import com.gamsa.activity.domain.Institute; import com.gamsa.activity.dto.ActivityFilterRequest; +import com.gamsa.activity.dto.ActivityFindDistanceOrderRequest; import com.gamsa.activity.repository.ActivityRepository; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -65,6 +66,12 @@ public Slice findSlice(ActivityFilterRequest request, Pageable pageabl return new SliceImpl<>(List.of(activity)); } + @Override + public Slice findSliceDistanceOrder(ActivityFilterRequest filterRequest, + ActivityFindDistanceOrderRequest distanceOrderRequest, Pageable pageable) { + return new SliceImpl<>(List.of(activity)); + } + @Override public Optional findById(Long activityId) { return Optional.of(activity);