diff --git a/src/main/java/project/flipnote/group/controller/GroupController.java b/src/main/java/project/flipnote/group/controller/GroupController.java index e41b2c54..efe9104a 100644 --- a/src/main/java/project/flipnote/group/controller/GroupController.java +++ b/src/main/java/project/flipnote/group/controller/GroupController.java @@ -23,9 +23,9 @@ public class GroupController { @PostMapping("") public ResponseEntity create( - @AuthenticationPrincipal AuthPrinciple userAuth, + @AuthenticationPrincipal AuthPrinciple authPrinciple, @Valid @RequestBody GroupCreateRequest req) { - GroupCreateResponse res = groupService.create(userAuth, req); + GroupCreateResponse res = groupService.create(authPrinciple, req); return ResponseEntity.status(HttpStatus.CREATED).body(res); } } diff --git a/src/main/java/project/flipnote/group/entity/GroupPermission.java b/src/main/java/project/flipnote/group/entity/GroupPermission.java index 0f35db27..306b3355 100644 --- a/src/main/java/project/flipnote/group/entity/GroupPermission.java +++ b/src/main/java/project/flipnote/group/entity/GroupPermission.java @@ -1,11 +1,6 @@ package project.flipnote.group.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -23,11 +18,12 @@ public class GroupPermission { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Enumerated(EnumType.STRING) @Column(nullable = false, length = 50, unique = true) - private String name; + private GroupPermissionStatus name; @Builder - private GroupPermission(String name) { + private GroupPermission(GroupPermissionStatus name) { this.name = name; } } diff --git a/src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java b/src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java new file mode 100644 index 00000000..6db90199 --- /dev/null +++ b/src/main/java/project/flipnote/group/entity/GroupPermissionStatus.java @@ -0,0 +1,5 @@ +package project.flipnote.group.entity; + +public enum GroupPermissionStatus { + INVITE, KICK, JOIN_REQUEST_MANAGE +} diff --git a/src/main/java/project/flipnote/group/entity/GroupRole.java b/src/main/java/project/flipnote/group/entity/GroupRole.java deleted file mode 100644 index aeeda259..00000000 --- a/src/main/java/project/flipnote/group/entity/GroupRole.java +++ /dev/null @@ -1,5 +0,0 @@ -package project.flipnote.group.entity; - -public enum GroupRole { - HEAD_MANAGER, MANAGER, STAFF; -} diff --git a/src/main/java/project/flipnote/group/entity/GroupRolePermission.java b/src/main/java/project/flipnote/group/entity/GroupRolePermission.java index c24b19ba..4baaba9b 100644 --- a/src/main/java/project/flipnote/group/entity/GroupRolePermission.java +++ b/src/main/java/project/flipnote/group/entity/GroupRolePermission.java @@ -30,10 +30,10 @@ public class GroupRolePermission { @Enumerated(EnumType.STRING) @Column(name = "role", nullable = false) - private GroupRole role; + private GroupMemberRole role; @Builder - private GroupRolePermission(Group group, GroupPermission groupPermission, GroupRole role) { + private GroupRolePermission(Group group, GroupPermission groupPermission, GroupMemberRole role) { this.group = group; this.groupPermission = groupPermission; this.role = role; diff --git a/src/main/java/project/flipnote/group/exception/GroupErrorCode.java b/src/main/java/project/flipnote/group/exception/GroupErrorCode.java index cb4eaac3..3604efe4 100644 --- a/src/main/java/project/flipnote/group/exception/GroupErrorCode.java +++ b/src/main/java/project/flipnote/group/exception/GroupErrorCode.java @@ -10,6 +10,7 @@ @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 이하여야 합니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/project/flipnote/group/model/GroupCreateDto.java b/src/main/java/project/flipnote/group/model/GroupCreateDto.java new file mode 100644 index 00000000..e06fd141 --- /dev/null +++ b/src/main/java/project/flipnote/group/model/GroupCreateDto.java @@ -0,0 +1,48 @@ +package project.flipnote.group.model; + +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import project.flipnote.group.entity.Category; + +public class GroupCreateDto { + public record Request( + @NotBlank + @Size(max = 50) + String name, + + @NotNull + Category category, + + @NotBlank + String description, + + @NotNull + Boolean applicationRequired, + + @NotNull + Boolean publicVisible, + + @NotNull + @Min(1) + @Max(100) + Integer maxMember, + + @URL + String image + ) { + } + + public record Response( + Long groupId + ){ + public static Response from(Long groupId) { + return new Response(groupId); + } + } +} diff --git a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java index a87f3f1b..c6470394 100644 --- a/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupMemberRepository.java @@ -2,9 +2,18 @@ import org.springframework.data.jpa.repository.JpaRepository; +import project.flipnote.group.entity.Group; + import org.springframework.stereotype.Repository; import project.flipnote.group.entity.GroupMember; +import project.flipnote.user.entity.UserProfile; + +import java.util.Optional; @Repository public interface GroupMemberRepository extends JpaRepository { + Optional findByGroupAndUser(Group group, UserProfile userProfile); + + long countByGroup_Id(Long groupId); + } diff --git a/src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java b/src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java index 7efcafbe..e2b9c823 100644 --- a/src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupPermissionRepository.java @@ -4,7 +4,9 @@ import org.springframework.stereotype.Repository; import project.flipnote.group.entity.GroupPermission; +import project.flipnote.group.entity.GroupPermissionStatus; @Repository public interface GroupPermissionRepository extends JpaRepository { + GroupPermission findByName(GroupPermissionStatus name); } diff --git a/src/main/java/project/flipnote/group/repository/GroupRepository.java b/src/main/java/project/flipnote/group/repository/GroupRepository.java index 3cbb231a..1959fc9f 100644 --- a/src/main/java/project/flipnote/group/repository/GroupRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupRepository.java @@ -1,10 +1,22 @@ package project.flipnote.group.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import jakarta.persistence.LockModeType; import project.flipnote.group.entity.Group; +import project.flipnote.group.entity.GroupMember; + +import java.util.List; +import java.util.Optional; @Repository public interface GroupRepository extends JpaRepository { + Optional findById(Long groupId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select g from Group g where g.id = :id") + Optional findByIdForUpdate(Long groupId); } diff --git a/src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java b/src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java index f8637e08..f61756b0 100644 --- a/src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java +++ b/src/main/java/project/flipnote/group/repository/GroupRolePermissionRepository.java @@ -2,9 +2,13 @@ import org.springframework.data.jpa.repository.JpaRepository; +import project.flipnote.group.entity.Group; +import project.flipnote.group.entity.GroupMemberRole; +import project.flipnote.group.entity.GroupPermission; import org.springframework.stereotype.Repository; import project.flipnote.group.entity.GroupRolePermission; @Repository public interface GroupRolePermissionRepository extends JpaRepository { + boolean existsByGroupAndRoleAndGroupPermission(Group group, GroupMemberRole role, GroupPermission groupPermission); } diff --git a/src/main/java/project/flipnote/group/service/GroupService.java b/src/main/java/project/flipnote/group/service/GroupService.java index b00ff005..eba053c5 100644 --- a/src/main/java/project/flipnote/group/service/GroupService.java +++ b/src/main/java/project/flipnote/group/service/GroupService.java @@ -14,7 +14,6 @@ import project.flipnote.group.entity.GroupMember; import project.flipnote.group.entity.GroupMemberRole; import project.flipnote.group.entity.GroupPermission; -import project.flipnote.group.entity.GroupRole; import project.flipnote.group.entity.GroupRolePermission; import project.flipnote.group.exception.GroupErrorCode; import project.flipnote.group.model.GroupCreateRequest; @@ -24,6 +23,7 @@ import project.flipnote.group.repository.GroupRepository; import project.flipnote.group.repository.GroupRolePermissionRepository; import project.flipnote.user.entity.UserProfile; +import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.repository.UserProfileRepository; @@ -40,18 +40,18 @@ public class GroupService { private final UserProfileRepository userProfileRepository; //유저 정보 조회 - public UserProfile findUser(AuthPrinciple userAuth) { - return userProfileRepository.findById(userAuth.userId()).orElseThrow( + public UserProfile findUser(AuthPrinciple authPrinciple) { + return userProfileRepository.findByIdAndStatus(authPrinciple.userId(), UserStatus.ACTIVE).orElseThrow( () -> new BizException(UserErrorCode.USER_NOT_FOUND) ); } //그룹 생성 @Transactional - public GroupCreateResponse create(AuthPrinciple userAuth, GroupCreateRequest req) { + public GroupCreateResponse create(AuthPrinciple authPrinciple, GroupCreateRequest req) { //1. 유저 조회 - UserProfile user = findUser(userAuth); + UserProfile userProfile = findUser(authPrinciple); //2. 인원수 검증 validateMaxMember(req.maxMember()); @@ -60,21 +60,21 @@ public GroupCreateResponse create(AuthPrinciple userAuth, GroupCreateRequest req Group group = createGroup(req); //4. 그룹 회원 정보 생성 - saveGroupOwner(group, user); + saveGroupOwner(group, userProfile); //5. 그룹 내의 모든 권한 조회 initializeGroupPermissions(group); return GroupCreateResponse.from(group.getId()); } - + /* 최초 그룹 권한 설정 */ private void initializeGroupPermissions(Group group) { List groupPermissions = groupPermissionRepository.findAll(); - List groupRolePermissions = Arrays.stream(GroupRole.values()) + List groupRolePermissions = Arrays.stream(GroupMemberRole.values()) .flatMap(role -> groupPermissions.stream() .map(permission -> GroupRolePermission.builder() .group(group) @@ -85,7 +85,7 @@ private void initializeGroupPermissions(Group group) { groupRolePermissionRepository.saveAll(groupRolePermissions); } - + /* 그룹 생성 메서드 */ @@ -106,7 +106,7 @@ private Group createGroup(GroupCreateRequest req) { return saveGroup; } - + /* 그룹 생성시 오너 멤버 추가 */ diff --git a/src/main/java/project/flipnote/groupjoin/controller/GroupJoinController.java b/src/main/java/project/flipnote/groupjoin/controller/GroupJoinController.java new file mode 100644 index 00000000..2ae492c6 --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/controller/GroupJoinController.java @@ -0,0 +1,76 @@ +package project.flipnote.groupjoin.controller; + +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import lombok.RequiredArgsConstructor; +import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.groupjoin.model.*; +import project.flipnote.groupjoin.service.GroupJoinService; + +@RestController +@RequestMapping("/v1") +@RequiredArgsConstructor +public class GroupJoinController { + + private final GroupJoinService groupJoinService; + + //가입 신청 요청 + @PostMapping("/groups/{groupId}/joins") + public ResponseEntity joinRequest( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @PathVariable("groupId") Long groupId, + @Valid @RequestBody GroupJoinRequest req) { + GroupJoinResponse res = groupJoinService.joinRequest(authPrinciple, groupId, req); + + return ResponseEntity.status(HttpStatus.CREATED).body(res); + } + + //그룹 내 가입 신청한 리스트 조회 + @GetMapping("/groups/{groupId}/joins") + public ResponseEntity findGroupJoinList( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @PathVariable("groupId") Long groupId) { + GroupJoinListResponse res = groupJoinService.findGroupJoinList(authPrinciple, groupId); + + return ResponseEntity.ok(res); + } + + //가입 신청 응답 + @PatchMapping("/groups/{groupId}/joins/{joinId}") + public ResponseEntity respondToJoinRequest( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @PathVariable("groupId") Long groupId, + @PathVariable("joinId") Long joinId, + @Valid @RequestBody GroupJoinRespondRequest req) { + + GroupJoinRespondResponse res = groupJoinService.respondToJoinRequest(authPrinciple, groupId, joinId, req); + + return ResponseEntity.ok(res); + } + + //가입 신청 삭제 + @DeleteMapping("/groups/{groupId}/joins/{joinId}") + public ResponseEntity groupJoinDelete( + @AuthenticationPrincipal AuthPrinciple authPrinciple, + @PathVariable("groupId") Long groupId, + @PathVariable("joinId") Long joinId + ) { + groupJoinService.groupJoinDelete(authPrinciple, groupId, joinId); + + return ResponseEntity.noContent().build(); + } + + //내가 신청한 가입신청 리스트 조회 + @GetMapping("/groups/joins/me") + public ResponseEntity findGroupJoinMe( + @AuthenticationPrincipal AuthPrinciple authPrinciple + ) { + FindGroupJoinListMeResponse res = groupJoinService.findGroupJoinListMe(authPrinciple); + + return ResponseEntity.ok(res); + } +} diff --git a/src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java b/src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java new file mode 100644 index 00000000..223d943b --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/entity/GroupJoin.java @@ -0,0 +1,65 @@ +package project.flipnote.groupjoin.entity; + +import jakarta.persistence.Column; +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; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import project.flipnote.common.entity.BaseEntity; +import project.flipnote.group.entity.Group; +import project.flipnote.user.entity.UserProfile; + +@Getter +@Entity +@Table(name = "group_joins") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GroupJoin extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private UserProfile user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "group_id", nullable = false) + private Group group; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private GroupJoinStatus status; + + private String joinIntro; + + @Builder + public GroupJoin + ( + UserProfile user, + Group group, + GroupJoinStatus status, + String joinIntro + ) + + { + this.user = user; + this.group = group; + this.status = status; + this.joinIntro = joinIntro; + } + + public void updateStatus(GroupJoinStatus status) { + this.status = status; + } +} diff --git a/src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java b/src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java new file mode 100644 index 00000000..2825038c --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/entity/GroupJoinStatus.java @@ -0,0 +1,5 @@ +package project.flipnote.groupjoin.entity; + +public enum GroupJoinStatus { + ACCEPT, PENDING, REJECT, CANCEL +} diff --git a/src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java b/src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java new file mode 100644 index 00000000..476f3d52 --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/exception/GroupJoinErrorCode.java @@ -0,0 +1,26 @@ +package project.flipnote.groupjoin.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import project.flipnote.common.exception.ErrorCode; + +@Getter +@RequiredArgsConstructor +public enum GroupJoinErrorCode implements ErrorCode { + USER_NOT_IN_GROUP(HttpStatus.NOT_FOUND, "GROUP_JOIN_001", "그룹에 유저가 존재하지 않습니다."), + USER_NOT_PERMISSION(HttpStatus.FORBIDDEN, "GROUP_JOIN_002", "그룹 내 권한이 없습니다."), + GROUP_IS_NOT_PUBLIC(HttpStatus.FORBIDDEN, "GROUP_JOIN_003", "그룹이 비공개입니다."), + NOT_EXIST_JOIN(HttpStatus.NOT_FOUND, "GROUP_JOIN_004", "가입 신청이 존재하지 않습니다."), + ALREADY_JOINED_GROUP(HttpStatus.CONFLICT, "GROUP_JOIN_005", "이미 신청한 그룹입니다."), + GROUP_IS_ALREADY_MAX_MEMBER(HttpStatus.CONFLICT, "GROUP_JOIN_006", "그룹 정원이 가득 찼습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public int getStatus() { + return httpStatus.value(); + } +} diff --git a/src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java b/src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java new file mode 100644 index 00000000..b9c3565c --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java @@ -0,0 +1,13 @@ +package project.flipnote.groupjoin.model; + +import java.util.List; + +import project.flipnote.groupjoin.entity.GroupJoin; + +public record FindGroupJoinListMeResponse( + List groupJoins +) { + public static FindGroupJoinListMeResponse from(List groupJoins) { + return new FindGroupJoinListMeResponse(groupJoins); + } +} diff --git a/src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java b/src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java new file mode 100644 index 00000000..96cec14a --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java @@ -0,0 +1,13 @@ +package project.flipnote.groupjoin.model; + +import project.flipnote.groupjoin.entity.GroupJoin; + +import java.util.List; + +public record GroupJoinListResponse( + List groupJoins +) { + public static GroupJoinListResponse from(List groupJoins) { + return new GroupJoinListResponse(groupJoins); + } +} diff --git a/src/main/java/project/flipnote/groupjoin/model/GroupJoinRequest.java b/src/main/java/project/flipnote/groupjoin/model/GroupJoinRequest.java new file mode 100644 index 00000000..bb415110 --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/model/GroupJoinRequest.java @@ -0,0 +1,6 @@ +package project.flipnote.groupjoin.model; + +public record GroupJoinRequest( + String joinIntro +) { +} diff --git a/src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondRequest.java b/src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondRequest.java new file mode 100644 index 00000000..06501efc --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondRequest.java @@ -0,0 +1,8 @@ +package project.flipnote.groupjoin.model; + +import project.flipnote.groupjoin.entity.GroupJoinStatus; + +public record GroupJoinRespondRequest( + GroupJoinStatus status +) { +} diff --git a/src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java b/src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java new file mode 100644 index 00000000..68626b8b --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/model/GroupJoinRespondResponse.java @@ -0,0 +1,9 @@ +package project.flipnote.groupjoin.model; + +public record GroupJoinRespondResponse( + Long groupJoinId +) { + public static GroupJoinRespondResponse from(Long groupJoinId) { + return new GroupJoinRespondResponse(groupJoinId); + } +} diff --git a/src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java b/src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java new file mode 100644 index 00000000..bf45c875 --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/model/GroupJoinResponse.java @@ -0,0 +1,11 @@ +package project.flipnote.groupjoin.model; + +import project.flipnote.groupjoin.entity.GroupJoinStatus; + +public record GroupJoinResponse( + Long groupJoinId, + GroupJoinStatus status) { + public static GroupJoinResponse from(Long groupJoinId, GroupJoinStatus status) { + return new GroupJoinResponse(groupJoinId, status); + } +} diff --git a/src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java b/src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java new file mode 100644 index 00000000..54780ead --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java @@ -0,0 +1,19 @@ +package project.flipnote.groupjoin.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import project.flipnote.group.entity.Group; +import project.flipnote.groupjoin.entity.GroupJoin; +import project.flipnote.user.entity.UserProfile; + +import java.util.List; + +@Repository +public interface GroupJoinRepository extends JpaRepository { + List findAllByGroup(Group group); + + List findAllByUser(UserProfile userProfile); + + boolean existsByGroup_idAndUser_id(Long groupId, Long userId); +} diff --git a/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java b/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java new file mode 100644 index 00000000..71e78665 --- /dev/null +++ b/src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java @@ -0,0 +1,244 @@ +package project.flipnote.groupjoin.service; + + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; +import project.flipnote.common.exception.BizException; +import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.group.entity.*; +import project.flipnote.group.exception.GroupErrorCode; +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.entity.GroupJoin; +import project.flipnote.groupjoin.entity.GroupJoinStatus; +import project.flipnote.groupjoin.exception.GroupJoinErrorCode; +import project.flipnote.groupjoin.model.*; +import project.flipnote.groupjoin.repository.GroupJoinRepository; +import project.flipnote.user.entity.UserProfile; +import project.flipnote.user.entity.UserStatus; +import project.flipnote.user.exception.UserErrorCode; +import project.flipnote.user.repository.UserProfileRepository; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GroupJoinService { + private final GroupRepository groupRepository; + private final UserProfileRepository userProfileRepository; + private final GroupJoinRepository groupJoinRepository; + private final GroupMemberRepository groupMemberRepository; + private final GroupRolePermissionRepository groupRolePermissionRepository; + private final GroupPermissionRepository groupPermissionRepository; + + //유저 정보 조회 + private UserProfile findUser(AuthPrinciple authPrinciple) { + return userProfileRepository.findByIdAndStatus(authPrinciple.userId(), UserStatus.ACTIVE).orElseThrow( + () -> new BizException(UserErrorCode.USER_NOT_FOUND) + ); + } + + //그룹 정보 조회 + private Group findGroup(Long groupId) { + return groupRepository.findById(groupId).orElseThrow( + () -> new BizException(GroupErrorCode.GROUP_NOT_FOUND) + ); + } + + //중복조회 + private Boolean existGroupJoin(Group group, UserProfile userProfile) { + return groupJoinRepository.existsByGroup_idAndUser_id(group.getId(), userProfile.getId()); + } + + private void checkMaxMember(Group group) { + + Group lockedGroup = groupRepository.findByIdForUpdate(group.getId()).orElseThrow( + () -> new BizException(GroupErrorCode.GROUP_NOT_FOUND) + ); + + long currentMemberCount = groupMemberRepository.countByGroup_Id(lockedGroup.getId()); + if (currentMemberCount >= lockedGroup.getMaxMember()) { + throw new BizException(GroupJoinErrorCode.GROUP_IS_ALREADY_MAX_MEMBER); + } + } + + //그룹 내 권한 정보 조회 + private Boolean hasPermission(Group group, UserProfile userProfile) { + GroupMember groupMember = groupMemberRepository.findByGroupAndUser(group, userProfile).orElseThrow( + () -> new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP) + ); + + GroupPermission groupPermission = groupPermissionRepository.findByName(GroupPermissionStatus.JOIN_REQUEST_MANAGE); + + return groupRolePermissionRepository.existsByGroupAndRoleAndGroupPermission( + group, + groupMember.getRole(), + groupPermission); + } + + //그룹 내 모든 가입신청 요청 조회 + private List findGroupJoins(Group group) { + return groupJoinRepository.findAllByGroup(group); + } + + //가입 신청 조회 + private GroupJoin findGroupJoin(Long joinId) { + return groupJoinRepository.findById(joinId).orElseThrow( + () -> new BizException(GroupJoinErrorCode.NOT_EXIST_JOIN) + ); + } + + //가입 신청 요청 + @Transactional + public GroupJoinResponse joinRequest(AuthPrinciple authPrinciple, Long groupId, GroupJoinRequest req) { + //유저 조회 + UserProfile user = findUser(authPrinciple); + //그룹 조회 + Group group = findGroup(groupId); + + if (existGroupJoin(group, user)) { + throw new BizException(GroupJoinErrorCode.ALREADY_JOINED_GROUP); + } + + //비공개 그룹일 경우 + if (!group.getPublicVisible()) { + throw new BizException(GroupJoinErrorCode.GROUP_IS_NOT_PUBLIC); + } + + //그룹이 최대인원인 경우 + checkMaxMember(group); + + GroupJoinStatus status = GroupJoinStatus.ACCEPT; + //가입 신청이 필수이면 pending 아니면 바로 가입 + if (group.getApplicationRequired()) { + status = GroupJoinStatus.PENDING; + } + + GroupJoin groupJoin = GroupJoin.builder() + .group(group) + .user(user) + .joinIntro(req.joinIntro()) + .status(status) + .build(); + + groupJoinRepository.save(groupJoin); + + return GroupJoinResponse.from(groupJoin.getId(), groupJoin.getStatus()); + } + + //그룹 가입 신청 리스트 조회 + public GroupJoinListResponse findGroupJoinList(AuthPrinciple authPrinciple, Long groupId) { + //유저 조회 + UserProfile userProfile = findUser(authPrinciple); + + //그룹 조회 + Group group = findGroup(groupId); + + //그룹 내 권한 조회 + Boolean isExistPermission = hasPermission(group, userProfile); + + //권한 존재하지 않으면 에러 + if (!isExistPermission) { + throw new BizException(GroupJoinErrorCode.USER_NOT_PERMISSION); + } + + //그룹 내 가입 신청 리스트 조회 + List groupJoins = findGroupJoins(group); + + //반환 + return GroupJoinListResponse.from(groupJoins); + } + + //가입 신청 응답 + @Transactional + public GroupJoinRespondResponse respondToJoinRequest(AuthPrinciple authPrinciple, Long groupId, Long joinId, @Valid GroupJoinRespondRequest req) { + //유저 조회 + UserProfile user = findUser(authPrinciple); + + //그룹 조회 + Group group = findGroup(groupId); + + //그룹 내 권한 조회 + Boolean isExistPermission = hasPermission(group, user); + + //권한 존재하지 않으면 에러 + if (!isExistPermission) { + throw new BizException(GroupJoinErrorCode.USER_NOT_PERMISSION); + } + + //그룹 가입 신청 조회 + GroupJoin groupJoin = findGroupJoin(joinId); + + //최대 인원 조회 + if (req.status() == GroupJoinStatus.ACCEPT) { + checkMaxMember(group); + // 업데이트 + groupJoin.updateStatus(req.status()); + + groupJoinRepository.save(groupJoin); + + //그룹 멤버 추가 + GroupMember groupMember = GroupMember.builder() + .group(group) + .user(groupJoin.getUser()) + .role(GroupMemberRole.MEMBER) + .build(); + + groupMemberRepository.save(groupMember); + + return GroupJoinRespondResponse.from(groupJoin.getId()); + } + + groupJoin.updateStatus(req.status()); + + groupJoinRepository.save(groupJoin); + + return GroupJoinRespondResponse.from(groupJoin.getId()); + } + + //삭제 + @Transactional + public void groupJoinDelete(AuthPrinciple authPrinciple, Long groupId, Long joinId) { + //유저 조회 + UserProfile user = findUser(authPrinciple); + + //신청 조회 + GroupJoin groupJoin = groupJoinRepository.findById(joinId).orElseThrow( + () -> new BizException(GroupJoinErrorCode.NOT_EXIST_JOIN) + ); + + //그룹이 일치하지 않으면 에러 + if (!groupJoin.getGroup().getId().equals(groupId)) { + throw new BizException(GroupJoinErrorCode.USER_NOT_PERMISSION); + } + + //자신이 유저가 아니면 에러 + if (!groupJoin.getUser().getId().equals(user.getId())) { + throw new BizException(GroupJoinErrorCode.USER_NOT_PERMISSION); + } + + groupJoin.updateStatus(GroupJoinStatus.CANCEL); + + //삭제 + groupJoinRepository.save(groupJoin); + } + + //내가 신청한 리스트 조회 + public FindGroupJoinListMeResponse findGroupJoinListMe(AuthPrinciple authPrinciple) { + //유저 조회 + UserProfile user = findUser(authPrinciple); + + //유저별 그룹 신청 리스트 조회 + List groupJoins = groupJoinRepository.findAllByUser(user); + + return FindGroupJoinListMeResponse.from(groupJoins); + } +} diff --git a/src/test/java/project/flipnote/fixture/UserFixture.java b/src/test/java/project/flipnote/fixture/UserFixture.java new file mode 100644 index 00000000..a285aace --- /dev/null +++ b/src/test/java/project/flipnote/fixture/UserFixture.java @@ -0,0 +1,26 @@ +package project.flipnote.fixture; + +import org.springframework.test.util.ReflectionTestUtils; + +import project.flipnote.user.entity.UserProfile; + +public class UserFixture { + + public static final String ENCODED_PASSWORD = "encodedPass"; + public static final String USER_EMAIL = "test@test.com"; + + public static UserProfile createActiveUser() { + UserProfile userProfile = UserProfile.builder() + .email(USER_EMAIL) + .nickname("테스트닉네임") + .name("테스트이름") + .phone("+821012345678") + .smsAgree(true) + .profileImageUrl("test_image_url") + .build(); + + ReflectionTestUtils.setField(userProfile, "id", 1L); + + return userProfile; + } +} diff --git a/src/test/java/project/flipnote/group/service/GroupServiceTest.java b/src/test/java/project/flipnote/group/service/GroupServiceTest.java index 4b1bdbc4..5383ceec 100644 --- a/src/test/java/project/flipnote/group/service/GroupServiceTest.java +++ b/src/test/java/project/flipnote/group/service/GroupServiceTest.java @@ -20,16 +20,17 @@ import project.flipnote.auth.repository.EmailVerificationRedisRepository; import project.flipnote.common.exception.BizException; import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.fixture.UserFixture; import project.flipnote.group.entity.Category; import project.flipnote.group.entity.Group; import project.flipnote.group.entity.GroupPermission; +import project.flipnote.group.entity.GroupPermissionStatus; import project.flipnote.group.model.GroupCreateRequest; import project.flipnote.group.model.GroupCreateResponse; 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.user.entity.UserProfile; import project.flipnote.user.entity.UserStatus; import project.flipnote.user.repository.UserProfileRepository; @@ -51,7 +52,7 @@ class GroupServiceTest { GroupRolePermissionRepository groupRolePermissionRepository; @Mock - UserProfileRepository userRepository; + UserProfileRepository userProfileRepository; @Mock EmailVerificationRedisRepository emailVerificationRedisRepository; @@ -59,16 +60,17 @@ class GroupServiceTest { @Mock GroupMemberRepository groupMemberRepository; - UserProfile user; + UserProfile userProfile; AuthPrinciple authPrinciple; @BeforeEach void before() { - // user = UserFixture.createActiveUser(); - // authPrinciple = new AuthPrinciple(user.getId(), user.getEmail(), user.getRole(), user.getTokenVersion()); + // userProfile = UserFixture.createActiveUser(); + // authPrinciple = new AuthPrinciple(userProfile.getId(), userProfile.getEmail(), userProfile.getRole(), userProfile.getTokenVersion()); // 사용자 검증 로직 - given(userRepository.findByIdAndStatus(user.getId(), UserStatus.ACTIVE)).willReturn(Optional.of(user)); + given(userProfileRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of( + userProfile)); } // @Test @@ -82,14 +84,14 @@ void before() { // // // 그룹 퍼미션 미리 세팅 // List permissions = List.of( - // GroupPermission.builder().name("INVITE").build(), - // GroupPermission.builder().name("KICK").build(), - // GroupPermission.builder().name("JOIN_REQUEST_MANAGE").build() + // 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); + // GroupCreateResponse response = groupService.create(userPrincipal, req); // // // then // assertThat(response.groupId()).isEqualTo(1L); @@ -104,7 +106,7 @@ void before() { // // // // when & then - // assertThrows(BizException.class, () -> groupService.create(authPrinciple, req)); + // assertThrows(BizException.class, () -> groupService.create(userPrincipal, req)); // } // // @Test @@ -115,6 +117,6 @@ void before() { // ReflectionTestUtils.setField(group, "id", 1L); // // // when & then - // assertThrows(BizException.class, () -> groupService.create(authPrinciple, req)); + // assertThrows(BizException.class, () -> groupService.create(userPrincipal, req)); // } } diff --git a/src/test/java/project/flipnote/groupjoin/service/GroupJoinServiceTest.java b/src/test/java/project/flipnote/groupjoin/service/GroupJoinServiceTest.java new file mode 100644 index 00000000..0065030d --- /dev/null +++ b/src/test/java/project/flipnote/groupjoin/service/GroupJoinServiceTest.java @@ -0,0 +1,283 @@ +package project.flipnote.groupjoin.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.lang.reflect.Field; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import project.flipnote.common.exception.BizException; +import project.flipnote.common.security.dto.AuthPrinciple; +import project.flipnote.fixture.UserFixture; +import project.flipnote.group.entity.Group; +import project.flipnote.group.exception.GroupErrorCode; +import project.flipnote.group.repository.GroupMemberRepository; +import project.flipnote.group.repository.GroupPermissionRepository; +import project.flipnote.group.repository.GroupRepository; +import project.flipnote.groupjoin.entity.GroupJoin; +import project.flipnote.groupjoin.entity.GroupJoinStatus; +import project.flipnote.groupjoin.exception.GroupJoinErrorCode; +import project.flipnote.groupjoin.model.GroupJoinRequest; +import project.flipnote.groupjoin.model.GroupJoinResponse; +import project.flipnote.groupjoin.repository.GroupJoinRepository; +import project.flipnote.user.entity.UserProfile; +import project.flipnote.user.entity.UserStatus; +import project.flipnote.user.repository.UserProfileRepository; + +@ExtendWith(MockitoExtension.class) +class GroupJoinServiceTest { + + @InjectMocks + GroupJoinService groupJoinService; + + @Mock + UserProfileRepository userProfileRepository; + + @Mock + GroupRepository groupRepository; + + @Mock + GroupJoinRepository groupJoinRepository; + + @Mock + GroupMemberRepository groupMemberRepository; + + @Mock + GroupPermissionRepository groupPermissionRepository; + + UserProfile userProfile; + AuthPrinciple authPrinciple; + Group group; + + @BeforeEach + void before() { + // userProfile = UserFixture.createActiveUser(); + // userPrincipal = new UserPrincipal(userProfile.getId(), userProfile.getEmail(), userProfile.getRole(), userProfile.getTokenVersion()); + // + // // 사용자 검증 로직 + // given(userRepository.findByIdAndStatus(userProfile.getId(), UserStatus.ACTIVE)).willReturn(Optional.of( + // userProfile)); + } + + // @Test + // void 가입신청_요청_성공() { + // //given + // group = Group.builder().name("123").imageUrl("12312").publicVisible(true).applicationRequired(true).build(); + // given(groupRepository.findById(1L)).willReturn(Optional.of(group)); + // GroupJoinRequest groupJoinRequest = new GroupJoinRequest("안녕하세요."); + // when(groupJoinRepository.save(any())).thenAnswer(invocation -> { + // GroupJoin join = invocation.getArgument(0); + // + // // reflection으로 id 필드 설정 + // Field idField = GroupJoin.class.getDeclaredField("id"); + // idField.setAccessible(true); + // idField.set(join, 1L); + // + // return join; + // }); + // + // //when + // GroupJoinResponse res = groupJoinService.joinRequest(userPrincipal, 1L, groupJoinRequest); + // + // //then + // assertEquals(1L, res.groupJoinId()); + // assertEquals(GroupJoinStatus.PENDING, res.status()); + // } + // + // @Test + // void 가입신청_요청_성공_가입_신청_필수가아닐경우() { + // //given + // group = Group.builder().name("123").imageUrl("12312").publicVisible(true).applicationRequired(false).build(); + // given(groupRepository.findById(1L)).willReturn(Optional.of(group)); + // GroupJoinRequest groupJoinRequest = new GroupJoinRequest("안녕하세요."); + // when(groupJoinRepository.save(any())).thenAnswer(invocation -> { + // GroupJoin join = invocation.getArgument(0); + // + // // reflection으로 id 필드 설정 + // Field idField = GroupJoin.class.getDeclaredField("id"); + // idField.setAccessible(true); + // idField.set(join, 1L); + // + // return join; + // }); + // + // //when + // GroupJoinResponse res = groupJoinService.joinRequest(userPrincipal, 1L, groupJoinRequest); + // + // //then + // assertEquals(GroupJoinStatus.ACCEPT, res.status()); + // } + // + // @Test + // void 가입신청_요청_실패_그룹_존재_없을_경우() { + // // given + // GroupJoinRequest groupJoinRequest = new GroupJoinRequest("안녕하세요."); + // + // //when + // when(groupRepository.findById(2L)) + // .thenReturn(Optional.empty()); + // + // //then + // BizException exception = assertThrows( + // BizException.class, + // () -> groupJoinService.joinRequest(userPrincipal, 2L, groupJoinRequest) + // ); + // + // assertEquals(GroupErrorCode.GROUP_NOT_FOUND, exception.getErrorCode()); + // } + // + // @Test + // void 가입신청_요청_실패_그룹_비공개일_경우() { + // // given + // GroupJoinRequest groupJoinRequest = new GroupJoinRequest("안녕하세요."); + // group = Group.builder().name("123").imageUrl("12312").publicVisible(false).applicationRequired(true).build(); + // given(groupRepository.findById(1L)).willReturn(Optional.of(group)); + // + // // when + // + // // then + // BizException exception = assertThrows( + // BizException.class, + // () -> groupJoinService.joinRequest(userPrincipal, 1L, groupJoinRequest) + // ); + // + // assertEquals(GroupJoinErrorCode.GROUP_IS_NOT_PUBLIC, exception.getErrorCode()); + // } + // + // @Test + // void 가입신청_요청_실패_최대인원인_경우() { + // // given + // GroupJoinRequest groupJoinRequest = new GroupJoinRequest("안녕하세요."); + // group = Group.builder().maxMember(100).name("123").imageUrl("12312").publicVisible(true).applicationRequired(true).build(); + // given(groupRepository.findById(1L)).willReturn(Optional.of(group)); + // given(groupMemberRepository.countByGroup_Id(any())).willReturn(200L); + // // when + // + // // then + // BizException exception = assertThrows( + // BizException.class, + // () -> groupJoinService.joinRequest(userPrincipal, 1L, groupJoinRequest) + // ); + // + // assertEquals(GroupJoinErrorCode.GROUP_IS_ALREADY_MAX_MEMBER, exception.getErrorCode()); + // } + // + // @Test + // void 가입신청_요청_실패_이미_신청한_경우() { + // // given + // group = Group.builder() + // .name("123") + // .maxMember(100) + // .imageUrl("12312") + // .publicVisible(true) + // .applicationRequired(true) + // .build(); + // given(groupRepository.findById(1L)).willReturn(Optional.of(group)); + // + // GroupJoinRequest groupJoinRequest = new GroupJoinRequest("안녕하세요."); + // + // GroupJoin alreadyJoined = GroupJoin.builder() + // .group(group) + // .user(userProfile) + // .joinIntro("이미 있음") + // .status(GroupJoinStatus.PENDING) + // .build(); + // // 이미 신청한 이력이 있다고 가정 + // given(groupJoinRepository.existsByGroup_idAndUser_id(group.getId(), userProfile.getId())).willReturn(true); + // + // // when + // BizException exception = assertThrows( + // BizException.class, + // () -> groupJoinService.joinRequest(userPrincipal, 1L, groupJoinRequest) + // ); + // + // // then + // assertEquals(GroupJoinErrorCode.ALREADY_JOINED_GROUP, exception.getErrorCode()); + // } + // + // @Test + // void 가입신청_삭제_성공_본인_신청내역_취소() throws Exception { + // // given + // group = Group.builder().build(); + // Field idField = Group.class.getDeclaredField("id"); + // idField.setAccessible(true); + // idField.set(group, 1L); + // + // GroupJoin groupJoin = GroupJoin.builder() + // .group(group) + // .user(userProfile) + // .status(GroupJoinStatus.PENDING) + // .build(); + // + // // 리플렉션으로 ID 강제 주입 + // idField = GroupJoin.class.getDeclaredField("id"); + // idField.setAccessible(true); + // idField.set(groupJoin, 1L); + // + // given(groupJoinRepository.findById(1L)).willReturn(Optional.of(groupJoin)); + // + // // when + // assertDoesNotThrow(() -> groupJoinService.groupJoinDelete(userPrincipal, 1L, 1L)); + // + // // then + // assertEquals(GroupJoinStatus.CANCEL, groupJoin.getStatus()); + // verify(groupJoinRepository).save(any()); + // } + // + // @Test + // void 가입신청_삭제_실패_본인_아님() throws Exception { + // // given + // // 그룹 생성 + // group = Group.builder().build(); + // Field groupIdField = Group.class.getDeclaredField("id"); + // groupIdField.setAccessible(true); + // groupIdField.set(group, 1L); + // + // // 가입 신청자의 유저 (user1) + // UserProfile userProfile1 = UserProfile.builder() + // .email("USER_EMAIL") + // .password("ENCODED_PASSWORD") + // .nickname("테스트닉네임") + // .name("테스트이름") + // .phone("+821012345678") + // .smsAgree(true) + // .profileImageUrl("test_image_url") + // .build(); + // + // ReflectionTestUtils.setField(userProfile1, "id", 2L); + // + // // 로그인한 사용자 (userAuth.user ≠ user1) + // // user는 테스트 클래스의 필드에 이미 있음 + // + // GroupJoin groupJoin = GroupJoin.builder() + // .group(group) + // .user(userProfile1) // 신청자는 user1 + // .status(GroupJoinStatus.PENDING) + // .build(); + // + // // ID 주입 + // Field joinIdField = GroupJoin.class.getDeclaredField("id"); + // joinIdField.setAccessible(true); + // joinIdField.set(groupJoin, 1L); + // + // // when: userAuth (user)가 user1의 신청을 삭제 시도 + // given(groupJoinRepository.findById(1L)).willReturn(Optional.of(groupJoin)); + // + // // then + // BizException exception = assertThrows( + // BizException.class, + // () -> groupJoinService.groupJoinDelete(userPrincipal, 1L, 1L) + // ); + // + // assertEquals(GroupJoinErrorCode.USER_NOT_PERMISSION, exception.getErrorCode()); + // } + +}