diff --git a/src/main/java/com/palettee/archive/controller/dto/response/ArchiveRedisList.java b/src/main/java/com/palettee/archive/controller/dto/response/ArchiveRedisList.java new file mode 100644 index 00000000..6c983cf1 --- /dev/null +++ b/src/main/java/com/palettee/archive/controller/dto/response/ArchiveRedisList.java @@ -0,0 +1,8 @@ +package com.palettee.archive.controller.dto.response; + +import java.util.List; + +public record ArchiveRedisList( + List archives +) { +} diff --git a/src/main/java/com/palettee/archive/controller/dto/response/ArchiveRedisResponse.java b/src/main/java/com/palettee/archive/controller/dto/response/ArchiveRedisResponse.java new file mode 100644 index 00000000..6b3d9f30 --- /dev/null +++ b/src/main/java/com/palettee/archive/controller/dto/response/ArchiveRedisResponse.java @@ -0,0 +1,38 @@ +package com.palettee.archive.controller.dto.response; + +import com.palettee.archive.domain.Archive; +import java.io.Serializable; + +public record ArchiveRedisResponse( + Long archiveId, + String title, + String description, + String introduction, + String username, + String type, + boolean canComment, + long likeCount, + boolean isLiked, + String imageUrl, + String createDate +) implements Serializable { + + public static ArchiveRedisResponse toResponse(Archive archive) { + String imageUrl = archive.getArchiveImages().isEmpty() ? "" : archive.getArchiveImages().get(0).getImageUrl(); + + return new ArchiveRedisResponse( + archive.getId(), + archive.getTitle(), + archive.getDescription(), + archive.getIntroduction(), + archive.getUser().getName(), + archive.getType().name(), + archive.isCanComment(), + archive.getLikeCount(), + false, + imageUrl, + archive.getCreateAt().toLocalDate().toString() + ); + } + +} diff --git a/src/main/java/com/palettee/archive/controller/dto/response/ArchiveSimpleResponse.java b/src/main/java/com/palettee/archive/controller/dto/response/ArchiveSimpleResponse.java index 47ad101d..6a9dbee0 100644 --- a/src/main/java/com/palettee/archive/controller/dto/response/ArchiveSimpleResponse.java +++ b/src/main/java/com/palettee/archive/controller/dto/response/ArchiveSimpleResponse.java @@ -2,6 +2,7 @@ import com.palettee.archive.domain.Archive; import com.palettee.likes.repository.LikeRepository; +import java.io.Serializable; public record ArchiveSimpleResponse( Long archiveId, @@ -15,10 +16,9 @@ public record ArchiveSimpleResponse( boolean isLiked, String imageUrl, String createDate -) { +) implements Serializable { public static ArchiveSimpleResponse toResponse(Archive archive, Long userId, LikeRepository likeRepository) { - String imageUrl = archive.getArchiveImages().isEmpty() ? "" : archive.getArchiveImages().get(0).getImageUrl(); return new ArchiveSimpleResponse( @@ -29,11 +29,26 @@ public static ArchiveSimpleResponse toResponse(Archive archive, Long userId, Lik archive.getUser().getName(), archive.getType().name(), archive.isCanComment(), - likeRepository.countArchiveLike(archive.getId()), + archive.getLikeCount(), likeRepository.existByUserAndArchive(archive.getId(), userId).isPresent(), imageUrl, archive.getCreateAt().toLocalDate().toString() ); } + public static ArchiveSimpleResponse changeToSimpleResponse(ArchiveRedisResponse it, Long userId, LikeRepository likeRepository) { + return new ArchiveSimpleResponse( + it.archiveId(), + it.title(), + it.description(), + it.introduction(), + it.username(), + it.type(), + it.canComment(), + it.likeCount(), + likeRepository.existByUserAndArchive(it.archiveId(), userId).isPresent(), + it.imageUrl(), + it.createDate() + ); + } } diff --git a/src/main/java/com/palettee/archive/domain/Archive.java b/src/main/java/com/palettee/archive/domain/Archive.java index cd1dafc2..8fd0aac8 100644 --- a/src/main/java/com/palettee/archive/domain/Archive.java +++ b/src/main/java/com/palettee/archive/domain/Archive.java @@ -31,6 +31,7 @@ public class Archive extends BaseEntity { private int hits; private int archiveOrder; + private int likeCount; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") @@ -55,6 +56,7 @@ public Archive(String title, String description, String introduction, this.canComment = canComment; this.hits = 0; this.archiveOrder = 0; + this.likeCount = 0; this.user = user; this.user.addArchive(this); @@ -81,8 +83,8 @@ public Archive update(ArchiveUpdateRequest req) { return this; } - public void hit() { - this.hits++; + public void like() { + this.likeCount++; } public void setOrder() { @@ -96,4 +98,8 @@ public void updateOrder(Integer order) { public boolean isNotOpenComment() { return !this.canComment; } + + public void setHit(long totalHits) { + this.hits = (int) totalHits; + } } diff --git a/src/main/java/com/palettee/archive/event/HitEvent.java b/src/main/java/com/palettee/archive/event/HitEvent.java new file mode 100644 index 00000000..e372ffbb --- /dev/null +++ b/src/main/java/com/palettee/archive/event/HitEvent.java @@ -0,0 +1,7 @@ +package com.palettee.archive.event; + +public record HitEvent( + Long archiveId, + String email +) { +} diff --git a/src/main/java/com/palettee/archive/event/HitEventListener.java b/src/main/java/com/palettee/archive/event/HitEventListener.java new file mode 100644 index 00000000..39c34e56 --- /dev/null +++ b/src/main/java/com/palettee/archive/event/HitEventListener.java @@ -0,0 +1,36 @@ +package com.palettee.archive.event; + +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class HitEventListener { + + private static final String SET_KEY_PREFIX = "hit:archiveId:"; + private static final String VALUE_KEY_PREFIX = "incr:hit:archiveId:"; + + private final RedisTemplate redisTemplate; + + @Async + @EventListener(value = HitEvent.class) + public void onHit(HitEvent event) { + + String setKey = SET_KEY_PREFIX + event.archiveId(); + Long addResult = redisTemplate.opsForSet().add(setKey, event.email()); + + if (addResult == null || addResult != 1L) { + return; + } + + redisTemplate.expire(setKey, Duration.ofDays(1)); + + String valueKey = VALUE_KEY_PREFIX + event.archiveId(); + redisTemplate.opsForValue().increment(valueKey); + } + +} diff --git a/src/main/java/com/palettee/archive/event/LikeEvent.java b/src/main/java/com/palettee/archive/event/LikeEvent.java new file mode 100644 index 00000000..195fa986 --- /dev/null +++ b/src/main/java/com/palettee/archive/event/LikeEvent.java @@ -0,0 +1,7 @@ +package com.palettee.archive.event; + +public record LikeEvent( + Long archiveId, + Long userId +) { +} diff --git a/src/main/java/com/palettee/archive/event/LikeEventListener.java b/src/main/java/com/palettee/archive/event/LikeEventListener.java new file mode 100644 index 00000000..e304ad77 --- /dev/null +++ b/src/main/java/com/palettee/archive/event/LikeEventListener.java @@ -0,0 +1,23 @@ +package com.palettee.archive.event; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class LikeEventListener { + + private static final String LIKE_KEY = "like:archiveId:"; + + private final RedisTemplate redisTemplate; + + @Async + @EventListener(value = LikeEvent.class) + public void onLike(LikeEvent event) { + redisTemplate.opsForValue().increment(LIKE_KEY + event.archiveId()); + } + +} diff --git a/src/main/java/com/palettee/archive/repository/ArchiveRedisRepository.java b/src/main/java/com/palettee/archive/repository/ArchiveRedisRepository.java new file mode 100644 index 00000000..e9a965aa --- /dev/null +++ b/src/main/java/com/palettee/archive/repository/ArchiveRedisRepository.java @@ -0,0 +1,142 @@ +package com.palettee.archive.repository; + +import com.palettee.archive.controller.dto.response.ArchiveRedisList; +import com.palettee.archive.controller.dto.response.ArchiveRedisResponse; +import com.palettee.archive.domain.Archive; +import io.jsonwebtoken.io.SerializationException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@Slf4j +@RequiredArgsConstructor +public class ArchiveRedisRepository { + + private static final String INCR_PATTERN = "incr:hit:archiveId:*"; + private static final String STD_PATTERN = "std:hit:archiveId:"; + private static final String TOP_ARCHIVE = "top_archives"; + private static final String DELIMITER = ":"; + + private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplateForArchive; + + private final ArchiveRepository archiveRepository; + + @Transactional + public void settleHits() { + Set incrKeys = redisTemplate.keys(INCR_PATTERN); + if (incrKeys == null || incrKeys.isEmpty()) { + return; + } + + for (String incrKey : incrKeys) { + String archiveId = extractId(incrKey); + + String incrHits = redisTemplate.opsForValue().get(incrKey); + long incrCount = incrHits == null ? 0 : Long.parseLong(incrHits); + + String stdKey = STD_PATTERN + archiveId; + String stdHits = redisTemplate.opsForValue().get(stdKey); + long stdCount = stdHits == null ? 0 : Long.parseLong(stdHits); + + long totalHits = stdCount + incrCount; + + updateHitCount(Long.parseLong(archiveId), totalHits); + + redisTemplate.opsForValue().set(stdKey, String.valueOf(totalHits)); + redisTemplate.delete(incrKey); + } + } + + private void updateHitCount(Long archiveId, long totalHits) { + Archive archive = archiveRepository.findById(archiveId).orElseThrow(); + archive.setHit(totalHits); + } + + private String extractId(String incrKey) { + return incrKey.split(DELIMITER)[3]; + } + + @Transactional(readOnly = true) + public void updateArchiveList() { + List top4IncrKeys = getTop4IncrKeys(); + List result = archiveRepository.findArchivesInIds(top4IncrKeys); + + int redisSize = result.size(); + if (redisSize < 4) { + int remaining = 4 - redisSize; + List additionalFromDB = archiveRepository.findTopArchives(remaining); + result.addAll(additionalFromDB); + } + + List redis = result.stream() + .map(ArchiveRedisResponse::toResponse) + .toList(); + log.info("Redis에 저장될 데이터: {}", redis); + redisTemplateForArchive.opsForValue().set(TOP_ARCHIVE, new ArchiveRedisList(redis), 1, TimeUnit.HOURS); + } + + private List getTop4IncrKeys() { + String hitPattern = "incr:hit:archiveId:*"; + String likePattern = "like:archiveId:*"; + + // Redis에서 키 가져오기 + Set hitKeys = redisTemplate.keys(hitPattern); + Set likeKeys = redisTemplate.keys(likePattern); + + if (hitKeys == null || likeKeys == null) { + return Collections.emptyList(); + } + + // 점수 계산 + Map archiveScores = new HashMap<>(); + for (String hitKey : hitKeys) { + Long archiveId = extractArchiveId(hitKey); + Integer hitCount = getValueAsInt(hitKey); + + String likeKey = "like:archiveId:" + archiveId; + Integer likeCount = getValueAsInt(likeKey); + + int score = hitCount + (likeCount * 5); + archiveScores.put(archiveId, score); + } + + // 정렬 및 상위 4개 추출 + return archiveScores.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(4) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + + private Long extractArchiveId(String key) { + return Long.valueOf(key.replace("incr:hit:archiveId:", "")); + } + + private Integer getValueAsInt(String key) { + String value = redisTemplate.opsForValue().get(key); + return value != null ? Integer.parseInt(value) : 0; + } + + public ArchiveRedisList getTopArchives() { + try { + ArchiveRedisList result = (ArchiveRedisList) redisTemplateForArchive.opsForValue().get(TOP_ARCHIVE); + return result == null ? new ArchiveRedisList(new ArrayList<>()) : result; + } catch (SerializationException e) { + log.error("Redis 역직렬화 실패: {}", e.getMessage()); + redisTemplateForArchive.delete(TOP_ARCHIVE); + return new ArchiveRedisList(new ArrayList<>()); + } + } +} diff --git a/src/main/java/com/palettee/archive/repository/ArchiveRepository.java b/src/main/java/com/palettee/archive/repository/ArchiveRepository.java index 95fad5a3..ac2f07fe 100644 --- a/src/main/java/com/palettee/archive/repository/ArchiveRepository.java +++ b/src/main/java/com/palettee/archive/repository/ArchiveRepository.java @@ -15,9 +15,6 @@ public interface ArchiveRepository extends JpaRepository, Archive @Query("select a from Archive a where a.id in :ids order by a.archiveOrder desc") Slice findAllInIds(@Param("ids") List ids, Pageable pageable); - @Query("select a from Archive a order by a.hits desc, a.id desc limit 5") - List getMainArchives(); - @Query("SELECT a.type AS type, COUNT(a) AS count FROM Archive a GROUP BY a.type") List countByArchiveType(); @@ -29,4 +26,14 @@ public interface ArchiveRepository extends JpaRepository, Archive @Query("SELECT a FROM Archive a where a.user.id = :userId") List findAllByUserId(Long userId); + + @Modifying + @Query("UPDATE Archive a SET a.hits = :hitCount WHERE a.id = :archiveId") + void updateHitCount(@Param("archiveId") Long archiveId, @Param("hitCount") Long hitCount); + + @Query("select a from Archive a order by (a.hits + a.likeCount * 5) desc, a.id desc limit :limit") + List findTopArchives(@Param("limit") int limit); + + @Query("select a from Archive a where a.id in :ids") + List findArchivesInIds(@Param("ids") List ids); } diff --git a/src/main/java/com/palettee/archive/service/ArchiveScheduler.java b/src/main/java/com/palettee/archive/service/ArchiveScheduler.java new file mode 100644 index 00000000..73caa345 --- /dev/null +++ b/src/main/java/com/palettee/archive/service/ArchiveScheduler.java @@ -0,0 +1,20 @@ +package com.palettee.archive.service; + +import com.palettee.archive.repository.ArchiveRedisRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ArchiveScheduler { + + private final ArchiveRedisRepository archiveRedisRepository; + + @Scheduled(cron = "0 * * * * *") + public void updateMainArchive() { + archiveRedisRepository.updateArchiveList(); + archiveRedisRepository.settleHits(); + } + +} diff --git a/src/main/java/com/palettee/archive/service/ArchiveService.java b/src/main/java/com/palettee/archive/service/ArchiveService.java index de230c40..703c88dd 100644 --- a/src/main/java/com/palettee/archive/service/ArchiveService.java +++ b/src/main/java/com/palettee/archive/service/ArchiveService.java @@ -3,6 +3,8 @@ import com.palettee.archive.controller.dto.request.*; import com.palettee.archive.controller.dto.response.*; import com.palettee.archive.domain.*; +import com.palettee.archive.event.HitEvent; +import com.palettee.archive.event.LikeEvent; import com.palettee.archive.exception.*; import com.palettee.archive.repository.*; import com.palettee.likes.domain.LikeType; @@ -15,6 +17,7 @@ import com.palettee.user.repository.UserRepository; import java.util.*; import lombok.*; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.*; import org.springframework.stereotype.*; import org.springframework.transaction.annotation.*; @@ -31,6 +34,8 @@ public class ArchiveService { private final LikeRepository likeRepository; private final UserRepository userRepository; private final NotificationService notificationService; + private final ArchiveRedisRepository archiveRedisRepository; + private final ApplicationEventPublisher publisher; @Transactional public ArchiveResponse registerArchive(ArchiveRegisterRequest archiveRegisterRequest, User user) { @@ -71,10 +76,11 @@ private Long getOptionalUserId(User optionalUser) { return optionalUser == null ? 0L : optionalUser.getId(); } - public ArchiveListResponse getMainArchive(User optionalUser) { - Long myId = getOptionalUserId(optionalUser); - List result = archiveRepository.getMainArchives().stream() - .map(it -> ArchiveSimpleResponse.toResponse(it, myId, likeRepository)) + public ArchiveListResponse getMainArchive(User contextUser) { + Long userId = contextUser == null ? 0L : contextUser.getId(); + List result = archiveRedisRepository.getTopArchives().archives() + .stream() + .map(it -> ArchiveSimpleResponse.changeToSimpleResponse(it, userId, likeRepository)) .toList(); return new ArchiveListResponse(result,null, null); } @@ -82,8 +88,9 @@ public ArchiveListResponse getMainArchive(User optionalUser) { @Transactional public ArchiveDetailResponse getArchiveDetail(Long archiveId, User user) { Archive archive = getArchive(archiveId); - archive.hit(); Long userId = user == null ? 0L : user.getId(); + String email = user == null ? "" : user.getEmail(); + publisher.publishEvent(new HitEvent(archiveId, email)); return ArchiveDetailResponse.toResponse( archive, userId, @@ -125,11 +132,6 @@ public ArchiveListResponse searchArchive(String searchKeyword, Pageable pageable Long myId = getOptionalUserId(optionalUser); Slice archives = archiveRepository.searchArchive(searchKeyword, ids, pageable); -// List archiveIds = archives.stream() -// .map(Archive::getId) -// .toList(); -// List colorCounts = archiveRepository.countLikeArchiveByArchiveType(archiveIds); - List list = archives .map(it -> ArchiveSimpleResponse.toResponse(it, myId, likeRepository)) .toList(); @@ -204,7 +206,7 @@ public ArchiveResponse likeArchive(Long archiveId, User user) { Long targetId = archive.getUser().getId(); notificationService.send(NotificationRequest.like(targetId, user.getName())); - + publisher.publishEvent(new LikeEvent(archiveId, findUser.getId())); return new ArchiveResponse(archive.getId()); } diff --git a/src/main/java/com/palettee/global/configs/RedisConfig.java b/src/main/java/com/palettee/global/configs/RedisConfig.java index fa5b54f8..b6ca4be9 100644 --- a/src/main/java/com/palettee/global/configs/RedisConfig.java +++ b/src/main/java/com/palettee/global/configs/RedisConfig.java @@ -1,7 +1,9 @@ package com.palettee.global.configs; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.palettee.chat.controller.dto.response.ChatResponse; import com.palettee.global.redis.sub.RedisSubscriber; @@ -137,7 +139,22 @@ public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { .build(); } + @Bean(name = "redisTemplateForArchive") + public RedisTemplate redisTemplateForArchive(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return template; + } + @Bean + public GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, + ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); + return new GenericJackson2JsonRedisSerializer(objectMapper); + } @Bean(name = "redisObjectMapper") // LocalDateTime 직렬화 할 때 오류 발생 -> jsr310 Module 추가 diff --git a/src/main/java/com/palettee/user/domain/User.java b/src/main/java/com/palettee/user/domain/User.java index 0ba87bf4..fea401fd 100644 --- a/src/main/java/com/palettee/user/domain/User.java +++ b/src/main/java/com/palettee/user/domain/User.java @@ -1,5 +1,6 @@ package com.palettee.user.domain; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.palettee.archive.domain.*; import com.palettee.gathering.domain.*; import com.palettee.global.entity.*; @@ -168,6 +169,7 @@ private User update( private final List relatedLinks = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) + @JsonIgnore private final List archives = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) diff --git a/src/test/java/com/palettee/archive/ArchiveRedisTest.java b/src/test/java/com/palettee/archive/ArchiveRedisTest.java new file mode 100644 index 00000000..3129384f --- /dev/null +++ b/src/test/java/com/palettee/archive/ArchiveRedisTest.java @@ -0,0 +1,158 @@ +package com.palettee.archive; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.palettee.archive.controller.dto.response.ArchiveRedisList; +import com.palettee.archive.controller.dto.response.ArchiveRedisResponse; +import com.palettee.archive.domain.Archive; +import com.palettee.archive.domain.ArchiveType; +import com.palettee.archive.event.HitEvent; +import com.palettee.archive.repository.ArchiveRedisRepository; +import com.palettee.archive.repository.ArchiveRepository; +import com.palettee.archive.service.ArchiveScheduler; +import com.palettee.archive.service.ArchiveService; +import com.palettee.user.domain.MajorJobGroup; +import com.palettee.user.domain.MinorJobGroup; +import com.palettee.user.domain.User; +import com.palettee.user.domain.UserRole; +import com.palettee.user.repository.UserRepository; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +public class ArchiveRedisTest { + + @Autowired + private ArchiveRedisRepository archiveRedisRepository; + + @Autowired + private ArchiveRepository archiveRepository; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private RedisTemplate redisTemplateForArchive; + + @Autowired + private UserRepository userRepository; + + private User savedUser; + + @BeforeEach + void beforeEach() { + savedUser = userRepository.save( + User.builder() + .email("email").imageUrl("imageUrl").name("name").briefIntro("briefIntro") + .userRole(UserRole.USER) + .majorJobGroup(MajorJobGroup.DEVELOPER) + .minorJobGroup(MinorJobGroup.BACKEND) + .build() + ); + } + + @AfterEach + void tearDown() { + + archiveRepository.deleteAll(); + userRepository.deleteAll(); + + redisTemplate.keys("*").forEach(redisTemplate::delete); + redisTemplateForArchive.keys("*").forEach(redisTemplateForArchive::delete); + } + + @Test + void testSettleHits() { + // Given + + Archive archive = new Archive("title", "description", "introduction", ArchiveType.RED, true, savedUser); + archiveRepository.save(archive); + + String incrKey = "incr:hit:archiveId:" + archive.getId(); + String stdKey = "std:hit:archiveId:" + archive.getId(); + + redisTemplate.opsForValue().set(incrKey, "5"); + redisTemplate.opsForValue().set(stdKey, "10"); + + // When + archiveRedisRepository.settleHits(); + + // Then + assertThat(redisTemplate.opsForValue().get(stdKey)).isEqualTo("15"); + assertThat(redisTemplate.hasKey(incrKey)).isFalse(); + + Archive updatedArchive = archiveRepository.findById(archive.getId()).orElseThrow(); + assertThat(updatedArchive.getHits()).isEqualTo(15); + } + + @Test + void testRedisSerialization() { + ArchiveRedisResponse archive = new ArchiveRedisResponse(1L, "Test", "Description", "Intro", "User", "Type", true, 10L, false, "url", "2025-01-04"); + redisTemplateForArchive.opsForValue().set("test_archive", archive); + + ArchiveRedisResponse result = (ArchiveRedisResponse) redisTemplateForArchive.opsForValue().get("test_archive"); + assertNotNull(result); + assertEquals(archive.archiveId(), result.archiveId()); + } + + @Test + void testUpdateArchiveList() { + // Given + Archive archive1 = new Archive("Archive 1", "description", "introduction", ArchiveType.RED, true, savedUser); + Archive archive2 = new Archive("Archive 2", "description", "introduction", ArchiveType.RED, true, savedUser); + Archive archive3 = new Archive("Archive 2", "description", "introduction", ArchiveType.RED, true, savedUser); + Archive archive4 = new Archive("Archive 2", "description", "introduction", ArchiveType.RED, true, savedUser); + archiveRepository.saveAll(List.of(archive1, archive2, archive3, archive4)); + + String incrKey1 = "incr:hit:archiveId:" + archive1.getId(); + String incrKey2 = "incr:hit:archiveId:" + archive2.getId(); + + redisTemplate.opsForValue().set(incrKey1, "30"); + redisTemplate.opsForValue().set(incrKey2, "40"); + + // When + archiveRedisRepository.updateArchiveList(); + + // Then + ArchiveRedisList topArchives = archiveRedisRepository.getTopArchives(); + assertThat(topArchives.archives()).hasSize(4); + assertThat(topArchives.archives()) + .extracting(ArchiveRedisResponse::archiveId) + .containsExactlyInAnyOrder(archive2.getId(), archive1.getId(), archive4.getId(), archive3.getId()); + } + + @Test + @DisplayName("Hit 이벤트 처리 테스트") + public void testOnHitEvent() { + // given + String email = "test@example.com"; + Long archiveId = 1L; + + HitEvent hitEvent = new HitEvent(archiveId, email); + String setKey = "hit:archiveId:" + archiveId; + String valueKey = "incr:hit:archiveId:" + archiveId; + + // when + redisTemplate.opsForSet().add(setKey, email); + redisTemplate.opsForValue().increment(valueKey); + + // then + Long hitCount = redisTemplate.opsForValue().getOperations().opsForValue().increment(valueKey, 0); + assertThat(hitCount).isEqualTo(1L); + + Set setMembers = redisTemplate.opsForSet().members(setKey); + assertThat(setMembers).contains(email); + } + +} diff --git a/src/test/java/com/palettee/archive/ArchiveServiceTest.java b/src/test/java/com/palettee/archive/ArchiveServiceTest.java index 5204d3f1..93fa9afd 100644 --- a/src/test/java/com/palettee/archive/ArchiveServiceTest.java +++ b/src/test/java/com/palettee/archive/ArchiveServiceTest.java @@ -175,7 +175,6 @@ void getArchiveDetailTest() { assertThat(allImages.get(0).url()).isEqualTo("url1"); assertThat(allImages.get(1).url()).isEqualTo("url2"); - assertThat(archiveDetail.hits()).isEqualTo(1); assertThat(archiveDetail.title()).isEqualTo(request.title()); assertThat(archiveDetail.description()).isEqualTo(request.description()); assertThat(archiveDetail.type()).isEqualTo("RED"); @@ -219,7 +218,6 @@ void updateArchiveTest() { assertThat(allImages.get(0).url()).isEqualTo("url11"); assertThat(allImages.get(1).url()).isEqualTo("url12"); - assertThat(archiveDetail.hits()).isEqualTo(1); assertThat(archiveDetail.title()).isEqualTo(archiveUpdateRequest.title()); assertThat(archiveDetail.description()).isEqualTo(archiveUpdateRequest.description()); assertThat(archiveDetail.type()).isEqualTo("YELLOW");