diff --git a/src/main/java/project/flipnote/group/controller/GroupController.java b/src/main/java/project/flipnote/group/controller/GroupController.java index f10b21a9..e706783c 100644 --- a/src/main/java/project/flipnote/group/controller/GroupController.java +++ b/src/main/java/project/flipnote/group/controller/GroupController.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -24,6 +25,7 @@ public class GroupController { private final GroupService groupService; + //그룹 생성 API @PostMapping("") public ResponseEntity create( @AuthenticationPrincipal AuthPrinciple authPrinciple, @@ -32,6 +34,7 @@ public ResponseEntity create( return ResponseEntity.status(HttpStatus.CREATED).body(res); } + //그룹 상세 API @GetMapping("/{groupId}") public ResponseEntity findGroupDetail( @AuthenticationPrincipal AuthPrinciple authPrinciple, @@ -40,4 +43,15 @@ public ResponseEntity findGroupDetail( return ResponseEntity.ok(res); } + + //그룹 삭제 API + @DeleteMapping("/{groupId}") + public ResponseEntity deleteGroup( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @PathVariable("groupId") Long groupId + ) { + groupService.deleteGroup(authPrinciple, groupId); + + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/project/flipnote/group/entity/Group.java b/src/main/java/project/flipnote/group/entity/Group.java index d87398fc..39887dc5 100644 --- a/src/main/java/project/flipnote/group/entity/Group.java +++ b/src/main/java/project/flipnote/group/entity/Group.java @@ -29,7 +29,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "app_groups") @Entity -@SQLDelete(sql = "UPDATE app_groups SET deleted_at = now() WHERE id = ?") +@SQLDelete(sql = "UPDATE app_groups SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?") @SQLRestriction("deleted_at IS NULL") public class Group extends BaseEntity { diff --git a/src/main/java/project/flipnote/group/entity/GroupMember.java b/src/main/java/project/flipnote/group/entity/GroupMember.java index c5bc9aea..ec4949fd 100644 --- a/src/main/java/project/flipnote/group/entity/GroupMember.java +++ b/src/main/java/project/flipnote/group/entity/GroupMember.java @@ -7,6 +7,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -24,18 +25,16 @@ @Entity @Table(name = "group_members") @NoArgsConstructor(access = AccessLevel.PROTECTED) -@SQLDelete(sql = "UPDATE group_members SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?") -@SQLRestriction("deleted_at IS NULL") public class GroupMember extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "group_id", nullable = false) private Group group; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private UserProfile user; diff --git a/src/main/java/project/flipnote/group/exception/GroupErrorCode.java b/src/main/java/project/flipnote/group/exception/GroupErrorCode.java index d00d5a75..c927d8ef 100644 --- a/src/main/java/project/flipnote/group/exception/GroupErrorCode.java +++ b/src/main/java/project/flipnote/group/exception/GroupErrorCode.java @@ -9,11 +9,13 @@ @Getter @RequiredArgsConstructor public enum GroupErrorCode implements ErrorCode { - GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_002", "그룹이 존재하지 않습니다."), - INVALID_MAX_MEMBER(HttpStatus.BAD_REQUEST, "GROUP_001", "최대 인원 수는 1 이상 100 이하여야 합니다."), - USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_003", "그룹에 유저가 존재하지 않습니다."), - GROUP_IS_ALREADY_MAX_MEMBER(HttpStatus.CONFLICT, "GROUP_004", "그룹 정원이 가득 찼습니다."), - ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_005", "이미 그룹 회원입니다."); + GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_001", "그룹이 존재하지 않습니다."), + INVALID_MAX_MEMBER(HttpStatus.BAD_REQUEST, "GROUP_002", "최대 인원 수는 1 이상 100 이하여야 합니다."), + USER_NOT_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_003", "그룹 내 권한이 없습니다."), + USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_004", "그룹에 유저가 존재하지 않습니다."), + OTHER_USER_EXIST_IN_GROUP(HttpStatus.CONFLICT, "GROUP_005", "그룹내 오너 제외 유저가 존재합니다."), + GROUP_IS_ALREADY_MAX_MEMBER(HttpStatus.CONFLICT, "GROUP_006", "그룹 정원이 가득 찼습니다."), + ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_007", "이미 그룹 회원입니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java index dd61aa18..b0db41f4 100644 --- a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java @@ -21,5 +21,7 @@ public interface GroupMemberRepository extends JpaRepository Optional findByGroup_IdAndUser_Id(Long groupId, Long userId); + long countByGroup_idAndUser_idNot(Long groupId, Long userId); + List findByGroupAndRoleIn(Group group, List roles); } diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index 96f28b99..d3297902 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -54,8 +54,10 @@ public UserProfile validateUser(AuthPrinciple authPrinciple) { /* 그룹 내 유저 조회 */ - public boolean validateGroupInUser(UserProfile user, Long groupId) { - return groupMemberRepository.existsByGroup_idAndUser_id(groupId, user.getId()); + public GroupMember validateGroupInUser(UserProfile user, Long groupId) { + return groupMemberRepository.findByGroup_IdAndUser_Id(groupId, user.getId()).orElseThrow( + () -> new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP) + ); } /* @@ -137,11 +139,7 @@ private Group createGroup(GroupCreateRequest req) { .imageUrl(req.image()) .build(); - Group saveGroup = groupRepository.save(group); - - log.info("생성 시간: {}", group.getCreatedAt()); - - return saveGroup; + return groupRepository.save(group); } /* @@ -166,6 +164,17 @@ private void validateMaxMember(int maxMember) { } } + /* + 그룹 내 오너를 제외한 인원이 존재하는 경우 체크 + */ + private boolean checkUserNotExistInGroup(UserProfile user, Long groupId) { + long count = groupMemberRepository.countByGroup_idAndUser_idNot(groupId, user.getId()); + if (count > 0) { + return false; + } + return true; + } + /* 그룹 상세 정보 조회 */ @@ -178,14 +187,34 @@ public GroupDetailResponse findGroupDetail(AuthPrinciple authPrinciple, Long gro UserProfile user = validateUser(authPrinciple); //3. 그룹 내 유저 조회 - if (!validateGroupInUser(user, groupId)) { - throw new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP); - } + validateGroupInUser(user, groupId); return GroupDetailResponse.from(group); } + //그룹 삭제 메서드 + @Transactional public void deleteGroup(AuthPrinciple authPrinciple, Long groupId) { + //1. 그룹 조회 + Group group = validateGroup(groupId); + + //2. 유저 조회 + UserProfile user = validateUser(authPrinciple); + + //3. 그룹 내 유저 조회 + GroupMember groupMember = validateGroupInUser(user, groupId); + + //4. 유저 권환 조회 + if (!groupMember.getRole().equals(GroupMemberRole.OWNER)) { + throw new BizException(GroupErrorCode.USER_NOT_PERMISSION); + } + + //5. 오너를 제외한 모든 유저가 없어야 삭제 가능 + if (!checkUserNotExistInGroup(user, groupId)) { + throw new BizException(GroupErrorCode.OTHER_USER_EXIST_IN_GROUP); + } + + groupRepository.delete(group); } diff --git a/src/test/java/project/flipnote/group/service/GroupServiceTest.java b/src/test/java/project/flipnote/group/service/GroupServiceTest.java index cee94539..a59116f6 100644 --- a/src/test/java/project/flipnote/group/service/GroupServiceTest.java +++ b/src/test/java/project/flipnote/group/service/GroupServiceTest.java @@ -25,6 +25,8 @@ import project.flipnote.fixture.UserFixture; import project.flipnote.group.entity.Category; import project.flipnote.group.entity.Group; +import project.flipnote.group.entity.GroupMember; +import project.flipnote.group.entity.GroupMemberRole; import project.flipnote.group.entity.GroupPermission; import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.exception.GroupErrorCode; @@ -146,8 +148,14 @@ void before() { .imageUrl("www.~~~") .build(); + GroupMember groupMember = GroupMember.builder() + .group(group) + .user(userProfile) + .role(GroupMemberRole.MEMBER) + .build(); + given(groupRepository.findByIdAndDeletedAtIsNull(any())).willReturn(Optional.ofNullable(group)); - given(groupMemberRepository.existsByGroup_idAndUser_id(any(), any())).willReturn(true); + given(groupMemberRepository.findByGroup_IdAndUser_Id(any(), any())).willReturn(Optional.of(groupMember)); // 사용자 검증 로직 given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)) .willReturn(Optional.of(userProfile)); @@ -174,7 +182,7 @@ void before() { given(groupRepository.findByIdAndDeletedAtIsNull(any())).willReturn(Optional.ofNullable(group)); given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.ofNullable(userProfile)); - given(groupMemberRepository.existsByGroup_idAndUser_id(any(), any())).willReturn(false); + given(groupMemberRepository.findByGroup_IdAndUser_Id(any(), any())).willReturn(Optional.empty()); //when BizException exception = @@ -205,4 +213,97 @@ void before() { assertEquals(GroupErrorCode.GROUP_NOT_FOUND, exception.getErrorCode()); } + + @Test + public void 그룹_삭제_성공() throws Exception { + //given + Group group = Group.builder() + .name("그룹1") + .category(Category.IT) + .description("설명1") + .publicVisible(true) + .applicationRequired(true) + .maxMember(100) + .imageUrl("www.~~~") + .build(); + ReflectionTestUtils.setField(group, "id", 1L); + + GroupMember groupMember = GroupMember.builder() + .group(group) + .role(GroupMemberRole.OWNER) + .user(userProfile) + .build(); + + given(groupRepository.findByIdAndDeletedAtIsNull(1L)).willReturn(Optional.of(group)); + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(userProfile)); + given(groupMemberRepository.findByGroup_IdAndUser_Id(1L,1L)).willReturn(Optional.of(groupMember)); + given(groupMemberRepository.countByGroup_idAndUser_idNot(1L,1L)).willReturn(0L); + //when + groupService.deleteGroup(authPrinciple, group.getId()); + + //then + } + + @Test + public void 그룹_삭제_실패_오너아닌경우() throws Exception { + //given + Group group = Group.builder() + .name("그룹1") + .category(Category.IT) + .description("설명1") + .publicVisible(true) + .applicationRequired(true) + .maxMember(100) + .imageUrl("www.~~~") + .build(); + ReflectionTestUtils.setField(group, "id", 1L); + + GroupMember groupMember = GroupMember.builder() + .group(group) + .role(GroupMemberRole.MEMBER) + .user(userProfile) + .build(); + + given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.ofNullable(group)); + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.ofNullable(userProfile)); + given(groupMemberRepository.findByGroup_IdAndUser_Id(group.getId(),userProfile.getId())).willReturn(Optional.ofNullable(groupMember)); + // given(groupMemberRepository.countByGroup_idAndUser_idNot(1L,1L)).willReturn(0L); + + //when & then + BizException exception = + assertThrows(BizException.class, () -> groupService.deleteGroup(authPrinciple, 1L)); + + assertEquals(GroupErrorCode.USER_NOT_PERMISSION, exception.getErrorCode()); + } + + @Test + public void 그룹_삭제_실패_유저존재하는경우() throws Exception { + //given + Group group = Group.builder() + .name("그룹1") + .category(Category.IT) + .description("설명1") + .publicVisible(true) + .applicationRequired(true) + .maxMember(100) + .imageUrl("www.~~~") + .build(); + + GroupMember groupMember = GroupMember.builder() + .group(group) + .role(GroupMemberRole.OWNER) + .user(userProfile) + .build(); + + given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.ofNullable(group)); + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.ofNullable(userProfile)); + given(groupMemberRepository.findByGroup_IdAndUser_Id(group.getId(),userProfile.getId())).willReturn(Optional.ofNullable(groupMember)); + given(groupMemberRepository.countByGroup_idAndUser_idNot(group.getId(),userProfile.getId())).willReturn(2L); + + //when & then + BizException exception = + assertThrows(BizException.class, () -> groupService.deleteGroup(authPrinciple, group.getId())); + + assertEquals(GroupErrorCode.OTHER_USER_EXIST_IN_GROUP, exception.getErrorCode()); + } }