Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +25,7 @@
public class GroupController {
private final GroupService groupService;

//그룹 생성 API
@PostMapping("")
public ResponseEntity<GroupCreateResponse> create(
@AuthenticationPrincipal AuthPrinciple authPrinciple,
Expand All @@ -32,6 +34,7 @@ public ResponseEntity<GroupCreateResponse> create(
return ResponseEntity.status(HttpStatus.CREATED).body(res);
}

//그룹 상세 API
@GetMapping("/{groupId}")
public ResponseEntity<GroupDetailResponse> findGroupDetail(
@AuthenticationPrincipal AuthPrinciple authPrinciple,
Expand All @@ -40,4 +43,15 @@ public ResponseEntity<GroupDetailResponse> findGroupDetail(

return ResponseEntity.ok(res);
}

//그룹 삭제 API
@DeleteMapping("/{groupId}")
public ResponseEntity<Void> deleteGroup(
@AuthenticationPrincipal AuthPrinciple authPrinciple,
@PathVariable("groupId") Long groupId
) {
groupService.deleteGroup(authPrinciple, groupId);

return ResponseEntity.noContent().build();
}
}
2 changes: 1 addition & 1 deletion src/main/java/project/flipnote/group/entity/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
7 changes: 3 additions & 4 deletions src/main/java/project/flipnote/group/entity/GroupMember.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ public interface GroupMemberRepository extends JpaRepository<GroupMember, Long>

Optional<GroupMember> findByGroup_IdAndUser_Id(Long groupId, Long userId);

long countByGroup_idAndUser_idNot(Long groupId, Long userId);

List<GroupMember> findByGroupAndRoleIn(Group group, List<GroupMemberRole> roles);
}
49 changes: 39 additions & 10 deletions src/main/java/project/flipnote/group/service/GroupService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

/*
Expand Down Expand Up @@ -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);
}

/*
Expand All @@ -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;
}

/*
그룹 상세 정보 조회
*/
Expand All @@ -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);

}

Expand Down
105 changes: 103 additions & 2 deletions src/test/java/project/flipnote/group/service/GroupServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -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 =
Expand Down Expand Up @@ -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());
}
}
Loading