mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2026-06-13 15:39:12 +00:00
totalViews 필드 추가 및 배포완료 이벤트 개선
변경 사항: 1. EventStats에 totalViews 필드 추가 (모든 채널 노출 수 합계) 2. DistributionCompletedEvent에 expectedViews 필드 추가 3. DistributionCompletedConsumer 개선: - ChannelStats.impressions에 expectedViews 저장 - updateTotalViews() 메서드로 전체 노출 수 집계 4. SampleDataLoader에 채널별 예상 노출 수 설정: - 이벤트1: 총 20,000 (우리동네TV 5K, 지니TV 10K, 링고비즈 3K, SNS 2K) - 이벤트2: 총 14,000 - 이벤트3: 총 6,000 설계 다이어그램과 일치: - 채널별 예상 노출 수 저장 - 총 노출 수 실시간 집계 - 멱등성 및 캐시 무효화 유지 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
-5
@@ -188,6 +188,11 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
new BigDecimal("3500000"),
|
new BigDecimal("3500000"),
|
||||||
new BigDecimal("2000000")
|
new BigDecimal("2000000")
|
||||||
};
|
};
|
||||||
|
int[][] expectedViews = {
|
||||||
|
{5000, 10000, 3000, 2000}, // 이벤트1: 우리동네TV, 지니TV, 링고비즈, SNS
|
||||||
|
{3500, 7000, 2000, 1500}, // 이벤트2
|
||||||
|
{1500, 3000, 1000, 500} // 이벤트3
|
||||||
|
};
|
||||||
|
|
||||||
for (int i = 0; i < eventIds.length; i++) {
|
for (int i = 0; i < eventIds.length; i++) {
|
||||||
String eventId = eventIds[i];
|
String eventId = eventIds[i];
|
||||||
@@ -195,19 +200,19 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
|
|
||||||
// 1. 우리동네TV (TV)
|
// 1. 우리동네TV (TV)
|
||||||
publishDistributionEvent(eventId, "우리동네TV", "TV",
|
publishDistributionEvent(eventId, "우리동네TV", "TV",
|
||||||
distributionBudget.multiply(new BigDecimal("0.3")));
|
distributionBudget.multiply(new BigDecimal("0.3")), expectedViews[i][0]);
|
||||||
|
|
||||||
// 2. 지니TV (TV)
|
// 2. 지니TV (TV)
|
||||||
publishDistributionEvent(eventId, "지니TV", "TV",
|
publishDistributionEvent(eventId, "지니TV", "TV",
|
||||||
distributionBudget.multiply(new BigDecimal("0.3")));
|
distributionBudget.multiply(new BigDecimal("0.3")), expectedViews[i][1]);
|
||||||
|
|
||||||
// 3. 링고비즈 (CALL)
|
// 3. 링고비즈 (CALL)
|
||||||
publishDistributionEvent(eventId, "링고비즈", "CALL",
|
publishDistributionEvent(eventId, "링고비즈", "CALL",
|
||||||
distributionBudget.multiply(new BigDecimal("0.2")));
|
distributionBudget.multiply(new BigDecimal("0.2")), expectedViews[i][2]);
|
||||||
|
|
||||||
// 4. SNS (SNS)
|
// 4. SNS (SNS)
|
||||||
publishDistributionEvent(eventId, "SNS", "SNS",
|
publishDistributionEvent(eventId, "SNS", "SNS",
|
||||||
distributionBudget.multiply(new BigDecimal("0.2")));
|
distributionBudget.multiply(new BigDecimal("0.2")), expectedViews[i][3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("✅ DistributionCompleted 이벤트 12건 발행 완료 (3 이벤트 × 4 채널)");
|
log.info("✅ DistributionCompleted 이벤트 12건 발행 완료 (3 이벤트 × 4 채널)");
|
||||||
@@ -217,12 +222,13 @@ public class SampleDataLoader implements ApplicationRunner {
|
|||||||
* 개별 DistributionCompleted 이벤트 발행
|
* 개별 DistributionCompleted 이벤트 발행
|
||||||
*/
|
*/
|
||||||
private void publishDistributionEvent(String eventId, String channelName, String channelType,
|
private void publishDistributionEvent(String eventId, String channelName, String channelType,
|
||||||
BigDecimal distributionCost) throws Exception {
|
BigDecimal distributionCost, Integer expectedViews) throws Exception {
|
||||||
DistributionCompletedEvent event = DistributionCompletedEvent.builder()
|
DistributionCompletedEvent event = DistributionCompletedEvent.builder()
|
||||||
.eventId(eventId)
|
.eventId(eventId)
|
||||||
.channelName(channelName)
|
.channelName(channelName)
|
||||||
.channelType(channelType)
|
.channelType(channelType)
|
||||||
.distributionCost(distributionCost)
|
.distributionCost(distributionCost)
|
||||||
|
.expectedViews(expectedViews)
|
||||||
.build();
|
.build();
|
||||||
publishEvent(DISTRIBUTION_COMPLETED_TOPIC, event);
|
publishEvent(DISTRIBUTION_COMPLETED_TOPIC, event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ public class EventStats extends BaseTimeEntity {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Integer totalParticipants = 0;
|
private Integer totalParticipants = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 총 노출 수 (모든 채널의 노출 수 합계)
|
||||||
|
*/
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Builder.Default
|
||||||
|
private Integer totalViews = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 예상 ROI (%)
|
* 예상 ROI (%)
|
||||||
*/
|
*/
|
||||||
|
|||||||
+45
-4
@@ -3,6 +3,7 @@ package com.kt.event.analytics.messaging.consumer;
|
|||||||
import com.kt.event.analytics.entity.ChannelStats;
|
import com.kt.event.analytics.entity.ChannelStats;
|
||||||
import com.kt.event.analytics.messaging.event.DistributionCompletedEvent;
|
import com.kt.event.analytics.messaging.event.DistributionCompletedEvent;
|
||||||
import com.kt.event.analytics.repository.ChannelStatsRepository;
|
import com.kt.event.analytics.repository.ChannelStatsRepository;
|
||||||
|
import com.kt.event.analytics.repository.EventStatsRepository;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -11,6 +12,7 @@ import org.springframework.data.redis.core.RedisTemplate;
|
|||||||
import org.springframework.kafka.annotation.KafkaListener;
|
import org.springframework.kafka.annotation.KafkaListener;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +27,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
public class DistributionCompletedConsumer {
|
public class DistributionCompletedConsumer {
|
||||||
|
|
||||||
private final ChannelStatsRepository channelStatsRepository;
|
private final ChannelStatsRepository channelStatsRepository;
|
||||||
|
private final EventStatsRepository eventStatsRepository;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final RedisTemplate<String, String> redisTemplate;
|
private final RedisTemplate<String, String> redisTemplate;
|
||||||
|
|
||||||
@@ -64,15 +67,25 @@ public class DistributionCompletedConsumer {
|
|||||||
.build());
|
.build());
|
||||||
|
|
||||||
channelStats.setDistributionCost(event.getDistributionCost());
|
channelStats.setDistributionCost(event.getDistributionCost());
|
||||||
channelStatsRepository.save(channelStats);
|
|
||||||
log.info("✅ 채널 통계 업데이트: eventId={}, channel={}", eventId, channelName);
|
|
||||||
|
|
||||||
// 3. 캐시 무효화 (다음 조회 시 최신 배포 통계 반영)
|
// 예상 노출 수 저장
|
||||||
|
if (event.getExpectedViews() != null) {
|
||||||
|
channelStats.setImpressions(event.getExpectedViews());
|
||||||
|
}
|
||||||
|
|
||||||
|
channelStatsRepository.save(channelStats);
|
||||||
|
log.info("✅ 채널 통계 업데이트: eventId={}, channel={}, expectedViews={}",
|
||||||
|
eventId, channelName, event.getExpectedViews());
|
||||||
|
|
||||||
|
// 3. EventStats의 totalViews 업데이트 (모든 채널 노출 수 합계)
|
||||||
|
updateTotalViews(eventId);
|
||||||
|
|
||||||
|
// 4. 캐시 무효화 (다음 조회 시 최신 배포 통계 반영)
|
||||||
String cacheKey = CACHE_KEY_PREFIX + eventId;
|
String cacheKey = CACHE_KEY_PREFIX + eventId;
|
||||||
redisTemplate.delete(cacheKey);
|
redisTemplate.delete(cacheKey);
|
||||||
log.debug("🗑️ 캐시 무효화: {}", cacheKey);
|
log.debug("🗑️ 캐시 무효화: {}", cacheKey);
|
||||||
|
|
||||||
// 4. 멱등성 처리 완료 기록 (7일 TTL)
|
// 5. 멱등성 처리 완료 기록 (7일 TTL)
|
||||||
redisTemplate.opsForSet().add(PROCESSED_DISTRIBUTIONS_KEY, distributionKey);
|
redisTemplate.opsForSet().add(PROCESSED_DISTRIBUTIONS_KEY, distributionKey);
|
||||||
redisTemplate.expire(PROCESSED_DISTRIBUTIONS_KEY, IDEMPOTENCY_TTL_DAYS, TimeUnit.DAYS);
|
redisTemplate.expire(PROCESSED_DISTRIBUTIONS_KEY, IDEMPOTENCY_TTL_DAYS, TimeUnit.DAYS);
|
||||||
log.debug("✅ 멱등성 기록: distributionKey={}", distributionKey);
|
log.debug("✅ 멱등성 기록: distributionKey={}", distributionKey);
|
||||||
@@ -82,4 +95,32 @@ public class DistributionCompletedConsumer {
|
|||||||
throw new RuntimeException("DistributionCompleted 처리 실패", e);
|
throw new RuntimeException("DistributionCompleted 처리 실패", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모든 채널의 예상 노출 수를 합산하여 EventStats.totalViews 업데이트
|
||||||
|
*/
|
||||||
|
private void updateTotalViews(String eventId) {
|
||||||
|
try {
|
||||||
|
// 모든 채널 통계 조회
|
||||||
|
List<ChannelStats> channelStatsList = channelStatsRepository.findByEventId(eventId);
|
||||||
|
|
||||||
|
// 총 노출 수 계산
|
||||||
|
int totalViews = channelStatsList.stream()
|
||||||
|
.mapToInt(ChannelStats::getImpressions)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
// EventStats 업데이트
|
||||||
|
eventStatsRepository.findByEventId(eventId)
|
||||||
|
.ifPresentOrElse(
|
||||||
|
eventStats -> {
|
||||||
|
eventStats.setTotalViews(totalViews);
|
||||||
|
eventStatsRepository.save(eventStats);
|
||||||
|
log.info("✅ 총 노출 수 업데이트: eventId={}, totalViews={}", eventId, totalViews);
|
||||||
|
},
|
||||||
|
() -> log.warn("⚠️ 이벤트 통계 없음: eventId={}", eventId)
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ totalViews 업데이트 실패: eventId={}", eventId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -35,4 +35,9 @@ public class DistributionCompletedEvent {
|
|||||||
* 배포 비용
|
* 배포 비용
|
||||||
*/
|
*/
|
||||||
private BigDecimal distributionCost;
|
private BigDecimal distributionCost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 예상 노출 수
|
||||||
|
*/
|
||||||
|
private Integer expectedViews;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user