Conversation
그룹 호스트가 멤버를 추방, 차단 및 차단 해제할 수 있는 기능을 구현합니다. 추방/차단/차단 해제 기능을 지원하기 위해 새로운 오류 코드와 DTO를 도입합니다. 추방/차단/차단 해제 대상 멤버를 효율적으로 가져오기 위한 쿼리 저장소를 추가합니다. 추방 및 차단 기능은 그룹 호스트만 사용할 수 있으며, 모집 중이거나 그룹이 가득 찼을 때만 가능합니다. 호스트는 자신, 다른 호스트 또는 '참석' 상태가 아닌 멤버를 추방/차단할 수 없습니다. 차단 해제는 사용자를 '추방' 상태로 되돌려 원하는 경우 다시 참여할 수 있도록 합니다.
WalkthroughV2 모임 추방 및 차단 기능을 구현합니다. 새로운 에러 코드, DTO, 저장소 쿼리, 서비스 메서드, 엔티티 로직, 컨트롤러 엔드포인트, 그리고 통합 테스트를 추가하여 사용자 강퇴, 차단, 차단 해제 및 대상 목록 조회 기능을 지원합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Ctrl as GroupV2Controller
participant Svc as GroupV2AttendanceService
participant Repo as GroupUserV2QueryRepository
participant Entity as GroupUserV2
participant DB as Database
rect rgb(200, 220, 255)
Note over Client,DB: Ban Flow
Client->>Ctrl: POST /{groupId}/attendance/{targetUserId}/ban
Ctrl->>Svc: ban(bannerUserId, groupId, targetUserId)
Svc->>Repo: 권한 & 상태 검증
Repo->>DB: 사용자 정보 조회
DB-->>Repo: GroupUserV2 반환
Svc->>Entity: ban() 호출 (상태 검증 포함)
Entity-->>Svc: 상태 전환 (ATTEND → BANNED)
Svc->>DB: 변경 사항 저장
Svc-->>Ctrl: GroupUserV2StatusResponse
Ctrl-->>Client: 200 OK (응답)
end
rect rgb(220, 255, 220)
Note over Client,DB: Unban Flow
Client->>Ctrl: POST /{groupId}/attendance/{targetUserId}/unban
Ctrl->>Svc: unban(requesterUserId, groupId, targetUserId)
Svc->>Repo: 권한 검증
Repo->>DB: 차단된 사용자 정보 조회
DB-->>Repo: GroupUserV2 (status=BANNED)
Svc->>Entity: unban() 호출
Entity-->>Svc: 상태 전환 (BANNED → KICKED)
Svc->>DB: 변경 사항 저장
Svc-->>Ctrl: GroupUserV2StatusResponse
Ctrl-->>Client: 200 OK (응답)
end
rect rgb(255, 240, 200)
Note over Client,DB: Get Targets Flow
Client->>Ctrl: GET /{groupId}/attendance/banned-targets
Ctrl->>Svc: getBannedTargets(requesterUserId, groupId)
Svc->>Repo: 권한 검증 후 fetchBannedMembersExceptHost(groupId)
Repo->>DB: status=BANNED인 멤버 목록 조회
DB-->>Repo: List<AttendanceTargetRow>
Repo-->>Svc: 목록 반환
Svc->>Svc: AttendanceTargetItem으로 변환
Svc-->>Ctrl: GetBannedTargetsResponse
Ctrl-->>Client: 200 OK (목록 응답)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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.
Actionable comments posted: 1
🧹 Nitpick comments (5)
src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/GroupUserV2QueryRepositoryImpl.java (1)
39-41: 정적 임포트 일관성 개선을 고려해보세요.
GroupUserV2Status는 import문으로 가져와서 사용하고 있지만,GroupUserV2Role.HOST는 fully qualified name으로 사용하고 있습니다. 일관성을 위해 정적 임포트를 추가하는 것을 고려해보세요.🔎 제안된 수정
import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Status; +import team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Role; import team.wego.wegobackend.group.v2.domain.repository.GroupUserV2QueryRepository;그리고 쿼리에서:
- groupUserV2.groupRole.ne( - team.wego.wegobackend.group.v2.domain.entity.GroupUserV2Role.HOST) + groupUserV2.groupRole.ne(GroupUserV2Role.HOST)Also applies to: 64-66
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java (3)
372-377: 서비스 레이어와 도메인 레이어의 중복 검증서비스에서
target.getStatus() != GroupUserV2Status.ATTEND검증 후target.kick()/target.ban()을 호출하는데, 도메인 메서드 내부에서도 동일한 검증을 수행합니다. 방어적 프로그래밍 관점에서는 괜찮으나, 도메인 로직이 이미 검증을 수행하므로 서비스 레이어의 검증은 선택적입니다.현재 구현을 유지해도 무방하지만, 도메인에 비즈니스 규칙을 위임하는 DDD 관점에서는 서비스 레이어의 검증을 제거하는 것도 고려해볼 수 있습니다.
Also applies to: 424-429
459-470:AttendanceTargetRow→AttendanceTargetItem매핑 로직 중복동일한 매핑 로직이
getKickTargets,getBanTargets,getBannedTargets에서 반복됩니다. 헬퍼 메서드나AttendanceTargetItem에 팩토리 메서드를 추가하면 중복을 줄일 수 있습니다.🔎 제안: AttendanceTargetItem에 팩토리 메서드 추가
// AttendanceTargetItem.java에 추가 public static AttendanceTargetItem from(AttendanceTargetRow row) { return new AttendanceTargetItem( row.userId(), row.nickName(), row.profileImage(), row.groupUserId(), row.status(), row.joinedAt() ); }서비스에서 사용:
List<AttendanceTargetItem> targets = groupUserV2QueryRepository .fetchAttendMembersExceptHost(groupId) .stream() .map(AttendanceTargetItem::from) .toList();Also applies to: 490-501, 557-568
133-141: 승인제 모임에서 정원 체크 주석 개선 필요주석에서 "승인을 할 때만 정원 체크"라고 언급하지만, 바로 아래에서 정원 체크를 수행하고 있습니다. 주석과 코드가 불일치합니다. 이 블록은
APPROVAL_REQUIRED정책에서 재신청(requestJoin) 후 실행되는데, PENDING 상태이므로 ATTEND count에 영향을 주지 않습니다.주석을 명확히 하거나 불필요한 정원 체크를 제거하는 것을 고려해보세요.
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java (1)
11-20: 에러 코드 구성에 대한 선택적 제안새로운 에러 코드들이 파일 상단에 추가되어 기존 approve/reject 관련 코드들(라인 70+)과 분리되어 있습니다. 기능적으로는 문제없지만, 향후 유지보수를 위해 관련 에러 코드들을 논리적으로 그룹화하는 것을 고려해 보세요 (예: 모든 attendance 관련 코드를 한 곳에 모으기).
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/common/AttendanceTargetItem.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBanTargetsResponse.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBannedTargetsResponse.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetKickTargetsResponse.javasrc/main/java/team/wego/wegobackend/group/v2/application/dto/response/GroupUserV2StatusResponse.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/GroupUserV2Role.javasrc/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2QueryRepository.javasrc/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/GroupUserV2QueryRepositoryImpl.javasrc/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/projection/AttendanceTargetRow.javasrc/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.javasrc/test/http/group/v2/v2-group-ban-targets.httpsrc/test/http/group/v2/v2-group-ban-unban.httpsrc/test/http/group/v2/v2-group-kick-targets.httpsrc/test/http/group/v2/v2-group-kick.http
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java (1)
src/main/java/team/wego/wegobackend/group/domain/exception/GroupException.java (1)
GroupException(6-15)
⏰ 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: Agent
- GitHub Check: CodeQL analysis (java)
🔇 Additional comments (22)
src/test/http/group/v2/v2-group-ban-targets.http (1)
1-180: 테스트 시나리오가 포괄적이고 잘 구성되어 있습니다.ban-targets 조회 기능에 대한 테스트가 다음을 잘 커버하고 있습니다:
- 정상 케이스: HOST가 ban-targets 조회
- 권한 검증: MEMBER의 무단 조회 시도
- 엣지 케이스: 참여자가 없는 그룹의 빈 결과
테스트 흐름이 논리적이고 전역 변수를 통한 요청 체이닝도 적절합니다.
src/test/http/group/v2/v2-group-kick.http (1)
1-192: kick 기능 테스트가 매우 포괄적입니다.다음과 같은 중요한 시나리오를 모두 커버하고 있습니다:
- 정상 케이스: HOST가 MEMBER 추방
- 권한 검증: 비HOST의 추방 시도 차단
- 자기 자신 추방 방지
- 중복 추방 시도 처리
- 추방된 멤버의 재참여 시나리오
테스트 구조가 명확하고 상태 전이 검증이 잘 되어 있습니다.
src/test/http/group/v2/v2-group-ban-unban.http (1)
1-221: ban/unban 라이프사이클 테스트가 완벽하게 구성되어 있습니다.다음과 같은 핵심 시나리오를 모두 검증합니다:
- 차단 기능: ATTEND → BANNED 상태 전이
- 차단 해제: BANNED → KICKED 상태 전이
- 차단된 사용자의 재참여 차단
- 차단 해제 후 재참여 허용
- HOST 전용 권한 검증
상태 전이 로직과 권한 경계가 철저히 테스트되었습니다.
src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/projection/AttendanceTargetRow.java (1)
6-14: projection 레코드가 적절하게 설계되었습니다.QueryDSL 조회 결과를 담기 위한 projection 레코드로서:
- 필요한 모든 필드(userId, nickName, profileImage, groupUserId, status, joinedAt, leftAt)가 포함되어 있습니다
- 불변 레코드 패턴을 사용하여 데이터 무결성을 보장합니다
- 타임스탬프 필드(joinedAt, leftAt)를 모두 포함하여 다양한 정렬 요구사항을 지원합니다
src/main/java/team/wego/wegobackend/group/v2/application/dto/common/TargetMembership.java (2)
16-22: from() 팩토리 메서드가 명확하게 구현되었습니다.GroupUserV2 엔티티로부터 TargetMembership을 생성하는 로직이 간결하고 명확합니다. target.getId()와 target.getStatus()를 사용하여 필요한 정보만 추출합니다.
6-14: DTO 단순화가 적절하게 적용되었습니다.타임스탐프 필드(joinedAt, leftAt) 제거와 핵심 필드 유지가 이 DTO의 목적(상태 추적)에 부합합니다. of() 메서드의 시그니처 변경이 명확하고 직관적이며, 코드베이스 내 모든 사용처가 올바르게 업데이트되었습니다(of() 호출 0건, from() 호출 1건 모두 새 서명과 일치).
src/main/java/team/wego/wegobackend/group/v2/domain/repository/GroupUserV2QueryRepository.java (1)
6-11: 쿼리 저장소 인터페이스가 명확하고 잘 설계되었습니다.두 메서드 모두:
- 명확한 네이밍으로 의도가 분명합니다 (ExceptHost 접미사로 HOST 제외를 명시)
- 적절한 반환 타입(List)을 사용합니다
- 단일 책임 원칙을 준수합니다
비즈니스 요구사항(HOST는 추방/차단 대상이 아님)에 잘 부합하는 설계입니다.
src/main/java/team/wego/wegobackend/group/v2/application/dto/common/AttendanceTargetItem.java (1)
6-13: API 응답용 DTO가 적절하게 설계되었습니다.AttendanceTargetItem은 다음과 같은 이유로 잘 설계되었습니다:
- API 응답에 필요한 필드만 포함 (joinedAt은 포함하되 leftAt은 제외)
- AttendanceTargetRow(DB projection용)와 명확히 구분되는 역할
- 불변 레코드 패턴으로 데이터 안정성 보장
- GetKickTargetsResponse, GetBanTargetsResponse 등에서 일관되게 사용
클라이언트 관점에서 필요한 정보만 노출하는 적절한 추상화입니다.
src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2Role.java (1)
5-5: MANAGER 역할의 사용 현황을 명확히 해주세요.코드베이스를 검토한 결과, MANAGER 역할은
GroupUserV2Roleenum에만 정의되어 있으며 실제 기능에서 사용되지 않고 있습니다. 특히:
- 추방/차단 기능: HOST 권한만 허용 (line 354, 406 "✅ HOST only")
- 승인 기능:
getGroupRole() != GroupUserV2Role.MEMBER체크로 비MEMBER 역할 허용 (line 247, 317)- 실제 할당: 코드 전체에서
GroupUserV2Role.MANAGER로 사용자를 할당하는 곳이 없음"미정" 주석이 정확하게 현 상태를 반영하고 있습니다. MANAGER 역할의 정의 및 활용 계획을 명확히 하고, 추가 구현 없이 제거하거나 TODO 주석으로 표시하여 혼란을 방지해주세요.
src/test/http/group/v2/v2-group-kick-targets.http (1)
1-180: LGTM! 테스트 시나리오가 잘 구성되어 있습니다.HOST 권한 성공 케이스, MEMBER 권한 예외 케이스, 그리고 빈 targets 배열 시나리오까지 포괄적으로 테스트하고 있습니다.
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetKickTargetsResponse.java (1)
8-16: LGTM! DTO 구조가 적절합니다.Record 패턴과 정적 팩토리 메서드 사용이 적절합니다.
LocalDateTime.now()사용으로 테스트 시 시간 검증이 어려울 수 있으나, 응답 DTO의 serverTime 필드는 일반적으로 정밀한 테스트가 필요하지 않으므로 현재 구현도 충분합니다.src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBannedTargetsResponse.java (1)
7-16: LGTM!
GetKickTargetsResponse,GetBanTargetsResponse와 동일한 구조로 일관성 있게 구현되었습니다.src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetBanTargetsResponse.java (1)
7-16: LGTM!다른 타겟 응답 DTO들과 일관된 구조입니다.
src/main/java/team/wego/wegobackend/group/v2/infrastructure/querydsl/GroupUserV2QueryRepositoryImpl.java (1)
21-44: LGTM! 쿼리 로직이 적절합니다.HOST를 제외한 ATTEND/BANNED 멤버 조회 로직이 올바르게 구현되었습니다. BANNED 멤버의
leftAt정렬 시nullsLast()처리도 적절합니다.Also applies to: 46-69
src/main/java/team/wego/wegobackend/group/v2/domain/entity/GroupUserV2.java (3)
218-229:unban()로직 확인이 필요합니다.
unban()시 BANNED → KICKED로 전환하고leftAt을 현재 시간으로 업데이트합니다. 이 설계는 사용자가 다시 참여할 수 있도록 하는 의도로 보입니다.reAttend()메서드에서 KICKED 상태는 재참여가 허용되므로 로직이 일관성 있습니다.다만,
leftAt을 업데이트하는 것이 의미적으로 정확한지 확인해주세요. unban 시점이 "떠난 시점"으로 기록되는 것이 비즈니스 요구사항과 일치하는지 검토가 필요합니다.
61-67: LGTM! 생성자 및 팩토리 메서드 리팩토링이 적절합니다.생성자가 status 파라미터를 받도록 변경되어
create()와createPending()메서드가 더 명확해졌습니다.Also applies to: 70-72, 140-145
108-117: LGTM! 예외 처리가 개선되었습니다.
kick()및ban()메서드에서 상세한 컨텍스트(groupId, userId, status)를 포함한 예외를 던지도록 개선되어 디버깅이 용이해졌습니다.Also applies to: 119-130
src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2AttendanceService.java (2)
443-473:getKickTargets와getBanTargets가 동일한 데이터를 반환합니다.두 메서드 모두
fetchAttendMembersExceptHost(groupId)를 호출하여 ATTEND 상태의 멤버를 조회합니다. 이것이 의도된 동작인지 확인이 필요합니다. kick 대상과 ban 대상이 동일하다면, 하나의 엔드포인트로 통합하거나 명확한 주석을 추가하는 것을 고려해보세요.Also applies to: 475-504
339-389: LGTM! kick/ban/unban 핵심 로직이 적절하게 구현되었습니다.
- HOST 권한 검증이 일관되게 적용됨
- 자기 자신 및 HOST 대상 작업 방지
- 모임 상태(RECRUITING/FULL)에서만 작업 허용
- FULL → RECRUITING 자동 전환 로직 적용
Also applies to: 391-441, 506-539
src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GroupUserV2StatusResponse.java (1)
10-36: LGTM! 범용적인 상태 응답 DTO로 적절히 리네이밍되었습니다.approve/reject뿐만 아니라 kick/ban/unban 작업에도 사용되는 범용 응답으로 변경된 것이 적절합니다.
TargetMembership.from()메서드 호출도 올바르게 구현되어 있습니다.src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java (1)
26-32: DTO import 변경이 적절합니다.새로운 기능에 필요한 DTO들(
GetBanTargetsResponse,GetBannedTargetsResponse,GetKickTargetsResponse,GroupUserV2StatusResponse)이 올바르게 import 되었습니다.src/main/java/team/wego/wegobackend/group/domain/exception/GroupErrorCode.java (1)
11-69: 에러 코드 설계가 적절하고 포괄적입니다.kick/ban/unban 기능에 필요한 권한 검증(
NO_PERMISSION_TO_*)과 상태 제약 에러 코드들(GROUP_CANNOT_*,GROUP_USER_STATUS_NOT_ALLOWED_TO_*)이 잘 정의되었으며, HttpStatus 할당도 올바릅니다(권한 문제는 FORBIDDEN, 비즈니스 제약은 BAD_REQUEST).
GROUP_CANNOT_UNBAN_SELF가 없는 것은 의도적으로 올바른 설계입니다. unban은 HOST만 실행 가능하고, HOST는GROUP_CANNOT_UNBAN_HOST에러 코드로 차단되므로 자신을 해제할 수 없습니다. 따라서 별도의 self-unban 에러 코드는 불필요합니다.
| @PostMapping("/{groupId}/attendance/{targetUserId}/kick") | ||
| public ResponseEntity<ApiResponse<GroupUserV2StatusResponse>> kick( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @PathVariable Long groupId, | ||
| @PathVariable Long targetUserId | ||
| ) { | ||
| GroupUserV2StatusResponse response = | ||
| groupV2AttendanceService.kick(userDetails.getId(), groupId, targetUserId); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); | ||
| } | ||
|
|
||
| @PostMapping("/{groupId}/attendance/{targetUserId}/ban") | ||
| public ResponseEntity<ApiResponse<GroupUserV2StatusResponse>> ban( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @PathVariable Long groupId, | ||
| @PathVariable Long targetUserId | ||
| ) { | ||
| GroupUserV2StatusResponse response = | ||
| groupV2AttendanceService.ban(userDetails.getId(), groupId, targetUserId); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); | ||
| } | ||
|
|
||
| @GetMapping("/{groupId}/attendance/kick-targets") | ||
| public ResponseEntity<ApiResponse<GetKickTargetsResponse>> getKickTargets( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @PathVariable Long groupId | ||
| ) { | ||
| GetKickTargetsResponse response = | ||
| groupV2AttendanceService.getKickTargets(userDetails.getId(), groupId); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); | ||
| } | ||
|
|
||
| @GetMapping("/{groupId}/attendance/ban-targets") | ||
| public ResponseEntity<ApiResponse<GetBanTargetsResponse>> getBanTargets( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @PathVariable Long groupId | ||
| ) { | ||
| GetBanTargetsResponse response = | ||
| groupV2AttendanceService.getBanTargets(userDetails.getId(), groupId); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); | ||
| } | ||
|
|
||
| @PostMapping("/{groupId}/attendance/{targetUserId}/unban") | ||
| public ResponseEntity<ApiResponse<GroupUserV2StatusResponse>> unban( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @PathVariable Long groupId, | ||
| @PathVariable Long targetUserId | ||
| ) { | ||
| GroupUserV2StatusResponse response = | ||
| groupV2AttendanceService.unban(userDetails.getId(), groupId, targetUserId); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); | ||
| } | ||
|
|
||
| @GetMapping("/{groupId}/attendance/banned-targets") | ||
| public ResponseEntity<ApiResponse<GetBannedTargetsResponse>> getBannedTargets( | ||
| @AuthenticationPrincipal CustomUserDetails userDetails, | ||
| @PathVariable Long groupId | ||
| ) { | ||
| GetBannedTargetsResponse response = | ||
| groupV2AttendanceService.getBannedTargets(userDetails.getId(), groupId); | ||
|
|
||
| return ResponseEntity.ok(ApiResponse.success(HttpStatus.OK.value(), response)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find GroupV2ControllerDocs interface
echo "=== Searching for GroupV2ControllerDocs interface ==="
fd "GroupV2ControllerDocs" --type f
echo ""
echo "=== Checking content of GroupV2ControllerDocs ==="
fd "GroupV2ControllerDocs" --type f --exec cat {} \;
echo ""
echo "=== Searching for method signatures related to new endpoints ==="
rg "kick|ban|unban|getKickTargets|getBanTargets|getBannedTargets" --type java -g '*ControllerDocs*' -B 2 -A 2Repository: WeGo-Together/WeGo_BackEnd
Length of output: 6874
GroupV2ControllerDocs 인터페이스에 새로운 엔드포인트 메서드 추가 필요
새로운 kick/ban/unban 엔드포인트가 GroupV2Controller에 구현되었으나, 해당 인터페이스인 GroupV2ControllerDocs에는 대응하는 메서드 시그니처가 없습니다.
다음 메서드들을 @operation 어노테이션과 함께 GroupV2ControllerDocs 인터페이스에 추가해야 합니다:
- kick
- ban
- unban
- getKickTargets
- getBanTargets
- getBannedTargets
컨트롤러와 문서화 인터페이스의 메서드 시그니처를 동기화해야 합니다.
🤖 Prompt for AI Agents
In
src/main/java/team/wego/wegobackend/group/v2/presentation/GroupV2Controller.java
around lines 201-268, new endpoints kick/ban/unban and the three target-list
getters were implemented in the controller but the GroupV2ControllerDocs
interface lacks matching method signatures; update GroupV2ControllerDocs to add
corresponding method declarations for kick, ban, unban, getKickTargets,
getBanTargets, and getBannedTargets with the same parameters
(AuthenticationPrincipal CustomUserDetails, @PathVariable Long groupId, and
where applicable @PathVariable Long targetUserId) and return types, and annotate
each with @Operation (providing summary/description as appropriate) so the
controller and the documentation interface are synchronized.
There was a problem hiding this comment.
Pull request overview
This pull request implements member kick, ban, and unban functionality for V2 group meetings, allowing hosts to manage disruptive members. The implementation adds new service methods, controller endpoints, DTOs, error codes, and query repositories to support these features.
Key Changes
- Added kick, ban, and unban operations for group hosts with appropriate permission checks and state validations
- Introduced new query repository methods to efficiently fetch kick targets (ATTEND members) and banned targets (BANNED members)
- Refactored response DTOs, renaming
ApproveRejectGroupV2ResponsetoGroupUserV2StatusResponsefor broader reuse across attendance-related operations
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| v2-group-kick.http | HTTP test scenarios for kick functionality including permission checks and edge cases |
| v2-group-kick-targets.http | HTTP test scenarios for retrieving kickable members list |
| v2-group-ban-unban.http | HTTP test scenarios for ban/unban operations with permission and state validations |
| v2-group-ban-targets.http | HTTP test scenarios for retrieving ban targets and banned members |
| GroupV2Controller.java | Added 6 new endpoints: kick, ban, unban, getKickTargets, getBanTargets, getBannedTargets |
| GroupV2AttendanceService.java | Implemented business logic for kick/ban/unban operations with host-only permission checks |
| GroupUserV2.java | Updated kick/ban methods with proper error handling and added unban method for BANNED→KICKED transition |
| GroupUserV2QueryRepositoryImpl.java | New query repository with optimized queries to fetch ATTEND and BANNED members excluding host |
| GroupUserV2QueryRepository.java | Repository interface defining methods to fetch attendance targets |
| AttendanceTargetRow.java | Projection record for efficient member data retrieval from database |
| GroupUserV2StatusResponse.java | Renamed from ApproveRejectGroupV2Response for consistent use across operations |
| GetKickTargetsResponse.java | New DTO for kick targets list response |
| GetBanTargetsResponse.java | New DTO for ban targets list response |
| GetBannedTargetsResponse.java | New DTO for banned members list response |
| TargetMembership.java | Simplified to include only essential fields (removed joinedAt and leftAt) |
| AttendanceTargetItem.java | New DTO representing individual member in targets list |
| GroupErrorCode.java | Added 19 new error codes for kick/ban/unban operations and permission checks |
| GroupUserV2Role.java | Added clarifying comment for MANAGER role |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| "email": "ban_member2@example.com", | ||
| "password": "Test1234!@#", | ||
| "nickName": "BanMembers2", |
There was a problem hiding this comment.
There is a typo in the nickname. It should be "BanMember2" to be consistent with other member naming (without the extra 's' in "Members").
| "nickName": "BanMembers2", | |
| "nickName": "BanMember2", |
| @RequiredArgsConstructor | ||
| public enum GroupErrorCode implements ErrorCode { | ||
| NO_PERMISSION_TO_VIEW_BANNED_TARGETS(HttpStatus.FORBIDDEN, | ||
| "모임: BANNED(차단) 대상 목록 조회 권한이 없습니다. 모임 ID: %s 회원 ID: %s" |
There was a problem hiding this comment.
The error message uses "BANNED(차단)" which mixes Korean with a parenthetical Korean translation. For consistency with other error messages, consider using only Korean: "모임: 차단된 대상 목록 조회 권한이 없습니다. 모임 ID: %s 회원 ID: %s"
| "모임: BANNED(차단) 대상 목록 조회 권한이 없습니다. 모임 ID: %s 회원 ID: %s" | |
| "모임: 차단된 대상 목록 조회 권한이 없습니다. 모임 ID: %s 회원 ID: %s" |
| this.status = GroupUserV2Status.KICKED; // 재참여는 스스로 가능 | ||
| this.leftAt = LocalDateTime.now(); |
There was a problem hiding this comment.
In the unban() method, leftAt is set to LocalDateTime.now() when transitioning from BANNED to KICKED status. However, since the user was already banned (and leftAt was set during ban()), updating leftAt again may be semantically incorrect. Consider whether leftAt should remain as the original ban time or if it needs to be updated to reflect the unban time. If the latter, consider adding a comment explaining this design decision.
| this.status = GroupUserV2Status.KICKED; // 재참여는 스스로 가능 | |
| this.leftAt = LocalDateTime.now(); | |
| // Keep the original leftAt (ban/leave time); unbanning only relaxes rejoin conditions. | |
| this.status = GroupUserV2Status.KICKED; // 재참여는 스스로 가능 |
| { | ||
| "email": "ban_member1@example.com", | ||
| "password": "Test1234!@#", | ||
| "nickName": "BanMembers1", |
There was a problem hiding this comment.
There is a typo in the nickname. It should be "BanMember1" or "BanMember2" to be consistent with other member naming (without the extra 's' in "Members").
| "nickName": "BanMembers1", | |
| "nickName": "BanMember1", |
| public GroupUserV2StatusResponse kick(Long kickerUserId, Long groupId, Long targetUserId) { | ||
| if (kickerUserId == null || targetUserId == null) { | ||
| throw new GroupException(GroupErrorCode.USER_ID_NULL); | ||
| } | ||
|
|
||
| if (kickerUserId.equals(targetUserId)) { | ||
| throw new GroupException(GroupErrorCode.GROUP_CANNOT_KICK_SELF, groupId, kickerUserId); | ||
| } | ||
|
|
||
| GroupV2 group = groupV2Repository.findById(groupId) | ||
| .orElseThrow( | ||
| () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); | ||
|
|
||
| // ✅ HOST only | ||
| if (!group.getHost().getId().equals(kickerUserId)) { | ||
| throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_KICK, groupId, kickerUserId); | ||
| } | ||
|
|
||
| if (group.getHost().getId().equals(targetUserId)) { | ||
| throw new GroupException(GroupErrorCode.GROUP_CANNOT_KICK_HOST, groupId, targetUserId); | ||
| } | ||
|
|
||
| if (group.getStatus() != GroupV2Status.RECRUITING | ||
| && group.getStatus() != GroupV2Status.FULL) { | ||
| throw new GroupException(GroupErrorCode.GROUP_CANNOT_KICK_IN_STATUS, groupId, | ||
| group.getStatus().name()); | ||
| } | ||
|
|
||
| GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) | ||
| .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, | ||
| targetUserId)); | ||
|
|
||
| if (target.getStatus() != GroupUserV2Status.ATTEND) { | ||
| throw new GroupException( | ||
| GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_KICK, | ||
| groupId, targetUserId, target.getStatus().name() | ||
| ); | ||
| } | ||
|
|
||
| target.kick(); | ||
|
|
||
| long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, | ||
| GroupUserV2Status.ATTEND); | ||
|
|
||
| if (group.getStatus() == GroupV2Status.FULL && attendCount < group.getMaxParticipants()) { | ||
| group.changeStatus(GroupV2Status.RECRUITING); | ||
| } | ||
|
|
||
| return GroupUserV2StatusResponse.of(group, attendCount, targetUserId, target); | ||
| } | ||
|
|
||
| @Transactional | ||
| public GroupUserV2StatusResponse ban(Long bannerUserId, Long groupId, Long targetUserId) { | ||
| if (bannerUserId == null || targetUserId == null) { | ||
| throw new GroupException(GroupErrorCode.USER_ID_NULL); | ||
| } | ||
|
|
||
| if (bannerUserId.equals(targetUserId)) { | ||
| throw new GroupException(GroupErrorCode.GROUP_CANNOT_BAN_SELF, groupId, bannerUserId); | ||
| } | ||
|
|
||
| GroupV2 group = groupV2Repository.findById(groupId) | ||
| .orElseThrow( | ||
| () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); | ||
|
|
||
| // HOST only | ||
| if (!group.getHost().getId().equals(bannerUserId)) { | ||
| throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_BAN, groupId, bannerUserId); | ||
| } | ||
|
|
||
| if (group.getHost().getId().equals(targetUserId)) { | ||
| throw new GroupException(GroupErrorCode.GROUP_CANNOT_BAN_HOST, groupId, targetUserId); | ||
| } | ||
|
|
||
| if (group.getStatus() != GroupV2Status.RECRUITING | ||
| && group.getStatus() != GroupV2Status.FULL) { | ||
| throw new GroupException(GroupErrorCode.GROUP_CANNOT_BAN_IN_STATUS, groupId, | ||
| group.getStatus().name()); | ||
| } | ||
|
|
||
| GroupUserV2 target = groupUserV2Repository.findByGroupIdAndUserId(groupId, targetUserId) | ||
| .orElseThrow(() -> new GroupException(GroupErrorCode.GROUP_USER_NOT_FOUND, | ||
| targetUserId)); | ||
|
|
||
| if (target.getStatus() != GroupUserV2Status.ATTEND) { | ||
| throw new GroupException( | ||
| GroupErrorCode.GROUP_USER_STATUS_NOT_ALLOWED_TO_BAN, | ||
| groupId, targetUserId, target.getStatus().name() | ||
| ); | ||
| } | ||
|
|
||
| target.ban(); | ||
|
|
||
| long attendCount = groupUserV2Repository.countByGroupIdAndStatus(groupId, | ||
| GroupUserV2Status.ATTEND); | ||
|
|
||
| if (group.getStatus() == GroupV2Status.FULL && attendCount < group.getMaxParticipants()) { | ||
| group.changeStatus(GroupV2Status.RECRUITING); | ||
| } | ||
|
|
||
| return GroupUserV2StatusResponse.of(group, attendCount, targetUserId, target); | ||
| } |
There was a problem hiding this comment.
The kick() and ban() methods contain nearly identical validation logic (lines 340-377 and 392-429). Consider extracting the common validation logic into a private helper method to reduce code duplication and improve maintainability. The shared validations include: null checks, self-action check, group retrieval, host permission check, host target check, and group status check.
| public GetKickTargetsResponse getKickTargets(Long requesterUserId, Long groupId) { | ||
| if (requesterUserId == null) { | ||
| throw new GroupException(GroupErrorCode.USER_ID_NULL); | ||
| } | ||
|
|
||
| GroupV2 group = groupV2Repository.findById(groupId) | ||
| .orElseThrow( | ||
| () -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId)); | ||
|
|
||
| // HOST만 조회 가능 | ||
| if (!group.getHost().getId().equals(requesterUserId)) { | ||
| throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_VIEW_KICK_TARGETS, groupId, | ||
| requesterUserId); | ||
| } | ||
|
|
||
| List<AttendanceTargetItem> targets = groupUserV2QueryRepository | ||
| .fetchAttendMembersExceptHost(groupId) | ||
| .stream() | ||
| .map(r -> new AttendanceTargetItem( | ||
| r.userId(), | ||
| r.nickName(), | ||
| r.profileImage(), | ||
| r.groupUserId(), | ||
| r.status(), | ||
| r.joinedAt() | ||
| )) | ||
| .toList(); | ||
|
|
||
| return GetKickTargetsResponse.of(groupId, targets); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public GetBanTargetsResponse getBanTargets(Long requesterUserId, Long groupId) { | ||
| if (requesterUserId == 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.getHost().getId().equals(requesterUserId)) { | ||
| throw new GroupException(GroupErrorCode.NO_PERMISSION_TO_VIEW_BAN_TARGETS, groupId, | ||
| requesterUserId); | ||
| } | ||
|
|
||
| List<AttendanceTargetItem> targets = groupUserV2QueryRepository | ||
| .fetchAttendMembersExceptHost(groupId) | ||
| .stream() | ||
| .map(r -> new AttendanceTargetItem( | ||
| r.userId(), | ||
| r.nickName(), | ||
| r.profileImage(), | ||
| r.groupUserId(), | ||
| r.status(), | ||
| r.joinedAt() | ||
| )) | ||
| .toList(); | ||
|
|
||
| return GetBanTargetsResponse.of(groupId, targets); | ||
| } |
There was a problem hiding this comment.
The getKickTargets() and getBanTargets() methods are virtually identical (lines 444-473 and 476-504), both fetching the same data using fetchAttendMembersExceptHost(). Consider consolidating these into a single method or extracting the common logic into a helper method. If these methods should return different data in the future, the current implementation would require updating both methods.
| public enum GroupUserV2Role { | ||
| HOST, | ||
| MANAGER, | ||
| MANAGER, // 미정 |
There was a problem hiding this comment.
The comment "// 미정" (undecided/TBD) suggests that the MANAGER role's implementation is still pending. Consider clarifying this comment to explain what needs to be decided or when this will be finalized, or remove it if it's no longer relevant to this PR.
| MANAGER, // 미정 | |
| MANAGER, |
| this.status.name() | ||
| ); | ||
| } | ||
| this.status = GroupUserV2Status.KICKED; // 재참여는 스스로 가능 |
There was a problem hiding this comment.
The comment has an extra space before "재참여는". It should be "// 재참여는 스스로 가능" for consistency with Korean spacing conventions.
| this.status = GroupUserV2Status.KICKED; // 재참여는 스스로 가능 | |
| this.status = GroupUserV2Status.KICKED; // 재참여는 스스로 가능 |
| @@ -0,0 +1,17 @@ | |||
| package team.wego.wegobackend.group.v2.application.dto.response; | |||
|
|
|||
|
|
|||
There was a problem hiding this comment.
There is an extra blank line after the package declaration. Remove the blank line on line 3 for consistency with Java code style conventions.
📝 Pull Request
📌 PR 종류
해당하는 항목에 체크해주세요.
✨ 변경 내용
그룹 호스트가 멤버를 추방, 차단 및 차단 해제할 수 있는 기능을 구현합니다.
추방/차단/차단 해제 기능을 지원하기 위해 새로운 오류 코드와 DTO를 도입합니다.
추방/차단/차단 해제 대상 멤버를 효율적으로 가져오기 위한 쿼리 저장소를 추가합니다.
추방 및 차단 기능은 그룹 호스트만 사용할 수 있으며, 모집 중이거나 그룹이 가득 찼을 때만 가능합니다.
호스트는 자신, 다른 호스트 또는 '참석' 상태가 아닌 멤버를 추방/차단할 수 없습니다.
차단 해제는 사용자를 '추방' 상태로 되돌려 원하는 경우 다시 참여할 수 있도록 합니다.
🔍 관련 이슈
🧪 테스트
변경된 기능에 대한 테스트 범위 또는 테스트 결과를 작성해주세요.
🚨 확인해야 할 사항 (Checklist)
PR을 제출하기 전에 아래 항목들을 확인해주세요.
🙋 기타 참고 사항
리뷰어가 참고하면 좋을 만한 추가 설명이 있다면 적어주세요.
Summary by CodeRabbit
릴리스 노트
✏️ Tip: You can customize this high-level summary in your review settings.