diff --git a/src/asciidoc/api/group.adoc b/src/asciidoc/api/group.adoc index e931fef3..d70f3d36 100644 --- a/src/asciidoc/api/group.adoc +++ b/src/asciidoc/api/group.adoc @@ -84,6 +84,27 @@ include::{snippets}/group-controller-rest-docs-test/create-group_success/http-re include::{snippets}/group-controller-rest-docs-test/create-group_success/response-headers.adoc[] include::{snippets}/group-controller-rest-docs-test/create-group_success/http-response.adoc[] +==== 1-3. 그룹 탈퇴 + +*Description* + + +''' + +그룹 탈퇴를 위해 사용합니다. 그룹장은 그룹을 탈퇴할 수 없습니다. + +*Request* + + +''' + +include::{snippets}/group-entry-controller-rest-docs-test/leave-group_success/http-request.adoc[] +include::{snippets}/group-entry-controller-rest-docs-test/leave-group_success/path-parameters.adoc[] + +*Response* + + +''' + +include::{snippets}/group-entry-controller-rest-docs-test/leave-group_success/http-response.adoc[] + === 2. 그룹 가입 관련 API ==== 2-1. 초대 코드 생성 diff --git a/src/main/java/com/studypals/domain/groupManage/api/GroupEntryController.java b/src/main/java/com/studypals/domain/groupManage/api/GroupEntryController.java index 10bcaac2..e2ea7607 100644 --- a/src/main/java/com/studypals/domain/groupManage/api/GroupEntryController.java +++ b/src/main/java/com/studypals/domain/groupManage/api/GroupEntryController.java @@ -27,6 +27,7 @@ * - POST /groups/{groupId}/entry-code : 그룹 초대 코드 생성 * - GET /groups/summary : 그룹 대표 정보 조회 * - POST /groups/join : 공개 그룹에 가입 + * - DELETE /groups/{groupId}/leave : 그룹 탈퇴 * - POST /groups/entry-requests : 비공개 그룹 가입 요청 * - GET /groups/{groupId}/entry-requests : 그룹 가입 요청 목록 조회 * - POST /groups/entry-requests/{requestId}/accept : 그룹 가입 요청 승인 @@ -82,6 +83,13 @@ public ResponseEntity joinGroup(@AuthenticationPrincipal Long userId, @Val .build(); } + @DeleteMapping("/{groupId}/leave") + public ResponseEntity leaveGroup(@AuthenticationPrincipal Long userId, @PathVariable Long groupId) { + groupEntryService.leaveGroup(userId, groupId); + + return ResponseEntity.noContent().build(); + } + @PostMapping("/entry-requests") public ResponseEntity requestGroupParticipant( @AuthenticationPrincipal Long userId, @Valid @RequestBody GroupEntryReq req) { diff --git a/src/main/java/com/studypals/domain/groupManage/service/GroupEntryService.java b/src/main/java/com/studypals/domain/groupManage/service/GroupEntryService.java index 45990178..c9f9df44 100644 --- a/src/main/java/com/studypals/domain/groupManage/service/GroupEntryService.java +++ b/src/main/java/com/studypals/domain/groupManage/service/GroupEntryService.java @@ -56,6 +56,14 @@ public interface GroupEntryService { */ Long joinGroup(Long userId, GroupEntryReq entryInfo); + /** + * 가입된 그룹에서 탈퇴합니다. + * 그룹장은 탈퇴할 수 없습니다. + * @param userId 그룹 멤버 ID + * @param groupId 탈퇴할 그룹 ID + */ + void leaveGroup(Long userId, Long groupId); + /** * 비공개 그룹에 가입 요청을 보냅니다. * diff --git a/src/main/java/com/studypals/domain/groupManage/service/GroupEntryServiceImpl.java b/src/main/java/com/studypals/domain/groupManage/service/GroupEntryServiceImpl.java index 5a392a89..d7322c79 100644 --- a/src/main/java/com/studypals/domain/groupManage/service/GroupEntryServiceImpl.java +++ b/src/main/java/com/studypals/domain/groupManage/service/GroupEntryServiceImpl.java @@ -96,6 +96,16 @@ public Long joinGroup(Long userId, GroupEntryReq entryInfo) { return internalJoinGroup(member, group); } + @Override + @Transactional + public void leaveGroup(Long userId, Long groupId){ + Group group = groupReader.getById(groupId); + Member member = memberReader.getRef(userId); + + chatRoomWriter.leave(group.getChatRoom(), member); + groupMemberWriter.deleteMember(userId, group); + } + @Override @Transactional public Long requestParticipant(Long userId, GroupEntryReq entryInfo) { diff --git a/src/main/java/com/studypals/domain/groupManage/worker/GroupMemberWriter.java b/src/main/java/com/studypals/domain/groupManage/worker/GroupMemberWriter.java index 1514ca5f..019b5ad8 100644 --- a/src/main/java/com/studypals/domain/groupManage/worker/GroupMemberWriter.java +++ b/src/main/java/com/studypals/domain/groupManage/worker/GroupMemberWriter.java @@ -54,4 +54,17 @@ private GroupMember create(Member member, Group group, GroupRole role) { } return groupMember; } + + public void deleteMember(Long userId, Group group) { + GroupMember groupMember = groupMemberRepository.findByMemberIdAndGroupId(userId, group.getId()) + .orElseThrow(() -> { + String message = String.format( + "[GroupMemberWriter#deleteMember] member %d not found in group %d", userId, group.getId()); + return new GroupException(GroupErrorCode.GROUP_MEMBER_NOT_FOUND, message); + }); + if(groupMember.isLeader()) { + throw new GroupException(GroupErrorCode.GROUP_LEAVE_FAIL, "leader can't leave the group"); + } + groupMemberRepository.delete(groupMember); + } } diff --git a/src/test/java/com/studypals/domain/groupManage/restDocsTest/GroupEntryControllerRestDocsTest.java b/src/test/java/com/studypals/domain/groupManage/restDocsTest/GroupEntryControllerRestDocsTest.java index df8ebf0c..e779b79f 100644 --- a/src/test/java/com/studypals/domain/groupManage/restDocsTest/GroupEntryControllerRestDocsTest.java +++ b/src/test/java/com/studypals/domain/groupManage/restDocsTest/GroupEntryControllerRestDocsTest.java @@ -186,6 +186,25 @@ void joinGroup_success() throws Exception { responseHeaders(headerWithName("Location").description("추가된 그룹 멤버 id")))); } + @Test + @WithMockUser + void leaveGroup_success() throws Exception { + // given + Long groupId = 1L; + + // when + ResultActions result = mockMvc.perform(delete("/groups/{groupId}/leave", groupId)); + + // then + result.andExpect(status().isNoContent()) + .andDo(restDocs.document( + httpRequest(), + httpResponse(), + pathParameters(parameterWithName("groupId") + .description("탈퇴할 그룹 ID") + .attributes(constraints("not null"))))); + } + @Test @WithMockUser void requestParticipant_success() throws Exception { diff --git a/src/test/java/com/studypals/domain/groupManage/service/GroupEntryServiceTest.java b/src/test/java/com/studypals/domain/groupManage/service/GroupEntryServiceTest.java index 50c0d0a6..fe20ce92 100644 --- a/src/test/java/com/studypals/domain/groupManage/service/GroupEntryServiceTest.java +++ b/src/test/java/com/studypals/domain/groupManage/service/GroupEntryServiceTest.java @@ -240,6 +240,33 @@ void joinGroup_fail_groupMemberLimitExceed() { .isEqualTo(GroupErrorCode.GROUP_JOIN_FAIL); } + @Test + void leaveGroup_success() { + // given + Long userId = 1L; + Long groupId = 1L; + Group group = Group.builder() + .id(groupId) + .totalMember(10) + .maxMember(10) + .isApprovalRequired(false) + .build(); + Member member = Member.builder() + .id(userId) + .build(); + group.setChatRoom(mockChatRoom); + + given(groupReader.getById(groupId)).willReturn(group); + given(memberReader.getRef(userId)).willReturn(member); + + // when + groupEntryService.leaveGroup(userId, groupId); + + // then + verify(chatRoomWriter).leave(mockChatRoom, member); + verify(groupMemberWriter).deleteMember(userId, group); + } + @Test void requestParticipant_success() { // given diff --git a/src/test/java/com/studypals/domain/groupManage/worker/GroupMemberWriterTest.java b/src/test/java/com/studypals/domain/groupManage/worker/GroupMemberWriterTest.java index b0c45811..1557d72d 100644 --- a/src/test/java/com/studypals/domain/groupManage/worker/GroupMemberWriterTest.java +++ b/src/test/java/com/studypals/domain/groupManage/worker/GroupMemberWriterTest.java @@ -3,6 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -126,7 +129,6 @@ void createMember_fail_whileSave() { @Test void createMember_fail_memberLimitExceed() { // given - Long memberId = 1L; Long groupId = 1L; GroupErrorCode errorCode = GroupErrorCode.GROUP_JOIN_FAIL; @@ -139,4 +141,39 @@ void createMember_fail_memberLimitExceed() { .extracting("errorCode") .isEqualTo(errorCode); } + + @Test + void deleteMember_success() { + // given + Long userId = 1L; + Long groupId = 1L; + + given(mockGroup.getId()).willReturn(groupId); + given(groupMemberRepository.findByMemberIdAndGroupId(userId, groupId)).willReturn(Optional.of(mockGroupMember)); + given(mockGroupMember.isLeader()).willReturn(false); + + // when + groupMemberWriter.deleteMember(userId, mockGroup); + + // then + verify(groupMemberRepository).delete(mockGroupMember); + } + + @Test + void deleteMember_fail_leaderCannotLeave() { + // given + Long userId = 1L; + Long groupId = 1L; + GroupErrorCode errorCode = GroupErrorCode.GROUP_LEAVE_FAIL; + + given(mockGroup.getId()).willReturn(groupId); + given(groupMemberRepository.findByMemberIdAndGroupId(userId, groupId)).willReturn(Optional.of(mockGroupMember)); + given(mockGroupMember.isLeader()).willReturn(true); + + // when & then + assertThatThrownBy(() -> groupMemberWriter.deleteMember(userId, mockGroup)) + .isInstanceOf(GroupException.class) + .extracting("errorCode") + .isEqualTo(errorCode); + } }