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/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/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() + ); + } + } +} + 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/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); } 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() + ); + } + } + +} 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/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/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/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/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 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); + } + +} 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() + ); + } } 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/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 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, "경기도 성남시 중원구 망포동", "김별");