diff --git a/src/main/java/project/flipnote/infra/config/QuerydslConfig.java b/src/main/java/project/flipnote/common/config/QuerydslConfig.java similarity index 92% rename from src/main/java/project/flipnote/infra/config/QuerydslConfig.java rename to src/main/java/project/flipnote/common/config/QuerydslConfig.java index 075576d0..8b971d1f 100644 --- a/src/main/java/project/flipnote/infra/config/QuerydslConfig.java +++ b/src/main/java/project/flipnote/common/config/QuerydslConfig.java @@ -1,4 +1,4 @@ -package project.flipnote.infra.config; +package project.flipnote.common.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/project/flipnote/group/controller/GroupController.java b/src/main/java/project/flipnote/group/controller/GroupController.java index 980970cf..403b5b39 100644 --- a/src/main/java/project/flipnote/group/controller/GroupController.java +++ b/src/main/java/project/flipnote/group/controller/GroupController.java @@ -5,20 +5,25 @@ 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.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import project.flipnote.common.model.response.CursorPagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; 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.GroupInfo; +import project.flipnote.group.model.GroupListRequest; import project.flipnote.group.model.GroupPutRequest; import project.flipnote.group.model.GroupPutResponse; import project.flipnote.group.service.GroupService; @@ -78,4 +83,15 @@ public ResponseEntity findGroupMembers( return ResponseEntity.ok(res); } + + //그룹 전체 조회 + @GetMapping + public ResponseEntity> findGroup( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @Valid @ModelAttribute GroupListRequest req + ) { + CursorPagingResponse res = groupService.findGroup(authPrinciple, req); + + return ResponseEntity.ok(res); + } } diff --git a/src/main/java/project/flipnote/group/entity/GroupRolePermission.java b/src/main/java/project/flipnote/group/entity/GroupRolePermission.java index 4baaba9b..01618968 100644 --- a/src/main/java/project/flipnote/group/entity/GroupRolePermission.java +++ b/src/main/java/project/flipnote/group/entity/GroupRolePermission.java @@ -20,11 +20,11 @@ public class GroupRolePermission { @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 = "group_permission_id", nullable = false) private GroupPermission groupPermission; diff --git a/src/main/java/project/flipnote/group/exception/GroupErrorCode.java b/src/main/java/project/flipnote/group/exception/GroupErrorCode.java index ff58248f..4d581836 100644 --- a/src/main/java/project/flipnote/group/exception/GroupErrorCode.java +++ b/src/main/java/project/flipnote/group/exception/GroupErrorCode.java @@ -16,7 +16,8 @@ public enum GroupErrorCode implements ErrorCode { 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", "이미 그룹 회원입니다."), - INVALID_MEMBER_COUNT(HttpStatus.BAD_REQUEST, "GROUP_008", "그룹 내에 인원수보다 많게 수정해야합니다."); + INVALID_MEMBER_COUNT(HttpStatus.BAD_REQUEST, "GROUP_008", "현재 그룹 인원수보다 작게 설정할 수 없습니다."), + INVALID_CATEGORY(HttpStatus.BAD_REQUEST, "GROUP_009", "지원하지 않는 카테고리입니다." ); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/project/flipnote/group/model/GroupInfo.java b/src/main/java/project/flipnote/group/model/GroupInfo.java new file mode 100644 index 00000000..cee7a8ad --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupInfo.java @@ -0,0 +1,14 @@ +package project.flipnote.group.model; + +import project.flipnote.group.entity.Category; + +public record GroupInfo( + Long groupId, + String name, + String description, + Category category, + String imageUrl) { + public static GroupInfo from(Long groupId, String name, String description, Category category, String imageUrl) { + return new GroupInfo(groupId, name, description, category, imageUrl); + } +} diff --git a/src/main/java/project/flipnote/group/model/GroupListRequest.java b/src/main/java/project/flipnote/group/model/GroupListRequest.java new file mode 100644 index 00000000..f1b73056 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupListRequest.java @@ -0,0 +1,20 @@ +package project.flipnote.group.model; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import lombok.Getter; +import lombok.Setter; +import project.flipnote.common.model.request.CursorPagingRequest; + +@Setter +@Getter +public class GroupListRequest extends CursorPagingRequest { + + private String category; + + @Override + public PageRequest getPageRequest() { + return PageRequest.of(0, getSize(), Sort.by(Sort.Direction.DESC, "id")); + } +} diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java index d45348c8..f042fdd2 100644 --- a/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java @@ -3,7 +3,6 @@ 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; @@ -19,6 +18,7 @@ public class GroupMemberRepositoryImpl implements GroupMemberRepositoryCustom { QUserProfile userProfile = QUserProfile.userProfile; QGroupMember groupMember = QGroupMember.groupMember; + @Override public List findGroupMembers(Long groupId) { return queryFactory.select(Projections.constructor( GroupMemberInfo.class, @@ -32,5 +32,4 @@ public List findGroupMembers(Long groupId) { .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 2ee8c2f4..a974d52d 100644 --- a/src/main/java/project/flipnote/group/repository/GroupRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupRepository.java @@ -12,7 +12,7 @@ import project.flipnote.group.entity.Group; @Repository -public interface GroupRepository extends JpaRepository { +public interface GroupRepository extends JpaRepository, GroupRepositoryCustom { Optional findByIdAndDeletedAtIsNull(Long groupId); diff --git a/src/main/java/project/flipnote/group/repository/GroupRepositoryCustom.java b/src/main/java/project/flipnote/group/repository/GroupRepositoryCustom.java new file mode 100644 index 00000000..d410eb3b --- /dev/null +++ b/src/main/java/project/flipnote/group/repository/GroupRepositoryCustom.java @@ -0,0 +1,10 @@ +package project.flipnote.group.repository; + +import java.util.List; + +import project.flipnote.group.entity.Category; +import project.flipnote.group.model.GroupInfo; + +public interface GroupRepositoryCustom { + List findAllByCursor(Long lastId, Category category, int pageSize); +} diff --git a/src/main/java/project/flipnote/group/repository/GroupRepositoryImpl.java b/src/main/java/project/flipnote/group/repository/GroupRepositoryImpl.java new file mode 100644 index 00000000..0865cf17 --- /dev/null +++ b/src/main/java/project/flipnote/group/repository/GroupRepositoryImpl.java @@ -0,0 +1,50 @@ +package project.flipnote.group.repository; + +import java.util.List; + + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; +import project.flipnote.group.entity.Category; +import project.flipnote.group.entity.QGroup; +import project.flipnote.group.model.GroupInfo; + +@RequiredArgsConstructor +public class GroupRepositoryImpl implements GroupRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + QGroup group = QGroup.group; + + @Override + public List findAllByCursor(Long lastId, Category category, int pageSize) { + BooleanBuilder where = new BooleanBuilder() + .and(group.deletedAt.isNull()); + + if (lastId != null) { + where.and(group.id.lt(lastId)); + } + + if (category != null) { + where.and(group.category.eq(category)); + } + + return queryFactory.select(Projections.constructor( + GroupInfo.class, + group.id, + group.name, + group.description, + group.category, + group.imageUrl + )) + .from(group) + .where(where) + .orderBy(group.id.desc()) + .limit(pageSize+1) + .fetch(); + } + +} diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index b0cbc229..decc06be 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -9,7 +9,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import project.flipnote.common.exception.BizException; +import project.flipnote.common.model.response.CursorPagingResponse; import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.group.entity.Category; import project.flipnote.group.entity.Group; import project.flipnote.group.entity.GroupMember; import project.flipnote.group.entity.GroupMemberRole; @@ -21,6 +23,8 @@ import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; import project.flipnote.group.model.GroupDetailResponse; +import project.flipnote.group.model.GroupInfo; +import project.flipnote.group.model.GroupListRequest; import project.flipnote.group.model.GroupMemberInfo; import project.flipnote.group.model.GroupPutRequest; import project.flipnote.group.model.GroupPutResponse; @@ -40,6 +44,8 @@ @Transactional(readOnly = true) public class GroupService { + private static final int SIZE = 10; + private final GroupRepository groupRepository; private final GroupMemberRepository groupMemberRepository; private final GroupPermissionRepository groupPermissionRepository; @@ -50,12 +56,21 @@ public class GroupService { /* 유저 정보 조회 */ - public UserProfile validateUser(AuthPrinciple authPrinciple) { + public UserProfile getUser(AuthPrinciple authPrinciple) { return userProfileRepository.findByIdAndStatus(authPrinciple.userId(), UserStatus.ACTIVE).orElseThrow( () -> new BizException(UserErrorCode.USER_NOT_FOUND) ); } + /* + 유저 정보 조회 + */ + public void validateUser(AuthPrinciple authPrinciple) { + if(!userProfileRepository.existsByIdAndStatus(authPrinciple.userId(), UserStatus.ACTIVE)) { + throw new BizException(UserErrorCode.USER_NOT_FOUND); + } + } + /* 그룹 내 유저 검증 */ @@ -99,7 +114,7 @@ public Group getGroup(Long groupId) { public GroupCreateResponse create(AuthPrinciple authPrinciple, GroupCreateRequest req) { //1. 유저 조회 - UserProfile user = validateUser(authPrinciple); + UserProfile user = getUser(authPrinciple); //2. 인원수 검증 validateMaxMember(req.maxMember()); @@ -199,7 +214,7 @@ private void validateUserCount(Group group, int maxMember) { public GroupPutResponse changeGroup(AuthPrinciple authPrinciple, GroupPutRequest req, Long groupId) { //1. 유저 조회 - UserProfile user = validateUser(authPrinciple); + UserProfile user = getUser(authPrinciple); //2. 인원수 검증 validateMaxMember(req.maxMember()); @@ -223,8 +238,8 @@ public GroupPutResponse changeGroup(AuthPrinciple authPrinciple, GroupPutRequest /* 그룹 내 오너를 제외한 인원이 존재하는 경우 체크 */ - private boolean checkUserNotExistInGroup(UserProfile user, Long groupId) { - long count = groupMemberRepository.countByGroup_idAndUser_idNot(groupId, user.getId()); + private boolean checkUserNotExistInGroup(UserProfile user, Group group) { + long count = groupMemberRepository.countByGroup_idAndUser_idNot(group.getId(), user.getId()); if (count > 0) { return false; } @@ -248,7 +263,7 @@ public GroupDetailResponse findGroupDetail(AuthPrinciple authPrinciple, Long gro Group group = getGroup(groupId); //2. 유저 조회 - UserProfile user = validateUser(authPrinciple); + UserProfile user = getUser(authPrinciple); //3. 그룹 내 유저 조회 validateGroupInUser(user, groupId); @@ -263,7 +278,7 @@ public void deleteGroup(AuthPrinciple authPrinciple, Long groupId) { Group group = getGroup(groupId); //2. 유저 조회 - UserProfile user = validateUser(authPrinciple); + UserProfile user = getUser(authPrinciple); //3. 그룹 내 유저 조회 GroupMember groupMember = getGroupMember(user, groupId); @@ -274,10 +289,12 @@ public void deleteGroup(AuthPrinciple authPrinciple, Long groupId) { } //5. 오너를 제외한 모든 유저가 없어야 삭제 가능 - if (!checkUserNotExistInGroup(user, groupId)) { + if (!checkUserNotExistInGroup(user, group)) { throw new BizException(GroupErrorCode.OTHER_USER_EXIST_IN_GROUP); } + groupMemberRepository.delete(groupMember); + groupRepository.delete(group); } @@ -294,7 +311,7 @@ public FindGroupMemberResponse findGroupMembers(AuthPrinciple authPrinciple, Lon validateGroup(groupId); //2. 유저 조회 - UserProfile user = validateUser(authPrinciple); + UserProfile user = getUser(authPrinciple); //3. 그룹 내 유저 조회 validateGroupInUser(user, groupId); @@ -305,6 +322,38 @@ public FindGroupMemberResponse findGroupMembers(AuthPrinciple authPrinciple, Lon return FindGroupMemberResponse.from(groupMembers); } + public CursorPagingResponse findGroup(AuthPrinciple authPrinciple, GroupListRequest req) { + //1. 유저 검증 + validateUser(authPrinciple); + + //2. 카테고리 변환 + Category category = convertCategory(req.getCategory()); + + List groups = groupRepository.findAllByCursor(req.getCursorId(), category, req.getSize()); + + boolean hasNext = groups.size() > req.getSize(); + + if (hasNext) { + groups = groups.subList(0, req.getSize()); + } + + Long nextCursor = hasNext ? groups.get(groups.size() - 1).groupId() : null; + + return CursorPagingResponse.of(groups, hasNext, nextCursor); + } + + private Category convertCategory(String rawCategory) { + Category category = null; + if (rawCategory != null && !rawCategory.isBlank()) { + try { + category = Category.valueOf(rawCategory.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + throw new BizException(GroupErrorCode.INVALID_CATEGORY); + } + } + return category; + } + /** * 해당 회원에 그룹에 존재하는지 확인 * diff --git a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java index 3fa1f30a..d88930e0 100644 --- a/src/main/java/project/flipnote/user/repository/UserProfileRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserProfileRepository.java @@ -25,4 +25,6 @@ public interface UserProfileRepository extends JpaRepository @Query("SELECT up.nickname FROM UserProfile up WHERE up.id = :userId") Optional findNicknameById(@Param("userId") Long userId); + + boolean existsByIdAndStatus(Long userId, UserStatus userStatus); }