diff --git a/src/main/java/project/flipnote/group/controller/GroupController.java b/src/main/java/project/flipnote/group/controller/GroupController.java index efe9104a..f10b21a9 100644 --- a/src/main/java/project/flipnote/group/controller/GroupController.java +++ b/src/main/java/project/flipnote/group/controller/GroupController.java @@ -3,6 +3,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,6 +15,7 @@ import project.flipnote.common.security.dto.AuthPrinciple; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; +import project.flipnote.group.model.GroupDetailResponse; import project.flipnote.group.service.GroupService; @RequiredArgsConstructor @@ -28,4 +31,13 @@ public ResponseEntity create( GroupCreateResponse res = groupService.create(authPrinciple, req); return ResponseEntity.status(HttpStatus.CREATED).body(res); } + + @GetMapping("/{groupId}") + public ResponseEntity findGroupDetail( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @PathVariable("groupId") Long groupId) { + GroupDetailResponse res = groupService.findGroupDetail(authPrinciple, groupId); + + return ResponseEntity.ok(res); + } } diff --git a/src/main/java/project/flipnote/group/entity/Group.java b/src/main/java/project/flipnote/group/entity/Group.java index e87aafd2..ee138a75 100644 --- a/src/main/java/project/flipnote/group/entity/Group.java +++ b/src/main/java/project/flipnote/group/entity/Group.java @@ -1,5 +1,11 @@ package project.flipnote.group.entity; +import java.time.LocalDateTime; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; +import org.hibernate.annotations.Where; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -23,6 +29,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "app_groups") @Entity +@SQLDelete(sql = "UPDATE app_groups SET deleted_at = now() WHERE id = ?") +@SQLRestriction("deleted_at IS NULL") public class Group extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -51,6 +59,9 @@ public class Group extends BaseEntity { private String imageUrl; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + @Builder private Group( String name, diff --git a/src/main/java/project/flipnote/group/entity/GroupMember.java b/src/main/java/project/flipnote/group/entity/GroupMember.java index 0d41b3ea..c5bc9aea 100644 --- a/src/main/java/project/flipnote/group/entity/GroupMember.java +++ b/src/main/java/project/flipnote/group/entity/GroupMember.java @@ -1,5 +1,8 @@ package project.flipnote.group.entity; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -21,6 +24,8 @@ @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) diff --git a/src/main/java/project/flipnote/group/model/GroupDetailResponse.java b/src/main/java/project/flipnote/group/model/GroupDetailResponse.java new file mode 100644 index 00000000..839f1561 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupDetailResponse.java @@ -0,0 +1,41 @@ +package project.flipnote.group.model; + +import java.time.LocalDateTime; + +import project.flipnote.group.entity.Category; +import project.flipnote.group.entity.Group; + +public record GroupDetailResponse( + + String name, + + Category category, + + String description, + + Boolean applicationRequired, + + Boolean publicVisible, + + Integer maxMember, + + String imageUrl, + + LocalDateTime createdAt, + + LocalDateTime modifiedAt +) { + public static GroupDetailResponse from(Group group) { + return new GroupDetailResponse( + group.getName(), + group.getCategory(), + group.getDescription(), + group.getApplicationRequired(), + group.getPublicVisible(), + group.getMaxMember(), + group.getImageUrl(), + group.getCreatedAt(), + group.getModifiedAt() + ); + } +} diff --git a/src/main/java/project/flipnote/group/repository/GroupRepository.java b/src/main/java/project/flipnote/group/repository/GroupRepository.java index 1959fc9f..ec4279be 100644 --- a/src/main/java/project/flipnote/group/repository/GroupRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupRepository.java @@ -14,7 +14,7 @@ @Repository public interface GroupRepository extends JpaRepository { - Optional findById(Long groupId); + Optional findByIdAndDeletedAtIsNull(Long groupId); @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select g from Group g where g.id = :id") diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index 4e7049ac..2990dbcd 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -19,10 +19,12 @@ import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; +import project.flipnote.group.model.GroupDetailResponse; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupPermissionRepository; import project.flipnote.group.repository.GroupRepository; import project.flipnote.group.repository.GroupRolePermissionRepository; +import project.flipnote.groupjoin.exception.GroupJoinErrorCode; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; @@ -40,19 +42,40 @@ public class GroupService { private final GroupRolePermissionRepository groupRolePermissionRepository; private final UserProfileRepository userProfileRepository; - //유저 정보 조회 - public UserProfile findUser(AuthPrinciple authPrinciple) { + /* + 유저 정보 조회 + */ + public UserProfile validateUser(AuthPrinciple authPrinciple) { return userProfileRepository.findByIdAndStatus(authPrinciple.userId(), UserStatus.ACTIVE).orElseThrow( () -> new BizException(UserErrorCode.USER_NOT_FOUND) ); } + + /* + 그룹 내 유저 조회 + */ + public boolean validateGroupInUser(UserProfile user, Long groupId) { + return groupMemberRepository.existsByGroup_idAndUser_id(groupId, user.getId()); + } + + /* + 그룹 조회 + */ + public Group validateGroup(Long groupId) { + return groupRepository.findByIdAndDeletedAtIsNull(groupId).orElseThrow( + () -> new BizException(GroupErrorCode.GROUP_NOT_FOUND) + ); + } + - //그룹 생성 + /* + 그룹 생성 + */ @Transactional public GroupCreateResponse create(AuthPrinciple authPrinciple, GroupCreateRequest req) { //1. 유저 조회 - UserProfile userProfile = findUser(authPrinciple); + UserProfile user = validateUser(authPrinciple); //2. 인원수 검증 validateMaxMember(req.maxMember()); @@ -61,7 +84,7 @@ public GroupCreateResponse create(AuthPrinciple authPrinciple, GroupCreateReques Group group = createGroup(req); //4. 그룹 회원 정보 생성 - saveGroupOwner(group, userProfile); + saveGroupOwner(group, user); //5. 그룹 내의 모든 권한 조회 initializeGroupPermissions(group); @@ -135,11 +158,35 @@ private void saveGroupOwner(Group group, UserProfile user) { groupMemberRepository.save(groupMember); } - //인원수 검증 + /* + 인원수 검증 + */ private void validateMaxMember(int maxMember) { if (maxMember < 1 || maxMember > 100) { throw new BizException(GroupErrorCode.INVALID_MAX_MEMBER); } } + /* + 그룹 상세 정보 조회 + */ + public GroupDetailResponse findGroupDetail(AuthPrinciple authPrinciple, Long groupId) { + + //1. 그룹 조회 + Group group = validateGroup(groupId); + + //2. 유저 조회 + UserProfile user = validateUser(authPrinciple); + + //3. 그룹 내 유저 조회 + if (!validateGroupInUser(user, groupId)) { + throw new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP); + } + + return GroupDetailResponse.from(group); + } + + public void deleteGroup(AuthPrinciple authPrinciple, Long groupId) { + + } } diff --git a/src/test/java/project/flipnote/group/service/GroupServiceTest.java b/src/test/java/project/flipnote/group/service/GroupServiceTest.java index 5383ceec..cee94539 100644 --- a/src/test/java/project/flipnote/group/service/GroupServiceTest.java +++ b/src/test/java/project/flipnote/group/service/GroupServiceTest.java @@ -1,7 +1,7 @@ package project.flipnote.group.service; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; import java.util.List; @@ -17,6 +17,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.test.util.ReflectionTestUtils; + +import project.flipnote.auth.entity.AccountRole; import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.common.exception.BizException; import project.flipnote.common.security.dto.AuthPrinciple; @@ -25,12 +27,15 @@ import project.flipnote.group.entity.Group; import project.flipnote.group.entity.GroupPermission; import project.flipnote.group.entity.GroupPermissionStatus; +import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; +import project.flipnote.group.model.GroupDetailResponse; import project.flipnote.group.repository.GroupMemberRepository; import project.flipnote.group.repository.GroupPermissionRepository; import project.flipnote.group.repository.GroupRepository; import project.flipnote.group.repository.GroupRolePermissionRepository; +import project.flipnote.groupjoin.exception.GroupJoinErrorCode; import project.flipnote.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.repository.UserProfileRepository; @@ -65,58 +70,139 @@ class GroupServiceTest { @BeforeEach void before() { - // userProfile = UserFixture.createActiveUser(); - // authPrinciple = new AuthPrinciple(userProfile.getId(), userProfile.getEmail(), userProfile.getRole(), userProfile.getTokenVersion()); + userProfile = UserFixture.createActiveUser(); + authPrinciple = new AuthPrinciple(1L, userProfile.getId(), userProfile.getEmail(), AccountRole.USER, 1L); + } + + @Test + void 그룹_생성_성공() { + // given + GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, 100, "www.~~~"); + Group group = Group.builder().name(req.name()).build(); + ReflectionTestUtils.setField(group, "id", 1L); + + given(groupRepository.save(any(Group.class))).willReturn(group); + // 사용자 검증 로직 + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)) + .willReturn(Optional.of(userProfile)); + + // 그룹 퍼미션 미리 세팅 + List permissions = List.of( + GroupPermission.builder().name(GroupPermissionStatus.INVITE).build(), + GroupPermission.builder().name(GroupPermissionStatus.KICK).build(), + GroupPermission.builder().name(GroupPermissionStatus.JOIN_REQUEST_MANAGE).build() + ); + given(groupPermissionRepository.findAll()).willReturn(permissions); + + // when + GroupCreateResponse response = groupService.create(authPrinciple, req); + + // then + assertThat(response.groupId()).isEqualTo(1L); + } + @Test + void 그룹_생성_실패_음수() { + // given + GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, -100, "www.~~~"); + Group group = Group.builder().name(req.name()).build(); + ReflectionTestUtils.setField(group, "id", 1L); // 사용자 검증 로직 - given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of( - userProfile)); + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)) + .willReturn(Optional.of(userProfile)); + + // when & then + BizException exception = assertThrows( + BizException.class, () -> groupService.create(authPrinciple, req) + ); + + assertThat(exception.getErrorCode()).isEqualTo(GroupErrorCode.INVALID_MAX_MEMBER); } - // @Test - // void 그룹_생성_성공() { - // // given - // GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, 100, "www.~~~"); - // Group group = Group.builder().name(req.name()).build(); - // ReflectionTestUtils.setField(group, "id", 1L); - // - // given(groupRepository.save(any(Group.class))).willReturn(group); - // - // // 그룹 퍼미션 미리 세팅 - // List permissions = List.of( - // GroupPermission.builder().name(GroupPermissionStatus.INVITE).build(), - // GroupPermission.builder().name(GroupPermissionStatus.KICK).build(), - // GroupPermission.builder().name(GroupPermissionStatus.JOIN_REQUEST_MANAGE).build() - // ); - // given(groupPermissionRepository.findAll()).willReturn(permissions); - // - // // when - // GroupCreateResponse response = groupService.create(userPrincipal, req); - // - // // then - // assertThat(response.groupId()).isEqualTo(1L); - // } - // - // @Test - // void 그룹_생성_실패_음수() { - // // given - // GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, -100, "www.~~~"); - // Group group = Group.builder().name(req.name()).build(); - // ReflectionTestUtils.setField(group, "id", 1L); - // - // - // // when & then - // assertThrows(BizException.class, () -> groupService.create(userPrincipal, req)); - // } - // - // @Test - // void 그룹_생성_실패_초과() { - // // given - // GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, 200, "www.~~~"); - // Group group = Group.builder().name(req.name()).build(); - // ReflectionTestUtils.setField(group, "id", 1L); - // - // // when & then - // assertThrows(BizException.class, () -> groupService.create(userPrincipal, req)); - // } + @Test + void 그룹_생성_실패_초과() { + // given + GroupCreateRequest req = new GroupCreateRequest("그룹1", Category.ENGLISH, "설명1", true, true, 200, "www.~~~"); + Group group = Group.builder().name(req.name()).build(); + ReflectionTestUtils.setField(group, "id", 1L); + // 사용자 검증 로직 + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)) + .willReturn(Optional.of(userProfile)); + + // when & then + assertThrows(BizException.class, () -> groupService.create(authPrinciple, req)); + } + + @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(); + + given(groupRepository.findByIdAndDeletedAtIsNull(any())).willReturn(Optional.ofNullable(group)); + given(groupMemberRepository.existsByGroup_idAndUser_id(any(), any())).willReturn(true); + // 사용자 검증 로직 + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)) + .willReturn(Optional.of(userProfile)); + + //when + GroupDetailResponse res = groupService.findGroupDetail(authPrinciple, 1L); + + //then + assertEquals("그룹1", res.name()); + } + + @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(); + + 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); + + //when + BizException exception = + assertThrows(BizException.class, () -> groupService.findGroupDetail(authPrinciple, 1L)); + + //then + assertEquals(GroupJoinErrorCode.USER_NOT_IN_GROUP, 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(); + + given(groupRepository.findByIdAndDeletedAtIsNull(1L)).willReturn(Optional.empty()); + + //when & then + BizException exception = + assertThrows(BizException.class, () -> groupService.findGroupDetail(authPrinciple, 1L)); + + assertEquals(GroupErrorCode.GROUP_NOT_FOUND, exception.getErrorCode()); + } }