From a38964b5307fba869b9aa317f7e9f9dfa9859fa0 Mon Sep 17 00:00:00 2001 From: teho Date: Thu, 7 Nov 2024 10:36:02 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[#496]=20feat:=20postViewCountService=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1,=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=EB=A5=BC=20=EC=9C=84=ED=95=9C=20market=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=9D=98=20=EB=B9=84=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EB=9D=BD=20=ED=86=B5=ED=95=9C=20=EC=A0=9C=EC=96=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nanaland/domain/common/entity/Post.java | 4 ++ .../common/repository/PostRepository.java | 15 +++++++ .../common/service/PostViewCountService.java | 32 ++++++++++++++ .../ExperienceRepositoryCustom.java | 4 ++ .../repository/ExperienceRepositoryImpl.java | 43 +++++++++++++++++++ .../experience/service/ExperienceService.java | 10 ++++- 6 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/jeju/nanaland/domain/common/repository/PostRepository.java create mode 100644 src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java diff --git a/src/main/java/com/jeju/nanaland/domain/common/entity/Post.java b/src/main/java/com/jeju/nanaland/domain/common/entity/Post.java index 5f2c289d..522b9787 100644 --- a/src/main/java/com/jeju/nanaland/domain/common/entity/Post.java +++ b/src/main/java/com/jeju/nanaland/domain/common/entity/Post.java @@ -1,6 +1,7 @@ package com.jeju.nanaland.domain.common.entity; import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -28,6 +29,9 @@ public abstract class Post extends BaseEntity { @NotNull private Long priority; + @Column(name = "view_count", nullable = false, columnDefinition = "int default 0") + private int viewCount; + protected Post(ImageFile firstImageFile, Long priority) { this.firstImageFile = firstImageFile; this.priority = priority; diff --git a/src/main/java/com/jeju/nanaland/domain/common/repository/PostRepository.java b/src/main/java/com/jeju/nanaland/domain/common/repository/PostRepository.java new file mode 100644 index 00000000..e8471022 --- /dev/null +++ b/src/main/java/com/jeju/nanaland/domain/common/repository/PostRepository.java @@ -0,0 +1,15 @@ +package com.jeju.nanaland.domain.common.repository; + +import com.jeju.nanaland.domain.common.entity.Post; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + + +public interface PostRepository extends JpaRepository { + + @Modifying + @Query("UPDATE Post p SET p.viewCount = p.viewCount + 1 WHERE p.id = :postId") + void increaseViewCount(@Param("postId") Long postId); +} \ No newline at end of file diff --git a/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java b/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java new file mode 100644 index 00000000..5e330576 --- /dev/null +++ b/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java @@ -0,0 +1,32 @@ +package com.jeju.nanaland.domain.common.service; + +import com.jeju.nanaland.domain.common.repository.PostRepository; +import com.jeju.nanaland.global.util.RedisUtil; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PostViewCountService { + + private final PostRepository postRepository; + private final RedisUtil redisUtil; + + @Transactional + public void increaseViewCount(Long postId, Long memberId) { + String redisKey = "post_viewed_" + memberId + "_" + postId; + + // 30분 -> 1800초 + long cacheDurationSeconds = 1800L; + + // 30분 이내에 조회한 기록이 없으면 + if (redisUtil.getValue(redisKey) == null) { + // 조회수 증가 + postRepository.increaseViewCount(postId); + // 레디스에 등록 + redisUtil.setExpiringValue(redisKey, "viewed", cacheDurationSeconds); + } + + } +} diff --git a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryCustom.java b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryCustom.java index 58f0b6b5..6bdb02f0 100644 --- a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryCustom.java +++ b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryCustom.java @@ -17,6 +17,8 @@ public interface ExperienceRepositoryCustom { ExperienceCompositeDto findCompositeDtoById(Long id, Language language); + ExperienceCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id, Language language); + Page searchCompositeDtoByKeyword(String keyword, Language language, Pageable pageable); @@ -26,6 +28,8 @@ Page findExperienceThumbnails(Language language, Set getExperienceTypeKeywordSet(Long postId); + Set getExperienceTypeKeywordSetWithWithPessimisticLock(Long postId); + List findAllSearchPostForReviewDtoByLanguage(Language language); List findAllIds(); diff --git a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java index 8e2dc9ee..318e00f2 100644 --- a/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/experience/repository/ExperienceRepositoryImpl.java @@ -24,6 +24,7 @@ import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.LockModeType; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -66,6 +67,35 @@ public ExperienceCompositeDto findCompositeDtoById(Long id, Language language) { .fetchOne(); } + @Override + public ExperienceCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id, + Language language) { + return queryFactory + .select(new QExperienceCompositeDto( + experience.id, + imageFile.originUrl, + imageFile.thumbnailUrl, + experience.contact, + experience.homepage, + experienceTrans.language, + experienceTrans.title, + experienceTrans.content, + experienceTrans.address, + experienceTrans.addressTag, + experienceTrans.intro, + experienceTrans.details, + experienceTrans.time, + experienceTrans.amenity, + experienceTrans.fee + )) + .from(experience) + .leftJoin(experience.firstImageFile, imageFile) + .leftJoin(experience.experienceTrans, experienceTrans) + .where(experience.id.eq(id).and(experienceTrans.language.eq(language))) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .fetchOne(); + } + @Override public Page searchCompositeDtoByKeyword(String keyword, Language language, Pageable pageable) { @@ -171,6 +201,19 @@ public Set getExperienceTypeKeywordSet(Long postId) { return map.getOrDefault(postId, Collections.emptySet()); } + @Override + public Set getExperienceTypeKeywordSetWithWithPessimisticLock( + Long postId) { + Map> map = queryFactory + .selectFrom(experienceKeyword) + .where(experienceKeyword.experience.id.eq(postId)) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) // 비관적 락 추가 + .transform(GroupBy.groupBy(experienceKeyword.experience.id) + .as(GroupBy.set(experienceKeyword.experienceTypeKeyword))); + + return map.getOrDefault(postId, Collections.emptySet()); + } + @Override public List findAllSearchPostForReviewDtoByLanguage(Language language) { return queryFactory diff --git a/src/main/java/com/jeju/nanaland/domain/experience/service/ExperienceService.java b/src/main/java/com/jeju/nanaland/domain/experience/service/ExperienceService.java index 90df34ec..d5b1dd2d 100644 --- a/src/main/java/com/jeju/nanaland/domain/experience/service/ExperienceService.java +++ b/src/main/java/com/jeju/nanaland/domain/experience/service/ExperienceService.java @@ -11,6 +11,7 @@ import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.repository.ImageFileRepository; import com.jeju.nanaland.domain.common.service.PostService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.experience.dto.ExperienceCompositeDto; import com.jeju.nanaland.domain.experience.dto.ExperienceResponse.ExperienceDetailDto; import com.jeju.nanaland.domain.experience.dto.ExperienceResponse.ExperienceThumbnail; @@ -35,6 +36,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -46,6 +48,7 @@ public class ExperienceService implements PostService { private final ImageFileRepository imageFileRepository; private final SearchService searchService; private final ReviewRepository reviewRepository; + private final PostViewCountService postViewCountService; /** * Experience 객체 조회 @@ -110,12 +113,13 @@ public ExperienceThumbnailDto getExperienceList(MemberInfoDto memberInfoDto, .build(); } + @Transactional // 이색체험 상세 정보 조회 public ExperienceDetailDto getExperienceDetail(MemberInfoDto memberInfoDto, Long postId, boolean isSearch) { Language language = memberInfoDto.getLanguage(); - ExperienceCompositeDto experienceCompositeDto = experienceRepository.findCompositeDtoById( + ExperienceCompositeDto experienceCompositeDto = experienceRepository.findCompositeDtoByIdWithPessimisticLock( postId, language); // 해당 id의 포스트가 없는 경우 404 에러 @@ -138,13 +142,15 @@ public ExperienceDetailDto getExperienceDetail(MemberInfoDto memberInfoDto, Long images.addAll(imageFileRepository.findPostImageFiles(postId)); // 키워드 - Set keywordSet = experienceRepository.getExperienceTypeKeywordSet( + Set keywordSet = experienceRepository.getExperienceTypeKeywordSetWithWithPessimisticLock( postId); List keywords = keywordSet.stream() .map(experienceTypeKeyword -> experienceTypeKeyword.getValueByLocale(language) ).toList(); + postViewCountService.increaseViewCount(postId, member.getId()); + return ExperienceDetailDto.builder() .id(experienceCompositeDto.getId()) .title(experienceCompositeDto.getTitle()) From 9f06b3be16ae66953584a5f4498f9b6c8763543b Mon Sep 17 00:00:00 2001 From: teho Date: Thu, 7 Nov 2024 15:48:41 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[#496]=20feat:=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=98=20=EC=A6=9D=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EB=B9=84=EA=B4=80=EC=A0=81=20=EB=9D=BD=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=9C=20=EC=A1=B0=ED=9A=8C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/service/PostViewCountService.java | 2 +- .../experience/service/ExperienceService.java | 4 +- .../repository/FestivalRepositoryCustom.java | 2 + .../repository/FestivalRepositoryImpl.java | 32 ++++++++++ .../festival/service/FestivalService.java | 11 +++- .../repository/MarketRepositoryCustom.java | 2 + .../repository/MarketRepositoryImpl.java | 27 +++++++++ .../domain/market/service/MarketService.java | 10 +++- .../nana/repository/NanaRepository.java | 10 ++++ .../domain/nana/service/NanaService.java | 10 +++- .../repository/NatureRepositoryCustom.java | 5 +- .../repository/NatureRepositoryImpl.java | 42 +++++++++++-- .../domain/nature/service/NatureService.java | 20 +++++-- .../RestaurantRepositoryCustom.java | 6 ++ .../repository/RestaurantRepositoryImpl.java | 59 +++++++++++++++++++ .../restaurant/service/RestaurantService.java | 14 ++++- 16 files changed, 234 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java b/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java index 5e330576..f482c8c1 100644 --- a/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java +++ b/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java @@ -2,9 +2,9 @@ import com.jeju.nanaland.domain.common.repository.PostRepository; import com.jeju.nanaland.global.util.RedisUtil; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor diff --git a/src/main/java/com/jeju/nanaland/domain/experience/service/ExperienceService.java b/src/main/java/com/jeju/nanaland/domain/experience/service/ExperienceService.java index d5b1dd2d..0cb17a55 100644 --- a/src/main/java/com/jeju/nanaland/domain/experience/service/ExperienceService.java +++ b/src/main/java/com/jeju/nanaland/domain/experience/service/ExperienceService.java @@ -113,8 +113,9 @@ public ExperienceThumbnailDto getExperienceList(MemberInfoDto memberInfoDto, .build(); } - @Transactional + // 이색체험 상세 정보 조회 + @Transactional public ExperienceDetailDto getExperienceDetail(MemberInfoDto memberInfoDto, Long postId, boolean isSearch) { @@ -149,6 +150,7 @@ public ExperienceDetailDto getExperienceDetail(MemberInfoDto memberInfoDto, Long experienceTypeKeyword.getValueByLocale(language) ).toList(); + // 조회 수 증가 postViewCountService.increaseViewCount(postId, member.getId()); return ExperienceDetailDto.builder() diff --git a/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryCustom.java b/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryCustom.java index 6ab51bec..201d48dd 100644 --- a/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryCustom.java +++ b/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryCustom.java @@ -13,6 +13,8 @@ public interface FestivalRepositoryCustom { FestivalCompositeDto findCompositeDtoById(Long id, Language locale); + FestivalCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id, Language locale); + Page searchCompositeDtoByKeyword(String keyword, Language locale, Pageable pageable); diff --git a/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryImpl.java index 404ce9e4..2b305563 100644 --- a/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/festival/repository/FestivalRepositoryImpl.java @@ -16,6 +16,7 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.LockModeType; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -59,6 +60,37 @@ public FestivalCompositeDto findCompositeDtoById(Long id, Language language) { .fetchOne(); } + @Override + public FestivalCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id, Language language) { + return queryFactory + .select(new QFestivalCompositeDto( + festival.id, + imageFile.originUrl, + imageFile.thumbnailUrl, + festival.contact, + festival.homepage, + festivalTrans.language, + festivalTrans.title, + festivalTrans.content, + festivalTrans.address, + festivalTrans.addressTag, + festivalTrans.time, + festivalTrans.intro, + festivalTrans.fee, + festival.startDate, + festival.endDate, + festival.season + )) + .from(festival) + .leftJoin(festival.firstImageFile, imageFile) + .leftJoin(festival.festivalTrans, festivalTrans) + .where(festival.id.eq(id).and(festivalTrans.language.eq(language)) + .and(festival.status.eq(Status.ACTIVE)) + ) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .fetchOne(); + } + @Override public Page searchCompositeDtoByKeyword(String keyword, Language language, Pageable pageable) { diff --git a/src/main/java/com/jeju/nanaland/domain/festival/service/FestivalService.java b/src/main/java/com/jeju/nanaland/domain/festival/service/FestivalService.java index 499f432b..1790d3d9 100644 --- a/src/main/java/com/jeju/nanaland/domain/festival/service/FestivalService.java +++ b/src/main/java/com/jeju/nanaland/domain/festival/service/FestivalService.java @@ -13,6 +13,7 @@ import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.service.ImageFileService; import com.jeju.nanaland.domain.common.service.PostService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.festival.dto.FestivalCompositeDto; import com.jeju.nanaland.domain.festival.dto.FestivalResponse.FestivalDetailDto; @@ -25,7 +26,6 @@ import com.jeju.nanaland.global.exception.BadRequestException; import com.jeju.nanaland.global.exception.ErrorCode; import com.jeju.nanaland.global.exception.NotFoundException; -import jakarta.transaction.Transactional; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -38,6 +38,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -48,6 +49,7 @@ public class FestivalService implements PostService { private final MemberFavoriteService memberFavoriteService; private final SearchService searchService; private final ImageFileService imageFileService; + private final PostViewCountService postViewCountService; /** * Festival 객체 조회 @@ -144,9 +146,11 @@ public FestivalThumbnailDto getSeasonFestivalList(MemberInfoDto memberInfoDto, i } // 축제 상세 정보 조회 + @Transactional public FestivalDetailDto getFestivalDetail(MemberInfoDto memberInfoDto, Long id, boolean isSearch) { - FestivalCompositeDto compositeDtoById = festivalRepository.findCompositeDtoById(id, + FestivalCompositeDto compositeDtoById = festivalRepository.findCompositeDtoByIdWithPessimisticLock( + id, memberInfoDto.getLanguage()); if (compositeDtoById == null) { @@ -160,6 +164,9 @@ public FestivalDetailDto getFestivalDetail(MemberInfoDto memberInfoDto, Long id, boolean isPostInFavorite = memberFavoriteService.isPostInFavorite(memberInfoDto.getMember(), FESTIVAL, id); + // 조회 수 증가 + postViewCountService.increaseViewCount(id, memberInfoDto.getMember().getId()); + return FestivalDetailDto.builder() .id(compositeDtoById.getId()) .addressTag(compositeDtoById.getAddressTag()) diff --git a/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryCustom.java b/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryCustom.java index d0e8d257..3a497645 100644 --- a/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryCustom.java +++ b/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryCustom.java @@ -13,6 +13,8 @@ public interface MarketRepositoryCustom { MarketCompositeDto findCompositeDtoById(Long id, Language locale); + MarketCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id, Language locale); + Page findMarketThumbnails(Language locale, List addressTags, Pageable pageable); diff --git a/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryImpl.java index 16b64445..44cc5ce5 100644 --- a/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/market/repository/MarketRepositoryImpl.java @@ -17,6 +17,7 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.LockModeType; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -54,6 +55,32 @@ public MarketCompositeDto findCompositeDtoById(Long id, Language language) { .fetchOne(); } + @Override + public MarketCompositeDto findCompositeDtoByIdWithPessimisticLock(Long id, Language language) { + return queryFactory + .select(new QMarketCompositeDto( + market.id, + imageFile.originUrl, + imageFile.thumbnailUrl, + market.contact, + market.homepage, + marketTrans.language, + marketTrans.title, + marketTrans.content, + marketTrans.address, + marketTrans.addressTag, + marketTrans.time, + marketTrans.intro, + marketTrans.amenity + )) + .from(market) + .leftJoin(market.firstImageFile, imageFile) + .leftJoin(market.marketTrans, marketTrans) + .where(market.id.eq(id).and(marketTrans.language.eq(language))) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .fetchOne(); + } + @Override public Page findMarketThumbnails(Language language, List addressTags, Pageable pageable) { diff --git a/src/main/java/com/jeju/nanaland/domain/market/service/MarketService.java b/src/main/java/com/jeju/nanaland/domain/market/service/MarketService.java index 61eba4f3..254d0101 100644 --- a/src/main/java/com/jeju/nanaland/domain/market/service/MarketService.java +++ b/src/main/java/com/jeju/nanaland/domain/market/service/MarketService.java @@ -10,6 +10,7 @@ import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.service.ImageFileService; import com.jeju.nanaland.domain.common.service.PostService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.market.dto.MarketCompositeDto; import com.jeju.nanaland.domain.market.dto.MarketResponse; @@ -28,6 +29,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -38,6 +40,7 @@ public class MarketService implements PostService { private final MemberFavoriteService memberFavoriteService; private final SearchService searchService; private final ImageFileService imageFileService; + private final PostViewCountService postViewCountService; /** * Market 객체 조회 @@ -98,10 +101,12 @@ public MarketResponse.MarketThumbnailDto getMarketList(MemberInfoDto memberInfoD } // 전통시장 상세 정보 조회 + @Transactional public MarketResponse.MarketDetailDto getMarketDetail(MemberInfoDto memberInfoDto, Long id, boolean isSearch) { - MarketCompositeDto marketCompositeDto = marketRepository.findCompositeDtoById(id, + MarketCompositeDto marketCompositeDto = marketRepository.findCompositeDtoByIdWithPessimisticLock( + id, memberInfoDto.getLanguage()); if (marketCompositeDto == null) { @@ -117,6 +122,9 @@ public MarketResponse.MarketDetailDto getMarketDetail(MemberInfoDto memberInfoDt boolean isFavorite = memberFavoriteService.isPostInFavorite(memberInfoDto.getMember(), MARKET, id); + // 조회 수 증가 + postViewCountService.increaseViewCount(id, memberInfoDto.getMember().getId()); + return MarketResponse.MarketDetailDto.builder() .id(marketCompositeDto.getId()) .images(imageFileService.getPostImageFilesByPostIdIncludeFirstImage(id, diff --git a/src/main/java/com/jeju/nanaland/domain/nana/repository/NanaRepository.java b/src/main/java/com/jeju/nanaland/domain/nana/repository/NanaRepository.java index 74ce8f88..0ea58cb1 100644 --- a/src/main/java/com/jeju/nanaland/domain/nana/repository/NanaRepository.java +++ b/src/main/java/com/jeju/nanaland/domain/nana/repository/NanaRepository.java @@ -1,10 +1,20 @@ package com.jeju.nanaland.domain.nana.repository; import com.jeju.nanaland.domain.nana.entity.Nana; +import io.lettuce.core.dynamic.annotation.Param; +import jakarta.persistence.LockModeType; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; public interface NanaRepository extends JpaRepository, NanaRepositoryCustom { Optional findNanaById(Long id); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT n FROM Nana n WHERE n.id = :id") + Optional findNanaByIdWithPessimisticLock(@Param("id") Long id); + + } diff --git a/src/main/java/com/jeju/nanaland/domain/nana/service/NanaService.java b/src/main/java/com/jeju/nanaland/domain/nana/service/NanaService.java index 0f9119f6..86d4b34c 100644 --- a/src/main/java/com/jeju/nanaland/domain/nana/service/NanaService.java +++ b/src/main/java/com/jeju/nanaland/domain/nana/service/NanaService.java @@ -12,6 +12,7 @@ import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.repository.ImageFileRepository; import com.jeju.nanaland.domain.common.service.PostService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.hashtag.entity.Hashtag; import com.jeju.nanaland.domain.hashtag.repository.HashtagRepository; @@ -39,6 +40,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -51,6 +53,7 @@ public class NanaService implements PostService { private final MemberFavoriteService memberFavoriteService; private final SearchService searchService; private final ImageFileRepository imageFileRepository; + private final PostViewCountService postViewCountService; /** @@ -141,14 +144,14 @@ public NanaResponse.PreviewPageDto getNanaThumbnails(Language locale, int page, * @param isSearch * @return */ - + @Transactional public NanaResponse.DetailPageDto getNanaDetail(MemberInfoDto memberInfoDto, Long postId, boolean isSearch) { Language language = memberInfoDto.getLanguage(); // nana 찾아서 - Nana nana = nanaRepository.findNanaById(postId) + Nana nana = nanaRepository.findNanaByIdWithPessimisticLock(postId) .orElseThrow(() -> new NotFoundException(ErrorCode.NANA_NOT_FOUND.getMessage())); // nanaTitle 찾아서 @@ -180,6 +183,9 @@ public NanaResponse.DetailPageDto getNanaDetail(MemberInfoDto memberInfoDto, Lon List contentDetailDtos = getNanaDetailDtosFromNanaContents( nanaContents, language, eachNanaContentImages); + // 조회 수 증가 + postViewCountService.increaseViewCount(postId, memberInfoDto.getMember().getId()); + return NanaResponse.DetailPageDto.builder() .id(nana.getId()) .firstImage(new ImageFileDto(nana.getFirstImageFile().getOriginUrl(), diff --git a/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryCustom.java b/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryCustom.java index 1e4dd204..820fe1b0 100644 --- a/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryCustom.java +++ b/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryCustom.java @@ -13,10 +13,13 @@ public interface NatureRepositoryCustom { NatureCompositeDto findNatureCompositeDto(Long id, Language locale); + NatureCompositeDto findNatureCompositeDtoWithPessimisticLock(Long id, Language locale); + Page searchCompositeDtoByKeyword(String keyword, Language locale, Pageable pageable); - Page findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(Language locale, List addressTags, + Page findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(Language locale, + List addressTags, String keyword, Pageable pageable); PostPreviewDto findPostPreviewDto(Long postId, Language language); diff --git a/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryImpl.java index a968fedc..abd17f20 100644 --- a/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/nature/repository/NatureRepositoryImpl.java @@ -17,6 +17,7 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.LockModeType; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -64,6 +65,36 @@ public NatureCompositeDto findNatureCompositeDto(Long natureId, Language languag .fetchOne(); } + @Override + public NatureCompositeDto findNatureCompositeDtoWithPessimisticLock(Long natureId, + Language language) { + return queryFactory + .select(new QNatureCompositeDto( + nature.id, + imageFile.originUrl, + imageFile.thumbnailUrl, + nature.contact, + natureTrans.language, + natureTrans.title, + natureTrans.content, + natureTrans.address, + natureTrans.addressTag, + natureTrans.intro, + natureTrans.details, + natureTrans.time, + natureTrans.amenity, + natureTrans.fee + )) + .from(nature) + .leftJoin(nature.firstImageFile, imageFile) + .leftJoin(nature.natureTrans, natureTrans) + .where(nature.id.eq(natureId) + .and(natureTrans.language.eq(language)) + ) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .fetchOne(); + } + /** * 7대 자연 검색 페이징 조회 * @@ -125,14 +156,15 @@ public Page searchCompositeDtoByKeyword(String keyword, Lang /** * 7대 자연 프리뷰 페이징 조회 * - * @param language 언어 - * @param addressTags 지역명 - * @param keyword 키워드 - * @param pageable 페이징 정보 + * @param language 언어 + * @param addressTags 지역명 + * @param keyword 키워드 + * @param pageable 페이징 정보 * @return 7대 자연 검색 페이징 정보 */ @Override - public Page findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(Language language, + public Page findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc( + Language language, List addressTags, String keyword, Pageable pageable) { List resultDto = queryFactory .select(new QNatureResponse_PreviewDto( diff --git a/src/main/java/com/jeju/nanaland/domain/nature/service/NatureService.java b/src/main/java/com/jeju/nanaland/domain/nature/service/NatureService.java index ba1ab3f3..410ba883 100644 --- a/src/main/java/com/jeju/nanaland/domain/nature/service/NatureService.java +++ b/src/main/java/com/jeju/nanaland/domain/nature/service/NatureService.java @@ -10,6 +10,7 @@ import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.service.ImageFileService; import com.jeju.nanaland.domain.common.service.PostService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.member.dto.MemberResponse.MemberInfoDto; import com.jeju.nanaland.domain.nature.dto.NatureCompositeDto; @@ -26,6 +27,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -36,6 +38,7 @@ public class NatureService implements PostService { private final MemberFavoriteService memberFavoriteService; private final SearchService searchService; private final ImageFileService imageFileService; + private final PostViewCountService postViewCountService; /** * Nature 객체 조회 @@ -73,11 +76,11 @@ public PostPreviewDto getPostPreviewDto(Long postId, Category category, Language /** * 7대 자연 프리뷰 리스트 조회 * - * @param memberInfoDto 회원 정보 - * @param addressTags 지역명 - * @param keyword 키워드 - * @param page 페이지 Number - * @param size 페이지 Size + * @param memberInfoDto 회원 정보 + * @param addressTags 지역명 + * @param keyword 키워드 + * @param page 페이지 Number + * @param size 페이지 Size * @return 7대 자연 프리뷰 리스트 */ public NatureResponse.PreviewPageDto getNaturePreview(MemberInfoDto memberInfoDto, @@ -111,9 +114,11 @@ public NatureResponse.PreviewPageDto getNaturePreview(MemberInfoDto memberInfoDt * @return 7대 자연 상세 정보 * @throws NotFoundException 존재하는 7대 자연이 없는 경우 */ + @Transactional public NatureResponse.DetailDto getNatureDetail(MemberInfoDto memberInfoDto, Long natureId, boolean isSearch) { - NatureCompositeDto natureCompositeDto = natureRepository.findNatureCompositeDto(natureId, + NatureCompositeDto natureCompositeDto = natureRepository.findNatureCompositeDtoWithPessimisticLock( + natureId, memberInfoDto.getLanguage()); if (natureCompositeDto == null) { @@ -129,6 +134,9 @@ public NatureResponse.DetailDto getNatureDetail(MemberInfoDto memberInfoDto, Lon boolean isFavorite = memberFavoriteService.isPostInFavorite(memberInfoDto.getMember(), NATURE, natureId); + // 조회 수 증가 + postViewCountService.increaseViewCount(natureId, memberInfoDto.getMember().getId()); + return NatureResponse.DetailDto.builder() .id(natureCompositeDto.getId()) .addressTag(natureCompositeDto.getAddressTag()) diff --git a/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryCustom.java b/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryCustom.java index 7cc2d410..7db6dfb3 100644 --- a/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryCustom.java +++ b/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryCustom.java @@ -20,10 +20,16 @@ Page findRestaurantThumbnails(Language language, RestaurantCompositeDto findCompositeDtoById(Long postId, Language language); + RestaurantCompositeDto findCompositeDtoByIdWithPessimisticLock(Long postId, Language language); + Set getRestaurantTypeKeywordSet(Long postId); + Set getRestaurantTypeKeywordSetWithPessimisticLock(Long postId); + List getRestaurantMenuList(Long postId, Language language); + List getRestaurantMenuListWithPessimisticLock(Long postId, Language language); + Page searchCompositeDtoByKeyword(String keyword, Language language, Pageable pageable); diff --git a/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryImpl.java b/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryImpl.java index 871c6be8..da0244f3 100644 --- a/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryImpl.java +++ b/src/main/java/com/jeju/nanaland/domain/restaurant/repository/RestaurantRepositoryImpl.java @@ -26,6 +26,7 @@ import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.LockModeType; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -106,6 +107,34 @@ public RestaurantCompositeDto findCompositeDtoById(Long postId, Language languag .fetchOne(); } + @Override + public RestaurantCompositeDto findCompositeDtoByIdWithPessimisticLock(Long postId, + Language language) { + return queryFactory + .select(new QRestaurantCompositeDto( + restaurant.id, + imageFile.originUrl, + imageFile.thumbnailUrl, + restaurant.contact, + restaurantTrans.language, + restaurantTrans.title, + restaurantTrans.content, + restaurantTrans.address, + restaurantTrans.addressTag, + restaurantTrans.time, + restaurant.homepage, + restaurant.instagram, + restaurantTrans.service + )) + .from(restaurant) + .innerJoin(restaurant.restaurantTrans, restaurantTrans) + .innerJoin(restaurant.firstImageFile, imageFile) + .where(restaurant.id.eq(postId) + .and(restaurantTrans.language.eq(language))) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .fetchOne(); + } + @Override public Set getRestaurantTypeKeywordSet(Long postId) { Map> map = queryFactory @@ -117,6 +146,18 @@ public Set getRestaurantTypeKeywordSet(Long postId) { return map.getOrDefault(postId, Collections.emptySet()); } + @Override + public Set getRestaurantTypeKeywordSetWithPessimisticLock(Long postId) { + Map> map = queryFactory + .selectFrom(restaurantKeyword) + .where(restaurantKeyword.restaurant.id.eq(postId)) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .transform(GroupBy.groupBy(restaurantKeyword.restaurant.id) + .as(GroupBy.set(restaurantKeyword.restaurantTypeKeyword))); + + return map.getOrDefault(postId, Collections.emptySet()); + } + @Override public List getRestaurantMenuList(Long postId, Language language) { return queryFactory @@ -133,6 +174,24 @@ public List getRestaurantMenuList(Long postId, Language langu .fetch(); } + @Override + public List getRestaurantMenuListWithPessimisticLock(Long postId, + Language language) { + return queryFactory + .select(new QRestaurantResponse_RestaurantMenuDto( + restaurantMenu.menuName, + restaurantMenu.price, + imageFile.originUrl, + imageFile.thumbnailUrl + )) + .from(restaurantMenu) + .leftJoin(restaurantMenu.firstImageFile, imageFile) + .where(restaurantMenu.restaurantTrans.restaurant.id.eq(postId) + .and(restaurantTrans.language.eq(language))) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .fetch(); + } + @Override public Page searchCompositeDtoByKeyword(String keyword, Language language, Pageable pageable) { diff --git a/src/main/java/com/jeju/nanaland/domain/restaurant/service/RestaurantService.java b/src/main/java/com/jeju/nanaland/domain/restaurant/service/RestaurantService.java index 3428fe68..3e9b340d 100644 --- a/src/main/java/com/jeju/nanaland/domain/restaurant/service/RestaurantService.java +++ b/src/main/java/com/jeju/nanaland/domain/restaurant/service/RestaurantService.java @@ -12,6 +12,7 @@ import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.repository.ImageFileRepository; import com.jeju.nanaland.domain.common.service.PostService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.member.dto.MemberResponse.MemberInfoDto; import com.jeju.nanaland.domain.member.entity.Member; @@ -36,6 +37,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -47,6 +49,7 @@ public class RestaurantService implements PostService { private final ReviewRepository reviewRepository; private final SearchService searchService; private final ImageFileRepository imageFileRepository; + private final PostViewCountService postViewCountService; /** * Restaurant 객체 조회 @@ -111,11 +114,12 @@ public RestaurantThumbnailDto getRestaurantList(MemberInfoDto memberInfoDto, } // 맛집 상세 정보 조회 + @Transactional public RestaurantDetailDto getRestaurantDetail(MemberInfoDto memberInfoDto, Long postId, boolean isSearch) { Language language = memberInfoDto.getLanguage(); - RestaurantCompositeDto restaurantCompositeDto = restaurantRepository.findCompositeDtoById( + RestaurantCompositeDto restaurantCompositeDto = restaurantRepository.findCompositeDtoByIdWithPessimisticLock( postId, language); // 해당 id의 포스트가 없는 경우 404 에러 @@ -138,7 +142,7 @@ public RestaurantDetailDto getRestaurantDetail(MemberInfoDto memberInfoDto, Long images.addAll(imageFileRepository.findPostImageFiles(postId)); // 키워드 - Set keywordSet = restaurantRepository.getRestaurantTypeKeywordSet( + Set keywordSet = restaurantRepository.getRestaurantTypeKeywordSetWithPessimisticLock( postId); List keywords = keywordSet.stream() .map(experienceTypeKeyword -> @@ -146,9 +150,13 @@ public RestaurantDetailDto getRestaurantDetail(MemberInfoDto memberInfoDto, Long ).toList(); // 메뉴 - List menuDtoList = restaurantRepository.getRestaurantMenuList(postId, + List menuDtoList = restaurantRepository.getRestaurantMenuListWithPessimisticLock( + postId, language); + // 조회 수 증가 + postViewCountService.increaseViewCount(postId, member.getId()); + return RestaurantDetailDto.builder() .id(restaurantCompositeDto.getId()) .title(restaurantCompositeDto.getTitle()) From 8f3a04df41784cb51b49d9d8b9596285e8950882 Mon Sep 17 00:00:00 2001 From: teho Date: Thu, 7 Nov 2024 16:26:17 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[#496]=20feat:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ExperienceServiceTest.java | 5 ++++- .../festival/service/FestivalServiceTest.java | 7 ++++-- .../market/service/MarketServiceTest.java | 5 ++++- .../domain/nana/service/NanaServiceTest.java | 5 ++++- .../nature/service/NatureServiceTest.java | 22 ++++++++++++++----- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/jeju/nanaland/domain/experience/service/ExperienceServiceTest.java b/src/test/java/com/jeju/nanaland/domain/experience/service/ExperienceServiceTest.java index c639b2c8..86dc0368 100644 --- a/src/test/java/com/jeju/nanaland/domain/experience/service/ExperienceServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/experience/service/ExperienceServiceTest.java @@ -13,6 +13,7 @@ import com.jeju.nanaland.domain.common.entity.ImageFile; import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.repository.ImageFileRepository; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.experience.dto.ExperienceCompositeDto; import com.jeju.nanaland.domain.experience.dto.ExperienceResponse.ExperienceDetailDto; import com.jeju.nanaland.domain.experience.dto.ExperienceResponse.ExperienceThumbnail; @@ -59,6 +60,8 @@ class ExperienceServiceTest { ImageFileRepository imageFileRepository; @Mock ReviewRepository reviewRepository; + @Mock + private PostViewCountService postViewCountService; @Test @DisplayName("이색체험 preview 정보 조회") @@ -119,7 +122,7 @@ void getActivityList() { .build(); doReturn(experienceCompositeDto).when(experienceRepository) - .findCompositeDtoById(postId, language); + .findCompositeDtoByIdWithPessimisticLock(postId, language); doReturn(false).when(memberFavoriteService) .isPostInFavorite(memberInfoDto.getMember(), Category.EXPERIENCE, postId); doReturn(List.of()).when(imageFileRepository) // 빈 이미지 리스트 diff --git a/src/test/java/com/jeju/nanaland/domain/festival/service/FestivalServiceTest.java b/src/test/java/com/jeju/nanaland/domain/festival/service/FestivalServiceTest.java index e4da16ac..8c324c85 100644 --- a/src/test/java/com/jeju/nanaland/domain/festival/service/FestivalServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/festival/service/FestivalServiceTest.java @@ -16,6 +16,7 @@ import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.repository.ImageFileRepository; import com.jeju.nanaland.domain.common.service.ImageFileService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.festival.dto.FestivalCompositeDto; import com.jeju.nanaland.domain.festival.dto.FestivalResponse.FestivalDetailDto; @@ -50,6 +51,8 @@ class FestivalServiceTest { private MemberFavoriteService memberFavoriteService; @Mock private ImageFileService imageFileService; + @Mock + private PostViewCountService postViewCountService; @Mock @@ -123,9 +126,9 @@ void getFestivalDetail() { FestivalCompositeDto msFestivalCompositeDto = createFestivalCompositeDto(Language.MALAYSIA, startDate, endDate); - when(festivalRepository.findCompositeDtoById(1L, + when(festivalRepository.findCompositeDtoByIdWithPessimisticLock(1L, krMemberInfoDto.getLanguage())).thenReturn(krFestivalCompositeDto); - when(festivalRepository.findCompositeDtoById(1L, + when(festivalRepository.findCompositeDtoByIdWithPessimisticLock(1L, msMemberInfoDto.getLanguage())).thenReturn(msFestivalCompositeDto); when(memberFavoriteService.isPostInFavorite(any(), eq(FESTIVAL), anyLong())) .thenReturn(false); diff --git a/src/test/java/com/jeju/nanaland/domain/market/service/MarketServiceTest.java b/src/test/java/com/jeju/nanaland/domain/market/service/MarketServiceTest.java index 8293b125..5f0c6a64 100644 --- a/src/test/java/com/jeju/nanaland/domain/market/service/MarketServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/market/service/MarketServiceTest.java @@ -16,6 +16,7 @@ import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.repository.ImageFileRepository; import com.jeju.nanaland.domain.common.service.ImageFileService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.market.dto.MarketCompositeDto; import com.jeju.nanaland.domain.market.dto.MarketResponse.MarketDetailDto; @@ -60,6 +61,8 @@ class MarketServiceTest { ImageFileRepository imageFileRepository; @Mock private ImageFileService imageFileService; + @Mock + private PostViewCountService postViewCountService; @Test @DisplayName("전통시장 preview 정보 조회") @@ -144,7 +147,7 @@ void marketDetailTest() { ); doReturn(marketDetailDto).when(marketRepository) - .findCompositeDtoById(any(Long.class), eq(locale)); + .findCompositeDtoByIdWithPessimisticLock(any(Long.class), eq(locale)); doReturn(false).when(memberFavoriteService) .isPostInFavorite(any(Member.class), any(Category.class), any(Long.class)); doReturn(images).when(imageFileService) diff --git a/src/test/java/com/jeju/nanaland/domain/nana/service/NanaServiceTest.java b/src/test/java/com/jeju/nanaland/domain/nana/service/NanaServiceTest.java index 8d7294cb..cdb7382e 100644 --- a/src/test/java/com/jeju/nanaland/domain/nana/service/NanaServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/nana/service/NanaServiceTest.java @@ -13,6 +13,7 @@ import com.jeju.nanaland.domain.common.entity.ImageFile; import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.repository.ImageFileRepository; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.hashtag.repository.HashtagRepository; import com.jeju.nanaland.domain.member.dto.MemberResponse.MemberInfoDto; @@ -62,6 +63,8 @@ public class NanaServiceTest { private HashtagRepository hashtagRepository; @Mock private ImageFileRepository imageFileRepository; + @Mock + private PostViewCountService postViewCountService; @InjectMocks private NanaService nanaService; @@ -177,7 +180,7 @@ void getNanaDetail() { List> nanaContentImages = createNanaContentImage(); Category category = Category.NANA; - when(nanaRepository.findNanaById(anyLong())).thenReturn(Optional.of(nana)); + when(nanaRepository.findNanaByIdWithPessimisticLock(anyLong())).thenReturn(Optional.of(nana)); when(nanaTitleRepository.findNanaTitleByNanaAndLanguage(nana, language)).thenReturn( Optional.of(nanaTitle)); when(nanaContentRepository.findAllByNanaTitleOrderByPriority(nanaTitle)).thenReturn( diff --git a/src/test/java/com/jeju/nanaland/domain/nature/service/NatureServiceTest.java b/src/test/java/com/jeju/nanaland/domain/nature/service/NatureServiceTest.java index 6d7f7d49..15face0d 100644 --- a/src/test/java/com/jeju/nanaland/domain/nature/service/NatureServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/nature/service/NatureServiceTest.java @@ -18,6 +18,7 @@ import com.jeju.nanaland.domain.common.entity.ImageFile; import com.jeju.nanaland.domain.common.entity.Post; import com.jeju.nanaland.domain.common.service.ImageFileService; +import com.jeju.nanaland.domain.common.service.PostViewCountService; import com.jeju.nanaland.domain.favorite.service.MemberFavoriteService; import com.jeju.nanaland.domain.member.dto.MemberResponse.MemberInfoDto; import com.jeju.nanaland.domain.member.entity.Member; @@ -63,6 +64,8 @@ class NatureServiceTest { private SearchService searchService; @Mock private ImageFileService imageFileService; + @Mock + private PostViewCountService postViewCountService; @BeforeEach void setUp() { @@ -174,6 +177,7 @@ private NatureCompositeDto createNatureCompositeDto() { @Nested @DisplayName("7대 자연 프리뷰 페이징 조회 TEST") class GetNaturePreview { + @Test @DisplayName("성공 - 기본 케이스") void getNaturePreviewSuccess() { @@ -183,7 +187,8 @@ void getNaturePreviewSuccess() { Page naturePreviewDtos = createNaturePreviews(pageSize, totalSize); doReturn(naturePreviewDtos).when(natureRepository) - .findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(any(Language.class), anyList(), any(), any()); + .findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(any(Language.class), anyList(), + any(), any()); doReturn(new ArrayList<>()).when(memberFavoriteService) .getFavoritePostIdsWithMember(any(Member.class)); @@ -212,7 +217,8 @@ void getNaturePreviewSuccess_emptyList() { PageRequest.of(pageNumber, pageSize), 0); doReturn(naturePreviewDtos).when(natureRepository) - .findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(any(Language.class), anyList(), any(), any()); + .findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(any(Language.class), anyList(), + any(), any()); doReturn(new ArrayList<>()).when(memberFavoriteService) .getFavoritePostIdsWithMember(any(Member.class)); @@ -235,7 +241,8 @@ void getNaturePreviewSuccess_withFavorites() { List favoriteIds = List.of(1L); doReturn(naturePreviewDtos).when(natureRepository) - .findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(any(Language.class), anyList(), any(), any()); + .findAllNaturePreviewDtoOrderByPriorityAndCreatedAtDesc(any(Language.class), anyList(), + any(), any()); doReturn(favoriteIds).when(memberFavoriteService) .getFavoritePostIdsWithMember(any(Member.class)); @@ -254,11 +261,12 @@ void getNaturePreviewSuccess_withFavorites() { @Nested @DisplayName("7대 자연 상세 조회 TEST") class GetNatureDetail { + @Test @DisplayName("실패 - 해당 게시물이 존재하지 않는 경우") void getNatureDetailFail_postNotFound() { // given: 7대 자연 게시물이 존재하지 않도록 설정 - doReturn(null).when(natureRepository).findNatureCompositeDto(any(), any()); + doReturn(null).when(natureRepository).findNatureCompositeDtoWithPessimisticLock(any(), any()); // when: 7대 자연 상세 조회 // then: ErrorCode 검증 @@ -278,13 +286,15 @@ void getNatureDetailSuccess() { new ImageFileDto("origin url 2", "thumbnail url 2") ); - doReturn(natureCompositeDto).when(natureRepository).findNatureCompositeDto(any(), any()); + doReturn(natureCompositeDto).when(natureRepository) + .findNatureCompositeDtoWithPessimisticLock(any(), any()); doReturn(true).when(memberFavoriteService).isPostInFavorite(any(), any(), any()); doReturn(images).when(imageFileService) .getPostImageFilesByPostIdIncludeFirstImage(1L, natureCompositeDto.getFirstImage()); // when: 7대 자연 상세 조회 (검색이용) - NatureResponse.DetailDto natureDetail = natureService.getNatureDetail(memberInfoDto, 1L, true); + NatureResponse.DetailDto natureDetail = natureService.getNatureDetail(memberInfoDto, 1L, + true); // then: 7대 자연 상세 정보 검증 assertThat(natureDetail.getTitle()).isEqualTo(natureCompositeDto.getTitle()); From a6cdf7cd3851b5b696775ada0d4ff359872024ad Mon Sep 17 00:00:00 2001 From: teho Date: Thu, 7 Nov 2024 16:33:51 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[#496]=20feat:=20redis=20key=EA=B0=92=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nanaland/domain/common/service/PostViewCountService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java b/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java index f482c8c1..f25fb78b 100644 --- a/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java +++ b/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java @@ -15,7 +15,10 @@ public class PostViewCountService { @Transactional public void increaseViewCount(Long postId, Long memberId) { - String redisKey = "post_viewed_" + memberId + "_" + postId; + + // 8083은 dev, 도메인은 prod로 프로필을 설정하는 것이 어떨까요? + String env = System.getProperty("spring.profiles.active"); + String redisKey = env + "_post_viewed_" + memberId + "_" + postId; // 30분 -> 1800초 long cacheDurationSeconds = 1800L; From 13de9ce2ff61bff39f686531c69a73b8b281de23 Mon Sep 17 00:00:00 2001 From: teho Date: Fri, 8 Nov 2024 23:25:04 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[#496]=20feat:=20ec2=20redis=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B6=84=EB=A6=AC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=ED=82=A4=20=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nanaland/domain/common/service/PostViewCountService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java b/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java index f25fb78b..49591295 100644 --- a/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java +++ b/src/main/java/com/jeju/nanaland/domain/common/service/PostViewCountService.java @@ -16,9 +16,7 @@ public class PostViewCountService { @Transactional public void increaseViewCount(Long postId, Long memberId) { - // 8083은 dev, 도메인은 prod로 프로필을 설정하는 것이 어떨까요? - String env = System.getProperty("spring.profiles.active"); - String redisKey = env + "_post_viewed_" + memberId + "_" + postId; + String redisKey = "post_viewed_" + memberId + "_" + postId; // 30분 -> 1800초 long cacheDurationSeconds = 1800L;