GroupReviewService.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.activity.Activity;
import qwerty.chaekit.domain.group.review.GroupReview;
import qwerty.chaekit.domain.group.review.GroupReviewRepository;
import qwerty.chaekit.domain.group.review.GroupReviewTagRepository;
import qwerty.chaekit.domain.member.user.UserProfile;
import qwerty.chaekit.dto.group.review.GroupReviewFetchResponse;
import qwerty.chaekit.dto.group.review.GroupReviewPostRequest;
import qwerty.chaekit.dto.group.review.GroupReviewStatsResponse;
import qwerty.chaekit.dto.group.review.TagStatDto;
import qwerty.chaekit.dto.page.PageResponse;
import qwerty.chaekit.global.enums.ErrorCode;
import qwerty.chaekit.global.exception.BadRequestException;
import qwerty.chaekit.global.security.resolver.UserToken;
import qwerty.chaekit.mapper.GroupReviewMapper;
import qwerty.chaekit.service.util.EntityFinder;

import java.util.List;

@Service
@Transactional
@RequiredArgsConstructor
public class GroupReviewService {
    private final GroupReviewRepository groupReviewRepository;
    private final GroupReviewTagRepository groupReviewTagRepository;
    private final EntityFinder entityFinder;
    private final GroupReviewMapper groupReviewMapper;
    private final ActivityPolicy activityPolicy;

    public GroupReviewFetchResponse createReview(
            UserToken userToken, Long groupId, GroupReviewPostRequest request
    ) {
        UserProfile author = entityFinder.findUser(userToken.userId());
        ReadingGroup group = entityFinder.findGroup(groupId);
        Activity activity = entityFinder.findActivity(request.activityId());
        
        if (!activity.isFromGroup(group)) {
            throw new BadRequestException(ErrorCode.ACTIVITY_GROUP_MISMATCH);
        }
        
        activityPolicy.assertJoined(author, activity);

        GroupReview review = groupReviewRepository.findByActivityAndAuthor(activity, author)
                .map(existing -> {
                    // 기존 리뷰 수정
                    existing.setContent(request.content());
                    existing.setTags(request.tags());
                    return existing;
                })
                .orElseGet(() -> {
                    // 신규 리뷰 생성
                    GroupReview newReview = GroupReview.builder()
                            .group(group)
                            .author(author)
                            .activity(activity)
                            .content(request.content())
                            .build();
                    newReview.setTags(request.tags());
                    return newReview;
                });

        // 저장 (기존 리뷰라면 dirty checking, 새 리뷰라면 persist)
        groupReviewRepository.save(review);

        return groupReviewMapper.toFetchResponse(review);
    }
    
    public PageResponse<GroupReviewFetchResponse> getReviews(
            Long groupId, Pageable pageable
    ) {
        Page<GroupReviewFetchResponse> reviews = groupReviewRepository.findByGroupId(groupId, pageable)
                .map(groupReviewMapper::toFetchResponse);
        
        return PageResponse.of(reviews);
    }
    
    public GroupReviewStatsResponse getReviewStats(Long groupId) {
        ReadingGroup group = entityFinder.findGroup(groupId);
        
        long reviewCount = groupReviewRepository.countByGroup(group);
        List<TagStatDto> tagStats = groupReviewTagRepository.countTagsByGroupId(group);
        long tagCount = tagStats.stream()
                .mapToLong(TagStatDto::count)
                .sum();
        
        return GroupReviewStatsResponse.builder()
                .reviewCount(reviewCount)
                .tagStats(tagStats)
                .tagCount(tagCount)
                .build();
    }
}