diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java index 799e57fb..e8321344 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertAdminController.java @@ -1,10 +1,7 @@ package com.back.web7_9_codecrete_be.domain.concerts.controller; import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.result.SetResultResponse; -import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertDetailResponse; -import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem; -import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertTicketTimeSetRequest; -import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertUpdateRequest; +import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.*; import com.back.web7_9_codecrete_be.domain.concerts.service.ConcertNotifyService; import com.back.web7_9_codecrete_be.domain.concerts.service.ConcertService; import com.back.web7_9_codecrete_be.domain.concerts.service.KopisApiService; @@ -125,6 +122,13 @@ public RsData updateConcert() throws InterruptedException { return RsData.success(kopisApiService.updateConcertData()); } + @Operation(summary = "공연 조회수 갱신", description = "캐시에 저장되어 있는 조회수를 DB에 반영하고 캐싱된 공연 목록을 초기화합니다.") + @PostMapping("updateConcertViewCount") + public RsData updateConcertViewCount(){ + concertService.viewCountUpdate(); + return RsData.success(null); + } + @Operation(summary = "알림 이메일 전송", description = "예매일이 오늘인 공연에 대해 알림 이메일을 전송합니다.") @PostMapping("sendTicketingEmail") public RsData sendTicketingEmail(){ diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java index 384d96d2..c73da3c0 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/controller/ConcertController.java @@ -3,10 +3,7 @@ import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concert.ConcertListResponse; import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concertPlace.ConcertPlaceDetailResponse; import com.back.web7_9_codecrete_be.domain.concerts.dto.KopisApiDto.concertPlace.ConcertPlaceListResponse; -import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertDetailResponse; -import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem; -import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertLikeResponse; -import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ListSort; +import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.*; import com.back.web7_9_codecrete_be.domain.concerts.dto.concertPlace.PlaceDetailResponse; import com.back.web7_9_codecrete_be.domain.concerts.dto.ticketOffice.TicketOfficeElement; import com.back.web7_9_codecrete_be.domain.concerts.entity.TicketOffice; @@ -80,7 +77,7 @@ public RsData> getLikedConcertList( @Operation(summary = "공연 상세 조회", description = "공연에 대한 상세 목록을 조회합니다.") @GetMapping("concertDetail") - public ConcertDetailResponse getConcertDetail( + public RsData getConcertDetail( @RequestParam @Schema(description = """

조회 기준이 되는 concertId입니다.

@@ -90,7 +87,7 @@ public ConcertDetailResponse getConcertDetail( """) long concertId ) { - return concertService.getConcertDetail(concertId); + return RsData.success(concertService.getConcertDetail(concertId)); } @Operation(summary = "공연 예매처 조회", description = "공연에 대한 예매처들을 조회합니다.") diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertDetailResponse.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertDetailResponse.java index 3c9f535c..b4f53888 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertDetailResponse.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertDetailResponse.java @@ -1,5 +1,11 @@ package com.back.web7_9_codecrete_be.domain.concerts.dto.concert; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Getter; @@ -29,12 +35,18 @@ public class ConcertDetailResponse { @Schema(description = "콘서트 장 주소입니다.") private String placeAddress; + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) @Schema(description = "콘서트 예매 시작 날짜입니다.") private LocalDateTime ticketTime; + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) @Schema(description = "콘서트 시작 날짜입니다.",format = "yyyy-MM-dd") private LocalDate startDate; + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) @Schema(description = "콘서트 종료 날짜입니다.",format = "yyyy-MM-dd") private LocalDate endDate; @@ -55,4 +67,6 @@ public class ConcertDetailResponse { @Schema(description = "콘서트 이미지 목록입니다.") private List concertImageUrls; + + } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertItem.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertItem.java index 79856417..5132c2a1 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertItem.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/dto/concert/ConcertItem.java @@ -1,8 +1,15 @@ package com.back.web7_9_codecrete_be.domain.concerts.dto.concert; import com.back.web7_9_codecrete_be.domain.concerts.entity.Concert; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.Setter; import java.time.LocalDate; @@ -10,6 +17,7 @@ @Getter @Setter +@RequiredArgsConstructor public class ConcertItem { @Schema(description = "콘서트 Id입니다.") @@ -21,12 +29,18 @@ public class ConcertItem { @Schema(description = "콘서트 장소 이름입니다.") private String placeName; + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) @Schema(description = "콘서트 예매 시작 날짜입니다.",format = "yyyy-MM-ddThh:mm:ss") private LocalDateTime ticketTime; + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) @Schema(description = "콘서트 시작 날짜입니다.",format = "yyyy-MM-dd") private LocalDate startDate ; + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) @Schema(description = "콘서트 종료 날짜입니다.",format = "yyyy-MM-dd") private LocalDate endDate ; @@ -59,7 +73,7 @@ public ConcertItem(Concert concert) { this.likeCount = concert.getLikeCount(); } - public ConcertItem(long id, String name, String placeName,LocalDateTime ticketTime, LocalDate startDate, LocalDate endDate, String posterUrl, int maxPrice, int minPrice, int viewCount, int likeCount) { + public ConcertItem(long id, String name, String placeName, LocalDateTime ticketTime, LocalDate startDate, LocalDate endDate, String posterUrl, int maxPrice, int minPrice, int viewCount, int likeCount) { this.id = id; this.name = name; this.placeName = placeName; diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java index e5456d9d..9c78dc6d 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRedisRepository.java @@ -1,18 +1,36 @@ package com.back.web7_9_codecrete_be.domain.concerts.repository; +import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertDetailResponse; +import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem; +import com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ListSort; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.*; +import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Repository; +import java.util.*; import java.util.concurrent.TimeUnit; +@Slf4j @Repository @RequiredArgsConstructor public class ConcertRedisRepository { - private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; + private final RedisTemplate objectRedisTemplate; private static final String LOCK_FLAG_PREFIX = "initLoad: "; + private static final String CONCERT_DETAIL_PREFIX = "concertDetail: "; + + private static final String CONCERT_LIST_PREFIX = "concertList: "; + + private static final String VIEW_COUNT_MAP = "viewCountMap"; + + private static final int HOUR = 3600; + + // 최초 공연 로드 락 public void lockSave(String key, String value) { redisTemplate.opsForValue().set( LOCK_FLAG_PREFIX + key, @@ -29,4 +47,118 @@ public void unlockSave(String key) { redisTemplate.delete(LOCK_FLAG_PREFIX + key); } + // 공연 목록 캐싱 + public void listSave(ListSort sort, Pageable pageable, List list) { + String key = CONCERT_LIST_PREFIX + sort.name() + pageable.getPageNumber(); + objectRedisTemplate.opsForValue().set(key, list, HOUR, TimeUnit.SECONDS); + } + + // 공연 목록 가져오기 + public List getConcertsList(Pageable pageable, ListSort sort) { + String key = CONCERT_LIST_PREFIX + sort.name() + pageable.getPageNumber(); + Object object = objectRedisTemplate.opsForValue().get(key); + List list = (List) object; + if (list == null || list.isEmpty()) return List.of(); // null 이 아닌 empty 값 반환 + return list; + } + + // 캐싱된 모든 공연 목록 삭제 + public void deleteAllConcertsList() { + deleteAllItemsByPREFIX(CONCERT_LIST_PREFIX); + } + + // 공연 상세 캐싱 + public void detailSave(long concertId, ConcertDetailResponse concertDetailResponse) { + objectRedisTemplate.opsForValue().set( + CONCERT_DETAIL_PREFIX + concertId, + concertDetailResponse, + HOUR, + TimeUnit.SECONDS + ); + } + + // todo : 객체 일부의 값만 바뀌는거니 해당 값만 바꿔서 저장하거나 Redis 내부의 값만 갱신할 수 있는 방법 찾기 + public ConcertDetailResponse getDetail(long concertId) { + ConcertDetailResponse concertDetailResponse = (ConcertDetailResponse) objectRedisTemplate.opsForValue().get(CONCERT_DETAIL_PREFIX + concertId); + if (concertDetailResponse == null) return null; + int viewCount = concertDetailResponse.getViewCount(); + viewCountSet(concertId, viewCount + 1); + concertDetailResponse.setViewCount(viewCount + 1); + detailSave(concertId, concertDetailResponse); + return concertDetailResponse; + } + + // 공연 상세 삭제 + public void deleteDetail(String concertId) { + redisTemplate.delete(CONCERT_DETAIL_PREFIX + concertId); + } + + // 모든 공연 상세 삭제 + public void deleteAllConcertDetail() { + deleteAllItemsByPREFIX(CONCERT_DETAIL_PREFIX); + } + + // 조회수 처리 -> 좀 지저분한데 개선 여지 찾아보기 + public int viewCountSet(long concertId, int viewCount) { + Map rawMap = (Map) objectRedisTemplate.opsForValue().get(VIEW_COUNT_MAP); + + if (rawMap == null) { + Map viewCountMap = new HashMap<>(); + viewCountMap.put(concertId, viewCount); + objectRedisTemplate.opsForValue().set(VIEW_COUNT_MAP, viewCountMap); + } else { + Map viewCountMap = convertViewCountMap(rawMap); + viewCountMap.put(concertId, viewCount); + objectRedisTemplate.opsForValue().set(VIEW_COUNT_MAP, viewCountMap); + log.info(viewCountMap.size() + "view count size."); + } + return viewCount; + } + + // 조회수 맵 조회 + public Map getViewCountMap() { + Map rawMap = (Map) objectRedisTemplate.opsForValue().get(VIEW_COUNT_MAP); + if (rawMap == null) return null; + objectRedisTemplate.delete(VIEW_COUNT_MAP); + return convertViewCountMap(rawMap); + } + + // 조회수 맵 삭제 + public void deleteViewCountMap() { + objectRedisTemplate.delete(VIEW_COUNT_MAP); + } + + // String Integer 타입 맵을 Long Integer로 변환 + private Map convertViewCountMap(Map rawMap) { + Map viewCountMap = new HashMap<>(); + for (Map.Entry stringIntegerEntry : rawMap.entrySet()) { + viewCountMap.put(Long.parseLong(stringIntegerEntry.getKey()), stringIntegerEntry.getValue()); + } + return viewCountMap; + } + + // 해당 접두어의 모든 항목 삭제 + private void deleteAllItemsByPREFIX(String prefix) { + String pattern = CONCERT_LIST_PREFIX + "*"; + ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build(); + Set keys = redisTemplate.execute((RedisCallback>) connection -> { + Set keySet = new HashSet<>(); + try (Cursor cursor = connection.scan(options)) { + while (cursor.hasNext()) { + keySet.add(new String(cursor.next())); + } + } + return keySet; + } + ); + + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + log.info("Successfully deleted %s items with prefix: %s".formatted(keys.size() + "", prefix)); + } else { + log.info("no items with prefix: %s".formatted(prefix)); + } + } + + } diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRepository.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRepository.java index f12dbd46..3b1386ed 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRepository.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertRepository.java @@ -20,110 +20,60 @@ public interface ConcertRepository extends JpaRepository { @Query(""" SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) - FROM + c + FROM Concert c + JOIN FETCH + c.concertPlace cp """) List getConcertItems(Pageable pageable); - @Query(""" + @Query(value = """ SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) - FROM + c + FROM Concert c - ORDER BY + JOIN FETCH + c.concertPlace cp + ORDER BY c.apiConcertId -""") +""") // Kopis API의 ID 순서대로 List getConcertItemsOrderByApiId(Pageable pageable); @Query(""" SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) - FROM + c + FROM Concert c - ORDER BY + JOIN FETCH + c.concertPlace cp + ORDER BY c.viewCount - DESC -""") + DESC +""") // 조회수 기준 내림차순 List getConcertItemsOrderByViewCountDesc(Pageable pageable); @Query(""" SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) - FROM + c + FROM Concert c - ORDER BY + JOIN FETCH + c.concertPlace cp + ORDER BY c.likeCount - desc -""") + desc +""") // 좋아요 기준 내림차순 List getConcertItemsOrderByLikeCountDesc(Pageable pageable); @Query(""" SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) + c FROM Concert c + JOIN FETCH + c.concertPlace cp WHERE c.startDate >= :fromDate ORDER BY @@ -137,21 +87,11 @@ List getUpComingConcertItemsFromDateASC( @Query(""" SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) + c FROM Concert c + JOIN FETCH + c.concertPlace cp WHERE c.ticketTime >= :fromDate AND @@ -167,21 +107,11 @@ List getUpComingTicketingConcertItemsFromDateASC( @Query(""" SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) + c FROM Concert c + JOIN FETCH + c.concertPlace cp WHERE c.ticketTime IS NULL ORDER BY @@ -193,26 +123,16 @@ List getNoTicketTimeConcertList( ); @Query(""" - SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) - FROM + SELECT + c + FROM Concert c, ConcertLike cl - WHERE + JOIN FETCH + c.concertPlace cp + WHERE c.concertId = cl.concert.concertId - AND + AND cl.user.id = :userId ORDER BY cl.createDate @@ -223,21 +143,11 @@ List getLikedConcertsList(Pageable pageable, @Param("userId") Long userId); @Query(""" SELECT - new com.back.web7_9_codecrete_be.domain.concerts.dto.concert.ConcertItem( - c.concertId as id, - c.name as name, - c.concertPlace.placeName as placeName, - c.ticketTime as ticketTime, - c.startDate as startDate, - c.endDate as endDate, - c.posterUrl as posterUrl, - c.maxPrice as maxPrice, - c.minPrice as minPrice, - c.viewCount as viewCount, - c.likeCount as likeCount - ) + c FROM Concert c + JOIN FETCH + c.concertPlace cp WHERE c.name LIKE %:keyword% ORDER BY @@ -283,6 +193,15 @@ List getConcertItemsByKeyword( """) Integer concertViewCountUp(@Param("concertId") long concertId); + @Modifying + @Query(""" + UPDATE + Concert c + SET c.viewCount = :viewCount + where c.concertId = :concertId +""") + Integer concertViewCountSet(@Param("concertId") long concertId, @Param("viewCount") int viewCount); + @Modifying @Query(""" UPDATE diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java index bbeb0a7e..e007079d 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertService.java @@ -9,7 +9,10 @@ import com.back.web7_9_codecrete_be.global.error.code.ConcertErrorCode; import com.back.web7_9_codecrete_be.global.error.exception.BusinessException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,9 +21,12 @@ import java.time.LocalTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; +@Slf4j @Service @RequiredArgsConstructor +@EnableScheduling public class ConcertService { private final ConcertRepository concertRepository; @@ -32,28 +38,25 @@ public class ConcertService { private final ConcertImageRepository concertImageRepository; + private final ConcertRedisRepository concertRedisRepository; // 공연 목록 조회 public List getConcertsList(Pageable pageable, ListSort sort) { + List concertItems; + concertItems = concertRedisRepository.getConcertsList(pageable, sort); + + if(concertItems != null && !concertItems.isEmpty()) return concertItems; + switch (sort) { - case LIKE -> { - return concertRepository.getConcertItemsOrderByLikeCountDesc(pageable); - } - case VIEW -> { - return concertRepository.getConcertItemsOrderByViewCountDesc(pageable); - } - case TICKETING -> { - return concertRepository.getUpComingTicketingConcertItemsFromDateASC(pageable, LocalDateTime.of(LocalDate.now(), LocalTime.MIN)); - } - case UPCOMING -> { - return concertRepository.getUpComingConcertItemsFromDateASC(pageable,LocalDate.now()); - } - case REGISTERED -> { - return concertRepository.getConcertItemsOrderByApiId(pageable); - } + case LIKE -> concertItems = concertRepository.getConcertItemsOrderByLikeCountDesc(pageable); + case VIEW -> concertItems = concertRepository.getConcertItemsOrderByViewCountDesc(pageable); + case TICKETING -> concertItems = concertRepository.getUpComingTicketingConcertItemsFromDateASC(pageable, LocalDateTime.of(LocalDate.now(), LocalTime.MIN)); + case UPCOMING -> concertItems = concertRepository.getUpComingConcertItemsFromDateASC(pageable,LocalDate.now()); + case REGISTERED -> concertItems = concertRepository.getConcertItemsOrderByApiId(pageable); } - return concertRepository.getConcertItems(pageable); + concertRedisRepository.listSave(sort,pageable,concertItems); + return concertItems; } // 사용자가 좋아요 한 공연 목록 조회 @@ -74,21 +77,45 @@ public List getConcertListByKeyword(String keyword, Pageable pageab return concertRepository.getConcertItemsByKeyword(keyword, pageable); } - // 공연 상세 조회 조회시 조회수 1 증가 + // 공연 상세 조회 조회시 조회수 1 증가 -> 캐싱에 따른 조회수 불일치 해소를 어떻게 할 것인가? V -> 이제 캐싱된거 날리고 새로운 수치 반영 어케할 것인지 + 여러번 조회수 올릴 시 처리 어떻게 할지 @Transactional public ConcertDetailResponse getConcertDetail(long concertId) { - ConcertDetailResponse concertDetailResponse = concertRepository.getConcertDetailById(concertId); - List concertImages = concertImageRepository.getConcertImagesByConcert_ConcertId(concertId); - List concertImageUrls = new ArrayList<>(); - for(ConcertImage concertImage : concertImages){ - concertImageUrls.add(concertImage.getImageUrl()); + ConcertDetailResponse concertDetailResponse = concertRedisRepository.getDetail(concertId); + if(concertDetailResponse == null){ + concertDetailResponse = concertRepository.getConcertDetailById(concertId); + List concertImages = concertImageRepository.getConcertImagesByConcert_ConcertId(concertId); + List concertImageUrls = new ArrayList<>(); + for(ConcertImage concertImage : concertImages){ + concertImageUrls.add(concertImage.getImageUrl()); + } + + concertRepository.concertViewCountUp(concertId); + concertDetailResponse.setConcertImageUrls(concertImageUrls); + concertDetailResponse.setViewCount(concertDetailResponse.getViewCount() + 1); + concertRedisRepository.detailSave(concertId, concertDetailResponse); } - concertRepository.concertViewCountUp(concertId); - concertDetailResponse.setConcertImageUrls(concertImageUrls); - concertDetailResponse.setViewCount(concertDetailResponse.getViewCount() + 1); return concertDetailResponse; } + // 조회수 갱신 + @Transactional + @Scheduled(cron = "0 0 5 * * * ") + public void viewCountUpdate(){ + Map viewCountMap = concertRedisRepository.getViewCountMap(); + if(viewCountMap == null || viewCountMap.isEmpty()) { + log.info("viewCountMap is empty"); + } else{ + for (Map.Entry viewCountEntry : viewCountMap.entrySet()) { + concertRepository.concertViewCountSet(viewCountEntry.getKey(), viewCountEntry.getValue()); + } + concertRedisRepository.deleteViewCountMap(); + concertRedisRepository.deleteAllConcertsList(); + log.info("viewCount updated"); + } + } + + + // N+1 문제 발생해서 버림 /* public List getConcertsList2(Pageable pageable) { diff --git a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java index 52bbe76b..8ef25053 100644 --- a/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java +++ b/src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/KopisApiService.java @@ -188,6 +188,13 @@ public void setConcertsList() throws InterruptedException { @Transactional @Scheduled(cron = "0 0 2 * * Mon") public SetResultResponse updateConcertData() throws InterruptedException { + String key = "init"; + String value = concertRedisRepository.lockGet(key); + if(value != null) { + log.error("초기 업데이트가 진행중입니다."); + return null; + } + ConcertUpdateTime concertUpdateTime = concertUpdateTimeRepository.getReferenceById(1L); LocalDate lastUpdatedDate = concertUpdateTime.getUpdateTime().toLocalDate(); ConcertUpdateTime updatedTime = concertUpdateTime.setUpdateTime(LocalDateTime.now()); @@ -283,9 +290,13 @@ public SetResultResponse updateConcertData() throws InterruptedException { updatedConcertImages += saveConcertImages(concertDetail, savedConcert); } - Thread.sleep(300); } + + // 갱신 후 업데이트 시간 저장, 캐시의 데이터 삭제 + concertUpdateTimeRepository.save(updatedTime); + concertRedisRepository.deleteAllConcertsList(); + concertRedisRepository.deleteAllConcertDetail(); return new SetResultResponse(addedConcerts,updatedConcerts,addedConcertPlaces,updatedConcertPlaces,addedConcertImages,updatedConcertImages,addedTicketOffices,updatedTicketOffices); }