diff --git a/build.gradle b/build.gradle index d60d66f..f0aa325 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,8 @@ dependencies { // S2 implementation 'com.github.google:s2-geometry-library-java:2.0.0' + implementation 'org.hibernate.orm:hibernate-spatial' + implementation 'org.locationtech.jts:jts-core:1.19.0' // QueryDSL implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" diff --git a/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java b/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java index 45cc6d8..426021b 100644 --- a/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java +++ b/src/main/java/com/knu/ddip/ddipevent/application/service/DdipService.java @@ -3,6 +3,7 @@ import com.knu.ddip.ddipevent.application.dto.*; import com.knu.ddip.ddipevent.domain.DdipEvent; import com.knu.ddip.ddipevent.exception.DdipNotFoundException; +import com.knu.ddip.ddipevent.application.util.DistanceConverter; import com.knu.ddip.user.business.dto.UserEntityDto; import com.knu.ddip.user.business.service.UserRepository; import lombok.RequiredArgsConstructor; @@ -19,6 +20,7 @@ public class DdipService { private final DdipEventRepository ddipEventRepository; private final UserRepository userRepository; + private final DistanceConverter distanceConverter; @Transactional public DdipEventDetailDto createDdipEvent(CreateDdipRequestDto dto, UUID requesterId) { @@ -35,7 +37,7 @@ public List getDdipEventFeed(FeedRequestDto dto) { dto.sw_lat(), dto.sw_lon(), dto.ne_lat(), dto.ne_lon(), dto.sort(), dto.user_lat(), dto.user_lon()); return events.stream() - .map(this::convertToSummaryDto) + .map(event -> convertToSummaryDto(event, dto.user_lat(), dto.user_lon())) .toList(); } @@ -45,8 +47,8 @@ public DdipEventDetailDto getDdipEventDetail(UUID ddipId) { return convertToDetailDto(event); } - private DdipEventSummaryDto convertToSummaryDto(DdipEvent event) { - // TODO: distance(요청자와 사용자 사이의 거리) 계산 로직 추가 + private DdipEventSummaryDto convertToSummaryDto(DdipEvent event, Double userLat, Double userLon) { + double dist = distanceConverter.haversineMeters(event.getLatitude(), event.getLongitude(), userLat, userLon); return new DdipEventSummaryDto( event.getId().toString(), event.getTitle(), @@ -58,7 +60,7 @@ private DdipEventSummaryDto convertToSummaryDto(DdipEvent event) { event.getCreatedAt().toString(), event.getApplicants().size(), event.getContent(), - 0.0, // 임시값으로 0.0 + dist, event.getDifficulty() ); } diff --git a/src/main/java/com/knu/ddip/ddipevent/application/util/DistanceConverter.java b/src/main/java/com/knu/ddip/ddipevent/application/util/DistanceConverter.java new file mode 100644 index 0000000..97d7fe8 --- /dev/null +++ b/src/main/java/com/knu/ddip/ddipevent/application/util/DistanceConverter.java @@ -0,0 +1,22 @@ +package com.knu.ddip.ddipevent.application.util; + +import org.springframework.stereotype.Component; + +@Component +public class DistanceConverter { + + private final double EARTH_RADIUS_M = 6_371_000.0; + + public double haversineMeters(double lat1, double lon1, double lat2, double lon2) { + double phi1 = Math.toRadians(lat1); + double phi2 = Math.toRadians(lat2); + double dPhi = Math.toRadians(lat2 - lat1); + double dLambda = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dPhi / 2) * Math.sin(dPhi / 2) + + Math.cos(phi1) * Math.cos(phi2) + * Math.sin(dLambda / 2) * Math.sin(dLambda / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS_M * c; + } +} diff --git a/src/main/java/com/knu/ddip/ddipevent/domain/DdipEvent.java b/src/main/java/com/knu/ddip/ddipevent/domain/DdipEvent.java index 8f5cd40..b0e74d7 100644 --- a/src/main/java/com/knu/ddip/ddipevent/domain/DdipEvent.java +++ b/src/main/java/com/knu/ddip/ddipevent/domain/DdipEvent.java @@ -17,7 +17,6 @@ public class DdipEvent { private final UUID requesterId; private final Double latitude; private final Double longitude; - private final String cellId; private final Instant createdAt; private String title; private String content; diff --git a/src/main/java/com/knu/ddip/ddipevent/infrastructure/DdipMapper.java b/src/main/java/com/knu/ddip/ddipevent/infrastructure/DdipMapper.java index a440958..dbffa6d 100644 --- a/src/main/java/com/knu/ddip/ddipevent/infrastructure/DdipMapper.java +++ b/src/main/java/com/knu/ddip/ddipevent/infrastructure/DdipMapper.java @@ -6,23 +6,24 @@ import com.knu.ddip.ddipevent.infrastructure.entity.DdipEventEntity; import com.knu.ddip.ddipevent.infrastructure.entity.InteractionEntity; import com.knu.ddip.ddipevent.infrastructure.entity.PhotoEntity; -import com.knu.ddip.location.application.util.S2Converter; -import lombok.RequiredArgsConstructor; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.PrecisionModel; import org.springframework.stereotype.Component; import java.util.List; @Component -@RequiredArgsConstructor public class DdipMapper { - private final S2Converter s2Converter; + public static final int SRID = 4326; + private GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), SRID); public DdipEventEntity toEntity(DdipEvent domain) { DdipEventEntity entity = buildDdipEventEntity(domain); entity.setPhotos(mapPhotos(domain.getPhotos(), entity)); entity.setInteractions(mapInteractions(domain.getInteractions(), entity)); - entity.setCellId(s2Converter.toCellIdString(domain.getLatitude(),domain.getLongitude())); + entity.setLocalPoint(geometryFactory.createPoint(new Coordinate(domain.getLongitude(), domain.getLatitude()))); return entity; } @@ -87,7 +88,6 @@ public DdipEvent toDomain(DdipEventEntity entity) { .reward(entity.getReward()) .latitude(entity.getLatitude()) .longitude(entity.getLongitude()) - .cellId(entity.getCellId()) .createdAt(entity.getCreatedAt()) .status(entity.getStatus()) .selectedResponderId(entity.getSelectedResponderId()) diff --git a/src/main/java/com/knu/ddip/ddipevent/infrastructure/entity/DdipEventEntity.java b/src/main/java/com/knu/ddip/ddipevent/infrastructure/entity/DdipEventEntity.java index b47e5d8..bb63f0b 100644 --- a/src/main/java/com/knu/ddip/ddipevent/infrastructure/entity/DdipEventEntity.java +++ b/src/main/java/com/knu/ddip/ddipevent/infrastructure/entity/DdipEventEntity.java @@ -6,6 +6,7 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.annotations.UuidGenerator; import org.hibernate.type.SqlTypes; +import org.locationtech.jts.geom.Point; import java.time.Instant; import java.util.List; @@ -44,9 +45,9 @@ public class DdipEventEntity { @Column(nullable = false) private Double longitude; - @Column(nullable = false) + @Column(name = "local_point", columnDefinition = "POINT SRID 4326", nullable = false) @Setter - private String cellId; + private Point localPoint; @Column(nullable = false) private Instant createdAt; diff --git a/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventJpaRepository.java b/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventJpaRepository.java index 2a2e751..9dff0ba 100644 --- a/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventJpaRepository.java +++ b/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventJpaRepository.java @@ -2,10 +2,17 @@ import com.knu.ddip.ddipevent.infrastructure.entity.DdipEventEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.UUID; public interface DdipEventJpaRepository extends JpaRepository { - List findAllByCellIdIn(List cellIds); + @Query(value = """ + SELECT * FROM ddip_event + WHERE ST_CONTAINS(ST_Buffer(ST_SRID(POINT(:lng, :lat), 4326), :dist), local_point) + ORDER BY ST_Distance_Sphere(ST_SRID(POINT(:lng, :lat), 4326), local_point) + """, nativeQuery = true) + List findAllByDistance(@Param("lng") Double lng, @Param("lat") Double lat, @Param("dist") Double dist); } diff --git a/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImpl.java b/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImpl.java index f4ad711..7958c1c 100644 --- a/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImpl.java +++ b/src/main/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImpl.java @@ -2,15 +2,13 @@ import com.knu.ddip.ddipevent.application.service.DdipEventRepository; import com.knu.ddip.ddipevent.domain.DdipEvent; -import com.knu.ddip.ddipevent.domain.DdipStatus; import com.knu.ddip.ddipevent.infrastructure.DdipMapper; import com.knu.ddip.ddipevent.infrastructure.entity.DdipEventEntity; -import com.knu.ddip.location.application.service.LocationService; +import com.knu.ddip.ddipevent.application.util.DistanceConverter; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -23,8 +21,7 @@ public class DdipEventRepositoryImpl implements DdipEventRepository { private final DdipEventJpaRepository ddipEventJpaRepository; private final DdipMapper ddipMapper; - - private final LocationService locationService; + private final DistanceConverter distanceConverter; @Transactional @Override @@ -41,21 +38,33 @@ public Optional findById(UUID id) { @Override public List findWithinBounds(double swLat, double swLon, double neLat, double neLon, String sort, Double userLat, Double userLon) { - List cellIds = locationService.getNeighborCellIdsToRetrieveNearDdipRequest(swLat, swLon, neLat, neLon); + double dist = boundingBoxRadiusMeters(swLat, swLon, neLat, neLon, userLat, userLon); + + return ddipEventJpaRepository.findAllByDistance(userLon, userLat, dist).stream() + .map(ddipMapper::toDomain) + .toList(); + } - List ddipEventEntities = ddipEventJpaRepository.findAllByCellIdIn(cellIds); + private double boundingBoxRadiusMeters(double swLat, double swLon, double neLat, double neLon, Double userLat, Double userLon) { + double[][] locations = new double[][]{ + {swLat, swLon}, + {neLat, neLon}, - Comparator comparator = (o1, o2) -> { - double dist1 = Math.pow(userLat - o1.getLatitude(), 2) + Math.pow(userLon - o1.getLongitude(), 2); - double dist2 = Math.pow(userLat - o2.getLatitude(), 2) + Math.pow(userLon - o2.getLongitude(), 2); - return dist1 - dist2 >= 0 ? 1 : -1; + {neLat, swLon}, + {swLat, neLon}, + + {neLat, userLon}, + {swLat, userLon}, + + {userLat, neLon}, + {userLat, swLon}, }; - // 유저와 이벤트 거리 비교해서 거리 가까운 순 정렬 - return ddipEventEntities.stream() - .filter(event -> event.getStatus().equals(DdipStatus.OPEN)) - .sorted(comparator) - .map(ddipMapper::toDomain) - .toList(); + double maxDist = -1; + for (double[] location : locations) { + double dist = distanceConverter.haversineMeters(userLat, userLon, location[0], location[1]); + maxDist = Math.max(maxDist, dist); + } + return maxDist; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d379d61..a6ad101 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -13,11 +13,11 @@ spring.data.redis.port=${REDIS_PORT} spring.data.redis.password=${REDIS_PASSWORD} # JPA -#spring.jpa.properties.hibernate.format_sql=true -#spring.jpa.show-sql=true -spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=create spring.sql.init.mode=always spring.jpa.defer-datasource-initialization=true spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect -#spring.jpa.properties.hibernate.default_batch_fetch_size=1000 # TODO: ????. ?? ???? +spring.jpa.properties.hibernate.default_batch_fetch_size=1000 \ No newline at end of file diff --git a/src/main/resources/sql/spatial_index.sql b/src/main/resources/sql/spatial_index.sql new file mode 100644 index 0000000..6d90bde --- /dev/null +++ b/src/main/resources/sql/spatial_index.sql @@ -0,0 +1 @@ +CREATE SPATIAL INDEX idx_ddip_event_local_point ON ddip_event (local_point); \ No newline at end of file diff --git a/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java b/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java index f410ce3..256eab4 100644 --- a/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/application/service/DdipServiceTest.java @@ -7,6 +7,7 @@ import com.knu.ddip.ddipevent.domain.DdipEvent; import com.knu.ddip.ddipevent.domain.DdipStatus; import com.knu.ddip.ddipevent.exception.DdipNotFoundException; +import com.knu.ddip.ddipevent.application.util.DistanceConverter; import com.knu.ddip.user.business.dto.UserEntityDto; import com.knu.ddip.user.business.service.UserRepository; import org.junit.jupiter.api.DisplayName; @@ -40,6 +41,9 @@ class DdipServiceTest { @Mock private UserRepository userRepository; + @Mock + private DistanceConverter distanceConverter; + @DisplayName("띱 이벤트 생성 성공") @Test void givenCreateDdipRequest_whenCreateDdipEvent_thenDdipEventDetailDtoIsReturned() { @@ -86,6 +90,8 @@ void givenFeedRequest_whenGetDdipEventFeed_thenListOfDdipEventSummaryDtoIsReturn .requesterId(UUID.randomUUID()) .createdAt(Instant.now()) .applicants(new ArrayList<>()) + .latitude(0.0) + .longitude(0.0) .build(); List events = List.of(ddipEvent); diff --git a/src/test/java/com/knu/ddip/ddipevent/fixture/DdipEventFixture.java b/src/test/java/com/knu/ddip/ddipevent/fixture/DdipEventFixture.java index 13581e5..f97f4c0 100644 --- a/src/test/java/com/knu/ddip/ddipevent/fixture/DdipEventFixture.java +++ b/src/test/java/com/knu/ddip/ddipevent/fixture/DdipEventFixture.java @@ -24,7 +24,6 @@ public static DdipEventEntity createDdipEvent(Double lat, Double lon, DdipStatus .reward(1) .status(status) .title("title") - .cellId(cellId) .build(); return event; } diff --git a/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java b/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java index 2c31d27..7bdb7c2 100644 --- a/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/infrastructure/DdipMapperTest.java @@ -22,8 +22,7 @@ class DdipMapperTest { @BeforeEach void setUp() { - S2Converter s2Converter = new S2Converter(); - ddipMapper = new DdipMapper(s2Converter); + ddipMapper = new DdipMapper(); } @DisplayName("도메인을 엔티티로 변환 - 모든 필드 포함") diff --git a/src/test/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventJpaRepositoryTest.java b/src/test/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventJpaRepositoryTest.java deleted file mode 100644 index d2deeba..0000000 --- a/src/test/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventJpaRepositoryTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.knu.ddip.ddipevent.infrastructure.repository; - -import com.knu.ddip.config.IntegrationTestConfig; -import com.knu.ddip.config.MySQLTestContainerConfig; -import com.knu.ddip.config.RedisTestContainerConfig; -import com.knu.ddip.config.TestEnvironmentConfig; -import com.knu.ddip.ddipevent.infrastructure.entity.DdipEventEntity; -import com.knu.ddip.user.infrastructure.repository.UserRepositoryImpl; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; - -import java.util.List; - -import static com.knu.ddip.ddipevent.fixture.DdipEventFixture.createDdipEvent; -import static org.assertj.core.api.Assertions.assertThat; - -@DataJpaTest -@ExtendWith({RedisTestContainerConfig.class, MySQLTestContainerConfig.class, TestEnvironmentConfig.class}) -@Import({IntegrationTestConfig.class, UserRepositoryImpl.class}) -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -class DdipEventJpaRepositoryTest { - - @Autowired - DdipEventJpaRepository ddipEventJpaRepository; - - @Test - void findAllByCellIdInTest() { - // given - int targetEventsNumber = 3; - - List targetCellIds = List.of("TargetCellId0", "TargetCellId1", "TargetCellId2"); - String notTargetCellId = "NotTargetCellId"; - - for (int i = 0; i < targetEventsNumber; i++) { - DdipEventEntity event = createDdipEvent(); - event.setCellId(targetCellIds.get(i)); - ddipEventJpaRepository.save(event); - } - - DdipEventEntity notTargetEvent = createDdipEvent(); - notTargetEvent.setCellId(notTargetCellId); - ddipEventJpaRepository.save(notTargetEvent); - - // when - List events = ddipEventJpaRepository.findAllByCellIdIn(targetCellIds); - - // then - assertThat(events).hasSize(targetEventsNumber); - } - -} \ No newline at end of file diff --git a/src/test/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImplIntegrationTest.java b/src/test/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImplIntegrationTest.java index 2fa2705..dee8f04 100644 --- a/src/test/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImplIntegrationTest.java +++ b/src/test/java/com/knu/ddip/ddipevent/infrastructure/repository/DdipEventRepositoryImplIntegrationTest.java @@ -5,12 +5,8 @@ import com.knu.ddip.config.RedisTestContainerConfig; import com.knu.ddip.config.TestEnvironmentConfig; import com.knu.ddip.ddipevent.domain.DdipEvent; -import com.knu.ddip.ddipevent.domain.DdipStatus; -import com.knu.ddip.ddipevent.fixture.DdipEventFixture; -import com.knu.ddip.ddipevent.infrastructure.entity.DdipEventEntity; import com.knu.ddip.location.application.util.S2Converter; import com.knu.ddip.user.infrastructure.repository.UserRepositoryImpl; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -40,15 +37,17 @@ class DdipEventRepositoryImplIntegrationTest { @Test void findWithinBoundsTest() { // given - List ddipEvents = List.of( - DdipEventFixture.createDdipEvent(35.8880523, 128.6058911, DdipStatus.OPEN, "대운동장", s2Converter.toCellIdString(35.8880523, 128.6058911)), // 대운동장 - DdipEventFixture.createDdipEvent(35.8868876, 128.6082622, DdipStatus.OPEN, "공대9호관", s2Converter.toCellIdString(35.8868876, 128.6082622)), // 공대9호관 - DdipEventFixture.createDdipEvent(35.8880089, 128.6114594, DdipStatus.OPEN, "융복합관", s2Converter.toCellIdString(35.8880089, 128.6114594)) // 융복합관 + List ddipEvents = List.of( + DdipEvent.create("대운동장", "대운동장", 1, 35.8880523, 128.6058911, 1, UUID.randomUUID()), + DdipEvent.create("공대9호관", "공대9호관", 1, 35.8868876, 128.6082622, 1, UUID.randomUUID()), + DdipEvent.create("융복합관", "융복합관", 1, 35.8880089, 128.6114594, 1, UUID.randomUUID()) ); - ddipEventJpaRepository.saveAll(ddipEvents); + for (DdipEvent ddipEvent : ddipEvents) { + ddipEventRepositoryImpl.save(ddipEvent); + } // when - List sortDdipEvents = ddipEventRepositoryImpl.findWithinBounds(35.8853838, 128.6058911, 35.8955185, 128.6140665, "sort", 35.8886499, 128.6121487);// 일청담 + List sortDdipEvents = ddipEventRepositoryImpl.findWithinBounds(35.8853838, 128.6058911, 35.8955185, 128.6140665, "sort", 35.8886499, 128.6121487); // 일청담 // then assertThat(sortDdipEvents).hasSize(3) @@ -56,23 +55,4 @@ void findWithinBoundsTest() { .containsExactly("융복합관", "공대9호관", "대운동장"); // 거리 가까운 순 정렬 } - @Test - void findWithinBoundsWithOnlyOpenEventsTest() { - // given - List ddipEvents = List.of( - DdipEventFixture.createDdipEvent(35.8880523, 128.6058911, DdipStatus.OPEN, "대운동장", s2Converter.toCellIdString(35.8880523, 128.6058911)), // 대운동장 - DdipEventFixture.createDdipEvent(35.8868876, 128.6082622, DdipStatus.COMPLETED, "공대9호관", s2Converter.toCellIdString(35.8868876, 128.6082622)), // 공대9호관 - DdipEventFixture.createDdipEvent(35.8880089, 128.6114594, DdipStatus.COMPLETED, "융복합관", s2Converter.toCellIdString(35.8880089, 128.6114594)) // 융복합관 - ); - ddipEventJpaRepository.saveAll(ddipEvents); - - // when - List sortDdipEvents = ddipEventRepositoryImpl.findWithinBounds(35.8853838, 128.6058911, 35.8955185, 128.6140665, "sort", 35.8886499, 128.6121487); // 일청담 - - // then - assertThat(sortDdipEvents).hasSize(1) - .extracting(DdipEvent::getContent) - .containsExactly("대운동장"); - } - } \ No newline at end of file