Skip to content

Commit 25bf154

Browse files
Feat: [FN-126] 그룹 삭제 API 추가
Feat: [FN-126] 그룹 삭제 API 추가
2 parents d4efcf4 + 3c2fa90 commit 25bf154

7 files changed

Lines changed: 169 additions & 22 deletions

File tree

src/main/java/project/flipnote/group/controller/GroupController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.springframework.http.HttpStatus;
44
import org.springframework.http.ResponseEntity;
55
import org.springframework.security.core.annotation.AuthenticationPrincipal;
6+
import org.springframework.web.bind.annotation.DeleteMapping;
67
import org.springframework.web.bind.annotation.GetMapping;
78
import org.springframework.web.bind.annotation.PathVariable;
89
import org.springframework.web.bind.annotation.PostMapping;
@@ -24,6 +25,7 @@
2425
public class GroupController {
2526
private final GroupService groupService;
2627

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

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

4144
return ResponseEntity.ok(res);
4245
}
46+
47+
//그룹 삭제 API
48+
@DeleteMapping("/{groupId}")
49+
public ResponseEntity<Void> deleteGroup(
50+
@AuthenticationPrincipal AuthPrinciple authPrinciple,
51+
@PathVariable("groupId") Long groupId
52+
) {
53+
groupService.deleteGroup(authPrinciple, groupId);
54+
55+
return ResponseEntity.noContent().build();
56+
}
4357
}

