GroupMemberService.java
package qwerty.chaekit.service.group;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import qwerty.chaekit.domain.group.ReadingGroup;
import qwerty.chaekit.domain.group.groupmember.GroupMember;
import qwerty.chaekit.domain.group.groupmember.GroupMemberRepository;
import qwerty.chaekit.domain.member.user.UserProfile;
import qwerty.chaekit.dto.group.response.GroupJoinResponse;
import qwerty.chaekit.dto.group.response.GroupMemberResponse;
import qwerty.chaekit.dto.page.PageResponse;
import qwerty.chaekit.global.enums.ErrorCode;
import qwerty.chaekit.global.exception.BadRequestException;
import qwerty.chaekit.global.exception.ForbiddenException;
import qwerty.chaekit.global.security.resolver.UserToken;
import qwerty.chaekit.mapper.GroupMapper;
import qwerty.chaekit.service.notification.NotificationService;
import qwerty.chaekit.service.util.EmailNotificationService;
import qwerty.chaekit.service.util.EntityFinder;
import qwerty.chaekit.service.util.FileService;
@Service
@Transactional
@RequiredArgsConstructor
public class GroupMemberService {
private final GroupMemberRepository groupMemberRepository;
private final EmailNotificationService emailNotificationService;
private final NotificationService notificationService;
private final FileService fileService;
private final GroupMapper groupMapper;
private final EntityFinder entityFinder;
@Transactional(readOnly = true)
public PageResponse<GroupMemberResponse> getGroupMembers(long groupId, Pageable pageable) {
Page<GroupMemberResponse> page = groupMemberRepository.findByReadingGroupId(groupId, pageable)
.map(
groupMember -> GroupMemberResponse.of(
groupMember,
fileService.convertToPublicImageURL(groupMember.getUser().getProfileImageKey())
)
);
return PageResponse.of(page);
}
@Transactional
public GroupJoinResponse requestGroupJoin(UserToken userToken, long groupId) {
UserProfile userProfile = entityFinder.findUser(userToken.userId());
ReadingGroup group = entityFinder.findGroup(groupId);
if (group.isMemberAlreadyRequested(userProfile)) {
throw new ForbiddenException(ErrorCode.ALREADY_JOINED_GROUP);
}
GroupMember groupMember = group.addMember(userProfile);
if (group.isAutoApproval()) {
group.approveMember(userProfile);
notificationService.createGroupJoinApprovedNotification(
userProfile,
group.getGroupLeader(),
group
);
return GroupJoinResponse.of(groupMember);
}
notificationService.createGroupJoinRequestNotification(
group.getGroupLeader(),
userProfile,
group
);
return GroupJoinResponse.of(groupMember);
}
@Transactional
public GroupJoinResponse approveJoinRequest(UserToken userToken, long groupId, long userId) {
GroupApprovalContext ctx = prepareGroupApproval(userToken, groupId, userId);
UserProfile pendingMember = ctx.pendingMember;
UserProfile groupLeader = ctx.leader;
ReadingGroup group = ctx.group;
GroupMember groupMember = group.approveMember(pendingMember);
notificationService.createGroupJoinApprovedNotification(
pendingMember,
groupLeader,
group
);
emailNotificationService.sendReadingGroupApprovalEmail(pendingMember.getMember().getEmail());
return GroupJoinResponse.of(groupMember);
}
@Transactional
public void rejectJoinRequest(UserToken userToken, long groupId, long userId) {
GroupApprovalContext ctx = prepareGroupApproval(userToken, groupId, userId);
UserProfile pendingMember = ctx.pendingMember;
UserProfile groupLeader = ctx.leader;
ReadingGroup group = ctx.group;
group.rejectMember(pendingMember);
notificationService.createGroupJoinRejectedNotification(
pendingMember,
groupLeader,
group
);
}
private record GroupApprovalContext(
ReadingGroup group,
UserProfile leader,
UserProfile pendingMember
) {}
private GroupApprovalContext prepareGroupApproval(UserToken userToken, long groupId, long userId) {
UserProfile leaderProfile = entityFinder.findUser(userToken.userId());
ReadingGroup group = entityFinder.findGroup(groupId);
if (!group.isLeader(leaderProfile)) {
throw new ForbiddenException(ErrorCode.GROUP_LEADER_ONLY);
}
if (!group.isPendingMember(userId)) {
throw new ForbiddenException(ErrorCode.GROUP_MEMBER_NOT_PENDING);
}
UserProfile memberProfile = entityFinder.findUser(userId);
return new GroupApprovalContext(group, leaderProfile, memberProfile);
}
@Transactional
public void leaveGroup(UserToken userToken, long groupId) {
UserProfile userProfile = entityFinder.findUser(userToken.userId());
ReadingGroup group = entityFinder.findGroup(groupId);
if (group.getGroupLeader().getId().equals(userProfile.getId())) {
throw new ForbiddenException(ErrorCode.GROUP_LEADER_CANNOT_LEAVE);
}
group.removeMember(userProfile);
}
@Transactional(readOnly = true)
public PageResponse<GroupMemberResponse> fetchPendingList(Pageable pageable, UserToken userToken, long groupId) {
UserProfile user = entityFinder.findUser(userToken.userId());
ReadingGroup group = entityFinder.findGroup(groupId);
if (!group.isLeader(user)) {
throw new ForbiddenException(ErrorCode.GROUP_LEADER_ONLY);
}
Page<GroupMember> pendingMembersPage = groupMemberRepository.findByPendingMemberWithUser(group, pageable);
Page<GroupMemberResponse> page = pendingMembersPage.map(groupMapper::toGroupMemberResponse);
return PageResponse.of(page);
}
@Transactional
public void kickGroupMember(UserToken userToken, Long groupId, Long userId) {
UserProfile loginUser = entityFinder.findUser(userToken.userId());
UserProfile targetUser = entityFinder.findUser(userId);
ReadingGroup group = entityFinder.findGroup(groupId);
if (!group.isLeader(loginUser)) {
throw new ForbiddenException(ErrorCode.GROUP_LEADER_ONLY);
}
if (group.isLeader(targetUser)) {
throw new BadRequestException(ErrorCode.GROUP_LEADER_CANNOT_LEAVE);
}
if (group.isNotAcceptedMember(targetUser)) {
throw new BadRequestException(ErrorCode.GROUP_MEMBER_NOT_JOINED);
}
group.removeMember(targetUser);
notificationService.createGroupBannedNotification(targetUser, group);
}
}