From f7edb691dddda55de5db400a5ba03acfe382ea3c Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 17 Feb 2024 14:57:23 +0900 Subject: [PATCH 1/8] =?UTF-8?q?refactor=20:=20=EC=9C=84=EB=8F=84=EC=99=80?= =?UTF-8?q?=20=EA=B2=BD=EB=8F=84=20=EA=B0=92=EA=B0=9D=EC=B2=B4=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AcademyByLocationWithScrollRequest.java | 6 ++- .../AcademyFilterWithScrollRequest.java | 42 ++++++++++--------- .../domain/academy/util/GeometryUtil.java | 12 +++--- .../domain/academy/util/Latitude.java | 40 ++++++++++++++++++ .../domain/academy/util/Longitude.java | 41 ++++++++++++++++++ 5 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/util/Latitude.java create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/util/Longitude.java diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyByLocationWithScrollRequest.java b/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyByLocationWithScrollRequest.java index 8f4d45c62..2017b3353 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyByLocationWithScrollRequest.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyByLocationWithScrollRequest.java @@ -4,6 +4,8 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; import org.guzzing.studayserver.domain.academy.service.dto.param.AcademiesByLocationWithScrollParam; +import org.guzzing.studayserver.domain.academy.util.Latitude; +import org.guzzing.studayserver.domain.academy.util.Longitude; public record AcademyByLocationWithScrollRequest( @NotNull(message = "Latitude cannot be null") @@ -20,8 +22,8 @@ public record AcademyByLocationWithScrollRequest( public AcademiesByLocationWithScrollParam to(Long memberId) { return new AcademiesByLocationWithScrollParam( - lat, - lng, + Latitude.of(lat), + Longitude.of(lng), memberId, pageNumber ); diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyFilterWithScrollRequest.java b/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyFilterWithScrollRequest.java index 760ead3b1..6fa14e744 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyFilterWithScrollRequest.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyFilterWithScrollRequest.java @@ -4,29 +4,33 @@ import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotNull; + import java.util.List; + import org.guzzing.studayserver.domain.academy.controller.dto.validation.ValidCategoryName; import org.guzzing.studayserver.domain.academy.service.dto.param.AcademyFilterWithScrollParam; +import org.guzzing.studayserver.domain.academy.util.Latitude; +import org.guzzing.studayserver.domain.academy.util.Longitude; public record AcademyFilterWithScrollRequest( - @NotNull(message = "Latitude cannot be null") - @DecimalMin(value = "33.0", message = "Invalid latitude") - @DecimalMax(value = "38.0", message = "Invalid latitude") - Double lat, + @NotNull(message = "Latitude cannot be null") + @DecimalMin(value = "33.0", message = "Invalid latitude") + @DecimalMax(value = "38.0", message = "Invalid latitude") + Double lat, - @NotNull(message = "Longitude cannot be null") - @DecimalMin(value = "125.0", message = "Invalid longitude") - @DecimalMax(value = "130.0", message = "Invalid longitude") - Double lng, + @NotNull(message = "Longitude cannot be null") + @DecimalMin(value = "125.0", message = "Invalid longitude") + @DecimalMax(value = "130.0", message = "Invalid longitude") + Double lng, - @ValidCategoryName - List categories, + @ValidCategoryName + List categories, - Long desiredMinAmount, + Long desiredMinAmount, - Long desiredMaxAmount, + Long desiredMaxAmount, - int pageNumber + int pageNumber ) { @AssertTrue(message = "최소 희망 금액이 최대 희망 금액보다 클 수 없습니다.") @@ -52,12 +56,12 @@ private boolean isValidPageNumber() { public static AcademyFilterWithScrollParam to(AcademyFilterWithScrollRequest request) { return new AcademyFilterWithScrollParam( - request.lat, - request.lng, - request.categories(), - request.desiredMinAmount, - request.desiredMaxAmount, - request.pageNumber + Latitude.of(request.lat), + Longitude.of(request.lng), + request.categories(), + request.desiredMinAmount, + request.desiredMaxAmount, + request.pageNumber ); } } diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/util/GeometryUtil.java b/src/main/java/org/guzzing/studayserver/domain/academy/util/GeometryUtil.java index 4410201d7..05f60949c 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/util/GeometryUtil.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/util/GeometryUtil.java @@ -37,17 +37,17 @@ public static Location calculateLocationWithinRadiusInDirection( } public static String makeDiagonal( - Double baseLatitude, - Double baseLongitude, + Latitude baseLatitude, + Longitude baseLongitude, Double distance) { Location northEast = calculateLocationWithinRadiusInDirection( - baseLatitude, - baseLongitude, + baseLatitude.getValue(), + baseLongitude.getValue(), Direction.NORTHEAST.getBearing(), distance); Location southWest = calculateLocationWithinRadiusInDirection( - baseLatitude, - baseLongitude, + baseLatitude.getValue(), + baseLongitude.getValue(), Direction.SOUTHWEST.getBearing(), distance); diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/util/Latitude.java b/src/main/java/org/guzzing/studayserver/domain/academy/util/Latitude.java new file mode 100644 index 000000000..a94d60f3c --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/util/Latitude.java @@ -0,0 +1,40 @@ +package org.guzzing.studayserver.domain.academy.util; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.Assert; + +import java.util.Objects; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Latitude { + private double value; + + public static final double KOREA_NORTHERNMOST_POINT = 43; + public static final double KOREA_SOUTHERNMOST_POINT = 33; + + private Latitude(double value) { + Assert.isTrue(value <= KOREA_NORTHERNMOST_POINT && value >= KOREA_SOUTHERNMOST_POINT, + String.format( "위도(latitude)는 %s보다 크고, %s보다 작아야 합니다", KOREA_SOUTHERNMOST_POINT, KOREA_NORTHERNMOST_POINT)); + this.value = value; + } + + public static Latitude of(double value) { + return new Latitude(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Latitude latitude = (Latitude) o; + return Double.compare(latitude.value, value) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/util/Longitude.java b/src/main/java/org/guzzing/studayserver/domain/academy/util/Longitude.java new file mode 100644 index 000000000..bdf42e89b --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/util/Longitude.java @@ -0,0 +1,41 @@ +package org.guzzing.studayserver.domain.academy.util; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.Assert; + +import java.util.Objects; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Longitude { + private double value; + + public static final double KOREA_NORTHERNMOST_POINT = 132; + public static final double KOREA_SOUTHERNMOST_POINT = 124; + + private Longitude(double value) { + Assert.isTrue(value <= KOREA_NORTHERNMOST_POINT && value >= KOREA_SOUTHERNMOST_POINT, + String.format( "경도(longitude)는 %s보다 크고, %s보다 작아야 합니다", KOREA_SOUTHERNMOST_POINT, KOREA_NORTHERNMOST_POINT)); + this.value = value; + } + + public static Longitude of(double value) { + return new Longitude(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Longitude longitude = (Longitude) o; + return Double.compare(longitude.value, value) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + +} From 261694e8f4498f7c10cd1d91ff5e665276828291 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 17 Feb 2024 14:58:21 +0900 Subject: [PATCH 2/8] =?UTF-8?q?test=20:=20=EC=9C=84=EB=8F=84=20=EA=B2=BD?= =?UTF-8?q?=EB=8F=84=20=EA=B0=92=EA=B0=9D=EC=B2=B4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/academy/util/LatitudeTest.java | 20 +++++++++++++++++++ .../domain/academy/util/LongitudeTest.java | 19 ++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/test/java/org/guzzing/studayserver/domain/academy/util/LatitudeTest.java create mode 100644 src/test/java/org/guzzing/studayserver/domain/academy/util/LongitudeTest.java diff --git a/src/test/java/org/guzzing/studayserver/domain/academy/util/LatitudeTest.java b/src/test/java/org/guzzing/studayserver/domain/academy/util/LatitudeTest.java new file mode 100644 index 000000000..50a972731 --- /dev/null +++ b/src/test/java/org/guzzing/studayserver/domain/academy/util/LatitudeTest.java @@ -0,0 +1,20 @@ +package org.guzzing.studayserver.domain.academy.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LatitudeTest { + + @DisplayName("위도 범위에 범어나면 예외를 던진다.") + @ValueSource(doubles = {32, 100, 44}) + @ParameterizedTest + void createLatitude_outOfBounds_throwException(double latitude) { + assertThatThrownBy( + () -> Longitude.of(latitude)) + .isInstanceOf(IllegalArgumentException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/org/guzzing/studayserver/domain/academy/util/LongitudeTest.java b/src/test/java/org/guzzing/studayserver/domain/academy/util/LongitudeTest.java new file mode 100644 index 000000000..37a16a8ce --- /dev/null +++ b/src/test/java/org/guzzing/studayserver/domain/academy/util/LongitudeTest.java @@ -0,0 +1,19 @@ +package org.guzzing.studayserver.domain.academy.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LongitudeTest { + + @DisplayName("경도 범위에 범어나면 예외를 던진다.") + @ValueSource(doubles = {200, 123, 133}) + @ParameterizedTest + void createLongitude_outOfBounds_throwException(double longitude) { + assertThatThrownBy( + () -> Longitude.of(longitude)) + .isInstanceOf(IllegalArgumentException.class); + } + +} \ No newline at end of file From c587e192c1630e591251e5c98ec9c2ccb0c530f9 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 17 Feb 2024 14:59:42 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat=20:=20=EC=BB=A4=EC=84=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=ED=95=99=EC=9B=90=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../academy/AcademyQueryRepository.java | 23 +- .../academy/AcademyQueryRepositoryImpl.java | 297 +++++++++++------- ...ByLocationWithCursorRepositoryRequest.java | 8 + ...yLocationWithCursorRepositoryResponse.java | 58 ++++ 4 files changed, 262 insertions(+), 124 deletions(-) create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/repository/dto/AcademyByLocationWithCursorRepositoryRequest.java create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/repository/dto/AcademyByLocationWithCursorRepositoryResponse.java diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyQueryRepository.java b/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyQueryRepository.java index 473452c6f..5eae29753 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyQueryRepository.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyQueryRepository.java @@ -1,20 +1,21 @@ package org.guzzing.studayserver.domain.academy.repository.academy; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByFilterWithScroll; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByLocationWithScroll; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademyFilterCondition; +import org.guzzing.studayserver.domain.academy.repository.dto.*; public interface AcademyQueryRepository { + AcademyByLocationWithCursorRepositoryResponse findAcademiesByLocationByCursor( + AcademyByLocationWithCursorRepositoryRequest request); + AcademiesByLocationWithScroll findAcademiesByLocation( - String pointFormat, - Long memberId, - int pageNumber, - int pageSize); + String pointFormat, + Long memberId, + int pageNumber, + int pageSize); AcademiesByFilterWithScroll filterAcademies( - AcademyFilterCondition academyFilterCondition, - Long memberId, - int pageNumber, - int pageSize); + AcademyFilterCondition academyFilterCondition, + Long memberId, + int pageNumber, + int pageSize); } diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyQueryRepositoryImpl.java b/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyQueryRepositoryImpl.java index 7f1992180..05666accb 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyQueryRepositoryImpl.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyQueryRepositoryImpl.java @@ -2,12 +2,10 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.Query; + import java.util.List; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByFilterWithScroll; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByLocationWithScroll; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademyByFilterWithScroll; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademyByLocationWithScroll; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademyFilterCondition; + +import org.guzzing.studayserver.domain.academy.repository.dto.*; import org.hibernate.query.NativeQuery; import org.hibernate.transform.ResultTransformer; import org.hibernate.type.StandardBasicTypes; @@ -16,34 +14,96 @@ public class AcademyQueryRepositoryImpl implements AcademyQueryRepository { private static final String BLANK_QUERY = ""; private final EntityManager em; + private final int PAGE_SIZE = 10; public AcademyQueryRepositoryImpl(EntityManager em) { this.em = em; } + public AcademyByLocationWithCursorRepositoryResponse findAcademiesByLocationByCursor( + AcademyByLocationWithCursorRepositoryRequest request) { + + String nativeQuery = """ + SELECT + a.id AS academyId, + a.academy_name AS academyName, + a.phone_number AS phoneNumber, + a.full_address AS fullAddress, + a.latitude AS latitude, + a.longitude AS longitude, + a.shuttle AS shuttleAvailable, + (CASE WHEN l.academy_id IS NOT NULL THEN true ELSE false END) AS isLiked, + ac.category_id AS categoryId + FROM + academies AS a + INNER JOIN + academy_categories AS ac ON a.id = ac.academy_id + LEFT JOIN + likes AS l ON a.id = l.academy_id AND l.member_id = %s """; + + String formattedQuery = String.format(nativeQuery, request.memberId()); + formattedQuery += builderWhere(); + formattedQuery += whereWithinDistance(request.pointFormat()); + formattedQuery += makeCursor(request.lastAcademyId()); + formattedQuery += orderByAsc("a.id"); + formattedQuery += limit(); + + Query emNativeQuery = em.createNativeQuery(formattedQuery); + + List academiesByLocation = + emNativeQuery.unwrap(org.hibernate.query.NativeQuery.class) + .addScalar("academyId", StandardBasicTypes.LONG) + .addScalar("academyName", StandardBasicTypes.STRING) + .addScalar("fullAddress", StandardBasicTypes.STRING) + .addScalar("phoneNumber", StandardBasicTypes.STRING) + .addScalar("latitude", StandardBasicTypes.DOUBLE) + .addScalar("longitude", StandardBasicTypes.DOUBLE) + .addScalar("shuttleAvailable", StandardBasicTypes.STRING) + .addScalar("isLiked", StandardBasicTypes.BOOLEAN) + .addScalar("categoryId", StandardBasicTypes.LONG) + .setResultTransformer((tuple, aliases) -> new AcademyByLocationWithScroll( + (Long) tuple[0], + (String) tuple[1], + (String) tuple[2], + (String) tuple[3], + (Double) tuple[4], + (Double) tuple[5], + (String) tuple[6], + (boolean) tuple[7], + (Long) tuple[8] + )) + .getResultList(); + + return AcademyByLocationWithCursorRepositoryResponse.of( + academiesByLocation, + getBeforeLastId(academiesByLocation), + isHasNest(academiesByLocation.size()) + ); + } + public AcademiesByLocationWithScroll findAcademiesByLocation( - String pointFormat, - Long memberId, - int pageNumber, - int pageSize) { + String pointFormat, + Long memberId, + int pageNumber, + int pageSize) { String nativeQuery = """ - SELECT - a.id AS academyId, - a.academy_name AS academyName, - a.phone_number AS phoneNumber, - a.full_address AS fullAddress, - a.latitude AS latitude, - a.longitude AS longitude, - a.shuttle AS shuttleAvailable, - (CASE WHEN l.academy_id IS NOT NULL THEN true ELSE false END) AS isLiked, - ac.category_id AS categoryId - FROM - academies AS a - INNER JOIN - academy_categories AS ac ON a.id = ac.academy_id - LEFT JOIN - likes AS l ON a.id = l.academy_id AND l.member_id = %s """; + SELECT + a.id AS academyId, + a.academy_name AS academyName, + a.phone_number AS phoneNumber, + a.full_address AS fullAddress, + a.latitude AS latitude, + a.longitude AS longitude, + a.shuttle AS shuttleAvailable, + (CASE WHEN l.academy_id IS NOT NULL THEN true ELSE false END) AS isLiked, + ac.category_id AS categoryId + FROM + academies AS a + INNER JOIN + academy_categories AS ac ON a.id = ac.academy_id + LEFT JOIN + likes AS l ON a.id = l.academy_id AND l.member_id = %s """; String formattedQuery = String.format(nativeQuery, memberId); @@ -52,67 +112,66 @@ public AcademiesByLocationWithScroll findAcademiesByLocation( formattedQuery += whereWithinDistance(pointFormat); formattedQuery = makeScroll(pageNumber, pageSize, formattedQuery); - Query emNativeQuery = em.createNativeQuery( - formattedQuery); + Query emNativeQuery = em.createNativeQuery(formattedQuery); List academiesByLocation = - emNativeQuery.unwrap(org.hibernate.query.NativeQuery.class) - .addScalar("academyId", StandardBasicTypes.LONG) - .addScalar("academyName", StandardBasicTypes.STRING) - .addScalar("fullAddress", StandardBasicTypes.STRING) - .addScalar("phoneNumber", StandardBasicTypes.STRING) - .addScalar("latitude", StandardBasicTypes.DOUBLE) - .addScalar("longitude", StandardBasicTypes.DOUBLE) - .addScalar("shuttleAvailable", StandardBasicTypes.STRING) - .addScalar("isLiked", StandardBasicTypes.BOOLEAN) - .addScalar("categoryId", StandardBasicTypes.LONG) - .setResultTransformer((tuple, aliases) -> new AcademyByLocationWithScroll( - (Long) tuple[0], - (String) tuple[1], - (String) tuple[2], - (String) tuple[3], - (Double) tuple[4], - (Double) tuple[5], - (String) tuple[6], - (boolean) tuple[7], - (Long) tuple[8] - )) - .getResultList(); + emNativeQuery.unwrap(org.hibernate.query.NativeQuery.class) + .addScalar("academyId", StandardBasicTypes.LONG) + .addScalar("academyName", StandardBasicTypes.STRING) + .addScalar("fullAddress", StandardBasicTypes.STRING) + .addScalar("phoneNumber", StandardBasicTypes.STRING) + .addScalar("latitude", StandardBasicTypes.DOUBLE) + .addScalar("longitude", StandardBasicTypes.DOUBLE) + .addScalar("shuttleAvailable", StandardBasicTypes.STRING) + .addScalar("isLiked", StandardBasicTypes.BOOLEAN) + .addScalar("categoryId", StandardBasicTypes.LONG) + .setResultTransformer((tuple, aliases) -> new AcademyByLocationWithScroll( + (Long) tuple[0], + (String) tuple[1], + (String) tuple[2], + (String) tuple[3], + (Double) tuple[4], + (Double) tuple[5], + (String) tuple[6], + (boolean) tuple[7], + (Long) tuple[8] + )) + .getResultList(); return AcademiesByLocationWithScroll.of( - academiesByLocation, - isHasNest(academiesByLocation.size(), pageSize) + academiesByLocation, + isHasNest(academiesByLocation.size()) ); } public AcademiesByFilterWithScroll filterAcademies( - AcademyFilterCondition academyFilterCondition, - Long memberId, - int pageNumber, - int pageSize) { + AcademyFilterCondition academyFilterCondition, + Long memberId, + int pageNumber, + int pageSize) { String nativeQuery = """ - SELECT DISTINCT - a.id AS academyId, - a.academy_name AS academyName, - a.full_address AS fullAddress, - a.phone_number AS phoneNumber, - a.latitude, a.longitude, - a.shuttle AS shuttleAvailable, - (CASE WHEN l.academy_id IS NOT NULL THEN true ELSE false END) AS isLiked - FROM - academy_categories as ac - LEFT JOIN - academies AS a ON ac.academy_id = a.id - LEFT JOIN - likes AS l ON a.id = l.academy_id AND l.member_id = %s - WHERE - MBRContains(ST_LINESTRINGFROMTEXT(%s), a.point) - """; + SELECT DISTINCT + a.id AS academyId, + a.academy_name AS academyName, + a.full_address AS fullAddress, + a.phone_number AS phoneNumber, + a.latitude, a.longitude, + a.shuttle AS shuttleAvailable, + (CASE WHEN l.academy_id IS NOT NULL THEN true ELSE false END) AS isLiked + FROM + academy_categories as ac + LEFT JOIN + academies AS a ON ac.academy_id = a.id + LEFT JOIN + likes AS l ON a.id = l.academy_id AND l.member_id = %s + WHERE + MBRContains(ST_LINESTRINGFROMTEXT(%s), a.point) + """; String formattedQuery = String.format( - nativeQuery, - memberId, - academyFilterCondition.pointFormat()); + nativeQuery, + memberId, + academyFilterCondition.pointFormat()); formattedQuery = whereFilters(formattedQuery, academyFilterCondition); formattedQuery += orderByDesc("a.id"); formattedQuery = makeScroll(pageNumber, pageSize, formattedQuery); @@ -120,51 +179,51 @@ public AcademiesByFilterWithScroll filterAcademies( Query query = em.createNativeQuery(formattedQuery); List academyByFilter = query.unwrap(NativeQuery.class) - .addScalar("academyId", StandardBasicTypes.LONG) - .addScalar("academyName", StandardBasicTypes.STRING) - .addScalar("fullAddress", StandardBasicTypes.STRING) - .addScalar("phoneNumber", StandardBasicTypes.STRING) - .addScalar("latitude", StandardBasicTypes.DOUBLE) - .addScalar("longitude", StandardBasicTypes.DOUBLE) - .addScalar("shuttleAvailable", StandardBasicTypes.STRING) - .addScalar("isLiked", StandardBasicTypes.BOOLEAN) - .setResultTransformer( - new ResultTransformer() { - @Override - public Object transformTuple(Object[] tuple, String[] aliases) { - return new AcademyByFilterWithScroll( - (Long) tuple[0], - (String) tuple[1], - (String) tuple[2], - (String) tuple[3], - (Double) tuple[4], - (Double) tuple[5], - (String) tuple[6], - (boolean) tuple[7] - ); - } - - @Override - public List transformList(List collection) { - return collection; - } - } - ) - .getResultList(); + .addScalar("academyId", StandardBasicTypes.LONG) + .addScalar("academyName", StandardBasicTypes.STRING) + .addScalar("fullAddress", StandardBasicTypes.STRING) + .addScalar("phoneNumber", StandardBasicTypes.STRING) + .addScalar("latitude", StandardBasicTypes.DOUBLE) + .addScalar("longitude", StandardBasicTypes.DOUBLE) + .addScalar("shuttleAvailable", StandardBasicTypes.STRING) + .addScalar("isLiked", StandardBasicTypes.BOOLEAN) + .setResultTransformer( + new ResultTransformer() { + @Override + public Object transformTuple(Object[] tuple, String[] aliases) { + return new AcademyByFilterWithScroll( + (Long) tuple[0], + (String) tuple[1], + (String) tuple[2], + (String) tuple[3], + (Double) tuple[4], + (Double) tuple[5], + (String) tuple[6], + (boolean) tuple[7] + ); + } + + @Override + public List transformList(List collection) { + return collection; + } + } + ) + .getResultList(); return AcademiesByFilterWithScroll.of( - academyByFilter, - isHasNest(academyByFilter.size(), pageSize) + academyByFilter, + isHasNest(academyByFilter.size()) ); } private String builderWhere() { - return " where "; + return " WHERE "; } - private boolean isHasNest(int resultSize, int pageSize) { - return resultSize == pageSize; + private boolean isHasNest(int resultSize) { + return resultSize == PAGE_SIZE; } private String whereFilters(String formattedQuery, AcademyFilterCondition academyFilterCondition) { @@ -187,7 +246,7 @@ private String whereInCategories(AcademyFilterCondition academyFilterCondition) private String whereBetweenEducationFee(AcademyFilterCondition academyFilterCondition) { if (academyFilterCondition.desiredMinAmount() != null && academyFilterCondition.desiredMaxAmount() != null) { return " AND max_education_fee BETWEEN " + academyFilterCondition.desiredMinAmount() + " AND " - + academyFilterCondition.desiredMaxAmount(); + + academyFilterCondition.desiredMaxAmount(); } return BLANK_QUERY; } @@ -197,15 +256,27 @@ private String makeScroll(int pageNumber, int pageSize, String formattedQuery) { return formattedQuery; } + private String makeCursor(Long lastAcademyId) { + return " AND a.id >" + lastAcademyId; + } + + private String limit() { + return " LIMIT " + PAGE_SIZE; + } + private String orderByDesc(String columnName) { return String.format(" ORDER BY %s %s ", columnName, " DESC "); } + private String orderByAsc(String columnName) { + return String.format(" ORDER BY %s %s ", columnName, " ASC "); + } + private Long getBeforeLastId(List academiesByLocation) { if (academiesByLocation != null && !academiesByLocation.isEmpty()) { return academiesByLocation.get(academiesByLocation.size() - 1).academyId(); } - return null; + return 0L; } } diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/repository/dto/AcademyByLocationWithCursorRepositoryRequest.java b/src/main/java/org/guzzing/studayserver/domain/academy/repository/dto/AcademyByLocationWithCursorRepositoryRequest.java new file mode 100644 index 000000000..1d491e0b0 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/repository/dto/AcademyByLocationWithCursorRepositoryRequest.java @@ -0,0 +1,8 @@ +package org.guzzing.studayserver.domain.academy.repository.dto; + +public record AcademyByLocationWithCursorRepositoryRequest( + String pointFormat, + Long memberId, + Long lastAcademyId +) { +} diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/repository/dto/AcademyByLocationWithCursorRepositoryResponse.java b/src/main/java/org/guzzing/studayserver/domain/academy/repository/dto/AcademyByLocationWithCursorRepositoryResponse.java new file mode 100644 index 000000000..46a18d549 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/repository/dto/AcademyByLocationWithCursorRepositoryResponse.java @@ -0,0 +1,58 @@ +package org.guzzing.studayserver.domain.academy.repository.dto; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public record AcademyByLocationWithCursorRepositoryResponse( + Map> academiesByLocation, + Long lastAcademyId, + boolean hasNext +) { + + public static AcademyByLocationWithCursorRepositoryResponse of( + List academiesByLocation, + Long lastAcademyId, + boolean hasNext + ) { + Map> academyIdWithCategories = new ConcurrentHashMap<>(); + + academiesByLocation.forEach(academyByLocation -> academyIdWithCategories.computeIfAbsent( + AcademyByLocation.of(academyByLocation), + k -> new ArrayList<>()) + .add(academyByLocation.categoryId())); + + return new AcademyByLocationWithCursorRepositoryResponse( + academyIdWithCategories, + lastAcademyId, + hasNext + ); + } + + public record AcademyByLocation( + Long academyId, + String academyName, + String fullAddress, + String phoneNumber, + Double latitude, + Double longitude, + String shuttleAvailable, + boolean isLiked + ) { + + public static AcademyByLocation of(AcademyByLocationWithScroll academyByLocationWithScroll) { + return new AcademyByLocation( + academyByLocationWithScroll.academyId(), + academyByLocationWithScroll.academyName(), + academyByLocationWithScroll.fullAddress(), + academyByLocationWithScroll.phoneNumber(), + academyByLocationWithScroll.latitude(), + academyByLocationWithScroll.longitude(), + academyByLocationWithScroll.shuttleAvailable(), + academyByLocationWithScroll.isLiked() + ); + } + } + +} From fe2283e1f3af010d2ad13636fc0c4e624be15e99 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 17 Feb 2024 15:02:17 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat=20:=20=EC=BB=A4=EC=84=9C=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=ED=95=99=EC=9B=90=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../academy/service/AcademyService.java | 33 +++++++--- .../dto/param/AcademyByLocationParam.java | 29 +++++++++ .../AcademyByLocationWithCursorParam.java | 22 +++++++ .../AcademyByLocationWithCursorResults.java | 62 +++++++++++++++++++ 4 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyByLocationParam.java create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyByLocationWithCursorParam.java create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/service/dto/result/AcademyByLocationWithCursorResults.java diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/service/AcademyService.java b/src/main/java/org/guzzing/studayserver/domain/academy/service/AcademyService.java index 16c1ba1aa..47bff88bb 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/service/AcademyService.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/service/AcademyService.java @@ -11,18 +11,11 @@ import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByFilterWithScroll; import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByLocationWithScroll; import org.guzzing.studayserver.domain.academy.repository.dto.AcademyByFilterWithScroll; +import org.guzzing.studayserver.domain.academy.repository.dto.AcademyByLocationWithCursorRepositoryResponse; import org.guzzing.studayserver.domain.academy.repository.lesson.LessonRepository; import org.guzzing.studayserver.domain.academy.repository.review.ReviewCountRepository; -import org.guzzing.studayserver.domain.academy.service.dto.param.AcademiesByLocationWithScrollParam; -import org.guzzing.studayserver.domain.academy.service.dto.param.AcademiesByNameParam; -import org.guzzing.studayserver.domain.academy.service.dto.param.AcademyFilterWithScrollParam; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByLocationWithScrollResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByNameResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesFilterWithScrollResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademyAndLessonDetailResult; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademyFeeInfo; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademyGetResult; -import org.guzzing.studayserver.domain.academy.service.dto.result.LessonInfoToCreateDashboardResults; +import org.guzzing.studayserver.domain.academy.service.dto.param.*; +import org.guzzing.studayserver.domain.academy.service.dto.result.*; import org.guzzing.studayserver.domain.academy.util.GeometryUtil; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -87,6 +80,26 @@ public AcademiesByLocationWithScrollResults findAcademiesByLocationWithScroll( academiesByLocation); } + @Transactional(readOnly = true) + public AcademyByLocationWithCursorResults findAcademiesByLocationWithCursor( + AcademyByLocationWithCursorParam param) { + String diagonal = GeometryUtil.makeDiagonal(param.baseLatitude(), param.baseLongitude(), DISTANCE); + + AcademyByLocationWithCursorRepositoryResponse academiesByLocationByCursor = academyRepository.findAcademiesByLocationByCursor( + param.toAcademyByLocationWithCursorRequest(diagonal)); + + return AcademyByLocationWithCursorResults.to(academiesByLocationByCursor); + } + + @Transactional(readOnly = true) + public AcademyByLocationWithCursorResults findAcademiesByLocation( + AcademyByLocationParam param) { + AcademyByLocationWithCursorRepositoryResponse academiesByLocationByCursor = academyRepository.findAcademiesByLocationByCursor( + param.toAcademyByLocationWithCursorRequest()); + + return AcademyByLocationWithCursorResults.to(academiesByLocationByCursor); + } + @Transactional(readOnly = true) public AcademiesByNameResults findAcademiesByName(AcademiesByNameParam param) { PageRequest requestPageAble = PageRequest.of(param.pageNumber(), ACADEMY_NAME_SEARCH_PAGE_SIZE); diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyByLocationParam.java b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyByLocationParam.java new file mode 100644 index 000000000..f7389bf96 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyByLocationParam.java @@ -0,0 +1,29 @@ +package org.guzzing.studayserver.domain.academy.service.dto.param; + +import org.guzzing.studayserver.domain.academy.repository.dto.AcademyByLocationWithCursorRepositoryRequest; +import org.guzzing.studayserver.domain.academy.util.Latitude; +import org.guzzing.studayserver.domain.academy.util.Longitude; +import org.guzzing.studayserver.domain.academy.util.SqlFormatter; + +public record AcademyByLocationParam( + Latitude northEastLatitude, + Longitude northEastLongitude, + Latitude southWestbaseLatitude, + Longitude southWestLongitude, + Long memberId, + Long lastAcademyId +) { + public AcademyByLocationWithCursorRepositoryRequest toAcademyByLocationWithCursorRequest( + ) { + return new AcademyByLocationWithCursorRepositoryRequest( + SqlFormatter.makeDiagonalByLineString( + northEastLatitude, + northEastLongitude, + southWestbaseLatitude, + southWestLongitude + ), + memberId, + lastAcademyId + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyByLocationWithCursorParam.java b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyByLocationWithCursorParam.java new file mode 100644 index 000000000..d8f599007 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyByLocationWithCursorParam.java @@ -0,0 +1,22 @@ +package org.guzzing.studayserver.domain.academy.service.dto.param; + +import org.guzzing.studayserver.domain.academy.repository.dto.AcademyByLocationWithCursorRepositoryRequest; +import org.guzzing.studayserver.domain.academy.util.Latitude; +import org.guzzing.studayserver.domain.academy.util.Longitude; + +public record AcademyByLocationWithCursorParam( + Latitude baseLatitude, + Longitude baseLongitude, + Long memberId, + Long lastAcademyId +) { + public AcademyByLocationWithCursorRepositoryRequest toAcademyByLocationWithCursorRequest( + String diagonal + ) { + return new AcademyByLocationWithCursorRepositoryRequest( + diagonal, + memberId, + lastAcademyId + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/result/AcademyByLocationWithCursorResults.java b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/result/AcademyByLocationWithCursorResults.java new file mode 100644 index 000000000..58f63e533 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/result/AcademyByLocationWithCursorResults.java @@ -0,0 +1,62 @@ +package org.guzzing.studayserver.domain.academy.service.dto.result; + +import org.guzzing.studayserver.domain.academy.repository.dto.AcademyByLocationWithCursorRepositoryResponse; +import org.guzzing.studayserver.domain.academy.util.CategoryInfo; + +import java.util.List; + +public record AcademyByLocationWithCursorResults( + List academiesByLocationResults, + Long lastAcademyId, + boolean hasNext +) { + + public static AcademyByLocationWithCursorResults to( + AcademyByLocationWithCursorRepositoryResponse academyByLocationWithCursorRepositoryResponse) { + return new AcademyByLocationWithCursorResults( + academyByLocationWithCursorRepositoryResponse + .academiesByLocation() + .keySet() + .stream() + .map(academyByLocation -> + AcademiesByLocationWithCursorResult.from( + academyByLocation, + academyByLocationWithCursorRepositoryResponse.academiesByLocation(). + get(academyByLocation))) + .toList(), + academyByLocationWithCursorRepositoryResponse.lastAcademyId(), + academyByLocationWithCursorRepositoryResponse.hasNext()); + } + + public record AcademiesByLocationWithCursorResult( + Long academyId, + String academyName, + String address, + String contact, + List categories, + Double latitude, + Double longitude, + String shuttleAvailable, + boolean isLiked + ) { + + public static AcademiesByLocationWithCursorResult from( + AcademyByLocationWithCursorRepositoryResponse.AcademyByLocation academyByLocationWithScroll, + List categories) { + return new AcademiesByLocationWithCursorResult( + academyByLocationWithScroll.academyId(), + academyByLocationWithScroll.academyName(), + academyByLocationWithScroll.fullAddress(), + academyByLocationWithScroll.phoneNumber(), + categories.stream() + .map(CategoryInfo::getCategoryNameById) + .toList(), + academyByLocationWithScroll.latitude(), + academyByLocationWithScroll.longitude(), + academyByLocationWithScroll.shuttleAvailable(), + academyByLocationWithScroll.isLiked() + ); + } + + } +} \ No newline at end of file From 758eaa0e24c9d540bf0d70ba2b0bffcbbccf314f Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 17 Feb 2024 15:03:58 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor=20:=20=EC=9C=84=EB=8F=84=20?= =?UTF-8?q?=EA=B2=BD=EB=8F=84=20=EA=B0=92=EA=B0=9D=EC=B2=B4=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AcademiesByLocationWithScrollParam.java | 27 ++++++++++--------- .../param/AcademyFilterWithScrollParam.java | 6 +++-- .../domain/academy/util/SqlFormatter.java | 17 ++++++++++-- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademiesByLocationWithScrollParam.java b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademiesByLocationWithScrollParam.java index 96070866e..0e0482b73 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademiesByLocationWithScrollParam.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademiesByLocationWithScrollParam.java @@ -1,21 +1,24 @@ package org.guzzing.studayserver.domain.academy.service.dto.param; +import org.guzzing.studayserver.domain.academy.util.Latitude; +import org.guzzing.studayserver.domain.academy.util.Longitude; + public record AcademiesByLocationWithScrollParam( - Double baseLatitude, - Double baseLongitude, - Long memberId, - int pageNumber + Latitude baseLatitude, + Longitude baseLongitude, + Long memberId, + int pageNumber ) { public static AcademiesByLocationWithScrollParam of( - Double baseLatitude, - Double baseLongitude, - Long memberId, - int pageNumber) { + Latitude baseLatitude, + Longitude baseLongitude, + Long memberId, + int pageNumber) { return new AcademiesByLocationWithScrollParam( - baseLatitude, - baseLongitude, - memberId, - pageNumber); + baseLatitude, + baseLongitude, + memberId, + pageNumber); } } diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyFilterWithScrollParam.java b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyFilterWithScrollParam.java index 46660aa80..cb111961b 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyFilterWithScrollParam.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/service/dto/param/AcademyFilterWithScrollParam.java @@ -3,11 +3,13 @@ import java.util.List; import org.guzzing.studayserver.domain.academy.repository.dto.AcademyFilterCondition; import org.guzzing.studayserver.domain.academy.util.CategoryInfo; +import org.guzzing.studayserver.domain.academy.util.Latitude; +import org.guzzing.studayserver.domain.academy.util.Longitude; import org.guzzing.studayserver.domain.academy.util.SqlFormatter; public record AcademyFilterWithScrollParam( - Double baseLatitude, - Double baseLongitude, + Latitude baseLatitude, + Longitude baseLongitude, List categories, Long desiredMinAmount, Long desiredMaxAmount, diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/util/SqlFormatter.java b/src/main/java/org/guzzing/studayserver/domain/academy/util/SqlFormatter.java index 7478c3f27..5c8c5f33b 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/util/SqlFormatter.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/util/SqlFormatter.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.stream.Collectors; + import org.guzzing.studayserver.domain.academy.model.vo.Location; import org.guzzing.studayserver.global.error.response.ErrorCode; @@ -19,9 +20,21 @@ public static String makeWhereInString(List values) { public static String makeDiagonalByLineString(Location northEast, Location southWest) { return String.format( - LINESTRING_SQL, - northEast.getLatitude(), northEast.getLongitude(), southWest.getLatitude(), southWest.getLongitude() + LINESTRING_SQL, + northEast.getLatitude(), northEast.getLongitude(), southWest.getLatitude(), southWest.getLongitude() ); } + + public static String makeDiagonalByLineString( + Latitude northEastLatitude, Longitude northEastLongitude, Latitude southWestLatitude, Longitude southWestLongitude + ) { + return String.format( + LINESTRING_SQL, + northEastLatitude.getValue(), + northEastLongitude.getValue(), + southWestLatitude.getValue(), + southWestLongitude.getValue() + ); + } } From be2fc2fc79b52ab1a4ab0a13ee44ceff62056775 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 17 Feb 2024 15:05:40 +0900 Subject: [PATCH 6/8] =?UTF-8?q?test=20:=20=EC=BB=A4=EC=84=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=ED=95=99=EC=9B=90=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../academy/service/AcademyServiceTest.java | 53 ++++++-- .../fixture/academy/AcademyFixture.java | 115 ++++++++++-------- 2 files changed, 111 insertions(+), 57 deletions(-) diff --git a/src/test/java/org/guzzing/studayserver/domain/academy/service/AcademyServiceTest.java b/src/test/java/org/guzzing/studayserver/domain/academy/service/AcademyServiceTest.java index 402218ca0..6cc2bb63c 100644 --- a/src/test/java/org/guzzing/studayserver/domain/academy/service/AcademyServiceTest.java +++ b/src/test/java/org/guzzing/studayserver/domain/academy/service/AcademyServiceTest.java @@ -15,16 +15,8 @@ import org.guzzing.studayserver.domain.academy.repository.review.ReviewCountRepository; import org.guzzing.studayserver.domain.academy.service.dto.param.AcademiesByNameParam; import org.guzzing.studayserver.domain.academy.service.dto.param.AcademyFilterWithScrollParam; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByLocationWithScrollResults; +import org.guzzing.studayserver.domain.academy.service.dto.result.*; import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByLocationWithScrollResults.AcademiesByLocationResultWithScroll; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByNameResult; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByNameResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesFilterWithScrollResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademyAndLessonDetailResult; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademyGetResult; -import org.guzzing.studayserver.domain.academy.service.dto.result.LessonGetResult; -import org.guzzing.studayserver.domain.academy.service.dto.result.LessonInfoToCreateDashboardResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.ReviewPercentGetResult; import org.guzzing.studayserver.domain.academy.util.CategoryInfo; import org.guzzing.studayserver.domain.member.model.Member; import org.guzzing.studayserver.domain.member.repository.MemberRepository; @@ -297,6 +289,49 @@ void getLessonInfosAboutAcademy() { } + @Test + @DisplayName(" 해당 지도의 중심 위치가 주어졌을 때 커서 기반으로 목록 조회를 한다.") + void getAcademiesByLocationWithCursor() { + // Given + lessonRepository.deleteAll(); + reviewCountRepository.deleteAll(); + academyCategoryRepository.deleteAll(); + academyRepository.deleteAll(); + + List academies = AcademyFixture.randomAcademiesWithinDistance(LATITUDE, LONGITUDE); + for (Academy academy : academies) { + Academy savedAcademy = academyRepository.save(academy); + lessonRepository.save(AcademyFixture.lessonForSunganm(savedAcademy)); + reviewCountRepository.save(AcademyFixture.reviewCountDefault(savedAcademy)); + + AcademyFixture.academyCategoryAboutSungnam(savedAcademy) + .forEach( + academyCategory -> academyCategoryRepository.save(academyCategory) + ); + } + List categoryNames = AcademyFixture.academyCategoryAboutSungnam(savedAcademyAboutSungnam).stream() + .map(academyCategory -> academyCategory.getCategoryId()) + .map(categoryId -> CategoryInfo.getCategoryNameById(categoryId)) + .toList(); + + int totalSize = academies.size(); + int expectedSearchedPageSize = Math.min(totalSize, LOCATION_PAGE_SIZE); + boolean expectedHasNext = totalSize >= LOCATION_PAGE_SIZE; + + //When + AcademyByLocationWithCursorResults academiesByLocationWithCursor = academyService.findAcademiesByLocationWithCursor( + AcademyFixture.academyByLocationWithCursorParam(LATITUDE, LONGITUDE)); + + //Then + assertThat(academiesByLocationWithCursor.academiesByLocationResults()) + .hasSize(expectedSearchedPageSize); + assertThat(academiesByLocationWithCursor.hasNext()).isEqualTo(expectedHasNext); + academiesByLocationWithCursor.academiesByLocationResults() + .stream() + .map(AcademyByLocationWithCursorResults.AcademiesByLocationWithCursorResult::categories) + .forEach(categoryName -> assertThat(categoryNames).isEqualTo(categoryName)); + } + private SavedAcademyAndLesson academySetUpForFilterAndDetail() { lessonRepository.deleteAll(); reviewCountRepository.deleteAll(); diff --git a/src/test/java/org/guzzing/studayserver/testutil/fixture/academy/AcademyFixture.java b/src/test/java/org/guzzing/studayserver/testutil/fixture/academy/AcademyFixture.java index d51784377..30879efd2 100644 --- a/src/test/java/org/guzzing/studayserver/testutil/fixture/academy/AcademyFixture.java +++ b/src/test/java/org/guzzing/studayserver/testutil/fixture/academy/AcademyFixture.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; + import org.guzzing.studayserver.domain.academy.facade.dto.AcademyDetailFacadeParam; import org.guzzing.studayserver.domain.academy.model.Academy; import org.guzzing.studayserver.domain.academy.model.AcademyCategory; @@ -13,9 +14,12 @@ import org.guzzing.studayserver.domain.academy.model.vo.academyinfo.AcademyInfo; import org.guzzing.studayserver.domain.academy.model.vo.academyinfo.ShuttleAvailability; import org.guzzing.studayserver.domain.academy.service.dto.param.AcademiesByLocationWithScrollParam; +import org.guzzing.studayserver.domain.academy.service.dto.param.AcademyByLocationWithCursorParam; import org.guzzing.studayserver.domain.academy.service.dto.param.AcademyFilterWithScrollParam; import org.guzzing.studayserver.domain.academy.util.CategoryInfo; import org.guzzing.studayserver.domain.academy.util.GeometryUtil; +import org.guzzing.studayserver.domain.academy.util.Latitude; +import org.guzzing.studayserver.domain.academy.util.Longitude; import org.locationtech.jts.geom.Point; public class AcademyFixture { @@ -25,13 +29,13 @@ public class AcademyFixture { public static List academyInfos() { return List.of( - AcademyInfo.of("유원우 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), - AcademyInfo.of("박세영 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), - AcademyInfo.of("김별 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), - AcademyInfo.of("김희석보스 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), - AcademyInfo.of("김유진 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), - AcademyInfo.of("김지성 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), - AcademyInfo.of("김지성 보습학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "입시, 검정 및 보습") + AcademyInfo.of("유원우 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), + AcademyInfo.of("박세영 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), + AcademyInfo.of("김별 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), + AcademyInfo.of("김희석보스 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), + AcademyInfo.of("김유진 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), + AcademyInfo.of("김지성 코딩학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "기타(대)"), + AcademyInfo.of("김지성 보습학원", "000-0000-0000", ShuttleAvailability.AVAILABLE.name(), "입시, 검정 및 보습") ); } @@ -39,10 +43,10 @@ public static Academy academySungnam() { AcademyInfo academyInfo = AcademyFixture.academyInfos().get(1); Academy academy = Academy.of( - hashCode(academyInfo.getAcademyName()), - academyInfo, - Address.of("경기도 성남시 중원구 망포동"), - Location.of(LATITUDE, LONGITUDE)); + hashCode(academyInfo.getAcademyName()), + academyInfo, + Address.of("경기도 성남시 중원구 망포동"), + Location.of(LATITUDE, LONGITUDE)); academy.changePoint(GeometryUtil.createPoint(LATITUDE, LONGITUDE)); return academy; @@ -52,10 +56,10 @@ public static Academy twoCategoriesAcademy() { AcademyInfo academyInfo = AcademyFixture.academyInfos().get(6); Academy academy = Academy.of( - hashCode(academyInfo.getAcademyName()), - academyInfo, - Address.of("경기도 성남시 중원구 망포동"), - Location.of(LATITUDE, LONGITUDE)); + hashCode(academyInfo.getAcademyName()), + academyInfo, + Address.of("경기도 성남시 중원구 망포동"), + Location.of(LATITUDE, LONGITUDE)); academy.changePoint(GeometryUtil.createPoint(LATITUDE, LONGITUDE)); return academy; @@ -63,16 +67,16 @@ public static Academy twoCategoriesAcademy() { public static List academies() { return academyInfos().stream() - .map(academyInfo -> { - Academy academy = Academy.of( - hashCode(academyInfo.getAcademyName()), - academyInfo, - Address.of("경기도 성남시 중원구 망포동"), - Location.of(37.4449168, 127.1388684)); - academy.changePoint(GeometryUtil.createPoint(LATITUDE, LONGITUDE)); - - return academy; - }).toList(); + .map(academyInfo -> { + Academy academy = Academy.of( + hashCode(academyInfo.getAcademyName()), + academyInfo, + Address.of("경기도 성남시 중원구 망포동"), + Location.of(37.4449168, 127.1388684)); + academy.changePoint(GeometryUtil.createPoint(LATITUDE, LONGITUDE)); + + return academy; + }).toList(); } /** @@ -86,14 +90,14 @@ public static List randomAcademiesWithinDistance(double latitude, doubl for (int i = 0; i < 5; i++) { Academy academy = Academy.of( - hashCode(academyInfos().get(i).getAcademyName()), - academyInfos().get(i), - Address.of("경기도 성남시 중원구 망포동"), - randomLocations.get(i)); + hashCode(academyInfos().get(i).getAcademyName()), + academyInfos().get(i), + Address.of("경기도 성남시 중원구 망포동"), + randomLocations.get(i)); academies.add(academy); Point point = GeometryUtil.createPoint( - randomLocations.get(i).getLatitude(), - randomLocations.get(i).getLongitude() + randomLocations.get(i).getLatitude(), + randomLocations.get(i).getLongitude() ); academy.changePoint(point); } @@ -114,37 +118,41 @@ public static ReviewCount reviewCountDefault(Academy academy) { } public static AcademiesByLocationWithScrollParam academiesByLocationWithScrollParam(double latitude, - double longitude) { - return AcademiesByLocationWithScrollParam.of(latitude, longitude, 1L, 0); + double longitude) { + return AcademiesByLocationWithScrollParam.of( + Latitude.of(latitude), + Longitude.of(longitude), + 1L, + 0); } public static AcademyFilterWithScrollParam academyFilterWithScrollParam( - Double latitude, - Double longitude, - Long desiredMinAmount, - Long desiredMaxAmount) { + Double latitude, + Double longitude, + Long desiredMinAmount, + Long desiredMaxAmount) { return new AcademyFilterWithScrollParam( - latitude, - longitude, - List.of( - CategoryInfo.TUTORING_SCHOOL.getCategoryName(), - CategoryInfo.MATH.getCategoryName()), - desiredMinAmount, - desiredMaxAmount, - 0 + Latitude.of(latitude), + Longitude.of(longitude), + List.of( + CategoryInfo.TUTORING_SCHOOL.getCategoryName(), + CategoryInfo.MATH.getCategoryName()), + desiredMinAmount, + desiredMaxAmount, + 0 ); } public static List academyCategoryAboutTwoCategories(Academy academyWithTwoCategories) { return List.of( - AcademyCategory.of(academyWithTwoCategories, CategoryInfo.TUTORING_SCHOOL.getId()), - AcademyCategory.of(academyWithTwoCategories, CategoryInfo.MATH.getId()) + AcademyCategory.of(academyWithTwoCategories, CategoryInfo.TUTORING_SCHOOL.getId()), + AcademyCategory.of(academyWithTwoCategories, CategoryInfo.MATH.getId()) ); } public static List academyCategoryAboutSungnam(Academy sungnamAcademy) { return List.of( - AcademyCategory.of(sungnamAcademy, CategoryInfo.COMPUTER.getId()) + AcademyCategory.of(sungnamAcademy, CategoryInfo.COMPUTER.getId()) ); } @@ -152,6 +160,17 @@ public static AcademyDetailFacadeParam academyDetailFacadeParam(Long memberId, L return AcademyDetailFacadeParam.of(memberId, academyId); } + public static AcademyByLocationWithCursorParam academyByLocationWithCursorParam( + double latitude, + double longitude + ) { + return new AcademyByLocationWithCursorParam( + Latitude.of(latitude), + Longitude.of(longitude), + 1L, + 0L + ); + } private static Long hashCode(String academyName) { return (long) Objects.hash("수원", academyName, "경기도 성남시 중원구 망포동", "김별"); From 4a016427af1f361179685d487b7ab1221c060bee Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 17 Feb 2024 15:06:26 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat=20:=20=EC=BB=A4=EC=84=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=ED=95=99=EC=9B=90=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../academy/controller/AcademyController.java | 28 ++++++---- .../AcademyByLocationWithCursorRequest.java | 31 +++++++++++ .../AcademyByLocationWithCursorResponses.java | 52 +++++++++++++++++++ 3 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyByLocationWithCursorRequest.java create mode 100644 src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/response/AcademyByLocationWithCursorResponses.java diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/controller/AcademyController.java b/src/main/java/org/guzzing/studayserver/domain/academy/controller/AcademyController.java index 931eba341..8c21aeda0 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/controller/AcademyController.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/controller/AcademyController.java @@ -2,21 +2,15 @@ import jakarta.validation.Valid; import org.guzzing.studayserver.domain.academy.controller.dto.request.AcademiesByNameRequest; +import org.guzzing.studayserver.domain.academy.controller.dto.request.AcademyByLocationWithCursorRequest; import org.guzzing.studayserver.domain.academy.controller.dto.request.AcademyByLocationWithScrollRequest; import org.guzzing.studayserver.domain.academy.controller.dto.request.AcademyFilterWithScrollRequest; -import org.guzzing.studayserver.domain.academy.controller.dto.response.AcademiesByLocationWithScrollResponses; -import org.guzzing.studayserver.domain.academy.controller.dto.response.AcademiesByNameResponses; -import org.guzzing.studayserver.domain.academy.controller.dto.response.AcademiesFilterWithScrollResponses; -import org.guzzing.studayserver.domain.academy.controller.dto.response.AcademyGetResponse; -import org.guzzing.studayserver.domain.academy.controller.dto.response.LessonInfoToCreateDashboardResponses; +import org.guzzing.studayserver.domain.academy.controller.dto.response.*; import org.guzzing.studayserver.domain.academy.facade.AcademyFacade; import org.guzzing.studayserver.domain.academy.facade.dto.AcademyDetailFacadeParam; import org.guzzing.studayserver.domain.academy.facade.dto.AcademyDetailFacadeResult; import org.guzzing.studayserver.domain.academy.service.AcademyService; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByLocationWithScrollResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByNameResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesFilterWithScrollResults; -import org.guzzing.studayserver.domain.academy.service.dto.result.LessonInfoToCreateDashboardResults; +import org.guzzing.studayserver.domain.academy.service.dto.result.*; import org.guzzing.studayserver.global.common.member.MemberId; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -67,6 +61,20 @@ public ResponseEntity findByLocationWith .body(AcademiesByLocationWithScrollResponses.from(academiesByLocationWithScroll)); } + @GetMapping( + path = "/complexes-cursor", + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity findByLocationWithCursor( + @ModelAttribute @Valid AcademyByLocationWithCursorRequest request, + @MemberId Long memberId + ) { + AcademyByLocationWithCursorResults academiesByLocationWithCursor + = academyService.findAcademiesByLocationWithCursor(request.to(memberId)); + + return ResponseEntity.status(HttpStatus.OK) + .body(AcademyByLocationWithCursorResponses.from(academiesByLocationWithCursor)); + } + @GetMapping( path = "/search", produces = MediaType.APPLICATION_JSON_VALUE) @@ -98,7 +106,7 @@ public ResponseEntity filterAcademies( path = "/{academyId}/lessons", produces = MediaType.APPLICATION_JSON_VALUE ) - public ResponseEntity getLessonInfosToCreateDashboard( + public ResponseEntity getLessonInfoToCreateDashboard( @PathVariable Long academyId) { LessonInfoToCreateDashboardResults lessonsInfoAboutAcademy = academyService.getLessonsInfoAboutAcademy( academyId); diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyByLocationWithCursorRequest.java b/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyByLocationWithCursorRequest.java new file mode 100644 index 000000000..654ad8a19 --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/request/AcademyByLocationWithCursorRequest.java @@ -0,0 +1,31 @@ +package org.guzzing.studayserver.domain.academy.controller.dto.request; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import org.guzzing.studayserver.domain.academy.service.dto.param.AcademyByLocationWithCursorParam; +import org.guzzing.studayserver.domain.academy.util.Latitude; +import org.guzzing.studayserver.domain.academy.util.Longitude; + +public record AcademyByLocationWithCursorRequest( + @NotNull(message = "Latitude cannot be null") + @DecimalMin(value = "-90", message = "Invalid latitude") + Double lat, + + @NotNull(message = "Longitude cannot be null") + @DecimalMin(value = "-180", message = "Invalid longitude") + Double lng, + + @PositiveOrZero(message = "학원 아이디는 음수일 수 없습니다.") + Long lastAcademyId +) { + + public AcademyByLocationWithCursorParam to(Long memberId) { + return new AcademyByLocationWithCursorParam( + Latitude.of(lat), + Longitude.of(lng), + memberId, + lastAcademyId + ); + } +} diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/response/AcademyByLocationWithCursorResponses.java b/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/response/AcademyByLocationWithCursorResponses.java new file mode 100644 index 000000000..9e75fb1cd --- /dev/null +++ b/src/main/java/org/guzzing/studayserver/domain/academy/controller/dto/response/AcademyByLocationWithCursorResponses.java @@ -0,0 +1,52 @@ +package org.guzzing.studayserver.domain.academy.controller.dto.response; + +import org.guzzing.studayserver.domain.academy.service.dto.result.AcademiesByLocationWithScrollResults; +import org.guzzing.studayserver.domain.academy.service.dto.result.AcademyByLocationWithCursorResults; + +import java.util.List; + +public record AcademyByLocationWithCursorResponses( + List academiesByLocationResults, + Long lastAcademyId, + boolean hasNext +) { + public static AcademyByLocationWithCursorResponses from(AcademyByLocationWithCursorResults results) { + return new AcademyByLocationWithCursorResponses( + results.academiesByLocationResults() + .stream() + .map(AcademyByLocationWithCursorResponse::from) + .toList(), + results.lastAcademyId(), + results.hasNext() + ); + } + + public record AcademyByLocationWithCursorResponse( + Long academyId, + String academyName, + String address, + String contact, + List categories, + Double latitude, + Double longitude, + String shuttleAvailable, + boolean isLiked + ) { + + public static AcademyByLocationWithCursorResponse from( + AcademyByLocationWithCursorResults.AcademiesByLocationWithCursorResult result) { + return new AcademyByLocationWithCursorResponse( + result.academyId(), + result.academyName(), + result.address(), + result.contact(), + result.categories(), + result.latitude(), + result.longitude(), + result.shuttleAvailable(), + result.isLiked() + ); + } + } +} + From 1feaa5db4e6e7f27bcd0678eb12d6b07d8f2ac53 Mon Sep 17 00:00:00 2001 From: byeolhaha Date: Sat, 17 Feb 2024 15:07:02 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat=20:=20=EC=BB=A4=EC=84=9C=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/academy/AcademyRepository.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyRepository.java b/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyRepository.java index d13882c29..0a7de76b2 100644 --- a/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyRepository.java +++ b/src/main/java/org/guzzing/studayserver/domain/academy/repository/academy/AcademyRepository.java @@ -1,12 +1,9 @@ package org.guzzing.studayserver.domain.academy.repository.academy; import java.util.Optional; + import org.guzzing.studayserver.domain.academy.model.Academy; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByFilterWithScroll; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByLocationWithScroll; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademiesByName; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademyFee; -import org.guzzing.studayserver.domain.academy.repository.dto.AcademyFilterCondition; +import org.guzzing.studayserver.domain.academy.repository.dto.*; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -27,14 +24,17 @@ public interface AcademyRepository { Optional findAcademyById(Long academyId); AcademiesByLocationWithScroll findAcademiesByLocation( - String pointFormat, - Long memberId, - int pageNumber, - int pageSize); + String pointFormat, + Long memberId, + int pageNumber, + int pageSize); AcademiesByFilterWithScroll filterAcademies( - AcademyFilterCondition academyFilterCondition, - Long memberId, - int pageNumber, - int pageSize); + AcademyFilterCondition academyFilterCondition, + Long memberId, + int pageNumber, + int pageSize); + + AcademyByLocationWithCursorRepositoryResponse findAcademiesByLocationByCursor( + AcademyByLocationWithCursorRepositoryRequest request); }