diff --git a/build.gradle b/build.gradle index a92a83e..7ab5311 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ configurations { } repositories { + maven { url "https://repo.osgeo.org/repository/release/"} mavenCentral() } @@ -30,6 +31,10 @@ dependencies { implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.12' implementation 'net.minidev:json-smart:2.4.9' + implementation 'com.google.maps:google-maps-services:0.2.7' + implementation 'org.apache.httpcomponents:httpclient:4.5.14' + implementation 'org.geotools:gt-main:28.5' + compileOnly 'org.projectlombok:lombok:1.18.30' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok:1.18.30' @@ -41,10 +46,8 @@ dependencies { runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' - implementation "com.querydsl:querydsl-jpa:5.0.0" annotationProcessor "com.querydsl:querydsl-apt:5.0.0" - } tasks.named('test') { diff --git a/src/main/java/com/gdscys/cokepoke/api/geocoding/GeocodingAPIService.java b/src/main/java/com/gdscys/cokepoke/api/geocoding/GeocodingAPIService.java new file mode 100644 index 0000000..1f545e4 --- /dev/null +++ b/src/main/java/com/gdscys/cokepoke/api/geocoding/GeocodingAPIService.java @@ -0,0 +1,70 @@ +package com.gdscys.cokepoke.api.geocoding; + +import org.geotools.referencing.GeodeticCalculator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClients; +import org.locationtech.jts.geom.Coordinate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Service +public class GeocodingAPIService { + @Value("${gcp.geocoding.api-key}") + private String apiKey; + + private final String GEOCODING_API_URL = "https://maps.googleapis.com/maps/api/geocode/json"; + + public double[] getCoordinates(String address) { + try { + HttpClient httpClient = HttpClients.createDefault(); + String encodedAddress = URLEncoder.encode(address, StandardCharsets.UTF_8.toString()); + HttpGet getRequest = new HttpGet(GEOCODING_API_URL + "?address=" + encodedAddress + "&key=" + apiKey); + + HttpResponse response = httpClient.execute(getRequest); + + if (response.getStatusLine().getStatusCode() == 200) { + BufferedReader reader = new BufferedReader(new InputStreamReader( + response.getEntity().getContent())); + + StringBuilder responseStringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + responseStringBuilder.append(line); + } + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(responseStringBuilder.toString()); + + double latitude = jsonNode.path("results").path(0).path("geometry").path("location").path("lat").asDouble(); + double longitude = jsonNode.path("results").path(0).path("geometry").path("location").path("lng").asDouble(); + + return new double[]{latitude, longitude}; + } + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + public double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + GeodeticCalculator calculator = new GeodeticCalculator(); + Coordinate coord1 = new Coordinate(lon1, lat1); + Coordinate coord2 = new Coordinate(lon2, lat2); + + calculator.setStartingGeographicPoint(coord1.x, coord1.y); + calculator.setDestinationGeographicPoint(coord2.x, coord2.y); + + return calculator.getOrthodromicDistance() / 1000; // Convert to kilometers + } + +} diff --git a/src/main/java/com/gdscys/cokepoke/auth/controller/AuthController.java b/src/main/java/com/gdscys/cokepoke/auth/controller/AuthController.java index 601599e..83ac291 100644 --- a/src/main/java/com/gdscys/cokepoke/auth/controller/AuthController.java +++ b/src/main/java/com/gdscys/cokepoke/auth/controller/AuthController.java @@ -27,7 +27,7 @@ public class AuthController { @PostMapping("/signup") public ResponseEntity signup(@RequestBody @Valid SignupRequest request) { - Member member = memberService.saveMember(request.getEmail(), request.getUsername(), request.getPassword()); + Member member = memberService.saveMember(request.getEmail(), request.getUsername(), request.getPassword(), request.getTimezone(), request.getAddress()); return ResponseEntity.status(HttpStatus.CREATED) .body(MemberResponse.of(member)); } diff --git a/src/main/java/com/gdscys/cokepoke/friendship/controller/FriendshipController.java b/src/main/java/com/gdscys/cokepoke/friendship/controller/FriendshipController.java index c9b373c..3d18b7d 100644 --- a/src/main/java/com/gdscys/cokepoke/friendship/controller/FriendshipController.java +++ b/src/main/java/com/gdscys/cokepoke/friendship/controller/FriendshipController.java @@ -3,11 +3,9 @@ import com.gdscys.cokepoke.friendship.dto.FriendshipRequest; import com.gdscys.cokepoke.friendship.dto.FriendshipResponse; import com.gdscys.cokepoke.friendship.service.FriendshipService; -import com.gdscys.cokepoke.member.domain.Member; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -29,7 +27,7 @@ public class FriendshipController { @PostMapping("/create") public ResponseEntity createFriendship(@RequestBody @Valid FriendshipRequest request) { String username = getLoginUsername(); - friendshipService.createFriendship(username, request.getToUsername()); + friendshipService.createFriendship(username, request.getToUsername(), request.getMyAddress()); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/com/gdscys/cokepoke/friendship/dto/FriendshipRequest.java b/src/main/java/com/gdscys/cokepoke/friendship/dto/FriendshipRequest.java index 7873205..5fab09c 100644 --- a/src/main/java/com/gdscys/cokepoke/friendship/dto/FriendshipRequest.java +++ b/src/main/java/com/gdscys/cokepoke/friendship/dto/FriendshipRequest.java @@ -11,5 +11,8 @@ public class FriendshipRequest { @NotBlank private String toUsername; + @NotBlank + private String myAddress; + protected FriendshipRequest() {} } diff --git a/src/main/java/com/gdscys/cokepoke/friendship/service/FriendshipService.java b/src/main/java/com/gdscys/cokepoke/friendship/service/FriendshipService.java index 87f8409..e37b1fc 100644 --- a/src/main/java/com/gdscys/cokepoke/friendship/service/FriendshipService.java +++ b/src/main/java/com/gdscys/cokepoke/friendship/service/FriendshipService.java @@ -1,5 +1,6 @@ package com.gdscys.cokepoke.friendship.service; +import com.gdscys.cokepoke.api.geocoding.GeocodingAPIService; import com.gdscys.cokepoke.friendship.domain.Friendship; import com.gdscys.cokepoke.friendship.repository.FriendshipRepository; import com.gdscys.cokepoke.member.domain.Member; @@ -19,14 +20,22 @@ public class FriendshipService implements IFriendshipService { private final FriendshipRepository friendshipRepository; private final MemberService memberService; + private final GeocodingAPIService geocodingAPIService; private static final int PAGE_SIZE = 15; @Override @Transactional - public void createFriendship(String username, String recipientUsername) { + public void createFriendship(String username, String recipientUsername, String requestAddress) { Member member = memberService.getMemberByUsername(username); + member.updateAddress(requestAddress); Member to = memberService.getMemberByUsername(recipientUsername); + + double[] requestingLocation = geocodingAPIService.getCoordinates(requestAddress); + double[] receivingLocation = geocodingAPIService.getCoordinates(to.getAddress()); + double distanceInKm = geocodingAPIService.calculateDistance(requestingLocation[0], requestingLocation[1], receivingLocation[0], receivingLocation[1]); + if (distanceInKm > 1.0) throw new IllegalArgumentException("You are too far away from " + to.getUsername()); + if (member.equals(to)) throw new IllegalArgumentException("You cannot be friends with yourself"); if (friendshipRepository.findByFromAndTo(member, to).isPresent()) { throw new IllegalArgumentException("You already sent a friend request"); diff --git a/src/main/java/com/gdscys/cokepoke/friendship/service/IFriendshipService.java b/src/main/java/com/gdscys/cokepoke/friendship/service/IFriendshipService.java index 86da5c5..ffc278c 100644 --- a/src/main/java/com/gdscys/cokepoke/friendship/service/IFriendshipService.java +++ b/src/main/java/com/gdscys/cokepoke/friendship/service/IFriendshipService.java @@ -1,13 +1,12 @@ package com.gdscys.cokepoke.friendship.service; import com.gdscys.cokepoke.friendship.domain.Friendship; -import com.gdscys.cokepoke.member.domain.Member; import java.util.List; public interface IFriendshipService { - void createFriendship(String username, String recipientUsername); + void createFriendship(String username, String recipientUsername, String requestAddress); Friendship getFriendshipById(Long friendshipId); Friendship getFriendshipByMembers(String username, String username2); diff --git a/src/main/java/com/gdscys/cokepoke/member/domain/Member.java b/src/main/java/com/gdscys/cokepoke/member/domain/Member.java index 8c8d009..574faf4 100644 --- a/src/main/java/com/gdscys/cokepoke/member/domain/Member.java +++ b/src/main/java/com/gdscys/cokepoke/member/domain/Member.java @@ -57,6 +57,9 @@ public class Member implements UserDetails { @Column(name = "timezone", nullable = false) private ZoneId timezone; + @Column(name = "address", nullable = false) + private String address; + @OneToOne(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private RefreshToken refreshToken; @@ -66,20 +69,22 @@ public class Member implements UserDetails { protected Member() {} - public Member(String email, String username, String passwordHash, Set roles) { + public Member(String email, String username, String passwordHash) { this.email = email; this.username = username; this.passwordHash = passwordHash; - this.roles = roles; + this.roles = new HashSet<>(); this.timezone = ZoneId.of("Asia/Seoul"); + this.address = "1 Gwanghwamun Square, Jongno-gu, Seoul, South Korea"; } - public Member(String email, String username, String passwordHash, Set roles, ZoneId timezone) { + public Member(String email, String username, String passwordHash, Set roles, ZoneId timezone, String address) { this.email = email; this.username = username; this.passwordHash = passwordHash; this.roles = roles; this.timezone = timezone; + this.address = address; } public void addRequested(Friendship friendship) { @@ -94,6 +99,9 @@ public void updatePassword(String passwordHash) { this.passwordHash = passwordHash; } + public void updateAddress(String address) { + this.address = address; + } @Override public Collection getAuthorities() { return this.roles.stream() diff --git a/src/main/java/com/gdscys/cokepoke/member/dto/SignupRequest.java b/src/main/java/com/gdscys/cokepoke/member/dto/SignupRequest.java index 5093db7..9bb1daa 100644 --- a/src/main/java/com/gdscys/cokepoke/member/dto/SignupRequest.java +++ b/src/main/java/com/gdscys/cokepoke/member/dto/SignupRequest.java @@ -26,13 +26,21 @@ public class SignupRequest { @Length(min = 8, max = 20) private String confirmPassword; + @NotBlank + private String timezone; + + @NotBlank + private String address; + protected SignupRequest() {} @Builder - public SignupRequest(String email, String username, String password, String confirmPassword) { + public SignupRequest(String email, String username, String password, String confirmPassword, String timezone, String address) { this.email = email; this.username = username; this.password = password; this.confirmPassword = confirmPassword; + this.timezone = timezone; + this.address = address; } } diff --git a/src/main/java/com/gdscys/cokepoke/member/service/IMemberService.java b/src/main/java/com/gdscys/cokepoke/member/service/IMemberService.java index fa6f874..33eb12c 100644 --- a/src/main/java/com/gdscys/cokepoke/member/service/IMemberService.java +++ b/src/main/java/com/gdscys/cokepoke/member/service/IMemberService.java @@ -3,10 +3,11 @@ import com.gdscys.cokepoke.member.domain.Member; import com.gdscys.cokepoke.member.dto.UpdateMemberRequest; +import java.time.ZoneId; import java.util.List; public interface IMemberService { - Member saveMember(String email, String username, String password); + Member saveMember(String email, String username, String password, String timezone, String address); Member getMemberByUsername(String username); void updateMember(Member member, UpdateMemberRequest request); void deleteMember(Member member); diff --git a/src/main/java/com/gdscys/cokepoke/member/service/MemberService.java b/src/main/java/com/gdscys/cokepoke/member/service/MemberService.java index 1631bf3..76237bd 100644 --- a/src/main/java/com/gdscys/cokepoke/member/service/MemberService.java +++ b/src/main/java/com/gdscys/cokepoke/member/service/MemberService.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.ZoneId; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -37,8 +38,8 @@ public class MemberService implements IMemberService { private final static int PAGE_SIZE = 15; @Override - public Member saveMember(String email, String username, String password) { - Member member = new Member(email, username, passwordEncoder.encode(password), Set.of("USER")); + public Member saveMember(String email, String username, String password, String timezone, String address) { + Member member = new Member(email, username, passwordEncoder.encode(password), Set.of("USER"), ZoneId.of(timezone), address); return memberRepository.save(member); } diff --git a/src/main/java/com/gdscys/cokepoke/poke/controller/PokeController.java b/src/main/java/com/gdscys/cokepoke/poke/controller/PokeController.java index d2b5fa7..c1dc9fc 100644 --- a/src/main/java/com/gdscys/cokepoke/poke/controller/PokeController.java +++ b/src/main/java/com/gdscys/cokepoke/poke/controller/PokeController.java @@ -15,7 +15,6 @@ import java.util.stream.Collectors; import static com.gdscys.cokepoke.auth.SecurityUtil.getLoginUsername; -import static java.util.Arrays.stream; @Controller @RequiredArgsConstructor diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 20cf506..726908d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,6 +16,9 @@ spring: path: /h2-console jwt: secret: fpvuvttzKZKGmQ3UoD0QpU7oSDAmCKb8X6l78RULm1Rm01R5Itcg5C8Q9me5sffF +gcp: + geocoding: + api-key: AIzaSyCZ9Kh15ocpwy2GTliq-zKgu64h8_8qPzM logging: level: diff --git a/src/test/java/com/gdscys/cokepoke/controller/FriendshipControllerTest.java b/src/test/java/com/gdscys/cokepoke/controller/FriendshipControllerTest.java index 65e6b66..464fee9 100644 --- a/src/test/java/com/gdscys/cokepoke/controller/FriendshipControllerTest.java +++ b/src/test/java/com/gdscys/cokepoke/controller/FriendshipControllerTest.java @@ -46,11 +46,13 @@ public class FriendshipControllerTest { @Autowired private ObjectMapper objectMapper; + private final String address = "1 Gwanghwamun Square, Jongno-gu, Seoul, South Korea"; + @BeforeEach public void setup() { - memberRepository.save(new Member("test1@gmail.com", "test1", "test1", new HashSet<>())); - memberRepository.save(new Member("test2@gmail.com", "test2", "test2", new HashSet<>())); - memberRepository.save(new Member("test3@gmail.com", "test3", "test3", new HashSet<>())); + memberRepository.save(new Member("test1@gmail.com", "test1", "test1")); + memberRepository.save(new Member("test2@gmail.com", "test2", "test2")); + memberRepository.save(new Member("test3@gmail.com", "test3", "test3")); friendshipRepository.save(new Friendship(memberRepository.findByUsername("test1").get(), memberRepository.findByUsername("test3").get())); } @@ -58,7 +60,7 @@ public void setup() { @DisplayName("친구관계 만들기") @WithMockUser(username = "test1", password = "test1") public void create_friendship() throws Exception { - String content = objectMapper.writeValueAsString(new FriendshipRequest("test2")); + String content = objectMapper.writeValueAsString(new FriendshipRequest("test2", address)); mockMvc.perform(post("/friendship/create") .content(content) .contentType(MediaType.APPLICATION_JSON)) @@ -70,7 +72,7 @@ public void create_friendship() throws Exception { @DisplayName("나 자신과는 친구 관계 못함") @WithMockUser(username = "test1", password = "test1") public void create_self_friendship() throws Exception { - String content = objectMapper.writeValueAsString(new FriendshipRequest("test1")); + String content = objectMapper.writeValueAsString(new FriendshipRequest("test1", address)); mockMvc.perform(post("/friendship/create") .content(content) .contentType(MediaType.APPLICATION_JSON)) @@ -82,7 +84,20 @@ public void create_self_friendship() throws Exception { @DisplayName("이미 친구 신청했으면 다시 하지는 못함") @WithMockUser(username = "test1", password = "test1") public void reattempt_friendship() throws Exception { - String content = objectMapper.writeValueAsString(new FriendshipRequest("test3")); + String content = objectMapper.writeValueAsString(new FriendshipRequest("test3", address)); + mockMvc.perform(post("/friendship/create") + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()) + .andDo(print()); + } + + @Test + @DisplayName("멀면 친구 못함") + @WithMockUser(username = "test1", password = "test1") + public void far_friendship() throws Exception { + + String content = objectMapper.writeValueAsString(new FriendshipRequest("test2", "10 Downing Street, London SW1A 2AA, United Kingdom")); mockMvc.perform(post("/friendship/create") .content(content) .contentType(MediaType.APPLICATION_JSON)) @@ -99,4 +114,5 @@ public void reattempt_friendship() throws Exception { + } diff --git a/src/test/java/com/gdscys/cokepoke/controller/PokeControllerTest.java b/src/test/java/com/gdscys/cokepoke/controller/PokeControllerTest.java index 2335d9b..8ce7292 100644 --- a/src/test/java/com/gdscys/cokepoke/controller/PokeControllerTest.java +++ b/src/test/java/com/gdscys/cokepoke/controller/PokeControllerTest.java @@ -1,9 +1,6 @@ package com.gdscys.cokepoke.controller; import com.fasterxml.jackson.databind.ObjectMapper; -import com.gdscys.cokepoke.friendship.domain.Friendship; -import com.gdscys.cokepoke.friendship.dto.FriendshipRequest; -import com.gdscys.cokepoke.friendship.repository.FriendshipRepository; import com.gdscys.cokepoke.friendship.service.FriendshipService; import com.gdscys.cokepoke.member.domain.Member; import com.gdscys.cokepoke.member.repository.MemberRepository; @@ -44,15 +41,17 @@ public class PokeControllerTest { @Autowired private ObjectMapper objectMapper; + private final String address = "1 Gwanghwamun Square, Jongno-gu, Seoul, South Korea"; + @BeforeEach public void setup() { - memberRepository.save(new Member("test1@gmail.com", "test1", "test1", new HashSet<>())); - memberRepository.save(new Member("test2@gmail.com", "test2", "test2", new HashSet<>())); - memberRepository.save(new Member("test3@gmail.com", "test3", "test3", new HashSet<>())); + memberRepository.save(new Member("test1@gmail.com", "test1", "test1")); + memberRepository.save(new Member("test2@gmail.com", "test2", "test2")); + memberRepository.save(new Member("test3@gmail.com", "test3", "test3")); //test1 & test3 are friends - friendshipService.createFriendship("test1", "test3"); - friendshipService.createFriendship("test3", "test1"); + friendshipService.createFriendship("test1", "test3", address); + friendshipService.createFriendship("test3", "test1", address); } @Test