Skip to content

Commit 72517a8

Browse files
committed
feat: 상품 집계 정보 Redis ZSET에 적재
1 parent 73c0244 commit 72517a8

1 file changed

Lines changed: 40 additions & 4 deletions

File tree

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,81 @@
99
import com.loopers.domain.ProductMetricRepository;
1010
import lombok.RequiredArgsConstructor;
1111
import org.apache.kafka.clients.consumer.ConsumerRecord;
12+
import org.springframework.data.redis.core.StringRedisTemplate;
13+
import org.springframework.data.redis.core.ZSetOperations;
1214
import org.springframework.kafka.annotation.KafkaListener;
1315
import org.springframework.kafka.support.Acknowledgment;
1416
import org.springframework.stereotype.Component;
1517
import org.springframework.transaction.annotation.Transactional;
1618

19+
import java.time.Duration;
20+
import java.time.LocalDate;
21+
import java.time.ZoneId;
22+
import java.time.format.DateTimeFormatter;
23+
import java.util.HashMap;
1724
import java.util.List;
25+
import java.util.Map;
1826

1927
@Component
2028
@RequiredArgsConstructor
2129
public class KafkaOutboxConsumer {
2230
private final ProductMetricRepository productMetricRepository;
2331
private final ObjectMapper objectMapper;
32+
private final StringRedisTemplate redisTemplate;
33+
34+
private static final ZoneId KST = ZoneId.of("Asia/Seoul");
35+
private static final DateTimeFormatter YYYYMMDD = DateTimeFormatter.BASIC_ISO_DATE;
2436

2537
@KafkaListener(
26-
topics = {"product-viewed", "product-liked"},
38+
topics = {"product-viewed", "product-liked", "product-sales"},
2739
containerFactory = KafkaConfig.BATCH_LISTENER
2840
)
2941
@Transactional
3042
public void productViewedListener(
3143
List<ConsumerRecord<String, String>> messages,
3244
Acknowledgment acknowledgment
3345
) throws JsonProcessingException {
46+
47+
// 1) 배치 내 productId별 점수 누적
48+
Map<Long, Double> scoreDelta = new HashMap<>();
49+
3450
for (var record : messages) {
3551
OutboxEvent value = objectMapper.readValue(record.value(), OutboxEvent.class);
3652
Long productId = value.aggregateId();
37-
String eventType = value.eventType();
53+
ProductEventType eventType = ProductEventType.valueOf(value.eventType());
54+
55+
scoreDelta.merge(productId, weight(eventType), Double::sum);
3856

3957
ProductMetric productMetric = productMetricRepository.findByProductId(productId);
4058

4159
if (productMetric == null) {
42-
ProductMetric newProductMetric = ProductMetric.of(productId, ProductEventType.valueOf(eventType));
60+
ProductMetric newProductMetric = ProductMetric.of(productId, eventType);
4361

4462
productMetricRepository.save(newProductMetric);
4563
}
4664
else {
47-
productMetric.increaseProductMetric(ProductEventType.valueOf(eventType));
65+
productMetric.increaseProductMetric(eventType);
4866
}
67+
}
4968

69+
String key = "ranking:all:" + LocalDate.now(KST).format(YYYYMMDD);
70+
ZSetOperations<String, String> zset = redisTemplate.opsForZSet();
71+
72+
for (var e : scoreDelta.entrySet()) {
73+
zset.incrementScore(key, String.valueOf(e.getKey()), e.getValue()); // ZINCRBY
5074
}
75+
76+
// 3) 일간 키는 TTL 걸어두는 게 운영에 유리 (예: 8일 보관)
77+
redisTemplate.expire(key, Duration.ofDays(8));
78+
5179
acknowledgment.acknowledge();
5280
}
81+
82+
double weight(ProductEventType eventType) {
83+
return switch (eventType) {
84+
case PRODUCT_VIEWED -> 0.1;
85+
case PRODUCT_LIKED -> 0.3;
86+
case PRODUCT_SALES -> 0.6;
87+
};
88+
}
5389
}

0 commit comments

Comments
 (0)