From 7735c8472b845cf197d6aa56813a07d0f4ae3216 Mon Sep 17 00:00:00 2001 From: Hyowon Yang Date: Fri, 24 Oct 2025 16:08:32 +0900 Subject: [PATCH] =?UTF-8?q?totalViews=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=B0=B0=ED=8F=AC=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 변경 사항: 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 --- .../executionHistory/executionHistory.bin | Bin 968403 -> 968403 bytes .../executionHistory/executionHistory.lock | Bin 17 -> 17 bytes .gradle/8.10/fileHashes/fileHashes.bin | Bin 29797 -> 29797 bytes .gradle/8.10/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../8.10/fileHashes/resourceHashesCache.bin | Bin 23155 -> 23325 bytes .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .gradle/file-system.probe | Bin 8 -> 8 bytes .../analytics/config/SampleDataLoader.java | 16 ++++-- .../kt/event/analytics/entity/EventStats.java | 7 +++ .../DistributionCompletedConsumer.java | 49 ++++++++++++++++-- .../event/DistributionCompletedEvent.java | 5 ++ 11 files changed, 68 insertions(+), 9 deletions(-) diff --git a/.gradle/8.10/executionHistory/executionHistory.bin b/.gradle/8.10/executionHistory/executionHistory.bin index 261505416cedad5373bfe625cfef2274811f3739..d0bce7ae1f6c34c26d16373ad23a12c722aa0174 100644 GIT binary patch delta 1300 zcmaKrX;4#F7>09`8xlfty%$D^gbLcqA_$p4jV)H75J;=3B5qJY2_RB-5(vhD0)ql& zwO2h#Q%qDupoGQcvJ6IcTCieO1P2BID~>3et)X5KEo2eVXj61V| zXw0S+QPIfYDV2Y`L-)j5yW(RXZx!LbMeD%~2J?t>xwPjj)YY7FI(cQS?~4kaYA(h5 zD_nQ>A}8ZQLb$H~V_$~4Xb{UCm(_?>ULV8><#ov`d=U1MaC1CU~p zG9<+!WkiZiY>{x*9R5GV$YcKN8NuT!xZ1-@-IG`w;g)gLkqC!@chrmnC^+STLJ+RsSa5?gO>w zI8rptbgpa2N<8l0Z)zi@XmQ9?HH!#oD=7h4Q_ za&a!+d_{lIaU1A_C@CyP?k5kH%N#?Zf@LxaDz3gK{ysD$a!WZPZ%2Op%M|)0!TphBRE6PyW|6IMoh&QFyQHhA=tILi4?hQtCh$Gyr zZwb4JxbnJ}{WH!q3L4Wdn&)EA631*X`(#Zk1k^#yFU*D6qt(XIC#>fBhT9ItZasQ$ zc|(~QqyeKhSXLGk^z5*)nR@);A#kOg|%5x4PM<(Dzl zv2VO39pj`0-p8K|zHJTZoj-niLKu65v<^2ZCfyn?RVns;Hg44cEs+*Cd!pt}y~XM| yCGDEnb6^xqe-7CZ)92Yb$guAX{ZmIKb!QYi8Z)lX9ZXnOZ zG5uc&@67E6nao?IH*R_J0 zv^lS@f^GYuiOeTi1bz#hc5CT4dTYJNi#LB~eC3{DFEnOgVPs&iHJ`3% z#2&hRKMzX^)Am~??2kCNzmZ~@%{txAm%Xa}&q`(>W&vW>?SEFXrFcw#@6RZ$@;rEJ zW8AyKs-OO=R_l9pizy4Bi!dCUp171pW4ewH)4J`YpV_vv3RrB}cY#Nw<4^jH@R}2o zj@V8YsO37geRUvHiN*FZBld@a0?R~_Y@bI2z0VR^clnTxHOF*;IZTn$54f<;=Y6*3 z)3SxeMj$5#i%q^bNn(0{4Lj?0`D0vL+1r6m1!4{$<^*CcAm#>Qp6zzNyowq4Vu-77 zeL_-q)B`}QL?vSSJ2J_RYvRXe82yw z>pmzp8^WelOCI+RKc^Ls$yM+Fp)1AJcpT}u?>H^*eyb|Z?c~0HdJ+Jf4 zU*ekF%g$3P?|}^8Z7}`*em*^xT|nM+!2^8O(m%lBGmM!T1ok|dTF+b`m$Y@?dt;XRd74I&jkuaC1-V} zekn9DJ-g1yVgC9GppfIuf*Z4r?hagQIp^KSOpmDP@4Xl`x4&P+v!7AkG)BSL a<)^m?uFILq_2B_1EVfMG0wxqj!8!odcAdZg diff --git a/.gradle/8.10/executionHistory/executionHistory.lock b/.gradle/8.10/executionHistory/executionHistory.lock index 6eb6217e94d038ff80e2fd72b050c2a3f1b60f21..8cd68161ac07838745393fe2d6f2cfdbdb0fd007 100644 GIT binary patch literal 17 UcmZRsbes@(Px9DK1_(F+05ZV@;s5{u literal 17 UcmZRsbes@(Px9DK1_)RQ05Yls%K!iX diff --git a/.gradle/8.10/fileHashes/fileHashes.bin b/.gradle/8.10/fileHashes/fileHashes.bin index d1fba3b18a6406005c28d1704f8afa6406018ef8..31237b7a6d5c5050242660fd9c7616779ae5cbc6 100644 GIT binary patch delta 611 zcmaF*g7N7K#tpwi6~6G6y*5bK3DMBe<`Dmr|AT>nah512(?13<;1ruI9p<9Iyi4n7 zQJl~mhWC2~H@(mg1}owcy9`kzCOx@6tV|(0@!g!rLY))Ol{9CyT6w!dOp*zMC}O(2 zSv1^=l|_{6V$mjh`u*UY>tB);JBlE1q(cl7Nr(A9?Oh=k}67TMgLtH21f;O3gi z>+?V!zqx#}aQ-Bam5P&B=7a3M`EK&@4q?swZi={G(wINrC6?_uOph)K6~!6q?v zSWK=gP@kMvkiz}Evln7IgVAKiLiNoog`SK-?y5d|>Cd&-01CE@@(qS$NpEgJ@ zyMO;nV))sq#ygy)-hdQ|+*US$DB@36b(`DYL5k???H-$r*hAO zC}L{dEE;ab$|CYCqiu6=ObL*+sAk{f(}@Wz!ZTvuZPrg(!x^;FYybJ*@g}KllfS zr%d5oJX2(@Rv~jruX*AcY5jE0hT656j@@=vID^C_ZgX*Y(=nz=8 zxjR>Z5$NBT36t07C4fSpXtHqrBv1%^oxCz%4YG^#JsA~#FKzs>!s}?sGj6H# zZo%J`AiAyJLR9K#OjZmPmOoLlw_sP$&I46O>+XEN{|KTczYC&9n_=_(q98^dpc5q_ jBBIwP2c`*67AO&6zOmuLWc3n#{@Zza5INRPAgu=g*a5-U diff --git a/.gradle/8.10/fileHashes/fileHashes.lock b/.gradle/8.10/fileHashes/fileHashes.lock index 15d99919bdd08da1db1f2333c04e43b0d7d7f76d..70ec6d147dc423a65987aa0d92d9e0b2fb93c850 100644 GIT binary patch literal 17 VcmZS9`MA7`o8<*R0~jzZ1pq8J1OEU3 literal 17 VcmZS9`MA7`o8<*R0~jzB0RSun1Iz#b diff --git a/.gradle/8.10/fileHashes/resourceHashesCache.bin b/.gradle/8.10/fileHashes/resourceHashesCache.bin index 9b443462dc98ccf674dd908f496830973e28e0bb..13ffb51d8b125bd7d2608e4179382aaffe9ebdfa 100644 GIT binary patch delta 311 zcmeyog>mjS#tkMCjD4F;C4R67+}da6w`0Ysi40&6RXkZ$wn5;F>su4r|YRPg5JQn?+B0%nIE|5<;Bs|_mpd$X#7A~SzYa7bi<;a#ZU_RY4+ zw-}jt6E`aU5Z`F9gKM*+hmladF;H3(hy_7dU^@Tf1dYD%nPD6%vTNV}_5upZp{j`e zyY6`I9U*_U`V>Zu^7RIoD$d_1G?%z5DShYCr!WaQL19c4XSV6<|2f$5`m`9g_HV9r c>o#I4F=u<&yI6(coBHGh)9QYAM}t%V0QL`8G5`Po delta 55 zcmV-70LcHHwgL0B0kAX}0g1CU82AK}MICsP)g9Qg#U8K$vyC7b1GBy((E$Ps>9HaB N7qKuf3$sB;G7^cE73Kf{ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 7594446866dd84d74197f577cad79c8ed3a1aae7..83d75dffa4a0c21ac155bdca88a18dc386518f60 100644 GIT binary patch literal 17 VcmZQ(PG7Ze^~s_h1~6di1pqV$1lIrn literal 17 VcmZQ(PG7Ze^~s_h1~6c<1OPL$1cLwo diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index 6b17014bd9909e4688aa9add1cf25646fb1d67dd..1a773a29806ad9d59b94c2e45d9caa0d0e81eea3 100644 GIT binary patch literal 8 PcmZQzV4NlLdEP7l2oD0= literal 8 PcmZQzV4Nj#HJlp&2ABco diff --git a/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java b/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java index a9ce7b5..fd16ea7 100644 --- a/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java +++ b/analytics-service/src/main/java/com/kt/event/analytics/config/SampleDataLoader.java @@ -188,6 +188,11 @@ public class SampleDataLoader implements ApplicationRunner { new BigDecimal("3500000"), 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++) { String eventId = eventIds[i]; @@ -195,19 +200,19 @@ public class SampleDataLoader implements ApplicationRunner { // 1. 우리동네TV (TV) publishDistributionEvent(eventId, "우리동네TV", "TV", - distributionBudget.multiply(new BigDecimal("0.3"))); + distributionBudget.multiply(new BigDecimal("0.3")), expectedViews[i][0]); // 2. 지니TV (TV) publishDistributionEvent(eventId, "지니TV", "TV", - distributionBudget.multiply(new BigDecimal("0.3"))); + distributionBudget.multiply(new BigDecimal("0.3")), expectedViews[i][1]); // 3. 링고비즈 (CALL) publishDistributionEvent(eventId, "링고비즈", "CALL", - distributionBudget.multiply(new BigDecimal("0.2"))); + distributionBudget.multiply(new BigDecimal("0.2")), expectedViews[i][2]); // 4. 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 채널)"); @@ -217,12 +222,13 @@ public class SampleDataLoader implements ApplicationRunner { * 개별 DistributionCompleted 이벤트 발행 */ private void publishDistributionEvent(String eventId, String channelName, String channelType, - BigDecimal distributionCost) throws Exception { + BigDecimal distributionCost, Integer expectedViews) throws Exception { DistributionCompletedEvent event = DistributionCompletedEvent.builder() .eventId(eventId) .channelName(channelName) .channelType(channelType) .distributionCost(distributionCost) + .expectedViews(expectedViews) .build(); publishEvent(DISTRIBUTION_COMPLETED_TOPIC, event); } diff --git a/analytics-service/src/main/java/com/kt/event/analytics/entity/EventStats.java b/analytics-service/src/main/java/com/kt/event/analytics/entity/EventStats.java index 5d24094..4c48a67 100644 --- a/analytics-service/src/main/java/com/kt/event/analytics/entity/EventStats.java +++ b/analytics-service/src/main/java/com/kt/event/analytics/entity/EventStats.java @@ -49,6 +49,13 @@ public class EventStats extends BaseTimeEntity { @Builder.Default private Integer totalParticipants = 0; + /** + * 총 노출 수 (모든 채널의 노출 수 합계) + */ + @Column(nullable = false) + @Builder.Default + private Integer totalViews = 0; + /** * 예상 ROI (%) */ diff --git a/analytics-service/src/main/java/com/kt/event/analytics/messaging/consumer/DistributionCompletedConsumer.java b/analytics-service/src/main/java/com/kt/event/analytics/messaging/consumer/DistributionCompletedConsumer.java index 47770e8..894a584 100644 --- a/analytics-service/src/main/java/com/kt/event/analytics/messaging/consumer/DistributionCompletedConsumer.java +++ b/analytics-service/src/main/java/com/kt/event/analytics/messaging/consumer/DistributionCompletedConsumer.java @@ -3,6 +3,7 @@ package com.kt.event.analytics.messaging.consumer; import com.kt.event.analytics.entity.ChannelStats; import com.kt.event.analytics.messaging.event.DistributionCompletedEvent; import com.kt.event.analytics.repository.ChannelStatsRepository; +import com.kt.event.analytics.repository.EventStatsRepository; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; 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.stereotype.Component; +import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -25,6 +27,7 @@ import java.util.concurrent.TimeUnit; public class DistributionCompletedConsumer { private final ChannelStatsRepository channelStatsRepository; + private final EventStatsRepository eventStatsRepository; private final ObjectMapper objectMapper; private final RedisTemplate redisTemplate; @@ -64,15 +67,25 @@ public class DistributionCompletedConsumer { .build()); 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; redisTemplate.delete(cacheKey); log.debug("🗑️ 캐시 무효화: {}", cacheKey); - // 4. 멱등성 처리 완료 기록 (7일 TTL) + // 5. 멱등성 처리 완료 기록 (7일 TTL) redisTemplate.opsForSet().add(PROCESSED_DISTRIBUTIONS_KEY, distributionKey); redisTemplate.expire(PROCESSED_DISTRIBUTIONS_KEY, IDEMPOTENCY_TTL_DAYS, TimeUnit.DAYS); log.debug("✅ 멱등성 기록: distributionKey={}", distributionKey); @@ -82,4 +95,32 @@ public class DistributionCompletedConsumer { throw new RuntimeException("DistributionCompleted 처리 실패", e); } } + + /** + * 모든 채널의 예상 노출 수를 합산하여 EventStats.totalViews 업데이트 + */ + private void updateTotalViews(String eventId) { + try { + // 모든 채널 통계 조회 + List 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); + } + } } diff --git a/analytics-service/src/main/java/com/kt/event/analytics/messaging/event/DistributionCompletedEvent.java b/analytics-service/src/main/java/com/kt/event/analytics/messaging/event/DistributionCompletedEvent.java index c3a6e6f..e890918 100644 --- a/analytics-service/src/main/java/com/kt/event/analytics/messaging/event/DistributionCompletedEvent.java +++ b/analytics-service/src/main/java/com/kt/event/analytics/messaging/event/DistributionCompletedEvent.java @@ -35,4 +35,9 @@ public class DistributionCompletedEvent { * 배포 비용 */ private BigDecimal distributionCost; + + /** + * 예상 노출 수 + */ + private Integer expectedViews; }