src/main/java/project/flipnote/group/entity/Group.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
@NoArgsConstructor(access = AccessLevel.PROTECTED)
3030
@Table(name = "app_groups")
3131
@Entity
32-
@SQLDelete(sql = "UPDATE app_groups SET deleted_at = now() WHERE id = ?")
32+
@SQLDelete(sql = "UPDATE app_groups SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
3333
@SQLRestriction("deleted_at IS NULL")
3434
public class Group extends BaseEntity {
3535

src/main/java/project/flipnote/group/entity/GroupMember.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import jakarta.persistence.Entity;
88
import jakarta.persistence.EnumType;
99
import jakarta.persistence.Enumerated;
10+
import jakarta.persistence.FetchType;
1011
import jakarta.persistence.GeneratedValue;
1112
import jakarta.persistence.GenerationType;
1213
import jakarta.persistence.Id;
@@ -24,18 +25,16 @@
2425
@Entity
2526
@Table(name = "group_members")
2627
@NoArgsConstructor(access = AccessLevel.PROTECTED)
27-
@SQLDelete(sql = "UPDATE group_members SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
28-
@SQLRestriction("deleted_at IS NULL")
2928
public class GroupMember extends BaseEntity {
3029
@Id
3130
@GeneratedValue(strategy = GenerationType.IDENTITY)
3231
private Long id;
3332

34-
@ManyToOne
33+
@ManyToOne(fetch = FetchType.LAZY)
3534
@JoinColumn(name = "group_id", nullable = false)
3635
private Group group;
3736

38-
@ManyToOne
37+
@ManyToOne(fetch = FetchType.LAZY)
3938
@JoinColumn(name = "user_id", nullable = false)
4039
private UserProfile user;
4140

src/main/java/project/flipnote/group/exception/GroupErrorCode.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
@Getter
1010
@RequiredArgsConstructor
1111
public enum GroupErrorCode implements ErrorCode {
12-
GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_002", "그룹이 존재하지 않습니다."),
13-
INVALID_MAX_MEMBER(HttpStatus.BAD_REQUEST, "GROUP_001", "최대 인원 수는 1 이상 100 이하여야 합니다."),
14-
USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_003", "그룹에 유저가 존재하지 않습니다."),
15-
GROUP_IS_ALREADY_MAX_MEMBER(HttpStatus.CONFLICT, "GROUP_004", "그룹 정원이 가득 찼습니다."),
16-
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_005", "이미 그룹 회원입니다.");
12+
GROUP_NOT_FOUND(HttpStatus.NOT_FOUND, "GROUP_001", "그룹이 존재하지 않습니다."),
13+
INVALID_MAX_MEMBER(HttpStatus.BAD_REQUEST, "GROUP_002", "최대 인원 수는 1 이상 100 이하여야 합니다."),
14+
USER_NOT_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_003", "그룹 내 권한이 없습니다."),
15+
USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_004", "그룹에 유저가 존재하지 않습니다."),
16+
OTHER_USER_EXIST_IN_GROUP(HttpStatus.CONFLICT, "GROUP_005", "그룹내 오너 제외 유저가 존재합니다."),
17+
GROUP_IS_ALREADY_MAX_MEMBER(HttpStatus.CONFLICT, "GROUP_006", "그룹 정원이 가득 찼습니다."),
18+
ALREADY_GROUP_MEMBER(HttpStatus.CONFLICT, "GROUP_007", "이미 그룹 회원입니다.");
1719

1820
private final HttpStatus httpStatus;
1921
private final String code;

src/main/java/project/flipnote/group/repository/GroupMemberRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ public interface GroupMemberRepository extends JpaRepository<GroupMember, Long>
2121

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

24+
long countByGroup_idAndUser_idNot(Long groupId, Long userId);
25+
2426
List<GroupMember> findByGroupAndRoleIn(Group group, List<GroupMemberRole> roles);
2527
}

src/main/java/project/flipnote/group/service/GroupService.java

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ public UserProfile validateUser(AuthPrinciple authPrinciple) {
5454
/*
5555
그룹 내 유저 조회
5656
*/
57-
public boolean validateGroupInUser(UserProfile user, Long groupId) {
58-
return groupMemberRepository.existsByGroup_idAndUser_id(groupId, user.getId());
57+
public GroupMember validateGroupInUser(UserProfile user, Long groupId) {
58+
return groupMemberRepository.findByGroup_IdAndUser_Id(groupId, user.getId()).orElseThrow(
59+
() -> new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP)
60+
);
5961
}
6062

6163
/*
@@ -137,11 +139,7 @@ private Group createGroup(GroupCreateRequest req) {
137139
.imageUrl(req.image())
138140
.build();
139141

140-
Group saveGroup = groupRepository.save(group);
141-
142-
log.info("생성 시간: {}", group.getCreatedAt());
143-
144-
return saveGroup;
142+
return groupRepository.save(group);
145143
}
146144

147145
/*
@@ -166,6 +164,17 @@ private void validateMaxMember(int maxMember) {
166164
}
167165
}
168166

167+
/*
168+
그룹 내 오너를 제외한 인원이 존재하는 경우 체크
169+
*/
170+
private boolean checkUserNotExistInGroup(UserProfile user, Long groupId) {
171+
long count = groupMemberRepository.countByGroup_idAndUser_idNot(groupId, user.getId());
172+
if (count > 0) {
173+
return false;
174+
}
175+
return true;
176+
}
177+
169178
/*
170179
그룹 상세 정보 조회
171180
*/
@@ -178,14 +187,34 @@ public GroupDetailResponse findGroupDetail(AuthPrinciple authPrinciple, Long gro
178187
UserProfile user = validateUser(authPrinciple);
179188

180189
//3. 그룹 내 유저 조회
181-
if (!validateGroupInUser(user, groupId)) {
182-
throw new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP);
183-
}
190+
validateGroupInUser(user, groupId);
184191

185192
return GroupDetailResponse.from(group);
186193
}
187194

195+
//그룹 삭제 메서드
196+
@Transactional
188197
public void deleteGroup(AuthPrinciple authPrinciple, Long groupId) {
198+
//1. 그룹 조회
199+
Group group = validateGroup(groupId);
200+
201+
//2. 유저 조회
202+
UserProfile user = validateUser(authPrinciple);
203+
204+
//3. 그룹 내 유저 조회
205+
GroupMember groupMember = validateGroupInUser(user, groupId);
206+
207+
//4. 유저 권환 조회
208+
if (!groupMember.getRole().equals(GroupMemberRole.OWNER)) {
209+
throw new BizException(GroupErrorCode.USER_NOT_PERMISSION);
210+
}
211+
212+
//5. 오너를 제외한 모든 유저가 없어야 삭제 가능
213+
if (!checkUserNotExistInGroup(user, groupId)) {
214+
throw new BizException(GroupErrorCode.OTHER_USER_EXIST_IN_GROUP);
215+
}
216+
217+
groupRepository.delete(group);
189218

190219
}
191220

src/test/java/project/flipnote/group/service/GroupServiceTest.java

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import project.flipnote.fixture.UserFixture;
2626
import project.flipnote.group.entity.Category;
2727
import project.flipnote.group.entity.Group;
28+
import project.flipnote.group.entity.GroupMember;
29+
import project.flipnote.group.entity.GroupMemberRole;
2830
import project.flipnote.group.entity.GroupPermission;
2931
import project.flipnote.group.entity.GroupPermissionStatus;
3032
import project.flipnote.group.exception.GroupErrorCode;
@@ -146,8 +148,14 @@ void before() {
146148
.imageUrl("www.~~~")
147149
.build();
148150

151+
GroupMember groupMember = GroupMember.builder()
152+
.group(group)
153+
.user(userProfile)
154+
.role(GroupMemberRole.MEMBER)
155+
.build();
156+
149157
given(groupRepository.findByIdAndDeletedAtIsNull(any())).willReturn(Optional.ofNullable(group));
150-
given(groupMemberRepository.existsByGroup_idAndUser_id(any(), any())).willReturn(true);
158+
given(groupMemberRepository.findByGroup_IdAndUser_Id(any(), any())).willReturn(Optional.of(groupMember));
151159
// 사용자 검증 로직
152160
given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE))
153161
.willReturn(Optional.of(userProfile));
@@ -174,7 +182,7 @@ void before() {
174182

175183
given(groupRepository.findByIdAndDeletedAtIsNull(any())).willReturn(Optional.ofNullable(group));
176184
given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.ofNullable(userProfile));
177-
given(groupMemberRepository.existsByGroup_idAndUser_id(any(), any())).willReturn(false);
185+
given(groupMemberRepository.findByGroup_IdAndUser_Id(any(), any())).willReturn(Optional.empty());
178186

179187
//when
180188
BizException exception =
@@ -205,4 +213,97 @@ void before() {
205213

206214
assertEquals(GroupErrorCode.GROUP_NOT_FOUND, exception.getErrorCode());
207215
}
216+
217+
@Test
218+
public void 그룹_삭제_성공() throws Exception {
219+
//given
220+
Group group = Group.builder()
221+
.name("그룹1")
222+
.category(Category.IT)
223+
.description("설명1")
224+
.publicVisible(true)
225+
.applicationRequired(true)
226+
.maxMember(100)
227+
.imageUrl("www.~~~")
228+
.build();
229+
ReflectionTestUtils.setField(group, "id", 1L);
230+
231+
GroupMember groupMember = GroupMember.builder()
232+
.group(group)
233+
.role(GroupMemberRole.OWNER)
234+
.user(userProfile)
235+
.build();
236+
237+
given(groupRepository.findByIdAndDeletedAtIsNull(1L)).willReturn(Optional.of(group));
238+
given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(userProfile));
239+
given(groupMemberRepository.findByGroup_IdAndUser_Id(1L,1L)).willReturn(Optional.of(groupMember));
240+
given(groupMemberRepository.countByGroup_idAndUser_idNot(1L,1L)).willReturn(0L);
241+
//when
242+
groupService.deleteGroup(authPrinciple, group.getId());
243+
244+
//then
245+
}
246+
247+
@Test
248+
public void 그룹_삭제_실패_오너아닌경우() throws Exception {
249+
//given
250+
Group group = Group.builder()
251+
.name("그룹1")
252+
.category(Category.IT)
253+
.description("설명1")
254+
.publicVisible(true)
255+
.applicationRequired(true)
256+
.maxMember(100)
257+
.imageUrl("www.~~~")
258+
.build();
259+
ReflectionTestUtils.setField(group, "id", 1L);
260+
261+
GroupMember groupMember = GroupMember.builder()
262+
.group(group)
263+
.role(GroupMemberRole.MEMBER)
264+
.user(userProfile)
265+
.build();
266+
267+
given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.ofNullable(group));
268+
given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.ofNullable(userProfile));
269+
given(groupMemberRepository.findByGroup_IdAndUser_Id(group.getId(),userProfile.getId())).willReturn(Optional.ofNullable(groupMember));
270+
// given(groupMemberRepository.countByGroup_idAndUser_idNot(1L,1L)).willReturn(0L);
271+
272+
//when & then
273+
BizException exception =
274+
assertThrows(BizException.class, () -> groupService.deleteGroup(authPrinciple, 1L));
275+
276+
assertEquals(GroupErrorCode.USER_NOT_PERMISSION, exception.getErrorCode());
277+
}
278+
279+
@Test
280+
public void 그룹_삭제_실패_유저존재하는경우() throws Exception {
281+
//given
282+
Group group = Group.builder()
283+
.name("그룹1")
284+
.category(Category.IT)
285+
.description("설명1")
286+
.publicVisible(true)
287+
.applicationRequired(true)
288+
.maxMember(100)
289+
.imageUrl("www.~~~")
290+
.build();
291+
292+
GroupMember groupMember = GroupMember.builder()
293+
.group(group)
294+
.role(GroupMemberRole.OWNER)
295+
.user(userProfile)
296+
.build();
297+
298+
given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.ofNullable(group));
299+
given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.ofNullable(userProfile));
300+
given(groupMemberRepository.findByGroup_IdAndUser_Id(group.getId(),userProfile.getId())).willReturn(Optional.ofNullable(groupMember));
301+
given(groupMemberRepository.countByGroup_idAndUser_idNot(group.getId(),userProfile.getId())).willReturn(2L);
302+
303+
//when & then
304+
BizException exception =
305+
assertThrows(BizException.class, () -> groupService.deleteGroup(authPrinciple, group.getId()));
306+
307+
assertEquals(GroupErrorCode.OTHER_USER_EXIST_IN_GROUP, exception.getErrorCode());
308+
}
208309
}

0 commit comments

Comments
 (0)