diff --git a/.gitignore b/.gitignore index c2065bc2..429e9717 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +### QueryDSL ### +/src/main/generated/ + ### STS ### .apt_generated .classpath diff --git a/build.gradle b/build.gradle index b387035f..39f95277 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,29 @@ dependencies { testRuntimeOnly 'com.h2database:h2' implementation platform('software.amazon.awssdk:bom:2.20.56') implementation 'software.amazon.awssdk:s3' + + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" +} + +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile + +tasks.withType(JavaCompile).configureEach { + options.generatedSourceOutputDirectory.set(querydslDir) +} + +sourceSets { + main { + java { + srcDir querydslDir + } + } +} + +clean.doLast { + file(querydslDir).deleteDir() } tasks.named('test') { diff --git a/src/main/java/project/flipnote/group/controller/GroupController.java b/src/main/java/project/flipnote/group/controller/GroupController.java index 8b39ba96..f9204460 100644 --- a/src/main/java/project/flipnote/group/controller/GroupController.java +++ b/src/main/java/project/flipnote/group/controller/GroupController.java @@ -16,6 +16,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.group.model.FindGroupMemberResponse; +import project.flipnote.group.model.FindGroupMemberResponse; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; import project.flipnote.group.model.GroupDetailResponse; @@ -68,4 +70,14 @@ public ResponseEntity deleteGroup( return ResponseEntity.noContent().build(); } + + //그룹내 멤버 조회 + @GetMapping("/{groupId}/members") + public ResponseEntity findGroupMembers( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @PathVariable("groupId") Long groupId) { + FindGroupMemberResponse res = groupService.findGroupMembers(authPrinciple, groupId); + + return ResponseEntity.ok(res); + } } diff --git a/src/main/java/project/flipnote/group/model/FindGroupMemberResponse.java b/src/main/java/project/flipnote/group/model/FindGroupMemberResponse.java new file mode 100644 index 00000000..622d4c78 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/FindGroupMemberResponse.java @@ -0,0 +1,11 @@ +package project.flipnote.group.model; + +import java.util.List; + +public record FindGroupMemberResponse( + List groupMembers +) { + public static FindGroupMemberResponse from(List groupMembers) { + return new FindGroupMemberResponse(groupMembers); + } +} diff --git a/src/main/java/project/flipnote/group/model/GroupMemberInfo.java b/src/main/java/project/flipnote/group/model/GroupMemberInfo.java new file mode 100644 index 00000000..ff14edc7 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupMemberInfo.java @@ -0,0 +1,14 @@ +package project.flipnote.group.model; + +import project.flipnote.group.entity.GroupMemberRole; + +public record GroupMemberInfo( + Long id, + GroupMemberRole role, + String name, + String profile +) { + public static GroupMemberInfo from(Long id, GroupMemberRole role, String name, String profile) { + return new GroupMemberInfo(id, role, name, profile); + } +} diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java index b0db41f4..5473238b 100644 --- a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java @@ -12,7 +12,7 @@ import project.flipnote.user.entity.UserProfile; @Repository -public interface GroupMemberRepository extends JpaRepository { +public interface GroupMemberRepository extends JpaRepository, GroupMemberRepositoryCustom { Optional findByGroupAndUser(Group group, UserProfile userProfile); long countByGroup_Id(Long groupId); @@ -24,4 +24,6 @@ public interface GroupMemberRepository extends JpaRepository long countByGroup_idAndUser_idNot(Long groupId, Long userId); List findByGroupAndRoleIn(Group group, List roles); + + boolean existsByGroup_IdAndUser_Id(Long groupId, Long id); } diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryCustom.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryCustom.java new file mode 100644 index 00000000..10c0151e --- /dev/null +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryCustom.java @@ -0,0 +1,9 @@ +package project.flipnote.group.repository; + +import java.util.List; + +import project.flipnote.group.model.GroupMemberInfo; + +public interface GroupMemberRepositoryCustom { + List findGroupMembers(Long groupId); +} diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java new file mode 100644 index 00000000..d45348c8 --- /dev/null +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java @@ -0,0 +1,36 @@ +package project.flipnote.group.repository; + +import java.util.List; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; +import project.flipnote.group.entity.QGroupMember; +import project.flipnote.group.model.GroupMemberInfo; +import project.flipnote.user.entity.QUserProfile; + +@RequiredArgsConstructor +public class GroupMemberRepositoryImpl implements GroupMemberRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + QUserProfile userProfile = QUserProfile.userProfile; + QGroupMember groupMember = QGroupMember.groupMember; + + public List findGroupMembers(Long groupId) { + return queryFactory.select(Projections.constructor( + GroupMemberInfo.class, + userProfile.id, + groupMember.role, + userProfile.name, + userProfile.profileImageUrl + )) + .from(groupMember) + .join(groupMember.user, userProfile) + .where(groupMember.group.id.eq(groupId)) + .fetch(); + } + +} diff --git a/src/main/java/project/flipnote/group/repository/GroupRepository.java b/src/main/java/project/flipnote/group/repository/GroupRepository.java index dc317cce..2ee8c2f4 100644 --- a/src/main/java/project/flipnote/group/repository/GroupRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupRepository.java @@ -22,4 +22,6 @@ public interface GroupRepository extends JpaRepository { @Query("SELECT g.name FROM Group g WHERE g.id = :id") Optional findGroupNameById(@Param("id") Long id); + + boolean existsByIdAndDeletedAtIsNull(Long groupId); } diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index deb58193..d964890e 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -18,9 +18,11 @@ import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.entity.GroupRolePermission; import project.flipnote.group.exception.GroupErrorCode; +import project.flipnote.group.model.FindGroupMemberResponse; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; import project.flipnote.group.model.GroupDetailResponse; +import project.flipnote.group.model.GroupMemberInfo; import project.flipnote.group.model.GroupPutRequest; import project.flipnote.group.model.GroupPutResponse; import project.flipnote.group.repository.GroupMemberRepository; @@ -55,19 +57,37 @@ public UserProfile validateUser(AuthPrinciple authPrinciple) { ); } + /* + 그룹 내 유저 검증 + */ + public void validateGroupInUser(UserProfile user, Long groupId) { + if(!groupMemberRepository.existsByGroup_IdAndUser_Id(groupId, user.getId())) { + throw new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP); + } + } + /* 그룹 내 유저 조회 */ - public GroupMember validateGroupInUser(UserProfile user, Long groupId) { + public GroupMember getGroupMember(UserProfile user, Long groupId) { return groupMemberRepository.findByGroup_IdAndUser_Id(groupId, user.getId()).orElseThrow( () -> new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP) ); } + /* + 그룹 검증 + */ + public void validateGroup(Long groupId) { + if(!groupRepository.existsByIdAndDeletedAtIsNull(groupId)) { + throw new BizException(GroupErrorCode.GROUP_NOT_FOUND); + } + } + /* 그룹 조회 */ - public Group validateGroup(Long groupId) { + public Group getGroup(Long groupId) { return groupRepository.findByIdAndDeletedAtIsNull(groupId).orElseThrow( () -> new BizException(GroupErrorCode.GROUP_NOT_FOUND) ); @@ -185,11 +205,11 @@ public GroupPutResponse changeGroup(AuthPrinciple authPrinciple, GroupPutRequest //2. 인원수 검증 validateMaxMember(req.maxMember()); - //3. 그룹 가져오기 - Group group = validateGroup(groupId); + //3. 그룹 조회 + validateGroup(groupId); //4. 그룹 내 유저 조회 - GroupMember groupMember = validateGroupInUser(user, groupId); + GroupMember groupMember = getGroupMember(user, groupId); //5. 유저 권환 조회 if (!groupMember.getRole().equals(GroupMemberRole.OWNER)) { @@ -212,13 +232,21 @@ private boolean checkUserNotExistInGroup(UserProfile user, Long groupId) { return true; } + /* + 그룹 내 모든 멤버리스트 조회 + */ + private List findGroupMembers(Long groupId) { + //각 그룹멤버의 id를 가지고 유저를 찾고 유저명, 권한, 등등 가져오기 + return groupMemberRepository.findGroupMembers(groupId); + } + /* 그룹 상세 정보 조회 */ public GroupDetailResponse findGroupDetail(AuthPrinciple authPrinciple, Long groupId) { //1. 그룹 조회 - Group group = validateGroup(groupId); + Group group = getGroup(groupId); //2. 유저 조회 UserProfile user = validateUser(authPrinciple); @@ -233,13 +261,13 @@ public GroupDetailResponse findGroupDetail(AuthPrinciple authPrinciple, Long gro @Transactional public void deleteGroup(AuthPrinciple authPrinciple, Long groupId) { //1. 그룹 조회 - Group group = validateGroup(groupId); + Group group = getGroup(groupId); //2. 유저 조회 UserProfile user = validateUser(authPrinciple); //3. 그룹 내 유저 조회 - GroupMember groupMember = validateGroupInUser(user, groupId); + GroupMember groupMember = getGroupMember(user, groupId); //4. 유저 권환 조회 if (!groupMember.getRole().equals(GroupMemberRole.OWNER)) { @@ -255,8 +283,26 @@ public void deleteGroup(AuthPrinciple authPrinciple, Long groupId) { } + //그룹이름 찾는 메서드 public String findGroupName(Long groupId) { return groupRepository.findGroupNameById(groupId) .orElseThrow(() -> new BizException(GroupErrorCode.GROUP_NOT_FOUND)); } + + //그룹 내 멤버 조회 메서드 + public FindGroupMemberResponse findGroupMembers(AuthPrinciple authPrinciple, Long groupId) { + //1. 그룹 검증 + validateGroup(groupId); + + //2. 유저 조회 + UserProfile user = validateUser(authPrinciple); + + //3. 그룹 내 유저 조회 + validateGroupInUser(user, groupId); + + //4. 그룹 내 모든 유저 조회 + List groupMembers = findGroupMembers(groupId); + + return FindGroupMemberResponse.from(groupMembers); + } } diff --git a/src/main/java/project/flipnote/infra/config/QuerydslConfig.java b/src/main/java/project/flipnote/infra/config/QuerydslConfig.java new file mode 100644 index 00000000..075576d0 --- /dev/null +++ b/src/main/java/project/flipnote/infra/config/QuerydslConfig.java @@ -0,0 +1,20 @@ +package project.flipnote.infra.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class QuerydslConfig { + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/test/java/project/flipnote/group/service/GroupServiceTest.java b/src/test/java/project/flipnote/group/service/GroupServiceTest.java index e4bd6996..65f737f7 100644 --- a/src/test/java/project/flipnote/group/service/GroupServiceTest.java +++ b/src/test/java/project/flipnote/group/service/GroupServiceTest.java @@ -32,12 +32,15 @@ import project.flipnote.group.entity.GroupPermission; import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.exception.GroupErrorCode; +import project.flipnote.group.model.FindGroupMemberResponse; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; import project.flipnote.group.model.GroupDetailResponse; import project.flipnote.group.model.GroupPutRequest; import project.flipnote.group.model.GroupPutResponse; +import project.flipnote.group.model.GroupMemberInfo; import project.flipnote.group.repository.GroupMemberRepository; +import project.flipnote.group.repository.GroupMemberRepositoryImpl; import project.flipnote.group.repository.GroupPermissionRepository; import project.flipnote.group.repository.GroupRepository; import project.flipnote.group.repository.GroupRolePermissionRepository; @@ -71,6 +74,9 @@ class GroupServiceTest { @Mock GroupMemberRepository groupMemberRepository; + @Mock + GroupMemberRepositoryImpl groupMemberRepositoryImpl; + @Mock GroupPolicyService groupPolicyService; @@ -154,6 +160,7 @@ void before() { .maxMember(100) .imageUrl("www.~~~") .build(); + ReflectionTestUtils.setField(group, "id", 1L); GroupMember groupMember = GroupMember.builder() .group(group) @@ -161,8 +168,8 @@ void before() { .role(GroupMemberRole.MEMBER) .build(); - given(groupRepository.findByIdAndDeletedAtIsNull(any())).willReturn(Optional.of(group)); - given(groupMemberRepository.findByGroup_IdAndUser_Id(any(), any())).willReturn(Optional.of(groupMember)); + given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.of(group)); + given(groupMemberRepository.existsByGroup_IdAndUser_Id(any(), any())).willReturn(true); // 사용자 검증 로직 given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)) .willReturn(Optional.of(userProfile)); @@ -186,10 +193,11 @@ void before() { .maxMember(100) .imageUrl("www.~~~") .build(); + ReflectionTestUtils.setField(group, "id", 1L); - given(groupRepository.findByIdAndDeletedAtIsNull(any())).willReturn(Optional.ofNullable(group)); + given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.of(group)); given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.ofNullable(userProfile)); - given(groupMemberRepository.findByGroup_IdAndUser_Id(any(), any())).willReturn(Optional.empty()); + given(groupMemberRepository.existsByGroup_IdAndUser_Id(any(), any())).willReturn(false); //when BizException exception = @@ -295,6 +303,7 @@ void before() { .maxMember(100) .imageUrl("www.~~~") .build(); + ReflectionTestUtils.setField(group, "id", 1L); GroupMember groupMember = GroupMember.builder() .group(group) @@ -314,6 +323,42 @@ void before() { assertEquals(GroupErrorCode.OTHER_USER_EXIST_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(); + ReflectionTestUtils.setField(group, "id", 1L); + + GroupMember groupMember = GroupMember.builder() + .group(group) + .role(GroupMemberRole.OWNER) + .user(userProfile) + .build(); + ReflectionTestUtils.setField(groupMember, "id", 1L); + + List groupMembers = List.of(GroupMemberInfo.from(userProfile.getId(), groupMember.getRole(), userProfile.getName(), userProfile.getProfileImageUrl())); + + given(groupRepository.existsByIdAndDeletedAtIsNull(group.getId())).willReturn(true); + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(userProfile)); + given(groupMemberRepository.existsByGroup_IdAndUser_Id(group.getId(),userProfile.getId())).willReturn(true); + given(groupMemberRepository.findGroupMembers(group.getId())).willReturn(groupMembers); + //when + FindGroupMemberResponse res = groupService.findGroupMembers(authPrinciple, group.getId()); + //then + assertEquals(1, res.groupMembers().size()); + assertEquals(userProfile.getId(), res.groupMembers().get(0).id()); + then(groupRepository).should().existsByIdAndDeletedAtIsNull(group.getId()); + then(groupMemberRepository).should().findGroupMembers(group.getId()); + } + @Test public void 그룹_수정_성공() throws Exception { //given @@ -337,7 +382,7 @@ void before() { GroupPutRequest req = new GroupPutRequest("그룹1", Category.ENGLISH, "설명1", true, true, 100, "www.~~"); - given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.of(group)); + given(groupRepository.existsByIdAndDeletedAtIsNull(any())).willReturn(true); given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(userProfile)); given(groupMemberRepository.findByGroup_IdAndUser_Id(group.getId(),userProfile.getId())).willReturn(Optional.of(groupMember)); @@ -353,8 +398,8 @@ void before() { GroupPutResponse res = groupService.changeGroup(authPrinciple, req, group.getId()); //then - assertEquals(req.name(), group.getName()); - assertEquals(req.category(), group.getCategory()); + assertEquals(req.name(), res.name()); + assertEquals(req.category(), res.category()); } @Test @@ -380,7 +425,7 @@ void before() { GroupPutRequest req = new GroupPutRequest("그룹1", Category.ENGLISH, "설명1", true, true, 100, "www.~~"); - given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.of(group)); + given(groupRepository.existsByIdAndDeletedAtIsNull(any())).willReturn(true); given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(userProfile)); given(groupMemberRepository.findByGroup_IdAndUser_Id(group.getId(),userProfile.getId())).willReturn(Optional.of(groupMember)); //when @@ -414,7 +459,7 @@ void before() { GroupPutRequest req = new GroupPutRequest("그룹1", Category.ENGLISH, "설명1", true, true, 50, "www.~~"); - given(groupRepository.findByIdAndDeletedAtIsNull(group.getId())).willReturn(Optional.of(group)); + given(groupRepository.existsByIdAndDeletedAtIsNull(any())).willReturn(true); given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(userProfile)); given(groupMemberRepository.findByGroup_IdAndUser_Id(group.getId(),userProfile.getId())).willReturn(Optional.of(groupMember));