GroupService.java
package qwerty.chaekit.service.group;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import qwerty.chaekit.domain.group.ReadingGroup;
import qwerty.chaekit.domain.group.repository.GroupRepository;
import qwerty.chaekit.domain.highlight.Highlight;
import qwerty.chaekit.domain.highlight.repository.HighlightRepository;
import qwerty.chaekit.domain.member.user.UserProfile;
import qwerty.chaekit.dto.group.request.GroupPatchRequest;
import qwerty.chaekit.dto.group.request.GroupPostRequest;
import qwerty.chaekit.dto.group.request.GroupSortType;
import qwerty.chaekit.dto.group.response.GroupFetchResponse;
import qwerty.chaekit.dto.group.response.GroupPostResponse;
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.exception.NotFoundException;
import qwerty.chaekit.global.security.resolver.UserToken;
import qwerty.chaekit.mapper.GroupMapper;
import qwerty.chaekit.service.util.EntityFinder;
import qwerty.chaekit.service.util.FileService;
import java.util.List;
import java.util.Objects;
@Service
@Transactional
@RequiredArgsConstructor
public class GroupService {
private final GroupRepository groupRepository;
private final FileService fileService;
private final GroupMapper groupMapper;
private final EntityFinder entityFinder;
private final HighlightRepository highlightRepository;
@Transactional
public GroupPostResponse createGroup(UserToken userToken, GroupPostRequest request) {
UserProfile leader = entityFinder.findUser(userToken.userId());
if(groupRepository.existsReadingGroupByName(request.name())) {
throw new ForbiddenException(ErrorCode.GROUP_NAME_DUPLICATED);
}
String groupImageKey = fileService.uploadGroupImageIfPresent(request.groupImage());
ReadingGroup groupEntity = ReadingGroup.builder()
.name(request.name())
.groupLeader(leader)
.description(request.description())
.groupImageKey(groupImageKey)
.isAutoApproval(request.autoApproval() == null || request.autoApproval())
.build();
ReadingGroup savedGroup = groupRepository.save(groupEntity);
if(request.tags() != null) {
List<String> validTags = getValidTags(request.tags());
savedGroup.addTags(validTags);
}
savedGroup.addMember(leader).approve();
return GroupPostResponse.of(savedGroup, getGroupImageURL(savedGroup));
}
@Transactional(readOnly = true)
public PageResponse<GroupFetchResponse> getAllGroups(
UserToken userToken,
Pageable pageable,
List<String> tags,
GroupSortType sortBy
) {
boolean isAnonymous = userToken.isAnonymous();
Long userId = isAnonymous ? null : userToken.userId();
Page<ReadingGroup> page;
if (tags != null && !tags.isEmpty()) {
if (sortBy == GroupSortType.MEMBER_COUNT) {
page = groupRepository.findAllByTagsInOrderByMemberCountDesc(tags, pageable);
} else {
page = groupRepository.findAllByTagsIn(tags, getPageableByNewest(pageable));
}
} else { // no tags provided
if (sortBy == GroupSortType.MEMBER_COUNT) {
page = groupRepository.findAllOrderByMemberCountDesc(pageable);
} else {
page = groupRepository.findAll(getPageableByNewest(pageable));
}
}
Page<GroupFetchResponse> responsePage = page.map(group -> groupMapper.toGroupFetchResponse(group, userId));
return PageResponse.of(responsePage);
}
@Transactional(readOnly = true)
public PageResponse<GroupFetchResponse> getJoinedGroups(UserToken userToken, Pageable pageable) {
Long userId = userToken.userId();
Page<GroupFetchResponse> page = groupRepository.findAllByUserId(userId, pageable)
.map(group -> groupMapper.toGroupFetchResponse(group, userId));
return PageResponse.of(page);
}
@Transactional(readOnly = true)
public PageResponse<GroupFetchResponse> getCreatedGroups(UserToken userToken, Pageable pageable) {
Long userId = userToken.userId();
Page<GroupFetchResponse> page = groupRepository.findByGroupLeaderId(userId, pageable)
.map(group -> groupMapper.toGroupFetchResponse(group, userId));
return PageResponse.of(page);
}
@Transactional(readOnly = true)
public GroupFetchResponse fetchGroup(UserToken userToken, long groupId) {
boolean isAnonymous = userToken.isAnonymous();
Long userId = isAnonymous ? null : userToken.userId();
ReadingGroup group = groupRepository.findByIdWithTags(groupId)
.orElseThrow(() -> new NotFoundException(ErrorCode.GROUP_NOT_FOUND));
return groupMapper.toGroupFetchResponse(group, userId);
}
@Transactional
public GroupPostResponse updateGroup(UserToken userToken, long groupId, GroupPatchRequest request) {
UserProfile user = entityFinder.findUser(userToken.userId());
ReadingGroup group = entityFinder.findGroup(groupId);
if (!group.isLeader(user)) {
throw new ForbiddenException(ErrorCode.GROUP_UPDATE_FORBIDDEN);
}
if(request.description() != null) {
group.updateDescription(request.description());
}
if (request.tags() != null) {
List<String> validTags = getValidTags(request.tags());
group.removeAllTags();
group.addTags(validTags);
}
if (request.name() != null && !request.name().isBlank()) {
group.changeName(request.name());
}
if (request.autoApproval() != null) {
group.changeAutoApproval(request.autoApproval());
}
String imageKey = fileService.uploadGroupImageIfPresent(request.groupImage());
if(imageKey != null) {
group.updateGroupImageKey(imageKey);
}
return GroupPostResponse.of(group, getGroupImageURL(group));
}
@Transactional
public void deleteGroup(UserToken userToken, Long groupId) {
ReadingGroup group = entityFinder.findGroup(groupId);
if (!group.isLeader(userToken.userId())) {
throw new ForbiddenException(ErrorCode.GROUP_LEADER_ONLY);
}
// 1. 관련된 Highlight 찾아서 후처리
List<Highlight> highlights = highlightRepository.findByGroup(group);
for (Highlight highlight : highlights) {
highlight.detachActivity();
}
// 2. Activity 삭제
groupRepository.delete(group);
}
private String getGroupImageURL(ReadingGroup group) {
return fileService.convertToPublicImageURL(group.getGroupImageKey());
}
private static List<String> getValidTags(List<String> tags) {
List<String> validTags = tags.stream()
.filter(Objects::nonNull)
.map(String::trim) // 앞 뒤 공백 제거
.filter(tag -> !tag.isEmpty() && tag.length() <= 10)
.distinct()
.toList();
if (validTags.size() != tags.size()) {
throw new BadRequestException(ErrorCode.INVALID_TAG_LIST);
}
return validTags;
}
private static Pageable getPageableByNewest(Pageable pageable) {
return PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
pageable.getSort().isEmpty() ? Sort.by(Sort.Order.desc("createdAt")) : pageable.getSort()
);
}
}