55import inha .gdgoc .domain .admin .game .dto .response .MbtiTeamMatchResponse ;
66import inha .gdgoc .domain .game .entity .MbtiResult ;
77import inha .gdgoc .domain .game .repository .MbtiResultRepository ;
8+ import inha .gdgoc .domain .user .enums .UserRole ;
9+ import inha .gdgoc .domain .user .repository .UserRepository ;
810import java .util .ArrayDeque ;
911import java .util .ArrayList ;
10- import java .util .Collection ;
1112import java .util .Comparator ;
1213import java .util .Deque ;
1314import java .util .HashMap ;
2425@ RequiredArgsConstructor
2526@ Service
2627public 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}
0 commit comments