Skip to content

Commit 2060cfa

Browse files
committed
feat: TimeMatrix 중간지점 전략 구현 및 테스트 추가
1 parent 1045c1c commit 2060cfa

4 files changed

Lines changed: 223 additions & 5 deletions

File tree

src/main/java/com/ODG/ODG_back/strategy/MidpointStrategy.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import com.ODG.ODG_back.dto.midpoint.response.MidpointResponseDto;
44

5-
import java.util.List;
6-
75
public interface MidpointStrategy {
8-
List<MidpointResponseDto> calculateMidpoints(String inviteCode);
6+
MidpointResponseDto calculateMidpoints(String inviteCode);
97
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.ODG.ODG_back.strategy;
2+
3+
import com.ODG.ODG_back.dto.midpoint.response.MidpointResponseDto;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.math.BigDecimal;
8+
9+
@Component("subwayStrategy")
10+
@RequiredArgsConstructor
11+
public class SubwayMidpointStrategy implements MidpointStrategy{
12+
13+
@Override
14+
public MidpointResponseDto calculateMidpoints(String inviteCode) {
15+
return new MidpointResponseDto(1L, "dummy", BigDecimal.ZERO, BigDecimal.ZERO);
16+
}
17+
}
Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,110 @@
11
package com.ODG.ODG_back.strategy;
22

3+
import com.ODG.ODG_back.domain.Meeting;
4+
import com.ODG.ODG_back.domain.Midpoint;
5+
import com.ODG.ODG_back.domain.Participant;
6+
import com.ODG.ODG_back.domain.RecommendedMidpoint;
37
import com.ODG.ODG_back.dto.midpoint.response.MidpointResponseDto;
48
import com.ODG.ODG_back.external.google.GoogleMatrixApiClient;
9+
import com.ODG.ODG_back.mapper.MidpointMapper;
510
import com.ODG.ODG_back.repository.MeetingRepository;
11+
import com.ODG.ODG_back.repository.MidpointRepository;
612
import com.ODG.ODG_back.repository.RecommendedMidpointRepository;
713
import lombok.RequiredArgsConstructor;
814
import org.springframework.stereotype.Component;
915

16+
import java.util.ArrayList;
17+
import java.util.Comparator;
1018
import java.util.List;
1119

20+
import static java.util.stream.Collectors.groupingBy;
21+
1222
@Component("timeMatrixStrategy")
1323
@RequiredArgsConstructor
1424
public class TimeMatrixMidpointStrategy implements MidpointStrategy {
1525

1626
private final MeetingRepository meetingRepository;
1727
private final RecommendedMidpointRepository recommendedMidpointRepository;
1828
private final GoogleMatrixApiClient matrixApiClient; // 소요 시간 API 요청
29+
private final MidpointRepository midpointRepository;
30+
private final MidpointMapper midpointMapper;
1931

2032
@Override
21-
public List<MidpointResponseDto> calculateMidpoints(String inviteCode) {
22-
return List.of();
33+
public MidpointResponseDto calculateMidpoints(String inviteCode) {
34+
35+
Meeting meeting = meetingRepository.findByInviteCode(inviteCode);
36+
List<Participant> participants = meeting.getParticipants();
37+
List<Midpoint> allMidpoints = midpointRepository.findAll();
38+
39+
int[][] timeMatrix = buildTimeMatrix(participants, allMidpoints);
40+
MidpointScore best = scoreMidpoints(timeMatrix, allMidpoints, participants.size());
41+
42+
saveRecommendedMidpoint(best, meeting);
43+
return midpointMapper.toDto(best.midpoint());
44+
}
45+
46+
private int[][] buildTimeMatrix(List<Participant> participants, List<Midpoint> allMidpoints) {
47+
48+
List <String> destinations = allMidpoints.stream()
49+
.map(m -> m.getLatitude() + ", " + m.getLongitude())
50+
.toList();
51+
int[][] timeMatrix = new int[participants.size()][destinations.size()];
52+
53+
var grouped = participants.stream()
54+
.collect(groupingBy(Participant::getTransport));
55+
int rowIndex = 0;
56+
57+
for (var entry: grouped.entrySet()) {
58+
List<Participant> group = entry.getValue();
59+
List<String> origins = group.stream()
60+
.map(p -> p.getLatitude() + ", " + p.getLongitude())
61+
.toList();
62+
63+
int[][] groupMatrix = matrixApiClient.getTimeMatrix(origins, destinations, entry.getKey());
64+
65+
for (int i = 0; i < group.size(); i++) {
66+
timeMatrix[rowIndex++] = groupMatrix[i];
67+
}
68+
}
69+
return timeMatrix;
70+
}
71+
72+
private MidpointScore scoreMidpoints(int[][] timeMatrix, List<Midpoint> midpoints, int numParticipants) {
73+
List<MidpointScore> scored = new ArrayList<>();
74+
75+
for (int j = 0; j < midpoints.size(); j++) {
76+
int sum = 0;
77+
for (int i = 0; i < numParticipants; i++) {
78+
sum += timeMatrix[i][j];
79+
}
80+
double avg = sum / (double) numParticipants;
81+
82+
double totalDeviation = 0;
83+
for (int i = 0; i < numParticipants; i++) {
84+
totalDeviation += Math.abs(timeMatrix[i][j] - avg);
85+
}
86+
87+
scored.add(new MidpointScore(midpoints.get(j), avg, totalDeviation ));
88+
}
89+
90+
return scored.stream()
91+
.sorted(Comparator.comparingDouble(MidpointScore::avg)
92+
.thenComparingDouble(MidpointScore::totalDeviation))
93+
.findFirst()
94+
.orElseThrow(() -> new RuntimeException("No MIDPOINT found"));
2395
}
96+
97+
private void saveRecommendedMidpoint(MidpointScore best, Meeting meeting) {
98+
RecommendedMidpoint recommended = new RecommendedMidpoint(
99+
null,
100+
best.avg(),
101+
1,
102+
java.time.LocalDateTime.now(),
103+
meeting,
104+
best.midpoint()
105+
);
106+
recommendedMidpointRepository.save(recommended);
107+
}
108+
109+
private record MidpointScore(Midpoint midpoint, double avg, double totalDeviation) {}
24110
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.ODG.ODG_back.strategy;
2+
3+
import com.ODG.ODG_back.domain.Meeting;
4+
import com.ODG.ODG_back.domain.Midpoint;
5+
import com.ODG.ODG_back.domain.Participant;
6+
import com.ODG.ODG_back.dto.midpoint.response.MidpointResponseDto;
7+
import com.ODG.ODG_back.external.google.GoogleMatrixApiClient;
8+
import com.ODG.ODG_back.mapper.MidpointMapper;
9+
import com.ODG.ODG_back.repository.MeetingRepository;
10+
import com.ODG.ODG_back.repository.MidpointRepository;
11+
import com.ODG.ODG_back.repository.RecommendedMidpointRepository;
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.DisplayName;
14+
import org.junit.jupiter.api.Test;
15+
import org.mockito.InjectMocks;
16+
import org.mockito.Mock;
17+
import org.mockito.MockitoAnnotations;
18+
19+
import java.util.List;
20+
21+
import static com.ODG.ODG_back.domain.enums.TransportType.CAR;
22+
import static com.ODG.ODG_back.domain.enums.TransportType.PUBLIC;
23+
import static org.junit.jupiter.api.Assertions.*;
24+
import static org.mockito.ArgumentMatchers.any;
25+
import static org.mockito.ArgumentMatchers.eq;
26+
import static org.mockito.Mockito.when;
27+
28+
29+
class TimeMatrixMidpointStrategyTest {
30+
31+
@Mock
32+
private MeetingRepository meetingRepository;
33+
34+
@Mock
35+
private MidpointRepository midpointRepository;
36+
37+
@Mock
38+
private RecommendedMidpointRepository recommendedMidpointRepository;
39+
40+
@Mock
41+
private GoogleMatrixApiClient matrixApiClient;
42+
43+
@Mock
44+
private MidpointMapper midpointMapper;
45+
46+
@InjectMocks
47+
private TimeMatrixMidpointStrategy strategy;
48+
49+
@BeforeEach
50+
void setUp() {
51+
MockitoAnnotations.openMocks(this);
52+
}
53+
54+
@Test
55+
@DisplayName("중간지점 정확히 계산하기")
56+
void calculateMidpoint() {
57+
// given
58+
// 모임 정보 설정
59+
Meeting meeting = new Meeting();
60+
meeting.setInviteCode("abc123");
61+
when(meetingRepository.findByInviteCode("abc123")).thenReturn(meeting);
62+
63+
// 참가자 추가
64+
Participant p1 = Participant.builder()
65+
.name("민재")
66+
.transport(PUBLIC)
67+
.build();
68+
Participant p2 = Participant.builder()
69+
.name("승민")
70+
.transport(CAR)
71+
.build();
72+
Participant p3 = Participant.builder()
73+
.name("준우")
74+
.transport(PUBLIC)
75+
.build();
76+
meeting.setParticipants(List.of(p1, p2, p3));
77+
78+
// 후보 중간지점 (Midpoint) 설정
79+
Midpoint m1 = Midpoint.builder()
80+
.name("강남역")
81+
.build();
82+
Midpoint m2 = Midpoint.builder()
83+
.name("홍대입구")
84+
.build();
85+
Midpoint m3 = Midpoint.builder()
86+
.name("서울역")
87+
.build();
88+
when(midpointRepository.findAll()).thenReturn(List.of(m1, m2, m3));
89+
90+
// 모의 Google API 응답
91+
when(matrixApiClient.getTimeMatrix(any(), any(), eq(PUBLIC))).thenReturn(
92+
new int[][] {
93+
{30, 20, 27}, // p1 to m1, m2, m3
94+
{35, 22, 23} // p3 to m1, m2, m3
95+
}
96+
);
97+
98+
when(matrixApiClient.getTimeMatrix(any(), any(), eq(CAR))).thenReturn(
99+
new int[][] {
100+
{15, 18, 10} // p2 to m1, m2, m3
101+
}
102+
);
103+
104+
when(midpointMapper.toDto(any())).thenAnswer(invocation ->
105+
{
106+
Midpoint mp = invocation.getArgument(0);
107+
return new MidpointResponseDto(mp.getId(), mp.getName(), mp.getLatitude(), mp.getLongitude());
108+
});
109+
110+
// when
111+
MidpointResponseDto result = strategy.calculateMidpoints("abc123");
112+
113+
//then
114+
assertNotNull(result);
115+
assertEquals("홍대입구", result.getName());
116+
}
117+
}

0 commit comments

Comments
 (0)