diff --git a/build.gradle b/build.gradle index 1c94e37..b050034 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,13 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Lombok + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' } tasks.named('test') { diff --git a/src/main/java/com/srltas/runtogether/adapter/out/HaversineDistanceCalculator.java b/src/main/java/com/srltas/runtogether/adapter/out/HaversineDistanceCalculator.java new file mode 100644 index 0000000..62a5054 --- /dev/null +++ b/src/main/java/com/srltas/runtogether/adapter/out/HaversineDistanceCalculator.java @@ -0,0 +1,23 @@ +package com.srltas.runtogether.adapter.out; + +import com.srltas.runtogether.domain.model.location.DistanceCalculator; +import com.srltas.runtogether.domain.model.location.Location; + +public class HaversineDistanceCalculator implements DistanceCalculator { + + private static final double EARTH_RADIUS_KM = 6371; + + @Override + public double calculateDistanceBetween(Location location1, Location location2) { + double latitude = Math.toRadians(location1.getLatitude() - location2.getLatitude()); + double longitude = Math.toRadians(location1.getLongitude() - location2.getLongitude()); + + double a = Math.sin(latitude / 2) * Math.sin(latitude / 2) + + Math.cos(Math.toRadians(location2.getLatitude())) * Math.cos(Math.toRadians(location1.getLatitude())) + * Math.sin(longitude / 2) * Math.sin(longitude / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return EARTH_RADIUS_KM * c; + } +} diff --git a/src/main/java/com/srltas/runtogether/application/NeighborhoodVerificationService.java b/src/main/java/com/srltas/runtogether/application/NeighborhoodVerificationService.java new file mode 100644 index 0000000..68c273e --- /dev/null +++ b/src/main/java/com/srltas/runtogether/application/NeighborhoodVerificationService.java @@ -0,0 +1,30 @@ +package com.srltas.runtogether.application; + +import com.srltas.runtogether.domain.exception.NeighborhoodNotFoundException; +import com.srltas.runtogether.domain.exception.OutOfNeighborhoodBoundaryException; +import com.srltas.runtogether.domain.model.location.Location; +import com.srltas.runtogether.domain.model.neighborhood.Neighborhood; +import com.srltas.runtogether.domain.model.neighborhood.NeighborhoodRepository; +import com.srltas.runtogether.domain.model.user.User; +import com.srltas.runtogether.domain.model.user.UserRepository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class NeighborhoodVerificationService { + + private final NeighborhoodRepository neighborhoodRepository; + private final UserRepository userRepository; + + public void verifyAndRegisterNeighborhood(User user, Location currentLocation, String neighborhoodName) { + Neighborhood neighborhood = neighborhoodRepository.findByName(neighborhoodName) + .orElseThrow(() -> new NeighborhoodNotFoundException(neighborhoodName)); + + if (neighborhood.isWithinBoundary(currentLocation)) { + user.addVerifiedNeighborhood(neighborhood); + userRepository.save(user); + } else { + throw new OutOfNeighborhoodBoundaryException(neighborhoodName); + } + } +} diff --git a/src/main/java/com/srltas/runtogether/domain/exception/NeighborhoodNotFoundException.java b/src/main/java/com/srltas/runtogether/domain/exception/NeighborhoodNotFoundException.java new file mode 100644 index 0000000..65cbf91 --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/exception/NeighborhoodNotFoundException.java @@ -0,0 +1,8 @@ +package com.srltas.runtogether.domain.exception; + +public class NeighborhoodNotFoundException extends RuntimeException { + + public NeighborhoodNotFoundException(String neighborhoodName) { + super(String.format("Neighborhood not found: %s", neighborhoodName)); + } +} diff --git a/src/main/java/com/srltas/runtogether/domain/exception/OutOfNeighborhoodBoundaryException.java b/src/main/java/com/srltas/runtogether/domain/exception/OutOfNeighborhoodBoundaryException.java new file mode 100644 index 0000000..d64d4f6 --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/exception/OutOfNeighborhoodBoundaryException.java @@ -0,0 +1,8 @@ +package com.srltas.runtogether.domain.exception; + +public class OutOfNeighborhoodBoundaryException extends RuntimeException { + + public OutOfNeighborhoodBoundaryException(String neighborhoodName) { + super(String.format("User is outside of the boundary of neighborhood: %s", neighborhoodName)); + } +} diff --git a/src/main/java/com/srltas/runtogether/domain/model/location/DistanceCalculator.java b/src/main/java/com/srltas/runtogether/domain/model/location/DistanceCalculator.java new file mode 100644 index 0000000..e5ec43c --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/model/location/DistanceCalculator.java @@ -0,0 +1,5 @@ +package com.srltas.runtogether.domain.model.location; + +public interface DistanceCalculator { + double calculateDistanceBetween(Location location1, Location location2); +} diff --git a/src/main/java/com/srltas/runtogether/domain/model/location/Location.java b/src/main/java/com/srltas/runtogether/domain/model/location/Location.java new file mode 100644 index 0000000..29354c8 --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/model/location/Location.java @@ -0,0 +1,19 @@ +package com.srltas.runtogether.domain.model.location; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@EqualsAndHashCode +@RequiredArgsConstructor +public class Location { + + private final double latitude; + private final double longitude; + + public double calculateDistance(Location otherLocation, DistanceCalculator distanceCalculator) { + return distanceCalculator.calculateDistanceBetween(otherLocation, this); + } +} + diff --git a/src/main/java/com/srltas/runtogether/domain/model/neighborhood/Neighborhood.java b/src/main/java/com/srltas/runtogether/domain/model/neighborhood/Neighborhood.java new file mode 100644 index 0000000..ffead22 --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/model/neighborhood/Neighborhood.java @@ -0,0 +1,28 @@ +package com.srltas.runtogether.domain.model.neighborhood; + +import com.srltas.runtogether.domain.model.location.DistanceCalculator; +import com.srltas.runtogether.domain.model.location.Location; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@EqualsAndHashCode +@RequiredArgsConstructor +public class Neighborhood { + + @Getter + private final String name; + + @Getter + private final Location location; + + private final double boundaryRadius; + + private final DistanceCalculator distanceCalculator; + + public boolean isWithinBoundary(Location currentLocation) { + double distance = location.calculateDistance(currentLocation, distanceCalculator); + return distance <= boundaryRadius; + } +} diff --git a/src/main/java/com/srltas/runtogether/domain/model/neighborhood/NeighborhoodRepository.java b/src/main/java/com/srltas/runtogether/domain/model/neighborhood/NeighborhoodRepository.java new file mode 100644 index 0000000..c1fd52c --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/model/neighborhood/NeighborhoodRepository.java @@ -0,0 +1,7 @@ +package com.srltas.runtogether.domain.model.neighborhood; + +import java.util.Optional; + +public interface NeighborhoodRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/srltas/runtogether/domain/model/user/User.java b/src/main/java/com/srltas/runtogether/domain/model/user/User.java new file mode 100644 index 0000000..9a60a57 --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/model/user/User.java @@ -0,0 +1,20 @@ +package com.srltas.runtogether.domain.model.user; + +import java.util.ArrayList; +import java.util.List; + +import com.srltas.runtogether.domain.model.neighborhood.Neighborhood; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class User { + + private final Long id; + private final String name; + private List verifiedNeighborhoods = new ArrayList<>(); + + public void addVerifiedNeighborhood(Neighborhood neighborhood) { + this.verifiedNeighborhoods.add(new VerifiedNeighborhood(neighborhood)); + } +} diff --git a/src/main/java/com/srltas/runtogether/domain/model/user/UserRepository.java b/src/main/java/com/srltas/runtogether/domain/model/user/UserRepository.java new file mode 100644 index 0000000..7258383 --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/model/user/UserRepository.java @@ -0,0 +1,5 @@ +package com.srltas.runtogether.domain.model.user; + +public interface UserRepository { + void save(User user); +} diff --git a/src/main/java/com/srltas/runtogether/domain/model/user/VerifiedNeighborhood.java b/src/main/java/com/srltas/runtogether/domain/model/user/VerifiedNeighborhood.java new file mode 100644 index 0000000..39401ae --- /dev/null +++ b/src/main/java/com/srltas/runtogether/domain/model/user/VerifiedNeighborhood.java @@ -0,0 +1,16 @@ +package com.srltas.runtogether.domain.model.user; + +import java.time.LocalDateTime; + +import com.srltas.runtogether.domain.model.neighborhood.Neighborhood; + +public class VerifiedNeighborhood { + + private final Neighborhood neighborhood; + private final LocalDateTime verifiedAt; + + public VerifiedNeighborhood(Neighborhood neighborhood) { + this.neighborhood = neighborhood; + this.verifiedAt = LocalDateTime.now(); + } +} diff --git a/src/test/java/com/srltas/runtogether/adapter/out/HaversineDistanceCalculatorTest.java b/src/test/java/com/srltas/runtogether/adapter/out/HaversineDistanceCalculatorTest.java new file mode 100644 index 0000000..e1abfe1 --- /dev/null +++ b/src/test/java/com/srltas/runtogether/adapter/out/HaversineDistanceCalculatorTest.java @@ -0,0 +1,31 @@ +package com.srltas.runtogether.adapter.out; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.srltas.runtogether.domain.model.location.Location; + +class HaversineDistanceCalculatorTest { + + private final HaversineDistanceCalculator haversineDistanceCalculator = new HaversineDistanceCalculator(); + + private final Location seoul = new Location(37.566535, 126.977969); + private final Location busan = new Location(35.179554, 129.075641); + + @Test + public void testCalculateDistanceBetween_TwoLocations() { + double seoulBusanDistance = 325.0; + + double distance = haversineDistanceCalculator.calculateDistanceBetween(seoul, busan); + + assertEquals(seoulBusanDistance, distance, 1.0); + } + + @Test + public void testCalculateDistanceBetween_SameLocation() { + double distance = haversineDistanceCalculator.calculateDistanceBetween(seoul, seoul); + + assertEquals(0.0, distance); + } +} \ No newline at end of file diff --git a/src/test/java/com/srltas/runtogether/application/NeighborhoodVerificationServiceTest.java b/src/test/java/com/srltas/runtogether/application/NeighborhoodVerificationServiceTest.java new file mode 100644 index 0000000..a5ffc55 --- /dev/null +++ b/src/test/java/com/srltas/runtogether/application/NeighborhoodVerificationServiceTest.java @@ -0,0 +1,98 @@ +package com.srltas.runtogether.application; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import com.srltas.runtogether.domain.exception.NeighborhoodNotFoundException; +import com.srltas.runtogether.domain.exception.OutOfNeighborhoodBoundaryException; +import com.srltas.runtogether.domain.model.location.DistanceCalculator; +import com.srltas.runtogether.domain.model.location.Location; +import com.srltas.runtogether.domain.model.neighborhood.Neighborhood; +import com.srltas.runtogether.domain.model.neighborhood.NeighborhoodRepository; +import com.srltas.runtogether.domain.model.user.User; +import com.srltas.runtogether.domain.model.user.UserRepository; + +class NeighborhoodVerificationServiceTest { + + @Mock + private NeighborhoodRepository neighborhoodRepository; + + @Mock + private DistanceCalculator distanceCalculator; + + @Mock + private UserRepository userRepository; + + @InjectMocks + private NeighborhoodVerificationService neighborhoodVerificationService; + + private User user = new User(1L, "testUser"); + private String neighborhoodName = "Gangnam"; + private Location currentLocation = new Location(37.505858, 127.058319); + private Neighborhood neighborhood; + + @BeforeEach + public void setUp() { + openMocks(this); + neighborhood = new Neighborhood(neighborhoodName, new Location(37.517347, 127.047382), 7.0, distanceCalculator); + } + + @Nested + class WhenNeighborhoodIsFound { + + @BeforeEach + public void setUp() { + when(neighborhoodRepository.findByName(neighborhoodName)).thenReturn(Optional.of(neighborhood)); + } + + @Test + public void testVerifyAndRegisterNeighborhood_WithinBoundary() { + when(distanceCalculator.calculateDistanceBetween(currentLocation, neighborhood.getLocation())) + .thenReturn(5.0); + + neighborhoodVerificationService.verifyAndRegisterNeighborhood(user, currentLocation, neighborhoodName); + + verify(userRepository).save(user); + } + + @Test + public void testVerifyAndRegisterNeighborhood_OutsideBoundary() { + when(distanceCalculator.calculateDistanceBetween(currentLocation, neighborhood.getLocation())) + .thenReturn(15.0); + + OutOfNeighborhoodBoundaryException exception = assertThrows(OutOfNeighborhoodBoundaryException.class, + () -> { + neighborhoodVerificationService.verifyAndRegisterNeighborhood(user, currentLocation, neighborhoodName); + }); + + assertEquals(format("User is outside of the boundary of neighborhood: %s", neighborhoodName), exception.getMessage()); + verify(userRepository, never()).save(user); + } + } + + @Nested + class WhenNeighborhoodIsNotFound { + @Test + public void testVerifyAndRegisterNeighborhood_NeighborhoodNotFound() { + when(neighborhoodRepository.findByName("Homaesil")).thenReturn(Optional.empty()); + + NeighborhoodNotFoundException exception = assertThrows(NeighborhoodNotFoundException.class, + () -> { + neighborhoodVerificationService.verifyAndRegisterNeighborhood(user, currentLocation, neighborhoodName); + }); + + assertEquals(format("Neighborhood not found: %s", neighborhoodName), exception.getMessage()); + verify(userRepository, never()).save(user); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/srltas/runtogether/domain/model/neighborhood/NeighborhoodTest.java b/src/test/java/com/srltas/runtogether/domain/model/neighborhood/NeighborhoodTest.java new file mode 100644 index 0000000..7675c27 --- /dev/null +++ b/src/test/java/com/srltas/runtogether/domain/model/neighborhood/NeighborhoodTest.java @@ -0,0 +1,51 @@ +package com.srltas.runtogether.domain.model.neighborhood; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import com.srltas.runtogether.domain.model.location.DistanceCalculator; +import com.srltas.runtogether.domain.model.location.Location; + +class NeighborhoodTest { + + @Mock + private DistanceCalculator distanceCalculator; + + private Location currentLocation; + private Location neighborhoodLocation; + private Neighborhood neighborhood; + + @BeforeEach + public void setUp() { + openMocks(this); + + currentLocation = new Location(37.505858, 127.058319); + neighborhoodLocation = new Location(37.517347, 127.047382); + neighborhood = new Neighborhood("Gangnam", neighborhoodLocation, 7.0, distanceCalculator); + } + + @Test + public void testIsWithinBoundary_WhenUserInSideBoundary() { + when(distanceCalculator.calculateDistanceBetween(currentLocation, neighborhoodLocation)).thenReturn(5.0); + + boolean isInSide = neighborhood.isWithinBoundary(currentLocation); + + assertTrue(isInSide); + verify(distanceCalculator).calculateDistanceBetween(currentLocation, neighborhoodLocation); + } + + @Test + public void testIsWithinBoundary_WhenUserOutsideBoundary() { + when(distanceCalculator.calculateDistanceBetween(currentLocation, neighborhoodLocation)).thenReturn(10.0); + + boolean isInSide = neighborhood.isWithinBoundary(currentLocation); + + assertFalse(isInSide); + verify(distanceCalculator).calculateDistanceBetween(currentLocation, neighborhoodLocation); + } +} \ No newline at end of file