-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] V2 모임 승인 허가 및 거절 구현 #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5509e0c
120b338
5bc1b5c
e10e49f
a35b90b
08d7af8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package team.wego.wegobackend.group.v2.application.dto.common; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2; | ||
| import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Status; | ||
|
|
||
| public record TargetMembership( | ||
| Long userId, | ||
| Long groupUserId, | ||
| GroupUserV2Status status, | ||
| LocalDateTime joinedAt, | ||
| LocalDateTime leftAt | ||
| ) { | ||
| public static TargetMembership of(Long userId, GroupUserV2 groupUserV2) { | ||
| return new TargetMembership( | ||
| userId, | ||
| groupUserV2.getId(), | ||
| groupUserV2.getStatus(), | ||
| groupUserV2.getJoinedAt(), | ||
| groupUserV2.getLeftAt() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package team.wego.wegobackend.group.v2.application.dto.response; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import team.wego.wegobackend.group.v2.application.dto.common.TargetMembership; | ||
| import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2; | ||
| import team.wego.wegobackend.group.v2.domain.entity.GroupV2; | ||
| import team.wego.wegobackend.group.v2.domain.entity.GroupV2JoinPolicy; | ||
| import team.wego.wegobackend.group.v2.domain.entity.GroupV2Status; | ||
|
|
||
| public record ApproveRejectGroupV2Response( | ||
| Long groupId, | ||
| GroupV2Status groupStatus, | ||
| GroupV2JoinPolicy joinPolicy, | ||
| long participantCount, | ||
| int maxParticipants, | ||
|
|
||
| TargetMembership targetMembership, | ||
|
|
||
| LocalDateTime 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() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||||||||||||
| import team.wego.wegobackend.group.domain.exception.GroupErrorCode; | ||||||||||||||||
| import team.wego.wegobackend.group.domain.exception.GroupException; | ||||||||||||||||
| import team.wego.wegobackend.group.v2.application.dto.common.MyMembership; | ||||||||||||||||
| import team.wego.wegobackend.group.v2.application.dto.response.ApproveRejectGroupV2Response; | ||||||||||||||||
| import team.wego.wegobackend.group.v2.application.dto.response.AttendanceGroupV2Response; | ||||||||||||||||
| import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2; | ||||||||||||||||
| import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Role; | ||||||||||||||||
|
|
@@ -82,7 +83,7 @@ public AttendanceGroupV2Response attend(Long userId, Long groupId) { | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // 즉시 참여인 경우 | ||||||||||||||||
| if (group.getJoinPolicy() == GroupV2JoinPolicy.INSTANT) { | ||||||||||||||||
| if (group.getJoinPolicy() == GroupV2JoinPolicy.FREE) { | ||||||||||||||||
| if (groupUserV2 != null) { | ||||||||||||||||
| // LEFT, KICKED, REJECTED, CANCELLED -> 재참여 | ||||||||||||||||
| groupUserV2.reAttend(); // 내부에서 BANNED만 막고, ATTEND로 변경 | ||||||||||||||||
|
|
@@ -192,4 +193,138 @@ public AttendanceGroupV2Response left(Long userId, Long groupId) { | |||||||||||||||
| return AttendanceGroupV2Response.of(group, attendCount, membership); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @Transactional | ||||||||||||||||
| public ApproveRejectGroupV2Response approve(Long approverUserId, Long groupId, | ||||||||||||||||
| Long targetUserId) { | ||||||||||||||||
| if (approverUserId == null || targetUserId == null) { | ||||||||||||||||
| throw new GroupException(GroupErrorCode.USER_ID_NULL); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (approverUserId.equals(targetUserId)) { | ||||||||||||||||
| throw new GroupException(GroupErrorCode.CANNOT_APPROVE_SELF, groupId, approverUserId); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| 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); | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+227
to
+245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ 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 |
||||||||||||||||
|
|
||||||||||||||||
| 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, targetUserId, target); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| @Transactional | ||||||||||||||||
| public ApproveRejectGroupV2Response reject(Long approverUserId, Long groupId, | ||||||||||||||||
| Long targetUserId) { | ||||||||||||||||
| if (approverUserId == null || targetUserId == null) { | ||||||||||||||||
| throw new GroupException(GroupErrorCode.USER_ID_NULL); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (approverUserId.equals(targetUserId)) { | ||||||||||||||||
| throw new GroupException(GroupErrorCode.CANNOT_REJECT_SELF, groupId, approverUserId); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| 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); | ||||||||||||||||
|
Comment on lines
+286
to
+287
|
||||||||||||||||
| 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() | |
| ); |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 외 모든 역할)을 재검토할 것 |
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -178,4 +178,33 @@ public void leaveOrCancel() { | |||
| ); | ||||
| } | ||||
| } | ||||
|
|
||||
| public void approveJoin() { | ||||
| if (this.status != GroupUserV2Status.PENDING) { | ||||
| throw new GroupException( | ||||
| GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_APPROVE, | ||||
| this.group.getId(), | ||||
| this.user.getId(), | ||||
| this.status.name() | ||||
| ); | ||||
| } | ||||
| this.status = GroupUserV2Status.ATTEND; | ||||
| this.joinedAt = LocalDateTime.now(); | ||||
|
||||
| this.joinedAt = LocalDateTime.now(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| package team.wego.wegobackend.group.v2.domain.entity; | ||
|
|
||
| public enum GroupV2JoinPolicy { | ||
| INSTANT, // 참여 버튼 누르면 즉시 ATTEND | ||
| FREE, // 참여 버튼 누르면 즉시 ATTEND | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 상수 이름 변경 확인
🤖 Prompt for AI Agents |
||
| APPROVAL_REQUIRED, // 참여 버튼 누르면 신청 상태로 들어가고 HOST 승인 후 ATTEND | ||
| INVITE_ONLY // (미정)초대 받은 사람만 참여 | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.