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 8df7f0e..092c1d7 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 @@ -370,7 +370,20 @@ public class SampleDataLoader implements ApplicationRunner { // 각 참여자에 대해 ParticipantRegistered 이벤트 발행 for (int userId = startUser; userId <= endUser; userId++) { String participantId = String.format("user%03d", userId); // user001, user002, ... - String channel = channels[(userId - 1) % channels.length]; // 채널 순환 배정 + + // 채널별 가중치 기반 랜덤 배정 + // SNS: 45%, 우리동네TV: 25%, 지니TV: 20%, 링고비즈: 10% + int randomValue = random.nextInt(100); + String channel; + if (randomValue < 45) { + channel = "SNS"; // 0~44: 45% + } else if (randomValue < 70) { + channel = "우리동네TV"; // 45~69: 25% + } else if (randomValue < 90) { + channel = "지니TV"; // 70~89: 20% + } else { + channel = "링고비즈"; // 90~99: 10% + } ParticipantRegisteredEvent event = ParticipantRegisteredEvent.builder() .eventId(eventId) @@ -400,6 +413,11 @@ public class SampleDataLoader implements ApplicationRunner { log.info(" - 3개 이벤트 모두 참여: 30명"); log.info(" - 2개 이벤트 참여: 20명"); log.info(" - 1개 이벤트만 참여: 50명"); + log.info("📺 채널별 참여 비율 (가중치):"); + log.info(" - SNS: 45% (가장 높음)"); + log.info(" - 우리동네TV: 25%"); + log.info(" - 지니TV: 20%"); + log.info(" - 링고비즈: 10%"); log.info("========================================"); } @@ -424,12 +442,18 @@ public class SampleDataLoader implements ApplicationRunner { int baseParticipant = baseParticipantsPerHour[eventIndex]; int cumulativeParticipants = 0; - // 30일 치 hourly 데이터 생성 (2024-09-24 00:00부터) - java.time.LocalDateTime startDate = java.time.LocalDateTime.of(2024, 9, 24, 0, 0); + // 이벤트 ID에서 날짜 파싱 (evt_2025012301 → 2025-01-23) + String dateStr = eventId.substring(4); // "2025012301" + int year = Integer.parseInt(dateStr.substring(0, 4)); // 2025 + int month = Integer.parseInt(dateStr.substring(4, 6)); // 01 + int day = Integer.parseInt(dateStr.substring(6, 8)); // 23 - for (int day = 0; day < 30; day++) { + // 이벤트 시작일부터 30일 치 hourly 데이터 생성 + java.time.LocalDateTime startDate = java.time.LocalDateTime.of(year, month, day, 0, 0); + + for (int dayOffset = 0; dayOffset < 30; dayOffset++) { for (int hour = 0; hour < 24; hour++) { - java.time.LocalDateTime timestamp = startDate.plusDays(day).plusHours(hour); + java.time.LocalDateTime timestamp = startDate.plusDays(dayOffset).plusHours(hour); // 시간대별 참여자 수 변화 (낮 시간대 12~20시에 더 많음) int hourMultiplier = (hour >= 12 && hour <= 20) ? 2 : 1; @@ -462,7 +486,8 @@ public class SampleDataLoader implements ApplicationRunner { } } - log.info("✅ TimelineData 생성 완료: eventId={}, 30일 × 24시간 = 720건", eventId); + log.info("✅ TimelineData 생성 완료: eventId={}, 시작일={}-{:02d}-{:02d}, 30일 × 24시간 = 720건", + eventId, year, month, day); } log.info("✅ 전체 TimelineData 생성 완료: 3개 이벤트 × 30일 × 24시간 = 2,160건"); diff --git a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsDashboardResponse.java b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsDashboardResponse.java index 9fb9b3e..6ba1803 100644 --- a/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsDashboardResponse.java +++ b/analytics-service/src/main/java/com/kt/event/analytics/dto/response/AnalyticsDashboardResponse.java @@ -47,6 +47,21 @@ public class AnalyticsDashboardResponse { */ private RoiSummary roi; + /** + * 투자 비용 상세 + */ + private InvestmentDetails investment; + + /** + * 수익 상세 + */ + private RevenueDetails revenue; + + /** + * 비용 효율성 분석 + */ + private CostEfficiency costEfficiency; + /** * 마지막 업데이트 시간 */ diff --git a/analytics-service/src/main/java/com/kt/event/analytics/service/AnalyticsService.java b/analytics-service/src/main/java/com/kt/event/analytics/service/AnalyticsService.java index 919f944..2830a94 100644 --- a/analytics-service/src/main/java/com/kt/event/analytics/service/AnalyticsService.java +++ b/analytics-service/src/main/java/com/kt/event/analytics/service/AnalyticsService.java @@ -121,6 +121,15 @@ public class AnalyticsService { // ROI 요약 RoiSummary roiSummary = roiCalculator.calculateRoiSummary(eventStats); + // 투자 비용 상세 + InvestmentDetails investment = buildInvestmentDetails(eventStats, channelStatsList); + + // 수익 상세 + RevenueDetails revenue = buildRevenueDetails(eventStats); + + // 비용 효율성 + CostEfficiency costEfficiency = buildCostEfficiency(eventStats); + return AnalyticsDashboardResponse.builder() .eventId(eventStats.getEventId()) .eventTitle(eventStats.getEventTitle()) @@ -128,6 +137,9 @@ public class AnalyticsService { .summary(summary) .channelPerformance(channelPerformance) .roi(roiSummary) + .investment(investment) + .revenue(revenue) + .costEfficiency(costEfficiency) .lastUpdatedAt(LocalDateTime.now()) .dataSource("cached") .build(); @@ -212,4 +224,88 @@ public class AnalyticsService { return summaries; } + + /** + * 투자 비용 상세 구성 + * + * UserRoiAnalyticsService와 동일한 로직: + * - 실제 채널 배포 비용 집계 + * - 나머지 비용 분배: 경품 50%, 콘텐츠 제작 30%, 운영 20% + */ + private InvestmentDetails buildInvestmentDetails(EventStats eventStats, List channelStatsList) { + java.math.BigDecimal totalInvestment = eventStats.getTotalInvestment(); + + // ChannelStats에서 실제 배포 비용 집계 + java.math.BigDecimal actualDistribution = channelStatsList.stream() + .map(ChannelStats::getDistributionCost) + .reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add); + + // 나머지 비용 계산 (총 투자 - 실제 채널 배포 비용) + java.math.BigDecimal remaining = totalInvestment.subtract(actualDistribution); + + // 나머지 비용 분배: 경품 50%, 콘텐츠 제작 30%, 운영 20% + java.math.BigDecimal prizeCost = remaining.multiply(java.math.BigDecimal.valueOf(0.50)); + java.math.BigDecimal contentCreation = remaining.multiply(java.math.BigDecimal.valueOf(0.30)); + java.math.BigDecimal operation = remaining.multiply(java.math.BigDecimal.valueOf(0.20)); + + return InvestmentDetails.builder() + .total(totalInvestment) + .contentCreation(contentCreation) + .operation(operation) + .distribution(actualDistribution) + .prizeCost(prizeCost) + .channelCost(actualDistribution) // 채널비용은 배포비용과 동일 + .build(); + } + + /** + * 수익 상세 구성 + * + * UserRoiAnalyticsService와 동일한 로직: + * - 직접 매출 70%, 예상 추가 매출 30% + * - 신규 고객 40%, 기존 고객 60% + */ + private RevenueDetails buildRevenueDetails(EventStats eventStats) { + java.math.BigDecimal totalRevenue = eventStats.getExpectedRevenue(); + + // 매출 분배: 직접 매출 70%, 예상 추가 매출 30% + java.math.BigDecimal directSales = totalRevenue.multiply(java.math.BigDecimal.valueOf(0.70)); + java.math.BigDecimal expectedSales = totalRevenue.multiply(java.math.BigDecimal.valueOf(0.30)); + + // 신규 고객 40%, 기존 고객 60% + java.math.BigDecimal newCustomerRevenue = totalRevenue.multiply(java.math.BigDecimal.valueOf(0.40)); + java.math.BigDecimal existingCustomerRevenue = totalRevenue.multiply(java.math.BigDecimal.valueOf(0.60)); + + return RevenueDetails.builder() + .total(totalRevenue) + .directSales(directSales) + .expectedSales(expectedSales) + .newCustomerRevenue(newCustomerRevenue) + .existingCustomerRevenue(existingCustomerRevenue) + .brandValue(java.math.BigDecimal.ZERO) // 브랜드 가치는 별도 계산 필요 시 추가 + .build(); + } + + /** + * 비용 효율성 구성 + * + * UserRoiAnalyticsService와 동일한 로직: + * - 참여자당 비용 = 총투자 ÷ 총참여자수 + * - 참여자당 수익 = 총수익 ÷ 총참여자수 + */ + private CostEfficiency buildCostEfficiency(EventStats eventStats) { + int totalParticipants = eventStats.getTotalParticipants(); + java.math.BigDecimal totalInvestment = eventStats.getTotalInvestment(); + java.math.BigDecimal totalRevenue = eventStats.getExpectedRevenue(); + + double costPerParticipant = totalParticipants > 0 ? + totalInvestment.doubleValue() / totalParticipants : 0.0; + double revenuePerParticipant = totalParticipants > 0 ? + totalRevenue.doubleValue() / totalParticipants : 0.0; + + return CostEfficiency.builder() + .costPerParticipant(costPerParticipant) + .revenuePerParticipant(revenuePerParticipant) + .build(); + } } diff --git a/analytics-service/src/main/java/com/kt/event/analytics/service/ROICalculator.java b/analytics-service/src/main/java/com/kt/event/analytics/service/ROICalculator.java index 29196e4..844035b 100644 --- a/analytics-service/src/main/java/com/kt/event/analytics/service/ROICalculator.java +++ b/analytics-service/src/main/java/com/kt/event/analytics/service/ROICalculator.java @@ -60,43 +60,62 @@ public class ROICalculator { /** * 투자 비용 계산 + * + * UserRoiAnalyticsService와 동일한 로직: + * - ChannelStats에서 실제 배포 비용 집계 + * - 나머지 비용 분배: 경품 50%, 콘텐츠 제작 30%, 운영 20% */ private InvestmentDetails calculateInvestment(EventStats eventStats, List channelStats) { - BigDecimal distributionCost = channelStats.stream() + BigDecimal totalInvestment = eventStats.getTotalInvestment(); + + // ChannelStats에서 실제 배포 비용 집계 + BigDecimal actualDistribution = channelStats.stream() .map(ChannelStats::getDistributionCost) .reduce(BigDecimal.ZERO, BigDecimal::add); - BigDecimal contentCreation = eventStats.getTotalInvestment() - .multiply(BigDecimal.valueOf(0.4)); // 전체 투자의 40%를 콘텐츠 제작비로 가정 + // 나머지 비용 계산 (총 투자 - 실제 채널 배포 비용) + BigDecimal remaining = totalInvestment.subtract(actualDistribution); - BigDecimal operation = eventStats.getTotalInvestment() - .multiply(BigDecimal.valueOf(0.1)); // 10%를 운영비로 가정 + // 나머지 비용 분배: 경품 50%, 콘텐츠 제작 30%, 운영 20% + BigDecimal prizeCost = remaining.multiply(BigDecimal.valueOf(0.50)); + BigDecimal contentCreation = remaining.multiply(BigDecimal.valueOf(0.30)); + BigDecimal operation = remaining.multiply(BigDecimal.valueOf(0.20)); return InvestmentDetails.builder() + .total(totalInvestment) .contentCreation(contentCreation) - .distribution(distributionCost) .operation(operation) - .total(eventStats.getTotalInvestment()) + .distribution(actualDistribution) + .prizeCost(prizeCost) + .channelCost(actualDistribution) // 채널비용은 배포비용과 동일 .build(); } /** * 수익 계산 + * + * UserRoiAnalyticsService와 동일한 로직: + * - 직접 매출 70%, 예상 추가 매출 30% + * - 신규 고객 40%, 기존 고객 60% */ private RevenueDetails calculateRevenue(EventStats eventStats) { - BigDecimal directSales = eventStats.getExpectedRevenue() - .multiply(BigDecimal.valueOf(0.66)); // 예상 수익의 66%를 직접 매출로 가정 + BigDecimal totalRevenue = eventStats.getExpectedRevenue(); - BigDecimal expectedSales = eventStats.getExpectedRevenue() - .multiply(BigDecimal.valueOf(0.34)); // 34%를 예상 추가 매출로 가정 + // 매출 분배: 직접 매출 70%, 예상 추가 매출 30% + BigDecimal directSales = totalRevenue.multiply(BigDecimal.valueOf(0.70)); + BigDecimal expectedSales = totalRevenue.multiply(BigDecimal.valueOf(0.30)); - BigDecimal brandValue = BigDecimal.ZERO; // 브랜드 가치는 별도 계산 필요 + // 신규 고객 40%, 기존 고객 60% + BigDecimal newCustomerRevenue = totalRevenue.multiply(BigDecimal.valueOf(0.40)); + BigDecimal existingCustomerRevenue = totalRevenue.multiply(BigDecimal.valueOf(0.60)); return RevenueDetails.builder() + .total(totalRevenue) .directSales(directSales) .expectedSales(expectedSales) - .brandValue(brandValue) - .total(eventStats.getExpectedRevenue()) + .newCustomerRevenue(newCustomerRevenue) + .existingCustomerRevenue(existingCustomerRevenue) + .brandValue(BigDecimal.ZERO) // 브랜드 가치는 별도 계산 필요 시 추가 .build(); }