ActivityService.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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import qwerty.chaekit.domain.ebook.Ebook;
import qwerty.chaekit.domain.group.ReadingGroup;
import qwerty.chaekit.domain.group.activity.Activity;
import qwerty.chaekit.domain.group.activity.activitymember.ActivityMember;
import qwerty.chaekit.domain.group.activity.activitymember.ActivityMemberRepository;
import qwerty.chaekit.domain.group.activity.dto.ActivityWithCountsResponse;
import qwerty.chaekit.domain.group.activity.repository.ActivityRepository;
import qwerty.chaekit.domain.member.user.UserProfile;
import qwerty.chaekit.dto.group.activity.*;
import qwerty.chaekit.dto.page.PageResponse;
import qwerty.chaekit.global.enums.ErrorCode;
import qwerty.chaekit.global.exception.ForbiddenException;
import qwerty.chaekit.global.exception.NotFoundException;
import qwerty.chaekit.global.security.resolver.UserToken;
import qwerty.chaekit.service.ebook.EbookPolicy;
import qwerty.chaekit.service.util.EntityFinder;
import qwerty.chaekit.service.util.FileService;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
@RequiredArgsConstructor
public class ActivityService {
private final ActivityRepository activityRepository;
private final ActivityMemberRepository activityMemberRepository;
private final ActivityPolicy activityPolicy;
private final EntityFinder entityFinder;
private final EbookPolicy ebookPolicy;
private final FileService fileService;
public ActivityPostResponse createActivity(UserToken userToken, long groupId, ActivityPostRequest request) {
UserProfile user = entityFinder.findUser(userToken.userId());
ReadingGroup group = entityFinder.findGroup(groupId);
Ebook ebook = entityFinder.findEbook(request.bookId());
if (!group.isLeader(user)) {
throw new ForbiddenException(ErrorCode.GROUP_LEADER_ONLY);
}
activityPolicy.assertActivityPeriodValid(groupId, null, request.startTime(), request.endTime());
ebookPolicy.assertEBookPurchased(user, ebook);
Activity saved = activityRepository.save(
Activity.builder()
.group(group)
.book(ebook)
.startTime(request.startTime())
.endTime(request.endTime())
.description(request.description())
.build()
);
saved.addParticipant(user);
return ActivityPostResponse.of(saved);
}
public ActivityPostResponse updateActivity(UserToken userToken, long groupId, ActivityPatchRequest request) {
UserProfile user = entityFinder.findUser(userToken.userId());
ReadingGroup group = entityFinder.findGroup(groupId);
Activity activity = entityFinder.findActivity(request.activityId());
if (!activity.isFromGroup(group)) {
throw new ForbiddenException(ErrorCode.ACTIVITY_GROUP_MISMATCH);
}
if (!group.isLeader(user)) {
throw new ForbiddenException(ErrorCode.GROUP_LEADER_ONLY);
}
LocalDate newStartTime = Optional.ofNullable(request.startTime())
.orElse(activity.getStartTime());
LocalDate newEndTime = Optional.ofNullable(request.endTime())
.orElse(activity.getEndTime());
activityPolicy.assertActivityPeriodValid(groupId, activity.getId(), newStartTime, newEndTime);
activity.updateTime(newStartTime, newEndTime);
activity.updateDescription(request.description());
return ActivityPostResponse.of(activity);
}
@Transactional(readOnly = true)
public PageResponse<ActivityFetchResponse> fetchAllActivities(UserToken userToken, Pageable pageable, long groupId) {
Long userId = userToken.userId();
Page<ActivityFetchResponse> page = activityRepository.findByGroupIdWithCounts(groupId, pageable)
.map(
response -> ActivityFetchResponse.of(
response.activity(),
fileService.convertToPublicImageURL(response.activity().getBook().getCoverImageKey()),
userId != null && response.activity().isParticipant(entityFinder.findUser(userId)),
response.highlightCount(),
response.discussionCount()
));
return PageResponse.of(page);
}
public void joinActivity(UserToken userToken, long activityId) {
UserProfile user = entityFinder.findUser(userToken.userId());
Activity activity = entityFinder.findActivity(activityId);
activityPolicy.assertJoinable(user, activity);
activity.addParticipant(user);
}
public void leaveActivity(UserToken userToken, long activityId) {
UserProfile user = entityFinder.findUser(userToken.userId());
Activity activity = entityFinder.findActivity(activityId);
if (!activity.isParticipant(user)) {
throw new ForbiddenException(ErrorCode.ACTIVITY_NOT_JOINED);
}
activity.removeParticipant(user);
}
@Transactional(readOnly = true)
public ActivityFetchResponse fetchActivity(UserToken userToken, long activityId) {
Long userId = userToken.userId();
ActivityWithCountsResponse response = activityRepository.findByIdWithCounts(activityId)
.orElseThrow(() -> new NotFoundException(ErrorCode.ACTIVITY_NOT_FOUND));
Activity activity = response.activity();
activityPolicy.assertJoined(userId, activity.getId());
return ActivityFetchResponse.of(
activity,
fileService.convertToPublicImageURL(activity.getBook().getCoverImageKey()),
true,
response.highlightCount(),
response.discussionCount()
);
}
@Transactional(readOnly = true)
public PageResponse<ActivityFetchResponse> getMyActivities(UserToken userToken, Long bookId, Pageable pageable) {
UserProfile user = entityFinder.findUser(userToken.userId());
Page<ActivityMember> fetchResults;
if (bookId == null) {
fetchResults = activityMemberRepository.findByUser(user, pageable);
} else {
Ebook book = entityFinder.findEbook(bookId);
fetchResults = activityMemberRepository.findByUserAndActivity_Book(user, book, pageable);
}
Page<ActivityFetchResponse> page = fetchResults
.map(activityMember -> ActivityFetchResponse.of(
activityMember.getActivity(),
fileService.convertToPublicImageURL(activityMember.getUser().getProfileImageKey()),
true, -1L, -1L
));
return PageResponse.of(page);
}
@Transactional(readOnly = true)
public List<ActivityScoreResponse> getActivityTop5Scores(long activityId) {
return activityRepository.calculateTop5Scores(
activityId, PageRequest.of(0, 5))
.stream().map(score -> ActivityScoreResponse.builder()
.userId(score.user().getId())
.score(score.score())
.userProfileImageURL(fileService.convertToPublicImageURL(score.user().getProfileImageKey()))
.userNickname(score.user().getNickname())
.build()
).toList();
}
}