Conversation
그룹 호스트 또는 관리자가 보류 중인 사용자 가입 요청을 승인하거나 거부할 수 있는 기능을 구현합니다. 잘못된 사용자 상태 또는 권한 부족과 같은 다양한 승인/거부 시나리오에 대한 새로운 오류 코드를 도입합니다. 대상 사용자의 멤버십 정보를 나타내는 `TargetMembership` DTO를 추가합니다. `GroupV2AttendanceService`에 `approve` 및 `reject` 메서드를 포함하도록 업데이트합니다. 기본 그룹 가입 정책을 FREE로 변경하고 INSTANT를 FREE로 이름을 바꿉니다.
|
Warning Rate limit exceeded@LimdaeIl has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 9 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (2)
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Walkthrough이 PR은 V2 모임의 승인/거절 워크플로우를 구현합니다. 새로운 에러 코드들을 추가하고 도메인 엔티티에 승인·거절 상태전환 메서드를 추가했으며, 참여 정책 상수를 INSTANT에서 FREE로 변경하고 승인/거절용 DTO, 서비스 메서드, 컨트롤러 엔드포인트와 HTTP 테스트를 추가합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as GroupV2Controller
participant Service as GroupV2AttendanceService
participant GroupRepo as GroupV2Repository
participant UserRepo as GroupUserV2Repository
participant Entity as GroupUserV2
Client->>Controller: POST /{groupId}/attendance/{targetUserId}/approve
Controller->>Service: approve(approverUserId, groupId, targetUserId)
Service->>GroupRepo: 그룹 조회/상태 검증
Service->>Service: 참여정책 검증 (APPROVAL_REQUIRED)
Service->>Service: 승인자 권한 검증 (호스트/권한자)
Service->>UserRepo: 대상 GroupUserV2 조회
UserRepo->>Service: GroupUserV2 반환
Service->>Entity: entity.approveJoin()
Entity->>Entity: PENDING 검증 → ATTEND, joinedAt 갱신
Service->>GroupRepo: 참여자 수 확인 → 필요시 그룹 상태 → FULL로 전환
Service->>Controller: ApproveRejectGroupV2Response
Controller->>Client: HTTP 200 + 응답
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR implements approve/reject functionality for group join requests in the V2 group system. It allows group hosts or managers to approve or reject pending user join requests. Additionally, it renames the INSTANT join policy to FREE to better reflect its semantics.
Key changes include:
- New approve and reject endpoints for managing pending join requests
- Introduction of error codes for various approval/rejection scenarios
- Addition of
TargetMembershipDTO to represent target user's membership information - Renaming of
INSTANTjoin policy toFREEacross all test files and production code
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java | Adds approve and reject endpoints for attendance management |
| src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2JoinPolicy.java | Renames INSTANT to FREE for join policy |
| src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2.java | Updates default join policy from INSTANT to FREE |
| src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.java | Implements approveJoin and rejectJoin domain methods with status validation |
| src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java | Implements approve and reject service methods with permission and state validation |
| src/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.java | New response DTO for approve/reject operations |
| src/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.java | New DTO representing target user's membership details |
| src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java | Adds new error codes for approval/rejection scenarios |
| src/test/http/group/v2/v2-group-approve-reject.http | New comprehensive HTTP test scenarios for approve/reject functionality |
| src/test/http/group/v2/*.http | Updates test files to use FREE instead of INSTANT join policy |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 승인제 모임만 가능 | ||
| if (group.getJoinPolicy() != GroupV2JoinPolicy.APPROVAL_REQUIRED) { | ||
| throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, | ||
| groupId); |
There was a problem hiding this comment.
The error code GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED expects two parameters in its message format ("모임 ID: %s 참여 방식: %s"), but it is being called with only one parameter (groupId). This will cause a MissingFormatArgumentException at runtime. You need to either add the join policy as a second parameter or update the error message format to only include the groupId.
| groupId); | |
| groupId, | |
| group.getJoinPolicy().name()); |
| throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, | ||
| groupId); |
There was a problem hiding this comment.
The error code GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED expects two parameters in its message format ("모임 ID: %s 참여 방식: %s"), but it is being called with only one parameter (groupId). This will cause a MissingFormatArgumentException at runtime. You need to either add the join policy as a second parameter or update the error message format to only include the groupId.
| throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, | |
| groupId); | |
| throw new GroupException( | |
| GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, | |
| groupId, | |
| group.getJoinPolicy().name() | |
| ); |
| @Transactional | ||
| public ApproveRejectGroupV2Response approve(Long approverUserId, Long groupId, | ||
| Long targetUserId) { | ||
| if (approverUserId == null || targetUserId == null) { | ||
| throw new GroupException(GroupErrorCode.USER_ID_NULL); | ||
| } | ||
|
|
||
| GroupV2 group = groupV2Repository.findById(groupId) | ||
| .orElseThrow( | ||
| () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); | ||
|
|
||
| // 승인제 모임만 가능 | ||
| if (group.getJoinPolicy() != GroupV2JoinPolicy.APPROVAL_REQUIRED) { | ||
| throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, | ||
| groupId); | ||
| } | ||
|
|
||
| // 모임 상태 정책 | ||
| if (group.getStatus() != GroupV2Status.RECRUITING | ||
| && group.getStatus() != GroupV2Status.FULL) { | ||
| throw new GroupException( | ||
| GroupErrorCode.GROUP_CANNOT_APPROVE_IN_STATUS, | ||
| groupId, | ||
| group.getStatus().name() | ||
| ); | ||
| } | ||
|
|
||
| // 권한 체크: HOST 또는 (그룹 내 MANAGER/… 정책)만 승인 가능 | ||
| // host는 group.getHost()로 체크 | ||
| boolean isHost = group.getHost().getId().equals(approverUserId); | ||
| boolean canApprove = isHost; | ||
|
|
||
| if (!isHost) { | ||
| GroupUserV2 approverMembership = groupUserV2Repository.findByGroupIdAndUserId(groupId, | ||
| approverUserId) | ||
| .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, | ||
| approverUserId)); | ||
|
|
||
| canApprove = approverMembership.getGroupRole() != null | ||
| && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; | ||
| } | ||
|
|
||
| if (!canApprove) { | ||
| throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, | ||
| approverUserId); | ||
| } | ||
|
|
||
| GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) | ||
| .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, | ||
| targetUserId)); | ||
|
|
||
| // PENDING만 승인 가능 (도메인에서 검증) | ||
| target.approveJoin(); | ||
|
|
||
| long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, | ||
| GroupUserV2Status.ATTEND); | ||
|
|
||
| if (attendCount > group.getMaxParticipants()) { | ||
| throw new GroupException(GroupErrorCode.GROUP_IS_FULL, groupId); | ||
| } | ||
|
|
||
| // FULL 자동 전환 | ||
| if (attendCount == group.getMaxParticipants() | ||
| && group.getStatus() == GroupV2Status.RECRUITING) { | ||
| group.changeStatus(GroupV2Status.FULL); | ||
| } | ||
|
|
||
| return ApproveRejectGroupV2Response.of(group, attendCount, target); | ||
| } | ||
|
|
||
| @Transactional | ||
| public ApproveRejectGroupV2Response reject(Long approverUserId, Long groupId, | ||
| Long targetUserId) { | ||
| if (approverUserId == null || targetUserId == null) { | ||
| throw new GroupException(GroupErrorCode.USER_ID_NULL); | ||
| } | ||
|
|
||
| GroupV2 group = groupV2Repository.findById(groupId) | ||
| .orElseThrow( | ||
| () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); | ||
|
|
||
| if (group.getJoinPolicy() != GroupV2JoinPolicy.APPROVAL_REQUIRED) { | ||
| throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, | ||
| groupId); | ||
| } | ||
|
|
||
| if (group.getStatus() != GroupV2Status.RECRUITING | ||
| && group.getStatus() != GroupV2Status.FULL) { | ||
| throw new GroupException( | ||
| GroupErrorCode.GROUP_CANNOT_REJECT_IN_STATUS, | ||
| groupId, | ||
| group.getStatus().name() | ||
| ); | ||
| } | ||
|
|
||
| boolean isHost = group.getHost().getId().equals(approverUserId); | ||
| boolean canReject = isHost; | ||
|
|
||
| if (!isHost) { | ||
| GroupUserV2 approverMembership = groupUserV2Repository.findByGroupIdAndUserId(groupId, | ||
| approverUserId) | ||
| .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, | ||
| approverUserId)); | ||
|
|
||
| canReject = approverMembership.getGroupRole() != null | ||
| && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; // 예시 | ||
| } | ||
|
|
||
| if (!canReject) { | ||
| throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, | ||
| approverUserId); | ||
| } | ||
|
|
||
| GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) | ||
| .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, | ||
| targetUserId)); | ||
|
|
||
| target.rejectJoin(); | ||
|
|
||
| long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, | ||
| GroupUserV2Status.ATTEND); | ||
|
|
||
| // reject는 ATTEND 수가 바뀌지 않는 게 일반적이지만(대상은 PENDING), | ||
| // 혹시라도 상태 정책이 바뀌더라도 count는 최신으로 내려가도록 유지 | ||
| return ApproveRejectGroupV2Response.of(group, attendCount, target); | ||
| } |
There was a problem hiding this comment.
There is significant code duplication between the approve and reject methods. Both methods share identical validation logic for checking join policy, group status, and user permissions. Consider extracting the common validation logic into a private helper method to improve maintainability and reduce duplication. This would make the code easier to maintain and reduce the risk of inconsistencies if validation logic needs to change in the future.
| ); | ||
| } | ||
| this.status = GroupUserV2Status.ATTEND; | ||
| this.joinedAt = LocalDateTime.now(); |
There was a problem hiding this comment.
The joinedAt timestamp is being set when approving a join request, but this is semantically incorrect. The joinedAt timestamp was already set when the user initially made the PENDING request. Approving should only change the status to ATTEND and should not modify joinedAt. Only leftAt should be cleared. This could cause confusion when tracking when a user actually requested to join versus when they were approved.
| this.joinedAt = LocalDateTime.now(); |
| approverUserId)); | ||
|
|
||
| canReject = approverMembership.getGroupRole() != null | ||
| && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; // 예시 |
There was a problem hiding this comment.
The inline comment "예시" (example) on line 301 suggests that this is example code rather than production-ready logic. This indicates that the role-based permission logic may not be finalized. Consider removing this comment if the logic is correct, or if it's still under consideration, add a TODO comment to clarify what needs to be decided or implemented.
| && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; // 예시 | |
| && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; // TODO: 권한 정책 확정 후, REJECT 가능 역할 조건(현재: MEMBER 외 모든 역할)을 재검토할 것 |
| } | ||
|
|
||
| if (!canReject) { | ||
| throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, |
There was a problem hiding this comment.
The error code NO_PERMISSION_TO_APPROVE_JOIN is used in the reject method, but this error message says "참여 승인/거절 권한이 없습니다" (no permission to approve/reject join). While the message is technically correct for both operations, using an error code named specifically for approving in the reject method is inconsistent and could cause confusion during debugging or error tracking. Consider creating a separate NO_PERMISSION_TO_REJECT_JOIN error code or renaming the existing one to NO_PERMISSION_TO_MANAGE_JOIN_REQUEST to better represent both operations.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/test/http/group/v2/v2-group-attend-free.http (1)
52-66: 테스트 설명과 실제 값의 불일치를 수정하세요.Line 52의 주석과 Line 58의 제목, Line 65의 설명에서 여전히 "INSTANT"를 언급하고 있지만, Line 59에서는 실제로 "FREE"를 사용하고 있습니다. 테스트의 명확성을 위해 모든 참조를 "FREE"로 업데이트해야 합니다.
🔎 제안하는 수정
-### 1-2. 모임 V2 생성 (즉시 참여 INSTANT) +### 1-2. 모임 V2 생성 (즉시 참여 FREE) POST http://localhost:8080/api/v2/groups/create Content-Type: application/json Authorization: Bearer {{host2AccessToken}} { - "title": "V2 즉시참여(INSTANT) - attend/left/reAttend 테스트", + "title": "V2 즉시참여(FREE) - attend/left/reAttend 테스트", "joinPolicy": "FREE", "location": "서울 서초구", "locationDetail": "교대역 1번 출구 근처 카페", "startTime": "2026-12-20T19:00:00", "endTime": "2026-12-20T21:00:00", "tags": ["v2", "attendance", "left", "instant"], - "description": "INSTANT: attend 즉시 ATTEND, left는 ATTEND->LEFT, 재참여 가능", + "description": "FREE: attend 즉시 ATTEND, left는 ATTEND->LEFT, 재참여 가능", "maxParticipants": 5,
🧹 Nitpick comments (4)
src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.java (1)
182-194: approveJoin 시 joinedAt 타임스탬프 의미 재검토 권장Line 192에서
joinedAt을LocalDateTime.now()로 갱신합니다. 그러나 사용자가 PENDING 상태로 생성될 때 이미joinedAt이 설정되어 있습니다(Line 132 참조).비즈니스 요구사항 확인이 필요합니다:
- 승인 시점을 기록하려면 현재 구현이 적절합니다.
- 최초 신청 시점을 유지하려면 Line 192를 제거하거나 별도 필드(예:
approvedAt)를 추가해야 합니다.신청 이력 추적이나 분석을 위해 최초 신청 시점 정보가 필요할 수 있습니다.
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.java (1)
22-36: serverTime 타임존 처리 고려 권장Line 34에서
LocalDateTime.now()를 사용하여 서버 시간을 기록합니다.LocalDateTime은 타임존 정보를 포함하지 않으므로, 다양한 지역의 클라이언트와 통신할 때 혼란을 야기할 수 있습니다.명시적으로 UTC를 사용하거나
ZonedDateTime을 사용하는 것을 고려하세요:Instant.now() // UTC 타임스탬프 // 또는 ZonedDateTime.now(ZoneId.of("UTC"))src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java (1)
196-264: approve 메서드의 정원 체크 타이밍 이슈Line 248에서
target.approveJoin()을 호출하여 상태를 ATTEND로 변경한 후, Line 250-255에서 정원 초과를 확인합니다. 만약 정원이 초과되면 예외가 발생하지만, 이미 상태가 변경되었습니다.
@Transactional로 인해 롤백되지만, 정원 확인을approveJoin()호출 전에 수행하는 것이 더 명확하고 안전합니다:🔎 권장 수정안
+ // 정원 체크를 먼저 수행 + long currentAttendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, + GroupUserV2Status.ATTEND); + if (currentAttendCount >= group.getMaxParticipants()) { + throw new GroupException(GroupErrorCode.GROUP_IS_FULL, groupId); + } + // PENDING만 승인 가능 (도메인에서 검증) target.approveJoin(); long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, GroupUserV2Status.ATTEND); - - if (attendCount > group.getMaxParticipants()) { - throw new GroupException(GroupErrorCode.GROUP_IS_FULL, groupId); - }src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java (1)
39-42: 선택적 에러 코드의 필요성을 명확히 해주세요.주석에 "필요하면(선택)"이라고 명시되어 있어, 이 에러 코드의 필요성이 불명확합니다.
다음 중 하나를 선택해 주세요:
- 이 에러 코드가 명확히 필요한 시나리오가 있다면, 주석을 구체적인 사용 사례로 업데이트
- 기존
GROUP_USER_NOT_PENDING_STATUS로 충분하다면, 이 에러 코드 제거불확실한 상태로 에러 코드를 추가하면 향후 일관성 없는 사용으로 이어질 수 있습니다.
이 에러 코드의 사용 시나리오를 명확히 하는 데 도움이 필요하시면 말씀해 주세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.javasrc/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.javasrc/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.javasrc/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2.javasrc/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2JoinPolicy.javasrc/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.javasrc/test/http/group/v2/V2-group-left.httpsrc/test/http/group/v2/v2-group-approve-reject.httpsrc/test/http/group/v2/v2-group-attend-free.httpsrc/test/http/group/v2/v2-group-create.httpsrc/test/http/group/v2/v2-group-delete.httpsrc/test/http/group/v2/v2-group-get-list.httpsrc/test/http/group/v2/v2-group-get-me.httpsrc/test/http/group/v2/v2-group-get.httpsrc/test/http/group/v2/v2-group-update.http
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: CodeQL analysis (java)
- GitHub Check: Agent
🔇 Additional comments (16)
src/test/http/group/v2/V2-group-left.http (1)
61-61: LGTM!joinPolicy를 "FREE"로 올바르게 업데이트했습니다.
src/test/http/group/v2/v2-group-get-list.http (1)
63-63: LGTM!joinPolicy를 "FREE"로 올바르게 업데이트했습니다.
src/test/http/group/v2/v2-group-get.http (2)
64-64: LGTM!joinPolicy를 "FREE"로 올바르게 업데이트했습니다.
105-105: LGTM!joinPolicy를 "FREE"로 올바르게 업데이트했습니다.
src/test/http/group/v2/v2-group-get-me.http (1)
60-60: LGTM!joinPolicy를 "FREE"로 올바르게 업데이트했습니다.
src/test/http/group/v2/v2-group-delete.http (2)
60-60: LGTM!joinPolicy를 "FREE"로 올바르게 업데이트했습니다.
155-155: LGTM!joinPolicy를 "FREE"로 올바르게 업데이트했습니다.
src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2.java (1)
99-99: 이 리뷰 의견은 정정이 필요합니다.코드베이스 검증 결과, 원본 리뷰의 기본 전제가 정확하지 않습니다:
- GroupV2JoinPolicy 열거형에 INSTANT 값이 존재하지 않습니다. 현재 열거형은 FREE, APPROVAL_REQUIRED, INVITE_ONLY만 포함합니다.
- 코드 히스토리에서도 INSTANT 참조가 없습니다.
- CreateGroupV2Request에서 joinPolicy는 @NotNull로 표시되어 있어 필수 항목이므로 null이 전달될 수 없습니다.
따라서 "INSTANT에서 FREE로의 기본값 변경"은 실제로 발생하지 않았습니다. 99번 줄의 null 체크는 방어적 프로그래밍일 뿐입니다.
Likely an incorrect or invalid review comment.
src/test/http/group/v2/v2-group-create.http (1)
62-62: 테스트 데이터 업데이트 확인INSTANT에서 FREE로의 joinPolicy 변경이 모든 테스트 케이스에 일관되게 적용되었습니다.
Also applies to: 129-129, 190-190, 281-281
src/test/http/group/v2/v2-group-update.http (1)
63-63: 테스트 데이터 업데이트 확인joinPolicy 필드가 모든 모임 생성 테스트에 올바르게 추가되었습니다.
Also applies to: 215-215, 300-300
src/test/http/group/v2/v2-group-approve-reject.http (1)
1-202: 승인/거절 워크플로우 테스트 커버리지 우수새로운 테스트 파일이 다음을 포괄적으로 검증합니다:
- 정상 승인/거절 플로우
- 중복 승인/거절 시도
- 권한 검증
- 상태별 나가기 동작
src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.java (1)
196-207: rejectJoin 메서드 구현 확인거절 로직이 올바르게 구현되었습니다. PENDING 상태만 거절 가능하며, 적절한 타임스탬프 업데이트가 수행됩니다.
src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java (1)
184-194: reject 엔드포인트 구현 확인거절 엔드포인트가 승인 엔드포인트와 일관된 패턴으로 구현되었습니다.
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java (2)
86-86: joinPolicy 변경 확인INSTANT에서 FREE로의 변경이 올바르게 적용되었습니다.
266-321: reject 메서드 전반적 구현 확인에러 코드 이슈를 제외하면, 거절 로직의 전반적인 구조와 흐름은 적절합니다.
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java (1)
12-42: 미사용 에러 코드를 삭제해주세요.
GROUP_USER_NOT_PENDING_STATUS는 코드에서 정의되었으나 어디서도 사용되지 않습니다. 불필요한 복잡도를 줄이기 위해 제거하거나, 실제로 사용할 장소가 있다면 해당 메서드에서 활용해주세요.참고:
GROUP_USER_STATUS_NOT_ALLOWED_TO_APPROVE와GROUP_USER_STATUS_NOT_ALLOWED_TO_REJECT는 승인/거절 메서드에서 각각 사용되고 있으므로 유지가 필요합니다.Likely an incorrect or invalid review comment.
| public static TargetMembership from(GroupUserV2 gu) { | ||
| return new TargetMembership( | ||
| gu.getUser().getId(), | ||
| gu.getId(), | ||
| gu.getStatus(), | ||
| gu.getJoinedAt(), | ||
| gu.getLeftAt() | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find GroupUserV2 class and check @ManyToOne mapping for user field
fd GroupUserV2 --type f --extension java | head -20Repository: WeGo-Together/WeGo_BackEnd
Length of output: 397
🏁 Script executed:
# Read GroupUserV2 entity class to check the @ManyToOne mapping
cat -n src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 8454
🏁 Script executed:
# Read GroupUserV2Repository to check for fetch join queries
cat -n src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2Repository.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 1441
🏁 Script executed:
# Search for usages of TargetMembership.from() method
rg -n "TargetMembership\.from|\.from\(" src/main/java/team/wego/wegobackend/group/v2 -A 3 -B 3Repository: WeGo-Together/WeGo_BackEnd
Length of output: 18154
🏁 Script executed:
# Check ApproveRejectGroupV2Response to see how target is obtained
cat -n src/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 1538
🏁 Script executed:
# Check TargetMembership class definition
cat -n src/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 918
🏁 Script executed:
# Search for where ApproveRejectGroupV2Response is instantiated
rg -n "ApproveRejectGroupV2Response\." src/main/java/team/wego/wegobackend/group/v2/application -B 5 -A 5Repository: WeGo-Together/WeGo_BackEnd
Length of output: 2674
🏁 Script executed:
# Check GroupV2AttendanceService approve and reject methods to see how target is obtained
cat -n src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java | head -330 | tail -100Repository: WeGo-Together/WeGo_BackEnd
Length of output: 4790
🏁 Script executed:
# Check the complete approve method
sed -n '230,265p' src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 1441
🏁 Script executed:
# Search for all usages of findByGroupIdAndUserId (which doesn't have fetch join)
rg -n "findByGroupIdAndUserId" src/main/java/team/wego/wegobackend/group/v2 -B 5 -A 10Repository: WeGo-Together/WeGo_BackEnd
Length of output: 14619
🏁 Script executed:
# Check if there are other places where TargetMembership.from() is called
rg -n "TargetMembership\.from" src/main/java/team/wego/wegobackend/group/v2 -B 3 -A 3Repository: WeGo-Together/WeGo_BackEnd
Length of output: 1035
🏁 Script executed:
# Check if there's any way these GroupUserV2 objects could be accessed outside transaction
rg -n "@Transactional|\.findByGroupIdAndUserId" src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 767
GroupUserV2.user 지연 로딩 방지를 위해 fetch join 쿼리 사용 필요
findByGroupIdAndUserId()는 fetch join이 없어서 user 필드가 지연 로딩되며, 16번 줄의 gu.getUser().getId() 호출 시 추가 쿼리가 발생합니다. 현재는 @Transactional 범위 내에서 실행되어 작동하지만, 트랜잭션 외부에서 접근하면 LazyInitializationException이 발생할 수 있습니다.
findByGroupIdAndUserId(Long groupId, Long userId) 메서드에 fetch join을 추가하거나, user를 함께 로드하는 별도 메서드를 생성하세요.
| // 권한 체크: HOST 또는 (그룹 내 MANAGER/… 정책)만 승인 가능 | ||
| // host는 group.getHost()로 체크 | ||
| boolean isHost = group.getHost().getId().equals(approverUserId); | ||
| boolean canApprove = isHost; | ||
|
|
||
| if (!isHost) { | ||
| GroupUserV2 approverMembership = groupUserV2Repository.findByGroupIdAndUserId(groupId, | ||
| approverUserId) | ||
| .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, | ||
| approverUserId)); | ||
|
|
||
| canApprove = approverMembership.getGroupRole() != null | ||
| && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; | ||
| } | ||
|
|
||
| if (!canApprove) { | ||
| throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, | ||
| approverUserId); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
권한 검증 로직 중복
approve와 reject 메서드에서 동일한 권한 검증 로직이 반복됩니다(lines 223-241, 291-307). 이를 별도 메서드로 추출하여 중복을 제거하고 유지보수성을 향상시키세요.
🔎 리팩터링 제안
private void validateApprovalPermission(Long userId, Long groupId, GroupV2 group) {
boolean isHost = group.getHost().getId().equals(userId);
if (isHost) {
return;
}
GroupUserV2 membership = groupUserV2Repository.findByGroupIdAndUserId(groupId, userId)
.orElseThrow(() -> new GroupException(
GroupErrorCode.GROUP_USER_NOT_FOUND, userId));
boolean hasPermission = membership.getGroupRole() != null
&& membership.getGroupRole() != GroupUserV2Role.MEMBER;
if (!hasPermission) {
throw new GroupException(
GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, userId);
}
}그런 다음 approve/reject 메서드에서:
validateApprovalPermission(approverUserId, groupId, group);Also applies to: 291-307
🤖 Prompt for AI Agents
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java
lines 223-241 and 291-307: duplicate permission-check logic should be extracted
into a private helper method; implement a method (e.g.,
validateApprovalPermission(Long userId, Long groupId, GroupV2 group)) that
returns early if user is host, otherwise loads membership via
groupUserV2Repository.findByGroupIdAndUserId(...).orElseThrow(GROUP_USER_NOT_FOUND
with userId), then verifies membership.getGroupRole() is not null and not
GroupUserV2Role.MEMBER and throws NO_PERMISSION_TO_APPROVE_JOIN with groupId and
userId if not; replace the duplicated blocks in both approve and reject with a
single call to this new helper.
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java
Show resolved
Hide resolved
|
|
||
| public enum GroupV2JoinPolicy { | ||
| INSTANT, // 참여 버튼 누르면 즉시 ATTEND | ||
| FREE, // 참여 버튼 누르면 즉시 ATTEND |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "GroupV2JoinPolicy.java" 2>/dev/null | head -20Repository: WeGo-Together/WeGo_BackEnd
Length of output: 153
🏁 Script executed:
# Search for any references to INSTANT in Java files (excluding the main file itself)
rg -n '\bINSTANT\b' --type=java 2>/dev/null | head -50Repository: WeGo-Together/WeGo_BackEnd
Length of output: 52
🏁 Script executed:
# Search for references to "INSTANT" in test HTTP files
rg -n '"INSTANT"' src/test 2>/dev/null | head -50Repository: WeGo-Together/WeGo_BackEnd
Length of output: 52
🏁 Script executed:
cat -n ./src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2JoinPolicy.javaRepository: WeGo-Together/WeGo_BackEnd
Length of output: 367
🏁 Script executed:
# Check for INSTANT in configuration files, JSON, YAML, properties
rg -n 'INSTANT' --type-add 'config:*.{properties,yml,yaml,json,xml}' -tcfg 2>/dev/null || echo "No config files with INSTANT found"Repository: WeGo-Together/WeGo_BackEnd
Length of output: 104
🏁 Script executed:
# Search for INSTANT in all non-Java files (broader search)
rg -n '\bINSTANT\b' --type-list | grep -v java | head -20
# Then search those types
rg -n '\bINSTANT\b' -g '!*.java' 2>/dev/null | head -50Repository: WeGo-Together/WeGo_BackEnd
Length of output: 798
🏁 Script executed:
# Verify the enum is correctly used in other classes
rg -n 'GroupV2JoinPolicy\.' --type=java -A 2 2>/dev/null | head -100Repository: WeGo-Together/WeGo_BackEnd
Length of output: 2389
enum 상수 이름 변경 확인
INSTANT에서 FREE로 enum 상수명이 변경되었습니다. Java 코드베이스의 모든 참조가 올바르게 업데이트되었으나, 테스트 파일 src/test/http/group/v2/v2-group-attend-free.http (라인 52, 58, 65)의 주석에서 "INSTANT"에 대한 설명이 여전히 남아있습니다. 이 문서 주석도 "FREE"로 업데이트하세요.
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupV2JoinPolicy.java
line 4 the enum constant was renamed from INSTANT to FREE; update the related
test documentation comments in src/test/http/group/v2/v2-group-attend-free.http
(around lines 52, 58, 65) to replace any remaining "INSTANT" mentions with
"FREE" so the comments match the current enum name.
src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java
Show resolved
Hide resolved
`TargetMembership` 생성 시 사용자 ID를 입력받도록 수정합니다. 이 변경 사항은 `ApproveRejectGroupV2Response`에서 멤버십 정보와 올바른 사용자 ID가 연결되도록 하여, 그룹 승인 또는 거부 시 사용자 ID가 제대로 설정되지 않던 문제를 해결합니다.
그룹 내에서 사용자가 자신의 가입 요청을 승인하거나 거부하는 것을 방지하는 검사 기능을 구현합니다. 이 개선 사항은 사용자가 자신의 상태를 조작할 수 없도록 하여 그룹 관리 프로세스의 무결성을 유지합니다.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (5)
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java (5)
223-241: 권한 검증 로직 중복 제거를 권장합니다.
approve와reject메서드 간에 동일한 권한 검증 로직이 중복됩니다 (lines 223-241, 291-307). 별도 헬퍼 메서드로 추출하면 유지보수성이 향상됩니다.🔎 리팩터링 제안
private void validateApprovalPermission(Long userId, Long groupId, GroupV2 group) { boolean isHost = group.getHost().getId().equals(userId); if (isHost) { return; } GroupUserV2 membership = groupUserV2Repository.findByGroupIdAndUserId(groupId, userId) .orElseThrow(() -> new GroupException( GroupErrorCode.GROUP_USER_NOT_FOUND, userId)); boolean hasPermission = membership.getGroupRole() != null && membership.getGroupRole() != GroupUserV2Role.MEMBER; if (!hasPermission) { throw new GroupException( GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, userId); } }
208-211: 오류 코드 파라미터 누락으로 런타임 예외 발생.
GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED오류 코드는 메시지 포맷에 2개의 파라미터를 요구하지만 (모임 ID, 참여 방식), Line 210에서는groupId하나만 전달하고 있습니다. 이는 런타임에MissingFormatArgumentException을 발생시킵니다.🔎 수정 제안
if (group.getJoinPolicy() != GroupV2JoinPolicy.APPROVAL_REQUIRED) { throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, - groupId); + groupId, + group.getJoinPolicy().name()); }
277-280: 오류 코드 파라미터 누락으로 런타임 예외 발생.
approve메서드와 동일한 문제입니다.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED오류 코드는 2개의 파라미터를 요구하지만 Line 279에서는groupId하나만 전달하고 있습니다.🔎 수정 제안
if (group.getJoinPolicy() != GroupV2JoinPolicy.APPROVAL_REQUIRED) { throw new GroupException(GroupErrorCode.GROUP_JOIN_POLICY_NOT_APPROVAL_REQUIRED, - groupId); + groupId, + group.getJoinPolicy().name()); }
300-301: 권한 정책 확정 여부 확인이 필요합니다.Line 301의 "예시" 주석은 역할 기반 권한 로직이 아직 최종 확정되지 않았음을 시사합니다. 현재 로직이 정확하다면 주석을 제거하고, 추가 검토가 필요하다면 TODO 주석으로 명확히 해주세요.
🔎 제안
canReject = approverMembership.getGroupRole() != null - && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; // 예시 + && approverMembership.getGroupRole() != GroupUserV2Role.MEMBER; + // TODO: 권한 정책 확정 후, REJECT 가능 역할 조건 재검토
304-306: reject 메서드에서 잘못된 오류 코드 사용.Line 305에서
NO_PERMISSION_TO_APPROVE_JOIN오류 코드를 사용하고 있으나, 이는 거절(reject) 작업에 적합하지 않습니다. 별도의NO_PERMISSION_TO_REJECT_JOIN오류 코드를 사용하거나, 두 작업 모두에 적용 가능한 일반적인 오류 코드(예:NO_PERMISSION_TO_MANAGE_JOIN_REQUEST)로 변경해야 합니다.🔎 수정 제안
옵션 1: 별도의 reject 전용 오류 코드 생성
if (!canReject) { - throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_REJECT_JOIN, groupId, approverUserId); }옵션 2: 일반적인 오류 코드 사용
if (!canReject) { - throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_APPROVE_JOIN, groupId, + throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_MANAGE_JOIN_REQUEST, groupId, approverUserId); }참고: 선택한 오류 코드가
GroupErrorCode에 정의되어 있는지 확인하세요.
🧹 Nitpick comments (1)
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.java (1)
22-37: 서버 시간에 타임존 정보 포함을 고려하세요.
LocalDateTime.now()(Line 35)는 타임존 정보를 포함하지 않아 클라이언트가 다른 시간대에 있을 경우 혼란을 초래할 수 있습니다. API 응답에는 타임존 정보를 포함하는 것이 권장됩니다.🔎 개선 제안
다음 중 하나를 고려하세요:
방법 1: Instant 사용 (권장)
-import java.time.LocalDateTime; +import java.time.Instant; public record ApproveRejectGroupV2Response( Long groupId, GroupV2Status groupStatus, GroupV2JoinPolicy joinPolicy, long participantCount, int maxParticipants, TargetMembership targetMembership, - LocalDateTime serverTime + Instant serverTime ) { public static ApproveRejectGroupV2Response of( GroupV2 group, long participantCount, Long targetUserId, GroupUserV2 target ) { return new ApproveRejectGroupV2Response( group.getId(), group.getStatus(), group.getJoinPolicy(), participantCount, group.getMaxParticipants(), TargetMembership.of(targetUserId, target), - LocalDateTime.now() + Instant.now() ); } }방법 2: ZonedDateTime 사용
-import java.time.LocalDateTime; +import java.time.ZonedDateTime; - LocalDateTime serverTime + ZonedDateTime serverTime - LocalDateTime.now() + ZonedDateTime.now()
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/response/ApproveRejectGroupV2Response.javasrc/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.java
🔇 Additional comments (1)
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java (1)
86-86: 정책 이름 변경이 적절합니다.
INSTANT에서FREE로의 정책 이름 변경이 PR 목표와 일치하며, 즉시 참여를 나타내는 더 명확한 이름입니다.
📝 Pull Request
📌 PR 종류
해당하는 항목에 체크해주세요.
✨ 변경 내용
그룹 호스트 또는 관리자가 보류 중인 사용자 가입 요청을 승인하거나 거부할 수 있는 기능을 구현합니다.
잘못된 사용자 상태 또는 권한 부족과 같은 다양한 승인/거부 시나리오에 대한 새로운 오류 코드를 도입합니다.
대상 사용자의 멤버십 정보를 나타내는
TargetMembershipDTO를 추가합니다.GroupV2AttendanceService에approve및reject메서드를 포함하도록 업데이트합니다.기본 그룹 가입 정책을 FREE로 변경하고 INSTANT를 FREE로 이름을 바꿉니다.
🔍 관련 이슈
🧪 테스트
변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.
🚨 확인해야 할 사항 (Checklist)
PR을 제출하기 전에 아래 항목들을 확인해주세요.
🙋 기타 참고 사항
리뷰어가 참고하면 좋을 만한 추가 설명이 있다면 적어주세요.
Summary by CodeRabbit
릴리스 노트
New Features
Bug Fixes / Improvements
Tests
✏️ Tip: You can customize this high-level summary in your review settings.