diff --git a/modules/study/src/main/java/kr/spot/study/application/command/CreateStudyService.java b/modules/study/src/main/java/kr/spot/study/application/command/CreateStudyService.java index 8358e43b..7d52ace2 100644 --- a/modules/study/src/main/java/kr/spot/study/application/command/CreateStudyService.java +++ b/modules/study/src/main/java/kr/spot/study/application/command/CreateStudyService.java @@ -6,14 +6,12 @@ import kr.spot.study.domain.associations.StudyCategory; import kr.spot.study.domain.associations.StudyMember; import kr.spot.study.domain.associations.StudyRegion; -import kr.spot.study.domain.associations.StudyStats; import kr.spot.study.domain.associations.StudyStyle; import kr.spot.study.domain.vo.Fee; import kr.spot.study.infrastructure.jpa.StudyRepository; import kr.spot.study.infrastructure.jpa.associations.StudyCategoryRepository; import kr.spot.study.infrastructure.jpa.associations.StudyMemberRepository; import kr.spot.study.infrastructure.jpa.associations.StudyRegionRepository; -import kr.spot.study.infrastructure.jpa.associations.StudyStatsRepository; import kr.spot.study.infrastructure.jpa.associations.StudyStyleRepository; import kr.spot.study.presentation.command.dto.request.CreateStudyRequest; import lombok.RequiredArgsConstructor; @@ -34,18 +32,15 @@ public class CreateStudyService { private final StudyStyleRepository studyStyleRepository; private final StudyRegionRepository studyRegionRepository; private final StudyCategoryRepository studyCategoryRepository; - private final StudyStatsRepository studyStatsRepository; private final StudyMemberRepository studyMemberRepository; public long createStudy(CreateStudyRequest request, long leaderId, MultipartFile imageFile) { long studyId = idGenerator.nextId(); Study study = Study.of(studyId, leaderId, request.name(), request.maxMembers(), Fee.of(request.hasFee(), request.amount()), null, request.description(), request.isOnline()); - StudyStats studyStats = StudyStats.of(studyId); StudyMember studyMember = StudyMember.create(idGenerator.nextId(), studyId, leaderId); studyRepository.save(study); - studyStatsRepository.save(studyStats); studyMemberRepository.save(studyMember); saveAllStudyCategories(request, studyId); diff --git a/modules/study/src/main/java/kr/spot/study/application/command/StudyLikeService.java b/modules/study/src/main/java/kr/spot/study/application/command/StudyLikeService.java index 040e35be..68f607e9 100644 --- a/modules/study/src/main/java/kr/spot/study/application/command/StudyLikeService.java +++ b/modules/study/src/main/java/kr/spot/study/application/command/StudyLikeService.java @@ -1,8 +1,8 @@ package kr.spot.study.application.command; import kr.spot.IdGenerator; +import kr.spot.study.infrastructure.jpa.StudyRepository; import kr.spot.study.infrastructure.jpa.associations.StudyLikeRepository; -import kr.spot.study.infrastructure.jpa.associations.StudyStatsRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,7 +14,7 @@ public class StudyLikeService { private final IdGenerator idGenerator; private final StudyLikeRepository studyLikeRepository; - private final StudyStatsRepository studyStatsRepository; + private final StudyRepository studyRepository; public void likeStudy(long studyId, long memberId) { int inserted = studyLikeRepository.saveStudyLike(idGenerator.nextId(), studyId, memberId); @@ -23,14 +23,14 @@ public void likeStudy(long studyId, long memberId) { private void increaseLikeCount(long studyId, int inserted) { if (inserted == 1) { - studyStatsRepository.increaseLike(studyId); + studyRepository.increaseLike(studyId); } } public void unlikeStudy(long studyId, long memberId) { int deleted = studyLikeRepository.hardDelete(studyId, memberId); if (deleted > 0) { - studyStatsRepository.decreaseLike(studyId); + studyRepository.decreaseLike(studyId); } } } diff --git a/modules/study/src/main/java/kr/spot/study/application/query/GetStudyDetailService.java b/modules/study/src/main/java/kr/spot/study/application/query/GetStudyDetailService.java index b24d15eb..3f1022c4 100644 --- a/modules/study/src/main/java/kr/spot/study/application/query/GetStudyDetailService.java +++ b/modules/study/src/main/java/kr/spot/study/application/query/GetStudyDetailService.java @@ -7,13 +7,11 @@ import kr.spot.study.domain.Study; import kr.spot.study.domain.associations.StudyCategory; import kr.spot.study.domain.associations.StudyMember; -import kr.spot.study.domain.associations.StudyStats; import kr.spot.study.domain.enums.Category; import kr.spot.study.domain.enums.StudyMemberStatus; import kr.spot.study.infrastructure.jpa.StudyRepository; import kr.spot.study.infrastructure.jpa.associations.StudyCategoryRepository; import kr.spot.study.infrastructure.jpa.associations.StudyMemberRepository; -import kr.spot.study.infrastructure.jpa.associations.StudyStatsRepository; import kr.spot.study.presentation.query.dto.response.GetStudyInfoResponse; import kr.spot.study.presentation.query.dto.response.GetStudyInfoResponse.Statistics; import kr.spot.study.presentation.query.dto.response.GetStudyMembersResponse; @@ -32,16 +30,14 @@ public class GetStudyDetailService { private final StudyRepository studyRepository; private final StudyCategoryRepository studyCategoryRepository; - private final StudyStatsRepository studyStatsRepository; private final StudyMemberRepository studyMemberRepository; private final StudyViewCountService studyViewCountService; private final GetMemberInfoPort getMemberInfoPort; public GetStudyInfoResponse getStudyInfo(long studyId, long viewerId) { Study study = findStudy(studyId); - StudyStats stats = findStudyStats(studyId); List categories = findCategories(studyId); - Statistics statistics = buildStatistics(study, stats, studyId, viewerId); + Statistics statistics = buildStatistics(study, studyId, viewerId); return toStudyInfoResponse(study, categories, statistics); } @@ -58,23 +54,19 @@ private Study findStudy(long studyId) { return studyRepository.getStudyById(studyId); } - private StudyStats findStudyStats(long studyId) { - return studyStatsRepository.getByStudyId(studyId); - } - private List findCategories(long studyId) { return studyCategoryRepository.findAllByStudyId(studyId).stream() .map(StudyCategory::getCategory) .toList(); } - private Statistics buildStatistics(Study study, StudyStats stats, long studyId, long viewerId) { - long displayViewCount = studyViewCountService.calculateDisplayViewCount(stats, studyId, + private Statistics buildStatistics(Study study, long studyId, long viewerId) { + long displayViewCount = studyViewCountService.calculateDisplayViewCount(study, studyId, viewerId); return Statistics.of( study.getMaxMembers(), study.getCurrentMembers(), - stats.getLikeCount(), + study.getLikeCount(), displayViewCount ); } diff --git a/modules/study/src/main/java/kr/spot/study/application/query/StudyViewCountService.java b/modules/study/src/main/java/kr/spot/study/application/query/StudyViewCountService.java index 2c556274..c3fd5703 100644 --- a/modules/study/src/main/java/kr/spot/study/application/query/StudyViewCountService.java +++ b/modules/study/src/main/java/kr/spot/study/application/query/StudyViewCountService.java @@ -1,6 +1,6 @@ package kr.spot.study.application.query; -import kr.spot.study.domain.associations.StudyStats; +import kr.spot.study.domain.Study; import kr.spot.view.ViewAbuseGuard; import kr.spot.view.ViewCounter; import kr.spot.view.ViewableType; @@ -16,8 +16,8 @@ public class StudyViewCountService { private final ViewCounter viewCounter; private final ViewAbuseGuard viewAbuseGuard; - public long calculateDisplayViewCount(StudyStats stats, long studyId, long viewerId) { - long baseViewCount = stats.getViewCount(); + public long calculateDisplayViewCount(Study study, long studyId, long viewerId) { + long baseViewCount = study.getViewCount(); long viewDelta = getViewDeltaFromCounter(studyId, viewerId); return baseViewCount + viewDelta; } diff --git a/modules/study/src/main/java/kr/spot/study/domain/Study.java b/modules/study/src/main/java/kr/spot/study/domain/Study.java index 33052edd..faa94fa7 100644 --- a/modules/study/src/main/java/kr/spot/study/domain/Study.java +++ b/modules/study/src/main/java/kr/spot/study/domain/Study.java @@ -60,6 +60,10 @@ public class Study extends BaseEntity { @Column(nullable = false) private Boolean isOnline = false; + private Long viewCount; + + private Long likeCount; + public static Study of(Long id, Long leaderId, String name, Integer maxMembers, Fee fee, String description) { return of(id, leaderId, name, maxMembers, fee, null, description, false); @@ -75,7 +79,7 @@ public static Study of(Long id, Long leaderId, String name, Integer maxMembers, validateStudyNameIsNotBlank(name); validateMaxMembers(maxMembers); return new Study(id, leaderId, name, maxMembers, CURRENT_MEMBERS, fee, imageUrl, description, - RecruitingStatus.RECRUITING, isOnline != null && isOnline); + RecruitingStatus.RECRUITING, isOnline != null && isOnline, 0L, 0L); } private static void validateStudyNameIsNotBlank(String name) { diff --git a/modules/study/src/main/java/kr/spot/study/domain/associations/StudyStats.java b/modules/study/src/main/java/kr/spot/study/domain/associations/StudyStats.java deleted file mode 100644 index f5773615..00000000 --- a/modules/study/src/main/java/kr/spot/study/domain/associations/StudyStats.java +++ /dev/null @@ -1,33 +0,0 @@ -package kr.spot.study.domain.associations; - -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import kr.spot.domain.BaseEntity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.SQLRestriction; - -@Getter -@Entity -@SQLDelete(sql = "UPDATE study_stats SET status = 'INACTIVE' WHERE id = ?") -@SQLRestriction("status = 'ACTIVE'") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class StudyStats extends BaseEntity { - - @Id - private Long studyId; - - private Long viewCount; - - private Long likeCount; - - private Long commentCount; - - public static StudyStats of(Long studyId) { - return new StudyStats(studyId, 0L, 0L, 0L); - } -} diff --git a/modules/study/src/main/java/kr/spot/study/infrastructure/batch/StudyViewFlusher.java b/modules/study/src/main/java/kr/spot/study/infrastructure/batch/StudyViewFlusher.java index d103cd45..475d5074 100644 --- a/modules/study/src/main/java/kr/spot/study/infrastructure/batch/StudyViewFlusher.java +++ b/modules/study/src/main/java/kr/spot/study/infrastructure/batch/StudyViewFlusher.java @@ -1,6 +1,6 @@ package kr.spot.study.infrastructure.batch; -import kr.spot.study.infrastructure.jpa.associations.StudyStatsRepository; +import kr.spot.study.infrastructure.jpa.StudyRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -12,11 +12,11 @@ @RequiredArgsConstructor public class StudyViewFlusher { - private final StudyStatsRepository studyStatsRepository; + private final StudyRepository studyRepository; @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateViewCount(long studyId, long delta) { - studyStatsRepository.increaseViewBy(studyId, delta); + studyRepository.increaseViewBy(studyId, delta); log.debug("스터디 조회수 DB 업데이트: studyId={}, delta={}", studyId, delta); } } diff --git a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/StudyRepository.java b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/StudyRepository.java index 71a499eb..4d3052cf 100644 --- a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/StudyRepository.java +++ b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/StudyRepository.java @@ -5,6 +5,9 @@ import kr.spot.study.domain.Study; import kr.spot.study.domain.enums.RecruitingStatus; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface StudyRepository extends JpaRepository { @@ -16,4 +19,21 @@ default Study getStudyById(Long studyId) { long countByLeaderIdAndRecruitingStatus(long leaderId, RecruitingStatus recruitingStatus); boolean existsByLeaderId(long leaderId); + + @Modifying + @Query(""" + update Study s + set s.viewCount = s.viewCount + :delta, + s.updatedAt = CURRENT_TIMESTAMP + where s.id = :studyId + """) + int increaseViewBy(@Param("studyId") long studyId, @Param("delta") long delta); + + @Modifying + @Query("update Study s set s.likeCount = s.likeCount + 1 where s.id = :studyId") + int increaseLike(@Param("studyId") long studyId); + + @Modifying + @Query("update Study s set s.likeCount = case when s.likeCount > 0 then s.likeCount - 1 else 0 end where s.id = :studyId") + int decreaseLike(@Param("studyId") long studyId); } diff --git a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/associations/StudyStatsRepository.java b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/associations/StudyStatsRepository.java deleted file mode 100644 index c40490af..00000000 --- a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/associations/StudyStatsRepository.java +++ /dev/null @@ -1,37 +0,0 @@ -package kr.spot.study.infrastructure.jpa.associations; - -import java.util.Optional; -import kr.spot.code.status.ErrorStatus; -import kr.spot.exception.GeneralException; -import kr.spot.study.domain.associations.StudyStats; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface StudyStatsRepository extends JpaRepository { - - Optional findByStudyId(long studyId); - - default StudyStats getByStudyId(long studyId) { - return findByStudyId(studyId) - .orElseThrow(() -> new GeneralException(ErrorStatus._STUDY_NOT_FOUND)); - } - - @Modifying - @Query(""" - update StudyStats s - set s.viewCount = s.viewCount + :delta, - s.updatedAt = CURRENT_TIMESTAMP - where s.studyId = :studyId and s.status = 'ACTIVE' - """) - int increaseViewBy(@Param("studyId") long studyId, @Param("delta") long delta); - - @Modifying - @Query("update StudyStats s set s.likeCount = s.likeCount + 1 where s.studyId = :studyId") - int increaseLike(@Param("studyId") long studyId); - - @Modifying - @Query("update StudyStats s set s.likeCount = case when s.likeCount > 0 then s.likeCount - 1 else 0 end where s.studyId = :studyId") - int decreaseLike(@Param("studyId") long studyId); -} diff --git a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java index 085624a1..4b5c44c2 100644 --- a/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java +++ b/modules/study/src/main/java/kr/spot/study/infrastructure/jpa/querydsl/StudyQueryRepository.java @@ -1,5 +1,7 @@ package kr.spot.study.infrastructure.jpa.querydsl; +import static com.querydsl.jpa.JPAExpressions.selectOne; + import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -10,7 +12,6 @@ import kr.spot.study.domain.associations.QStudyLike; import kr.spot.study.domain.associations.QStudyMember; import kr.spot.study.domain.associations.QStudyRegion; -import kr.spot.study.domain.associations.QStudyStats; import kr.spot.study.domain.enums.Category; import kr.spot.study.domain.enums.FeeCategory; import kr.spot.study.domain.enums.RecruitingStatus; @@ -77,26 +78,20 @@ public List findMyPreferredRegionStudies( ) { QStudy study = QStudy.study; QStudyRegion studyRegion = QStudyRegion.studyRegion; - QStudyCategory studyCategory = QStudyCategory.studyCategory; - QStudyStats studyStats = QStudyStats.studyStats; return query .select(study) .from(study) - .join(studyRegion).on(studyRegion.studyId.eq(study.id)) - .leftJoin(studyCategory).on(studyCategory.studyId.eq(study.id)) - .leftJoin(studyStats).on(studyStats.studyId.eq(study.id)) .where( - studyRegion.regionCode.in(regionCodes), + existsRegion(study, studyRegion, regionCodes), + existsCategories(study, categories), eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), - inCategories(categories, studyCategory), eqIsOnline(isOnline, study), ltCursor(cursor, study) ) - .groupBy(study.id) .orderBy( - orderBy(sortBy, study, studyStats), + orderBy(sortBy, study), study.id.desc() ) .limit(limit) @@ -112,18 +107,15 @@ public long countMyPreferredRegionStudies( ) { QStudy study = QStudy.study; QStudyRegion studyRegion = QStudyRegion.studyRegion; - QStudyCategory studyCategory = QStudyCategory.studyCategory; return query - .select(study.id.countDistinct()) + .select(study.id.count()) .from(study) - .join(studyRegion).on(studyRegion.studyId.eq(study.id)) - .leftJoin(studyCategory).on(studyCategory.studyId.eq(study.id)) .where( - studyRegion.regionCode.in(regionCodes), + existsRegion(study, studyRegion, regionCodes), + existsCategories(study, categories), eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), - inCategories(categories, studyCategory), eqIsOnline(isOnline, study) ) .fetchOne(); @@ -139,24 +131,19 @@ public List findMyPreferredCategoryStudies( List categories ) { QStudy study = QStudy.study; - QStudyCategory studyCategory = QStudyCategory.studyCategory; - QStudyStats studyStats = QStudyStats.studyStats; return query .select(study) .from(study) - .leftJoin(studyCategory).on(studyCategory.studyId.eq(study.id)) - .leftJoin(studyStats).on(studyStats.studyId.eq(study.id)) .where( + existsCategories(study, categories), eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), - inCategories(categories, studyCategory), eqIsOnline(isOnline, study), ltCursor(cursor, study) ) - .groupBy(study.id) .orderBy( - orderBy(sortBy, study, studyStats), + orderBy(sortBy, study), study.id.desc() ) .limit(limit) @@ -170,16 +157,14 @@ public long countMyPreferredCategoryStudies( List categories ) { QStudy study = QStudy.study; - QStudyCategory studyCategory = QStudyCategory.studyCategory; return query - .select(study.id.countDistinct()) + .select(study.id.count()) .from(study) - .leftJoin(studyCategory).on(studyCategory.studyId.eq(study.id)) .where( + existsCategories(study, categories), eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), - inCategories(categories, studyCategory), eqIsOnline(isOnline, study) ) .fetchOne(); @@ -194,24 +179,19 @@ public List findRecruitingStudies( int limit ) { QStudy study = QStudy.study; - QStudyCategory studyCategory = QStudyCategory.studyCategory; - QStudyStats studyStats = QStudyStats.studyStats; return query .select(study) .from(study) - .leftJoin(studyCategory).on(studyCategory.studyId.eq(study.id)) - .leftJoin(studyStats).on(studyStats.studyId.eq(study.id)) .where( study.recruitingStatus.eq(RecruitingStatus.RECRUITING), + existsCategories(study, categories), eqFeeCategory(feeCategory, study), - inCategories(categories, studyCategory), eqIsOnline(isOnline, study), ltCursor(cursor, study) ) - .groupBy(study.id) .orderBy( - orderBy(sortBy, study, studyStats), + orderBy(sortBy, study), study.id.desc() ) .limit(limit) @@ -224,16 +204,14 @@ public long countRecruitingStudies( Boolean isOnline ) { QStudy study = QStudy.study; - QStudyCategory studyCategory = QStudyCategory.studyCategory; return query - .select(study.id.countDistinct()) + .select(study.id.count()) .from(study) - .leftJoin(studyCategory).on(studyCategory.studyId.eq(study.id)) .where( study.recruitingStatus.eq(RecruitingStatus.RECRUITING), + existsCategories(study, categories), eqFeeCategory(feeCategory, study), - inCategories(categories, studyCategory), eqIsOnline(isOnline, study) ) .fetchOne(); @@ -249,24 +227,19 @@ public List findStudiesByCategory( int limit ) { QStudy study = QStudy.study; - QStudyCategory studyCategory = QStudyCategory.studyCategory; - QStudyStats studyStats = QStudyStats.studyStats; return query .select(study) .from(study) - .join(studyCategory).on(studyCategory.studyId.eq(study.id)) - .leftJoin(studyStats).on(studyStats.studyId.eq(study.id)) .where( + existsCategory(study, category), eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), - eqCategory(category, studyCategory), eqIsOnline(isOnline, study), ltCursor(cursor, study) ) - .groupBy(study.id) .orderBy( - orderBy(sortBy, study, studyStats), + orderBy(sortBy, study), study.id.desc() ) .limit(limit) @@ -280,16 +253,14 @@ public long countStudiesByCategory( Boolean isOnline ) { QStudy study = QStudy.study; - QStudyCategory studyCategory = QStudyCategory.studyCategory; return query - .select(study.id.countDistinct()) + .select(study.id.count()) .from(study) - .join(studyCategory).on(studyCategory.studyId.eq(study.id)) .where( + existsCategory(study, category), eqRecruitingStatus(recruitingStatus, study), eqFeeCategory(feeCategory, study), - eqCategory(category, studyCategory), eqIsOnline(isOnline, study) ) .fetchOne(); @@ -303,34 +274,64 @@ private BooleanExpression eqFeeCategory(FeeCategory feeCategory, QStudy study) { return feeCategory == null ? null : study.fee.feeCategory.eq(feeCategory); } - private BooleanExpression inCategories(List categories, QStudyCategory studyCategory) { + private BooleanExpression eqIsOnline(Boolean isOnline, QStudy study) { + return isOnline == null ? null : study.isOnline.eq(isOnline); + } + + private BooleanExpression ltCursor(Long cursor, QStudy study) { + return cursor == null ? null : study.id.lt(cursor); + } + + private BooleanExpression existsCategories(QStudy study, List categories) { if (categories == null || categories.isEmpty()) { return null; } - return studyCategory.category.in(categories); - } - - private BooleanExpression eqCategory(Category category, QStudyCategory studyCategory) { - return category == null ? null : studyCategory.category.eq(category); + QStudyCategory studyCategory = QStudyCategory.studyCategory; + return selectOne() + .from(studyCategory) + .where( + studyCategory.studyId.eq(study.id), + studyCategory.category.in(categories) + ) + .exists(); } - private BooleanExpression eqIsOnline(Boolean isOnline, QStudy study) { - return isOnline == null ? null : study.isOnline.eq(isOnline); + private BooleanExpression existsCategory(QStudy study, Category category) { + if (category == null) { + return null; + } + QStudyCategory studyCategory = QStudyCategory.studyCategory; + return selectOne() + .from(studyCategory) + .where( + studyCategory.studyId.eq(study.id), + studyCategory.category.eq(category) + ) + .exists(); } - private BooleanExpression ltCursor(Long cursor, QStudy study) { - return cursor == null ? null : study.id.lt(cursor); + private BooleanExpression existsRegion(QStudy study, QStudyRegion studyRegion, List regionCodes) { + if (regionCodes == null || regionCodes.isEmpty()) { + return null; + } + return selectOne() + .from(studyRegion) + .where( + studyRegion.studyId.eq(study.id), + studyRegion.regionCode.in(regionCodes) + ) + .exists(); } - private OrderSpecifier orderBy(SortBy sortBy, QStudy study, QStudyStats studyStats) { + private OrderSpecifier orderBy(SortBy sortBy, QStudy study) { if (sortBy == null) { return study.id.desc(); } return switch (sortBy) { case RECENT -> study.id.desc(); - case LIKES -> studyStats.likeCount.desc(); - case HITS -> studyStats.viewCount.desc(); + case LIKES -> study.likeCount.desc(); + case HITS -> study.viewCount.desc(); }; } @@ -365,17 +366,14 @@ public long countLikedStudies(long memberId) { public List findRecruitingStudiesByCategories(List categories, int limit) { QStudy study = QStudy.study; - QStudyCategory studyCategory = QStudyCategory.studyCategory; return query .select(study) .from(study) - .join(studyCategory).on(studyCategory.studyId.eq(study.id)) .where( - studyCategory.category.in(categories), + existsCategories(study, categories), study.recruitingStatus.eq(RecruitingStatus.RECRUITING) ) - .groupBy(study.id) .orderBy(study.id.desc()) .limit(limit) .fetch(); @@ -383,17 +381,15 @@ public List findRecruitingStudiesByCategories(List categories, public List findPopularRecruitingStudies(List excludeIds, int limit) { QStudy study = QStudy.study; - QStudyStats studyStats = QStudyStats.studyStats; return query .select(study) .from(study) - .leftJoin(studyStats).on(studyStats.studyId.eq(study.id)) .where( study.recruitingStatus.eq(RecruitingStatus.RECRUITING), notInIds(excludeIds, study) ) - .orderBy(studyStats.likeCount.desc(), study.id.desc()) + .orderBy(study.likeCount.desc(), study.id.desc()) .limit(limit) .fetch(); } diff --git a/modules/study/src/test/java/kr/spot/study/application/command/CreateStudyServiceTest.java b/modules/study/src/test/java/kr/spot/study/application/command/CreateStudyServiceTest.java index 0d01749e..48cf04a5 100644 --- a/modules/study/src/test/java/kr/spot/study/application/command/CreateStudyServiceTest.java +++ b/modules/study/src/test/java/kr/spot/study/application/command/CreateStudyServiceTest.java @@ -15,7 +15,6 @@ import kr.spot.study.infrastructure.jpa.associations.StudyCategoryRepository; import kr.spot.study.infrastructure.jpa.associations.StudyMemberRepository; import kr.spot.study.infrastructure.jpa.associations.StudyRegionRepository; -import kr.spot.study.infrastructure.jpa.associations.StudyStatsRepository; import kr.spot.study.infrastructure.jpa.associations.StudyStyleRepository; import kr.spot.study.presentation.command.dto.request.CreateStudyRequest; import org.junit.jupiter.api.BeforeEach; @@ -52,9 +51,6 @@ class CreateStudyServiceTest { @Mock StudyCategoryRepository studyCategoryRepository; - @Mock - StudyStatsRepository studyStatsRepository; - @Mock StudyMemberRepository studyMemberRepository; @@ -69,7 +65,7 @@ class CreateStudyServiceTest { @BeforeEach void setUp() { createStudyService = new CreateStudyService(idGenerator, eventPublisher, studyRepository, - studyStyleRepository, studyRegionRepository, studyCategoryRepository, studyStatsRepository, + studyStyleRepository, studyRegionRepository, studyCategoryRepository, studyMemberRepository); } @@ -153,7 +149,6 @@ void should_save_all_related_entities() { // then verify(studyRepository).save(any(Study.class)); - verify(studyStatsRepository).save(any()); verify(studyMemberRepository).save(any()); verify(studyCategoryRepository).saveAll(any()); verify(studyStyleRepository).saveAll(any()); diff --git a/modules/study/src/test/java/kr/spot/study/application/command/StudyLikeServiceTest.java b/modules/study/src/test/java/kr/spot/study/application/command/StudyLikeServiceTest.java index eb581c0d..9ae3fb61 100644 --- a/modules/study/src/test/java/kr/spot/study/application/command/StudyLikeServiceTest.java +++ b/modules/study/src/test/java/kr/spot/study/application/command/StudyLikeServiceTest.java @@ -5,8 +5,8 @@ import static org.mockito.Mockito.verify; import kr.spot.IdGenerator; +import kr.spot.study.infrastructure.jpa.StudyRepository; import kr.spot.study.infrastructure.jpa.associations.StudyLikeRepository; -import kr.spot.study.infrastructure.jpa.associations.StudyStatsRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -26,7 +26,7 @@ class StudyLikeServiceTest { StudyLikeRepository studyLikeRepository; @Mock - StudyStatsRepository studyStatsRepository; + StudyRepository studyRepository; @InjectMocks StudyLikeService sut; @@ -51,7 +51,7 @@ void should_increase_like_count_when_new_like() { // then verify(studyLikeRepository).saveStudyLike(generatedId, studyId, memberId); - verify(studyStatsRepository).increaseLike(studyId); + verify(studyRepository).increaseLike(studyId); } @Test @@ -66,7 +66,7 @@ void should_not_increase_like_count_when_duplicate_like() { // then verify(studyLikeRepository).saveStudyLike(generatedId, studyId, memberId); - verify(studyStatsRepository, never()).increaseLike(studyId); + verify(studyRepository, never()).increaseLike(studyId); } } @@ -88,7 +88,7 @@ void should_decrease_like_count_when_like_exists() { // then verify(studyLikeRepository).hardDelete(studyId, memberId); - verify(studyStatsRepository).decreaseLike(studyId); + verify(studyRepository).decreaseLike(studyId); } @Test @@ -102,7 +102,7 @@ void should_not_decrease_like_count_when_like_not_exists() { // then verify(studyLikeRepository).hardDelete(studyId, memberId); - verify(studyStatsRepository, never()).decreaseLike(studyId); + verify(studyRepository, never()).decreaseLike(studyId); } } } diff --git a/modules/study/src/test/java/kr/spot/study/application/query/GetStudyDetailServiceTest.java b/modules/study/src/test/java/kr/spot/study/application/query/GetStudyDetailServiceTest.java index eb84da11..75bc1c9e 100644 --- a/modules/study/src/test/java/kr/spot/study/application/query/GetStudyDetailServiceTest.java +++ b/modules/study/src/test/java/kr/spot/study/application/query/GetStudyDetailServiceTest.java @@ -10,14 +10,12 @@ import kr.spot.study.domain.Study; import kr.spot.study.domain.associations.StudyCategory; import kr.spot.study.domain.associations.StudyMember; -import kr.spot.study.domain.associations.StudyStats; import kr.spot.study.domain.enums.Category; import kr.spot.study.domain.enums.StudyMemberStatus; import kr.spot.study.domain.vo.Fee; import kr.spot.study.infrastructure.jpa.StudyRepository; import kr.spot.study.infrastructure.jpa.associations.StudyCategoryRepository; import kr.spot.study.infrastructure.jpa.associations.StudyMemberRepository; -import kr.spot.study.infrastructure.jpa.associations.StudyStatsRepository; import kr.spot.study.presentation.query.dto.response.GetStudyInfoResponse; import kr.spot.study.presentation.query.dto.response.GetStudyMembersResponse; import kr.spot.study.presentation.query.dto.response.GetStudyMembersResponse.MemberResponse; @@ -40,9 +38,6 @@ class GetStudyDetailServiceTest { @Mock StudyCategoryRepository studyCategoryRepository; - @Mock - StudyStatsRepository studyStatsRepository; - @Mock StudyMemberRepository studyMemberRepository; @@ -66,8 +61,7 @@ class GetStudyInfoTest { @DisplayName("스터디 상세 정보를 정상적으로 반환한다") void should_return_study_info_successfully() { // given - Study study = createStudy(studyId, "알고리즘 스터디", 10, 5); - StudyStats stats = createStudyStats(studyId, 100L, 50L); + Study study = createStudy(studyId, "알고리즘 스터디", 10, 5, 100L, 50L); List categories = List.of( StudyCategory.of(1L, studyId, Category.LANGUAGE), StudyCategory.of(2L, studyId, Category.CERTIFICATION) @@ -75,9 +69,8 @@ void should_return_study_info_successfully() { long displayViewCount = 150L; given(studyRepository.getStudyById(studyId)).willReturn(study); - given(studyStatsRepository.getByStudyId(studyId)).willReturn(stats); given(studyCategoryRepository.findAllByStudyId(studyId)).willReturn(categories); - given(studyViewCountService.calculateDisplayViewCount(stats, studyId, viewerId)) + given(studyViewCountService.calculateDisplayViewCount(study, studyId, viewerId)) .willReturn(displayViewCount); // when @@ -97,14 +90,12 @@ void should_return_study_info_successfully() { @DisplayName("카테고리가 없는 스터디도 정상적으로 반환한다") void should_return_study_info_with_empty_categories() { // given - Study study = createStudy(studyId, "스터디", 5, 1); - StudyStats stats = createStudyStats(studyId, 10L, 5L); + Study study = createStudy(studyId, "스터디", 5, 1, 10L, 5L); long displayViewCount = 10L; given(studyRepository.getStudyById(studyId)).willReturn(study); - given(studyStatsRepository.getByStudyId(studyId)).willReturn(stats); given(studyCategoryRepository.findAllByStudyId(studyId)).willReturn(List.of()); - given(studyViewCountService.calculateDisplayViewCount(stats, studyId, viewerId)) + given(studyViewCountService.calculateDisplayViewCount(study, studyId, viewerId)) .willReturn(displayViewCount); // when @@ -115,18 +106,14 @@ void should_return_study_info_with_empty_categories() { assertThat(result.statistics().hitCount()).isEqualTo(displayViewCount); } - private Study createStudy(long id, String name, int maxMembers, int currentMembers) { + private Study createStudy(long id, String name, int maxMembers, int currentMembers, + long viewCount, long likeCount) { Study study = Study.of(id, 1L, name, maxMembers, Fee.of(false, 0), "스터디 설명"); ReflectionTestUtils.setField(study, "currentMembers", currentMembers); + ReflectionTestUtils.setField(study, "viewCount", viewCount); + ReflectionTestUtils.setField(study, "likeCount", likeCount); return study; } - - private StudyStats createStudyStats(long studyId, long viewCount, long likeCount) { - StudyStats stats = StudyStats.of(studyId); - ReflectionTestUtils.setField(stats, "viewCount", viewCount); - ReflectionTestUtils.setField(stats, "likeCount", likeCount); - return stats; - } } @Nested diff --git a/modules/study/src/test/java/kr/spot/study/application/query/StudyViewCountServiceTest.java b/modules/study/src/test/java/kr/spot/study/application/query/StudyViewCountServiceTest.java index 557d9091..549207be 100644 --- a/modules/study/src/test/java/kr/spot/study/application/query/StudyViewCountServiceTest.java +++ b/modules/study/src/test/java/kr/spot/study/application/query/StudyViewCountServiceTest.java @@ -5,7 +5,8 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import kr.spot.study.domain.associations.StudyStats; +import kr.spot.study.domain.Study; +import kr.spot.study.domain.vo.Fee; import kr.spot.view.ViewAbuseGuard; import kr.spot.view.ViewCounter; import kr.spot.view.ViewableType; @@ -41,7 +42,7 @@ class CalculateDisplayViewCountTest { @DisplayName("새로운 조회일 경우 조회수를 증가시키고 합산하여 반환한다") void should_increment_and_return_total_when_new_view() { // given - StudyStats stats = StudyStats.of(studyId); + Study study = Study.of(studyId, 1L, "테스트 스터디", 10, Fee.of(false, 0), "설명"); long expectedDelta = 5L; given(viewAbuseGuard.shouldCount(ViewableType.STUDY, studyId, viewerId)) @@ -50,7 +51,7 @@ void should_increment_and_return_total_when_new_view() { .willReturn(expectedDelta); // when - long result = sut.calculateDisplayViewCount(stats, studyId, viewerId); + long result = sut.calculateDisplayViewCount(study, studyId, viewerId); // then assertThat(result).isEqualTo(expectedDelta); @@ -61,7 +62,7 @@ void should_increment_and_return_total_when_new_view() { @DisplayName("중복 조회일 경우 증가 없이 현재 델타만 반환한다") void should_return_current_delta_when_duplicate_view() { // given - StudyStats stats = StudyStats.of(studyId); + Study study = Study.of(studyId, 1L, "테스트 스터디", 10, Fee.of(false, 0), "설명"); long currentDelta = 10L; given(viewAbuseGuard.shouldCount(ViewableType.STUDY, studyId, viewerId)) @@ -70,7 +71,7 @@ void should_return_current_delta_when_duplicate_view() { .willReturn(currentDelta); // when - long result = sut.calculateDisplayViewCount(stats, studyId, viewerId); + long result = sut.calculateDisplayViewCount(study, studyId, viewerId); // then assertThat(result).isEqualTo(currentDelta); @@ -82,16 +83,16 @@ void should_return_current_delta_when_duplicate_view() { @DisplayName("Redis 오류 발생 시 기본 조회수만 반환한다") void should_return_base_count_when_redis_error() { // given - StudyStats stats = StudyStats.of(studyId); + Study study = Study.of(studyId, 1L, "테스트 스터디", 10, Fee.of(false, 0), "설명"); given(viewAbuseGuard.shouldCount(ViewableType.STUDY, studyId, viewerId)) .willThrow(new RuntimeException("Redis connection failed")); // when - long result = sut.calculateDisplayViewCount(stats, studyId, viewerId); + long result = sut.calculateDisplayViewCount(study, studyId, viewerId); // then - assertThat(result).isEqualTo(stats.getViewCount()); + assertThat(result).isEqualTo(study.getViewCount()); } } } diff --git a/modules/study/src/test/java/kr/spot/study/infrastructure/batch/StudyViewFlusherTest.java b/modules/study/src/test/java/kr/spot/study/infrastructure/batch/StudyViewFlusherTest.java index 48cdcb5f..4f64f657 100644 --- a/modules/study/src/test/java/kr/spot/study/infrastructure/batch/StudyViewFlusherTest.java +++ b/modules/study/src/test/java/kr/spot/study/infrastructure/batch/StudyViewFlusherTest.java @@ -2,7 +2,7 @@ import static org.mockito.Mockito.verify; -import kr.spot.study.infrastructure.jpa.associations.StudyStatsRepository; +import kr.spot.study.infrastructure.jpa.StudyRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,14 +12,14 @@ class StudyViewFlusherTest { @Mock - StudyStatsRepository studyStatsRepository; + StudyRepository studyRepository; StudyViewFlusher flusher; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - flusher = new StudyViewFlusher(studyStatsRepository); + flusher = new StudyViewFlusher(studyRepository); } @Test @@ -33,6 +33,6 @@ void should_update_view_count_in_db() { flusher.updateViewCount(studyId, delta); // then - verify(studyStatsRepository).increaseViewBy(studyId, delta); + verify(studyRepository).increaseViewBy(studyId, delta); } }