diff --git a/TodaysFail-Common/src/main/java/com/todaysfail/common/consts/TodaysFailConst.java b/TodaysFail-Common/src/main/java/com/todaysfail/common/consts/TodaysFailConst.java index a385bc7..2f63682 100644 --- a/TodaysFail-Common/src/main/java/com/todaysfail/common/consts/TodaysFailConst.java +++ b/TodaysFail-Common/src/main/java/com/todaysfail/common/consts/TodaysFailConst.java @@ -12,6 +12,7 @@ public class TodaysFailConst { public static final String TOKEN_ISSUER = "TodaysFail"; public static final String ACCESS_TOKEN = "ACCESS_TOKEN"; public static final String REFRESH_TOKEN = "REFRESH_TOKEN"; + public static final String RECOMMEND_TAG_KEY = "RecommendTag"; public static final int MILLI_TO_SECOND = 1000; public static final int SERVICE_UNAVAILABLE = 503; diff --git a/TodaysFail-Domain/src/main/java/com/todaysfail/domains/failure/domain/Failure.java b/TodaysFail-Domain/src/main/java/com/todaysfail/domains/failure/domain/Failure.java index 2e904f4..e36d5f0 100644 --- a/TodaysFail-Domain/src/main/java/com/todaysfail/domains/failure/domain/Failure.java +++ b/TodaysFail-Domain/src/main/java/com/todaysfail/domains/failure/domain/Failure.java @@ -1,8 +1,10 @@ package com.todaysfail.domains.failure.domain; +import com.todaysfail.aop.event.Events; import com.todaysfail.common.BaseTimeEntity; import com.todaysfail.config.converter.LongArrayConverter; import com.todaysfail.domains.failure.exception.FailureNotOwnedByUserException; +import com.todaysfail.events.FailureRegisterEvent; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -12,6 +14,7 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.PostPersist; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -48,6 +51,11 @@ public class Failure extends BaseTimeEntity { private boolean secret; + @PostPersist + void registerEvent() { + Events.raise(new FailureRegisterEvent(this)); + } + public boolean isMine(Long userId) { return this.userId.equals(userId); } diff --git a/TodaysFail-Domain/src/main/java/com/todaysfail/events/FailureRegisterEvent.java b/TodaysFail-Domain/src/main/java/com/todaysfail/events/FailureRegisterEvent.java new file mode 100644 index 0000000..38557bd --- /dev/null +++ b/TodaysFail-Domain/src/main/java/com/todaysfail/events/FailureRegisterEvent.java @@ -0,0 +1,14 @@ +package com.todaysfail.events; + +import com.todaysfail.aop.event.DomainEvent; +import com.todaysfail.domains.failure.domain.Failure; +import lombok.Getter; + +@Getter +public class FailureRegisterEvent extends DomainEvent { + private final Failure failure; + + public FailureRegisterEvent(Failure failure) { + this.failure = failure; + } +} diff --git a/TodaysFail-Domain/src/main/java/com/todaysfail/events/handler/FailureRegisterEventHandler.java b/TodaysFail-Domain/src/main/java/com/todaysfail/events/handler/FailureRegisterEventHandler.java new file mode 100644 index 0000000..381b4de --- /dev/null +++ b/TodaysFail-Domain/src/main/java/com/todaysfail/events/handler/FailureRegisterEventHandler.java @@ -0,0 +1,44 @@ +package com.todaysfail.events.handler; + +import static com.todaysfail.common.consts.TodaysFailConst.RECOMMEND_TAG_KEY; + +import com.todaysfail.common.annotation.EventHandler; +import com.todaysfail.domains.failure.domain.Failure; +import com.todaysfail.domains.tag.domain.Tag; +import com.todaysfail.domains.tag.port.TagQueryPort; +import com.todaysfail.events.FailureRegisterEvent; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@EventHandler +@RequiredArgsConstructor +public class FailureRegisterEventHandler { + private final TagQueryPort tagQueryPort; + private final RedisTemplate redisTemplate; + + @Async + @TransactionalEventListener( + classes = FailureRegisterEvent.class, + phase = TransactionPhase.AFTER_COMMIT) + public void handleUserRegisterEvent(FailureRegisterEvent event) { + final Failure failure = event.getFailure(); + log.info("[DOMAIN EVENT : FailureRegisterEvent] failureId: {}", failure.getId()); + + List tagIds = failure.getTags(); + List tags = tagQueryPort.queryAllByIds(tagIds); + + tags.stream() + .forEach( + tag -> { + redisTemplate + .opsForZSet() + .incrementScore(RECOMMEND_TAG_KEY, tag.getTagName(), 1); + }); + } +} diff --git a/TodaysFail-Infrastructure/src/main/java/com/todaysfail/config/redis/RedisCacheConfig.java b/TodaysFail-Infrastructure/src/main/java/com/todaysfail/config/redis/RedisCacheConfig.java index 7e0ddd2..d7903c7 100644 --- a/TodaysFail-Infrastructure/src/main/java/com/todaysfail/config/redis/RedisCacheConfig.java +++ b/TodaysFail-Infrastructure/src/main/java/com/todaysfail/config/redis/RedisCacheConfig.java @@ -8,7 +8,9 @@ import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -32,4 +34,14 @@ public CacheManager oidcCacheManager(RedisConnectionFactory cf) { .cacheDefaults(redisCacheConfiguration) .build(); } + + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class)); + return redisTemplate; + } } diff --git a/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/TagController.java b/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/TagController.java index 32d8bf1..46f9636 100644 --- a/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/TagController.java +++ b/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/TagController.java @@ -1,7 +1,9 @@ package com.todaysfail.api.web.tag; +import com.todaysfail.api.web.tag.dto.response.TagRecommendResponse; import com.todaysfail.api.web.tag.dto.response.TagResponse; import com.todaysfail.api.web.tag.usecase.TagPopularUseCase; +import com.todaysfail.api.web.tag.usecase.TagRecommendUseCase; import com.todaysfail.api.web.tag.usecase.TagSearchUseCase; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -21,6 +23,7 @@ public class TagController { private final TagSearchUseCase tagSearchUseCase; private final TagPopularUseCase tagPopularUseCase; + private final TagRecommendUseCase tagRecommendUseCase; @Operation(summary = "태그를 검색합니다. (5개)") @GetMapping("/search") @@ -34,7 +37,9 @@ public List popular() { return tagPopularUseCase.execute(); } - // @Operation(summary = "추천 태그를 조회합니다.") - // @GetMapping("/recommend") - // TODO: 추천 태그 조회 API 구현 + @Operation(summary = "추천 태그를 조회합니다.") + @GetMapping("/recommend") + public List recommendTag() { + return tagRecommendUseCase.execute(); + } } diff --git a/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/dto/response/TagRecommendResponse.java b/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/dto/response/TagRecommendResponse.java new file mode 100644 index 0000000..084146a --- /dev/null +++ b/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/dto/response/TagRecommendResponse.java @@ -0,0 +1,9 @@ +package com.todaysfail.api.web.tag.dto.response; + +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; + +public record TagRecommendResponse(String tagName, double score) { + public static TagRecommendResponse from(TypedTuple tuple) { + return new TagRecommendResponse(tuple.getValue(), tuple.getScore()); + } +} diff --git a/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/usecase/TagRecommendUseCase.java b/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/usecase/TagRecommendUseCase.java new file mode 100644 index 0000000..5d45efe --- /dev/null +++ b/TodaysFail-Interface/src/main/java/com/todaysfail/api/web/tag/usecase/TagRecommendUseCase.java @@ -0,0 +1,26 @@ +package com.todaysfail.api.web.tag.usecase; + +import static com.todaysfail.common.consts.TodaysFailConst.RECOMMEND_TAG_KEY; + +import com.todaysfail.api.web.tag.dto.response.TagRecommendResponse; +import com.todaysfail.common.annotation.UseCase; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; + +@UseCase +@RequiredArgsConstructor +public class TagRecommendUseCase { + private final RedisTemplate redisTemplate; + + public List execute() { + String key = RECOMMEND_TAG_KEY; + ZSetOperations ZSetOperations = redisTemplate.opsForZSet(); + Set> typedTuples = ZSetOperations.reverseRangeWithScores(key, 0, 9); + return typedTuples.stream().map(TagRecommendResponse::from).collect(Collectors.toList()); + } +}