@@ -55,8 +55,8 @@ public AttendanceGroupV2Response attend(Long userId, Long groupId, String messag
5555 throw new GroupException (GroupErrorCode .USER_ID_NULL );
5656 }
5757
58- // 모임 체크
59- GroupV2 group = groupV2Repository .findById (groupId )
58+ // 모임 체크: for update로 가져오기
59+ GroupV2 group = groupV2Repository .findByIdForUpdate (groupId )
6060 .orElseThrow (
6161 () -> new GroupException (GroupErrorCode .GROUP_NOT_FOUND_BY_ID , groupId ));
6262
@@ -102,6 +102,13 @@ public AttendanceGroupV2Response attend(Long userId, Long groupId, String messag
102102
103103 // 즉시 참여인 경우
104104 if (group .getJoinPolicy () == GroupV2JoinPolicy .FREE ) {
105+ // 정원 체크(ATTEND만 카운트)
106+ long attendCount = groupUserV2Repository .countByGroupIdAndStatus (groupId ,
107+ GroupUserV2Status .ATTEND );
108+ if (attendCount >= group .getMaxParticipants ()) {
109+ throw new GroupException (GroupErrorCode .GROUP_IS_FULL , groupId );
110+ }
111+
105112 if (groupUserV2 != null ) {
106113 // LEFT, KICKED, REJECTED, CANCELLED -> 재참여
107114 groupUserV2 .reAttend (); // 내부에서 BANNED만 막고, ATTEND로 변경
@@ -111,15 +118,10 @@ public AttendanceGroupV2Response attend(Long userId, Long groupId, String messag
111118 groupUserV2Repository .save (groupUserV2 );
112119 }
113120
114- // 정원 체크(ATTEND만 카운트)
115- long attendCount = groupUserV2Repository .countByGroupIdAndStatus (groupId ,
116- GroupUserV2Status .ATTEND );
117- if (attendCount > group .getMaxParticipants ()) {
118- throw new GroupException (GroupErrorCode .GROUP_IS_FULL , groupId );
119- }
120-
121121 // FULL 자동 전환
122- if (attendCount == group .getMaxParticipants ()
122+ long newCount = groupUserV2Repository .countByGroupIdAndStatus (groupId , GroupUserV2Status .ATTEND );
123+
124+ if (newCount == group .getMaxParticipants ()
123125 && group .getStatus () == GroupV2Status .RECRUITING ) {
124126 group .changeStatus (GroupV2Status .FULL );
125127 }
@@ -130,7 +132,7 @@ public AttendanceGroupV2Response attend(Long userId, Long groupId, String messag
130132 eventPublisher .publishEvent (
131133 new GroupJoinedEvent (groupId , group .getHost ().getId (), userId ));
132134
133- return AttendanceGroupV2Response .of (group , attendCount , membership );
135+ return AttendanceGroupV2Response .of (group , newCount , membership );
134136 }
135137
136138 if (joinPolicy == GroupV2JoinPolicy .APPROVAL_REQUIRED ) {
@@ -152,16 +154,6 @@ public AttendanceGroupV2Response attend(Long userId, Long groupId, String messag
152154 long attendCount = groupUserV2Repository .countByGroupIdAndStatus (
153155 groupId ,
154156 GroupUserV2Status .ATTEND );
155- if (attendCount > group .getMaxParticipants ()) {
156- // 방금 reAttend로 늘었는데 초과하면 롤백시키기 위해 예외
157- throw new GroupException (GroupErrorCode .GROUP_IS_FULL , groupId );
158- }
159-
160- // FULL 자동 전환
161- if (attendCount == group .getMaxParticipants ()
162- && group .getStatus () == GroupV2Status .RECRUITING ) {
163- group .changeStatus (GroupV2Status .FULL );
164- }
165157
166158 // 내 멤버십 + 최신 카운트 + 모임 상태 응답
167159 MyMembership membership = MyMembership .from (groupUserV2 );
@@ -180,7 +172,7 @@ public AttendanceGroupV2Response left(Long userId, Long groupId) {
180172 throw new GroupException (GroupErrorCode .USER_ID_NULL );
181173 }
182174
183- GroupV2 group = groupV2Repository .findById (groupId )
175+ GroupV2 group = groupV2Repository .findByIdForUpdate (groupId )
184176 .orElseThrow (
185177 () -> new GroupException (GroupErrorCode .GROUP_NOT_FOUND_BY_ID ,
186178 groupId ));
@@ -241,7 +233,7 @@ public GroupUserV2StatusResponse approve(Long approverUserId, Long groupId,
241233 throw new GroupException (GroupErrorCode .CANNOT_APPROVE_SELF , groupId , approverUserId );
242234 }
243235
244- GroupV2 group = groupV2Repository .findById (groupId )
236+ GroupV2 group = groupV2Repository .findByIdForUpdate (groupId )
245237 .orElseThrow (
246238 () -> new GroupException (GroupErrorCode .GROUP_NOT_FOUND_BY_ID , groupId ));
247239
@@ -285,26 +277,31 @@ public GroupUserV2StatusResponse approve(Long approverUserId, Long groupId,
285277 .orElseThrow (() -> new GroupException (GroupErrorCode .GROUP_USER_NOT_FOUND ,
286278 targetUserId ));
287279
288- // PENDING만 승인 가능 (도메인에서 검증)
289- target .approveJoin ();
290-
291- eventPublisher . publishEvent (
292- new GroupJoinApprovedEvent ( groupId , approverUserId , targetUserId ));
280+ // 서비스 레벨 방어
281+ if ( target .getStatus () != GroupUserV2Status . PENDING ) {
282+ throw new GroupException ( GroupErrorCode . GROUP_USER_STATUS_NOT_ALLOWED_TO_APPROVE ,
283+ groupId , targetUserId , target . getStatus (). name ());
284+ }
293285
294- long attendCount = groupUserV2Repository .countByGroupIdAndStatus (groupId ,
286+ // 사전 정원 체크: 지금 ATTEND가 max-1 이하여야 승인 가능
287+ long beforeCount = groupUserV2Repository .countByGroupIdAndStatus (groupId ,
295288 GroupUserV2Status .ATTEND );
296-
297- if (attendCount > group .getMaxParticipants ()) {
289+ if (beforeCount >= group .getMaxParticipants ()) {
298290 throw new GroupException (GroupErrorCode .GROUP_IS_FULL , groupId );
299291 }
300292
301- // FULL 자동 전환
302- if (attendCount == group .getMaxParticipants ()
303- && group .getStatus () == GroupV2Status .RECRUITING ) {
293+ target .approveJoin (); // PENDING -> ATTEND
294+
295+ long newCount = beforeCount + 1 ;
296+
297+ if (newCount == group .getMaxParticipants () && group .getStatus () != GroupV2Status .FULL ) {
304298 group .changeStatus (GroupV2Status .FULL );
305299 }
306300
307- return GroupUserV2StatusResponse .of (group , attendCount , targetUserId , target );
301+ eventPublisher .publishEvent (
302+ new GroupJoinApprovedEvent (groupId , approverUserId , targetUserId ));
303+
304+ return GroupUserV2StatusResponse .of (group , newCount , targetUserId , target );
308305 }
309306
310307 @ Transactional
@@ -318,7 +315,7 @@ public GroupUserV2StatusResponse reject(Long approverUserId, Long groupId,
318315 throw new GroupException (GroupErrorCode .CANNOT_REJECT_SELF , groupId , approverUserId );
319316 }
320317
321- GroupV2 group = groupV2Repository .findById (groupId )
318+ GroupV2 group = groupV2Repository .findByIdForUpdate (groupId )
322319 .orElseThrow (
323320 () -> new GroupException (GroupErrorCode .GROUP_NOT_FOUND_BY_ID , groupId ));
324321
@@ -381,7 +378,7 @@ public GroupUserV2StatusResponse kick(Long kickerUserId, Long groupId, Long targ
381378 throw new GroupException (GroupErrorCode .GROUP_CANNOT_KICK_SELF , groupId , kickerUserId );
382379 }
383380
384- GroupV2 group = groupV2Repository .findById (groupId )
381+ GroupV2 group = groupV2Repository .findByIdForUpdate (groupId )
385382 .orElseThrow (
386383 () -> new GroupException (GroupErrorCode .GROUP_NOT_FOUND_BY_ID , groupId ));
387384
@@ -436,7 +433,7 @@ public GroupUserV2StatusResponse ban(Long bannerUserId, Long groupId, Long targe
436433 throw new GroupException (GroupErrorCode .GROUP_CANNOT_BAN_SELF , groupId , bannerUserId );
437434 }
438435
439- GroupV2 group = groupV2Repository .findById (groupId )
436+ GroupV2 group = groupV2Repository .findByIdForUpdate (groupId )
440437 .orElseThrow (
441438 () -> new GroupException (GroupErrorCode .GROUP_NOT_FOUND_BY_ID , groupId ));
442439
@@ -547,7 +544,7 @@ public GroupUserV2StatusResponse unban(Long requesterUserId, Long groupId, Long
547544 throw new GroupException (GroupErrorCode .USER_ID_NULL );
548545 }
549546
550- GroupV2 group = groupV2Repository .findById (groupId )
547+ GroupV2 group = groupV2Repository .findByIdForUpdate (groupId )
551548 .orElseThrow (
552549 () -> new GroupException (GroupErrorCode .GROUP_NOT_FOUND_BY_ID , groupId ));
553550
0 commit comments