From cd3d5ed069f7edd78d25989df2c5591c032573bf Mon Sep 17 00:00:00 2001 From: choyoungseo20 Date: Sat, 14 Jun 2025 18:46:42 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EB=A7=A4=EC=B9=AD=20=EC=95=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=EC=A6=98=20=EB=B0=8F=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../algorithm/AdjacencyGraphBuilder.java | 58 +++++++ .../matching/algorithm/BipartiteMatcher.java | 42 +++++ .../matching/algorithm/GeoUtils.java | 20 +++ .../matching/algorithm/Matcher.java | 143 ++++++++++++++++++ .../matching/converter/MatchingConverter.java | 18 +++ .../matching/domain/MatchRequest.java | 4 + .../matching/dto/MatchingDto.java | 28 ++++ .../repository/MatchingRepository.java | 8 + .../matching/scheduler/MatchingScheduler.java | 21 +++ .../matching/service/MatchingService.java | 53 +++++++ 10 files changed, 395 insertions(+) create mode 100644 src/main/java/com/example/silverbridgeX_user/matching/algorithm/AdjacencyGraphBuilder.java create mode 100644 src/main/java/com/example/silverbridgeX_user/matching/algorithm/BipartiteMatcher.java create mode 100644 src/main/java/com/example/silverbridgeX_user/matching/algorithm/GeoUtils.java create mode 100644 src/main/java/com/example/silverbridgeX_user/matching/algorithm/Matcher.java create mode 100644 src/main/java/com/example/silverbridgeX_user/matching/dto/MatchingDto.java create mode 100644 src/main/java/com/example/silverbridgeX_user/matching/scheduler/MatchingScheduler.java diff --git a/src/main/java/com/example/silverbridgeX_user/matching/algorithm/AdjacencyGraphBuilder.java b/src/main/java/com/example/silverbridgeX_user/matching/algorithm/AdjacencyGraphBuilder.java new file mode 100644 index 0000000..cd43c70 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/matching/algorithm/AdjacencyGraphBuilder.java @@ -0,0 +1,58 @@ +package com.example.silverbridgeX_user.matching.algorithm; + +import com.example.silverbridgeX_user.matching.domain.MatchRequest; +import com.example.silverbridgeX_user.user.domain.User; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +public class AdjacencyGraphBuilder { + + public static List[] buildAdjacencyGraph(List matchRequests) { + int n = matchRequests.size(); + List[] graph = new ArrayList[n + 1]; + + for (int i = 1; i <= n; i++) { + graph[i] = new ArrayList<>(); + } + + for (int i = 1; i <= n; i++) { + for (int j = i + 1; j <= n; j++) { + if (canConnect(matchRequests.get(i - 1), matchRequests.get(j - 1))) { + graph[i].add(j); + graph[j].add(i); + } + } + } + + return graph; + } + + private static boolean canConnect(MatchRequest a, MatchRequest b) { + User A = a.getUser(); + User B = b.getUser(); + +// String sexA = A.getSex(); +// String sexB = B.getSex(); +// +// if (sexA == null || sexB == null) return false; + + Double latA = A.getLatitude(); + Double lonA = A.getLongitude(); + Double latB = B.getLatitude(); + Double lonB = B.getLongitude(); + + if (latA == null || lonA == null || latB == null || lonB == null) return false; + +// LocalDate birthA = A.getBirth(); +// LocalDate birthB = B.getBirth(); +// +// if (birthA == null || birthB == null) return false; + +// return !sexA.equals(sexB) && +// GeoUtils.haversine(latA, lonA, latB, lonB) <= 3.0 && +// Math.abs(birthA.getYear() - birthB.getYear()) <= 10; + return GeoUtils.haversine(latA, lonA, latB, lonB) <= 3.0; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/matching/algorithm/BipartiteMatcher.java b/src/main/java/com/example/silverbridgeX_user/matching/algorithm/BipartiteMatcher.java new file mode 100644 index 0000000..c7ff585 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/matching/algorithm/BipartiteMatcher.java @@ -0,0 +1,42 @@ +package com.example.silverbridgeX_user.matching.algorithm; + +import java.util.List; + +public class BipartiteMatcher { + private final List[] adj; + private final Integer[] matching; // match[v] = u + private boolean[] visited; + + public BipartiteMatcher(List[] adj) { + this.adj = adj; + this.matching = new Integer[adj.length]; // 1-based index + } + + public int run() { + int res = 0; + for (int u = 1; u < adj.length; u++) { + visited = new boolean[adj.length]; + if (dfs(u)) { + res++; + } + } + return res; + } + + private boolean dfs(int u) { + for (int v : adj[u]) { + if (visited[v]) continue; + visited[v] = true; + + if (matching[v] == 0 || dfs(matching[v])) { + matching[v] = u; + return true; + } + } + return false; + } + + public Integer[] getMatching() { + return matching; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/matching/algorithm/GeoUtils.java b/src/main/java/com/example/silverbridgeX_user/matching/algorithm/GeoUtils.java new file mode 100644 index 0000000..03eb414 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/matching/algorithm/GeoUtils.java @@ -0,0 +1,20 @@ +package com.example.silverbridgeX_user.matching.algorithm; + +public class GeoUtils { + + private static final double EARTH_RADIUS_KM = 6371.0; + + public static double haversine(double lat1, double lon1, double lat2, double lon2) { + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + lat1 = Math.toRadians(lat1); + lat2 = Math.toRadians(lat2); + + double a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dLon / 2), 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/example/silverbridgeX_user/matching/algorithm/Matcher.java b/src/main/java/com/example/silverbridgeX_user/matching/algorithm/Matcher.java new file mode 100644 index 0000000..fa2a208 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/matching/algorithm/Matcher.java @@ -0,0 +1,143 @@ +package com.example.silverbridgeX_user.matching.algorithm; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.Queue; + +public class Matcher { + + private final List[] adj; + private final Integer[] matching; + private final Integer[] parent; + private final Integer[] color; + private final Integer[] visited; + private final Integer[] group; + + public Matcher(List[] adj) { + this.adj = adj; + this.matching = new Integer[adj.length]; + this.parent = new Integer[adj.length]; + this.color = new Integer[adj.length]; + this.visited = new Integer[adj.length]; + this.group = new Integer[adj.length]; + } + + public int run() { + int res = 0; + for (int i = 1; i < adj.length; i++) { + matching[i] = 0; + } + + for (int i = 1; i < adj.length; i++) { + if (matching[i] == 0) { + if (findAugmentPath(i)) { + res++; + } + } + } + + return res; + } + + private boolean findAugmentPath(int r) { + for (int i = 1; i < adj.length; i++) { + parent[i] = 0; + color[i] = -1; + visited[i] = 0; + group[i] = i; + } + + Queue q = new ArrayDeque<>(); + + color[r] = 0; + visited[r] = 1; + q.add(r); + + while (!q.isEmpty()) { + int u = q.poll(); + for (int v : adj[u]) { + if (color[v] == -1) { + parent[v] = u; + color[v] = 1; + + if (matching[v] == 0) { + flipAugmentPath(r, v); + return true; + } + + color[matching[v]] = 0; + visited[matching[v]] = 1; + q.add(matching[v]); + } + else if (color[v] == 0 && !group[u].equals(group[v])) { + int p = lca(group[r], group[u], group[v]); + + group_blossom(p, u, v); + group_blossom(p, v, u); + + for (int i = 1; i < adj.length; i++) { + if (visited[i] != 0 && color[i] != 0) { + color[i] = 0; + q.add(i); + } + } + } + } + } + return false; + } + + private void flipAugmentPath(int root, int u) { + while (parent[u] != root) { + int v = parent[u]; + int w = matching[v]; + + matching[u] = v; + matching[v] = u; + + matching[w] = 0; + u = w; + } + matching[u] = root; + matching[root] = u; + } + + private int lca(int root, int u, int v) { + Integer[] check = new Integer[adj.length]; + for (int i = 1; i < adj.length; i++) { + check[i] = 0; + } + + while (u != root) { + check[u] = 1; + u = group[parent[matching[u]]]; + } + + while (v != root) { + if (check[v] != 0) return v; + v = group[parent[matching[v]]]; + } + + return root; + } + + private void group_blossom(int p, int u, int v) { + while (group[u] != p) { + int nv = matching[u]; + int nu = parent[nv]; + if (visited[nv] == 0) { + visited[nv] = 1; + } + + parent[u] = v; + group[u] = p; + group[nv] = p; + u = nu; + v = nv; + } + } + + public Integer[] getMatching() { + return matching; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/matching/converter/MatchingConverter.java b/src/main/java/com/example/silverbridgeX_user/matching/converter/MatchingConverter.java index e88cd58..3bc47b1 100644 --- a/src/main/java/com/example/silverbridgeX_user/matching/converter/MatchingConverter.java +++ b/src/main/java/com/example/silverbridgeX_user/matching/converter/MatchingConverter.java @@ -2,8 +2,11 @@ import com.example.silverbridgeX_user.matching.domain.MatchRequest; import com.example.silverbridgeX_user.matching.domain.MatchStatus; +import com.example.silverbridgeX_user.matching.dto.MatchingDto; import com.example.silverbridgeX_user.user.domain.User; +import java.util.List; + public class MatchingConverter { public static MatchRequest toMatchRequest(User user) { @@ -12,4 +15,19 @@ public static MatchRequest toMatchRequest(User user) { .user(user) .build(); } + + public static MatchingDto.Request toMatchingDtoRequest(User a, User b) { + MatchingDto.Participant participantA = MatchingDto.Participant.builder() + .id(a.getId()) + .name(a.getNickname()) + .build(); + MatchingDto.Participant participantB = MatchingDto.Participant.builder() + .id(b.getId()) + .name(b.getNickname()) + .build(); + + return MatchingDto.Request.builder() + .participants(List.of(participantA, participantB)) + .build(); + } } diff --git a/src/main/java/com/example/silverbridgeX_user/matching/domain/MatchRequest.java b/src/main/java/com/example/silverbridgeX_user/matching/domain/MatchRequest.java index 70c9e4e..66d6e4a 100644 --- a/src/main/java/com/example/silverbridgeX_user/matching/domain/MatchRequest.java +++ b/src/main/java/com/example/silverbridgeX_user/matching/domain/MatchRequest.java @@ -22,4 +22,8 @@ public class MatchRequest extends BaseEntity { @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; + + public void updateStatus(MatchStatus status) { + this.status = status; + } } diff --git a/src/main/java/com/example/silverbridgeX_user/matching/dto/MatchingDto.java b/src/main/java/com/example/silverbridgeX_user/matching/dto/MatchingDto.java new file mode 100644 index 0000000..33f880b --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/matching/dto/MatchingDto.java @@ -0,0 +1,28 @@ +package com.example.silverbridgeX_user.matching.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +public class MatchingDto { + + @Builder + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Participant { + private Long id; + private String name; + } + + @Builder + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Request { + private List participants; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/matching/repository/MatchingRepository.java b/src/main/java/com/example/silverbridgeX_user/matching/repository/MatchingRepository.java index a38fb3a..4476daf 100644 --- a/src/main/java/com/example/silverbridgeX_user/matching/repository/MatchingRepository.java +++ b/src/main/java/com/example/silverbridgeX_user/matching/repository/MatchingRepository.java @@ -1,7 +1,15 @@ package com.example.silverbridgeX_user.matching.repository; import com.example.silverbridgeX_user.matching.domain.MatchRequest; +import com.example.silverbridgeX_user.matching.domain.MatchStatus; 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; public interface MatchingRepository extends JpaRepository { + + @Query("SELECT m FROM MatchRequest m JOIN FETCH m.user WHERE m.status = :status ORDER BY m.createdAt ASC") + List findAllWithUserByStatus(@Param("status") MatchStatus status); } diff --git a/src/main/java/com/example/silverbridgeX_user/matching/scheduler/MatchingScheduler.java b/src/main/java/com/example/silverbridgeX_user/matching/scheduler/MatchingScheduler.java new file mode 100644 index 0000000..f289cd3 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/matching/scheduler/MatchingScheduler.java @@ -0,0 +1,21 @@ +package com.example.silverbridgeX_user.matching.scheduler; + +import com.example.silverbridgeX_user.matching.service.MatchingService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +@RequiredArgsConstructor +public class MatchingScheduler { + + private final MatchingService matchingService; + + @Scheduled(cron = "0 18,38,58 * * * *") + public void executeMatching() { + System.out.println("매칭 알고리즘 실행됨: " + LocalDateTime.now()); + matchingService.executeMatchingAlgorithm(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/matching/service/MatchingService.java b/src/main/java/com/example/silverbridgeX_user/matching/service/MatchingService.java index 2e056e7..7ef3846 100644 --- a/src/main/java/com/example/silverbridgeX_user/matching/service/MatchingService.java +++ b/src/main/java/com/example/silverbridgeX_user/matching/service/MatchingService.java @@ -1,19 +1,35 @@ package com.example.silverbridgeX_user.matching.service; +import com.example.silverbridgeX_user.matching.algorithm.AdjacencyGraphBuilder; +import com.example.silverbridgeX_user.matching.algorithm.Matcher; import com.example.silverbridgeX_user.matching.converter.MatchingConverter; import com.example.silverbridgeX_user.matching.domain.MatchRequest; +import com.example.silverbridgeX_user.matching.domain.MatchStatus; +import com.example.silverbridgeX_user.matching.dto.MatchingDto; import com.example.silverbridgeX_user.matching.repository.MatchingRepository; import com.example.silverbridgeX_user.user.domain.User; +import com.example.silverbridgeX_user.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.ArrayList; +import java.util.List; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class MatchingService { + private final WebClient webClient; + + @Value("${chat.server.url}") + private String chatServerUrl; + private final MatchingRepository matchingRepository; + private final UserRepository userRepository; @Transactional public void saveMatchRequest(User user) { @@ -21,4 +37,41 @@ public void saveMatchRequest(User user) { matchingRepository.save(matchRequest); } + + + @Transactional + public void executeMatchingAlgorithm() { + List matchRequests = matchingRepository.findAllWithUserByStatus(MatchStatus.WAITING); // size: n + + List[] adj = AdjacencyGraphBuilder.buildAdjacencyGraph(matchRequests); // size: n + 1 + + + Matcher matcher = new Matcher(adj); // size: n + 1 + int matchCount = matcher.run(); + System.out.println(matchCount); + Integer[] matching = matcher.getMatching(); + + List requests = new ArrayList<>(); + for (int i = 1; i < matching.length; i++) { + if (matching[i] != null && i < matching[i]) { + MatchingDto.Request request = MatchingConverter.toMatchingDtoRequest( + matchRequests.get(i - 1).getUser(), + matchRequests.get(matching[i] - 1).getUser() + ); + requests.add(request); + + matchRequests.get(i - 1).updateStatus(MatchStatus.MATCHED); + matchRequests.get(matching[i] - 1).updateStatus(MatchStatus.MATCHED); + } + } + + webClient.post() + .uri(chatServerUrl + "/room/all") + .bodyValue(requests) + .retrieve() + .bodyToMono(Void.class) + .block(); + } + + } From d965274d806485d334508682ba6ae36ab3581680 Mon Sep 17 00:00:00 2001 From: choyoungseo20 Date: Sat, 14 Jun 2025 18:52:10 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EB=A7=A4=EC=B9=AD=20=EC=95=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=EC=A6=98=20=EB=B0=8F=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/silverbridgeX_user/user/domain/User.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java index fec31a4..6f6c261 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java +++ b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java @@ -16,6 +16,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; + +import java.time.LocalDate; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; @@ -41,13 +43,13 @@ public class User extends BaseEntity { private String sex; - private String birth; + private LocalDate birth; private String streetAddress; - private String latitude; + private Double latitude; - private String longitude; + private Double longitude; private String profileImage;