Skip to content

Commit ea8a6aa

Browse files
authored
Merge pull request #311 from CSE-Shaco/develop
feat: MBTI 팀매칭에 리드·오거나이저 제외 로직 추가
2 parents 5ad3016 + 6965317 commit ea8a6aa

4 files changed

Lines changed: 153 additions & 49 deletions

File tree

src/main/java/inha/gdgoc/domain/admin/game/dto/request/MbtiTeamMatchRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public record MbtiTeamMatchRequest(
1212
@Min(2) @Max(10) Integer teamSize
1313
) {
1414
public int resolvedTeamSize() {
15-
return teamSize == null ? 4 : teamSize;
15+
return teamSize == null ? 6 : teamSize;
1616
}
1717

1818
public record Candidate(

src/main/java/inha/gdgoc/domain/admin/game/dto/response/MbtiTeamMatchResponse.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public record Team(
2121
public record Member(
2222
String name,
2323
String studentId,
24-
String mbtiType
24+
String mbtiType,
25+
boolean hasMbtiResult
2526
) {
2627
}
2728

src/main/java/inha/gdgoc/domain/admin/game/service/MbtiAdminService.java

Lines changed: 149 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import inha.gdgoc.domain.admin.game.dto.response.MbtiTeamMatchResponse;
66
import inha.gdgoc.domain.game.entity.MbtiResult;
77
import inha.gdgoc.domain.game.repository.MbtiResultRepository;
8+
import inha.gdgoc.domain.user.enums.UserRole;
9+
import inha.gdgoc.domain.user.repository.UserRepository;
810
import java.util.ArrayDeque;
911
import java.util.ArrayList;
10-
import java.util.Collection;
1112
import java.util.Comparator;
1213
import java.util.Deque;
1314
import java.util.HashMap;
@@ -24,10 +25,28 @@
2425
@RequiredArgsConstructor
2526
@Service
2627
public class MbtiAdminService {
27-
28-
private static final String NO_RESULT_REASON = "NO_MBTI_RESULT";
28+
private static final String EXCLUDED_PRIVILEGED_ROLE_REASON = "EXCLUDED_PRIVILEGED_ROLE";
29+
private static final Map<String, List<String>> TEAMMATE_COMPATIBILITY = Map.ofEntries(
30+
Map.entry("LPTI", List.of("CPTF", "CPUI", "LSTF")),
31+
Map.entry("LPTF", List.of("LSTF", "CPTF", "LPUI")),
32+
Map.entry("LSTI", List.of("LPTF", "CPUF", "LPUF")),
33+
Map.entry("LSTF", List.of("LPTI", "CPTF", "LPUF")),
34+
Map.entry("CPTI", List.of("LSTF", "LPTI", "LPUI")),
35+
Map.entry("CPTF", List.of("LPTI", "LSTF", "LSUI")),
36+
Map.entry("CSTI", List.of("CPUI", "LPTF", "LPUI")),
37+
Map.entry("CSTF", List.of("LSTF", "CPTF", "CPUF")),
38+
Map.entry("LPUI", List.of("CPTF", "LSTF", "CSUI")),
39+
Map.entry("LPUF", List.of("LSTI", "LSTF", "CPUF")),
40+
Map.entry("LSUI", List.of("LPTF", "CPTI", "LPUI")),
41+
Map.entry("LSUF", List.of("LPTF", "CPTF", "CPUF")),
42+
Map.entry("CPUI", List.of("LPTI", "LSTI", "CSTI")),
43+
Map.entry("CPUF", List.of("LSTI", "LPUF", "CPTF")),
44+
Map.entry("CSUI", List.of("LPUI", "LSTF", "CPTF")),
45+
Map.entry("CSUF", List.of("CPTF", "LSTF", "CPUF"))
46+
);
2947

3048
private final MbtiResultRepository mbtiResultRepository;
49+
private final UserRepository userRepository;
3150

3251
@Transactional(readOnly = true)
3352
public Page<MbtiAdminResultRowResponse> searchResults(String keyword, Pageable pageable) {
@@ -71,6 +90,12 @@ public MbtiTeamMatchResponse matchTeams(MbtiTeamMatchRequest request) {
7190
List<String> studentIds = uniqueCandidates.stream()
7291
.map(MbtiTeamMatchRequest.Candidate::studentId)
7392
.toList();
93+
Map<String, UserRole> roleByStudentId = userRepository.findByStudentIdIn(studentIds).stream()
94+
.collect(
95+
LinkedHashMap::new,
96+
(acc, user) -> acc.putIfAbsent(user.getStudentId(), user.getUserRole()),
97+
Map::putAll
98+
);
7499

75100
Map<String, MbtiResult> resultMap = mbtiResultRepository.findByStudentIdIn(studentIds).stream()
76101
.collect(
@@ -80,55 +105,105 @@ public MbtiTeamMatchResponse matchTeams(MbtiTeamMatchRequest request) {
80105
);
81106

82107
List<MbtiTeamMatchResponse.Member> matchedMembers = new ArrayList<>();
108+
List<MbtiTeamMatchResponse.Member> unmatchedMembers = new ArrayList<>();
83109
List<MbtiTeamMatchResponse.UnmatchedCandidate> unmatched = new ArrayList<>();
84110

85111
for (MbtiTeamMatchRequest.Candidate candidate : uniqueCandidates) {
112+
UserRole userRole = roleByStudentId.get(candidate.studentId());
113+
if (userRole == UserRole.LEAD || userRole == UserRole.ORGANIZER) {
114+
unmatched.add(new MbtiTeamMatchResponse.UnmatchedCandidate(
115+
candidate.name(),
116+
candidate.studentId(),
117+
EXCLUDED_PRIVILEGED_ROLE_REASON
118+
));
119+
continue;
120+
}
121+
86122
MbtiResult matched = resultMap.get(candidate.studentId());
87123
if (matched == null) {
88-
unmatched.add(new MbtiTeamMatchResponse.UnmatchedCandidate(
124+
unmatchedMembers.add(new MbtiTeamMatchResponse.Member(
89125
candidate.name(),
90126
candidate.studentId(),
91-
NO_RESULT_REASON
127+
null,
128+
false
92129
));
93130
continue;
94131
}
95132

96133
matchedMembers.add(new MbtiTeamMatchResponse.Member(
97134
candidate.name().isEmpty() ? matched.getName() : candidate.name(),
98135
candidate.studentId(),
99-
matched.getMbtiType()
136+
matched.getMbtiType(),
137+
true
100138
));
101139
}
102140

103141
int teamSize = request.resolvedTeamSize();
104-
List<MbtiTeamMatchResponse.Team> teams = buildBalancedTeams(matchedMembers, teamSize);
142+
TeamBuildResult buildResult = buildBalancedTeams(matchedMembers, unmatchedMembers, teamSize);
105143

106144
return new MbtiTeamMatchResponse(
107145
rawCandidates.size(),
108146
uniqueCandidates.size(),
109-
matchedMembers.size(),
147+
buildResult.assignedCount(),
110148
unmatched.size(),
111149
teamSize,
112-
teams.size(),
113-
teams,
150+
buildResult.teams().size(),
151+
buildResult.teams(),
114152
unmatched
115153
);
116154
}
117155

118-
private List<MbtiTeamMatchResponse.Team> buildBalancedTeams(
119-
List<MbtiTeamMatchResponse.Member> members,
156+
private TeamBuildResult buildBalancedTeams(
157+
List<MbtiTeamMatchResponse.Member> matchedMembers,
158+
List<MbtiTeamMatchResponse.Member> unmatchedMembers,
120159
int teamSize
121160
) {
122-
if (members.isEmpty()) {
123-
return List.of();
161+
int totalMembers = matchedMembers.size() + unmatchedMembers.size();
162+
if (totalMembers == 0) {
163+
return new TeamBuildResult(List.of());
124164
}
125165

126-
int teamCount = (int) Math.ceil((double) members.size() / teamSize);
166+
int teamCount = (int) Math.ceil((double) totalMembers / teamSize);
167+
127168
List<TeamBucket> buckets = new ArrayList<>();
128169
for (int i = 0; i < teamCount; i += 1) {
129-
buckets.add(new TeamBucket(i + 1));
170+
int baseSize = totalMembers / teamCount + (i < totalMembers % teamCount ? 1 : 0);
171+
int unmatchedTarget = unmatchedMembers.size() / teamCount + (i < unmatchedMembers.size() % teamCount ? 1 : 0);
172+
unmatchedTarget = Math.min(unmatchedTarget, baseSize);
173+
buckets.add(new TeamBucket(i + 1, unmatchedTarget, baseSize - unmatchedTarget));
174+
}
175+
176+
for (MbtiTeamMatchResponse.Member member : unmatchedMembers) {
177+
TeamBucket bucket = buckets.stream()
178+
.filter(TeamBucket::canAcceptUnmatched)
179+
.min(Comparator.comparingInt(TeamBucket::size).thenComparingInt(TeamBucket::teamNumber))
180+
.orElseThrow();
181+
182+
bucket.addUnmatched(member);
130183
}
131184

185+
List<MbtiTeamMatchResponse.Member> orderedMatched = buildCompatibilitySeedOrder(matchedMembers);
186+
187+
for (MbtiTeamMatchResponse.Member member : orderedMatched) {
188+
TeamBucket bucket = buckets.stream()
189+
.filter(TeamBucket::canAcceptMatched)
190+
.max(
191+
Comparator.comparingInt((TeamBucket team) -> team.compatibilityScoreFor(member))
192+
.thenComparing(Comparator.comparingInt(TeamBucket::size).reversed())
193+
.thenComparing(Comparator.comparingInt(TeamBucket::teamNumber).reversed())
194+
)
195+
.orElseThrow();
196+
197+
bucket.addMatched(member);
198+
}
199+
200+
List<MbtiTeamMatchResponse.Team> teams = buckets.stream().map(TeamBucket::toResponse).toList();
201+
return new TeamBuildResult(teams);
202+
}
203+
204+
private List<MbtiTeamMatchResponse.Member> buildCompatibilitySeedOrder(
205+
List<MbtiTeamMatchResponse.Member> members
206+
) {
132207
Map<String, List<MbtiTeamMatchResponse.Member>> grouped = members.stream()
133208
.filter(Objects::nonNull)
134209
.collect(
@@ -142,29 +217,7 @@ private List<MbtiTeamMatchResponse.Team> buildBalancedTeams(
142217
.map(list -> (Deque<MbtiTeamMatchResponse.Member>) new ArrayDeque<>(list))
143218
.toList();
144219

145-
List<MbtiTeamMatchResponse.Member> ordered = interleaveByType(queues);
146-
147-
for (MbtiTeamMatchResponse.Member member : ordered) {
148-
TeamBucket bucket = buckets.stream()
149-
.min(
150-
Comparator.comparingInt(TeamBucket::size)
151-
.thenComparingInt(team -> team.countType(member.mbtiType()))
152-
.thenComparingInt(TeamBucket::teamNumber)
153-
)
154-
.orElseThrow();
155-
156-
bucket.add(member);
157-
}
158-
159-
return buckets.stream()
160-
.map(TeamBucket::toResponse)
161-
.toList();
162-
}
163-
164-
private List<MbtiTeamMatchResponse.Member> interleaveByType(
165-
Collection<Deque<MbtiTeamMatchResponse.Member>> queues
166-
) {
167-
List<MbtiTeamMatchResponse.Member> result = new ArrayList<>();
220+
List<MbtiTeamMatchResponse.Member> ordered = new ArrayList<>();
168221
boolean hasRemaining = true;
169222

170223
while (hasRemaining) {
@@ -175,14 +228,34 @@ private List<MbtiTeamMatchResponse.Member> interleaveByType(
175228
continue;
176229
}
177230

178-
result.add(member);
231+
ordered.add(member);
179232
if (!queue.isEmpty()) {
180233
hasRemaining = true;
181234
}
182235
}
183236
}
184237

185-
return result;
238+
return ordered;
239+
}
240+
241+
private static int pairCompatibilityScore(String sourceType, String targetType) {
242+
if (sourceType == null || targetType == null) {
243+
return 0;
244+
}
245+
246+
List<String> sourceMatches = TEAMMATE_COMPATIBILITY.getOrDefault(sourceType, List.of());
247+
List<String> targetMatches = TEAMMATE_COMPATIBILITY.getOrDefault(targetType, List.of());
248+
249+
boolean sourcePrefersTarget = sourceMatches.contains(targetType);
250+
boolean targetPrefersSource = targetMatches.contains(sourceType);
251+
252+
if (sourcePrefersTarget && targetPrefersSource) {
253+
return 3;
254+
}
255+
if (sourcePrefersTarget || targetPrefersSource) {
256+
return 1;
257+
}
258+
return 0;
186259
}
187260

188261
private String normalize(String value) {
@@ -191,16 +264,24 @@ private String normalize(String value) {
191264

192265
private static final class TeamBucket {
193266
private final int teamNumber;
267+
private final int unmatchedTarget;
268+
private final int matchedTarget;
194269
private final List<MbtiTeamMatchResponse.Member> members = new ArrayList<>();
195-
private final Map<String, Integer> typeCounts = new HashMap<>();
270+
private int unmatchedCount;
196271

197-
private TeamBucket(int teamNumber) {
272+
private TeamBucket(int teamNumber, int unmatchedTarget, int matchedTarget) {
198273
this.teamNumber = teamNumber;
274+
this.unmatchedTarget = unmatchedTarget;
275+
this.matchedTarget = matchedTarget;
276+
}
277+
278+
private void addMatched(MbtiTeamMatchResponse.Member member) {
279+
members.add(member);
199280
}
200281

201-
private void add(MbtiTeamMatchResponse.Member member) {
282+
private void addUnmatched(MbtiTeamMatchResponse.Member member) {
202283
members.add(member);
203-
typeCounts.merge(member.mbtiType(), 1, Integer::sum);
284+
unmatchedCount += 1;
204285
}
205286

206287
private int size() {
@@ -211,12 +292,33 @@ private int teamNumber() {
211292
return teamNumber;
212293
}
213294

214-
private int countType(String mbtiType) {
215-
return typeCounts.getOrDefault(mbtiType, 0);
295+
private boolean canAcceptUnmatched() {
296+
return unmatchedCount < unmatchedTarget && size() < unmatchedTarget + matchedTarget;
297+
}
298+
299+
private boolean canAcceptMatched() {
300+
return matchedCount() < matchedTarget && size() < unmatchedTarget + matchedTarget;
301+
}
302+
303+
private int matchedCount() {
304+
return size() - unmatchedCount;
305+
}
306+
307+
private int compatibilityScoreFor(MbtiTeamMatchResponse.Member candidate) {
308+
return members.stream()
309+
.filter(MbtiTeamMatchResponse.Member::hasMbtiResult)
310+
.mapToInt(member -> pairCompatibilityScore(candidate.mbtiType(), member.mbtiType()))
311+
.sum();
216312
}
217313

218314
private MbtiTeamMatchResponse.Team toResponse() {
219315
return new MbtiTeamMatchResponse.Team(teamNumber, List.copyOf(members));
220316
}
221317
}
318+
319+
private record TeamBuildResult(List<MbtiTeamMatchResponse.Team> teams) {
320+
private int assignedCount() {
321+
return teams.stream().mapToInt(team -> team.members().size()).sum();
322+
}
323+
}
222324
}

src/main/java/inha/gdgoc/domain/user/repository/UserRepository.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public interface UserRepository extends JpaRepository<User, Long>, UserRepositor
2222
Optional<User> findByOauthSubject(String oauthSubject);
2323

2424
boolean existsByStudentId(String studentId);
25+
List<User> findByStudentIdIn(Collection<String> studentIds);
2526
boolean existsByPhoneNumber(String phoneNumber);
2627
boolean existsByNameAndEmail(String name, String email);
2728
boolean existsByEmail(String email);

0 commit comments

Comments
 (0)