mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 10:06:24 +00:00
이벤트별 성과분석 대시보드 상세 정보 추가 및 Timeline 날짜 수정
## 주요 변경사항
### 1. Timeline 데이터 날짜 로직 수정
- **파일**: SampleDataLoader.java
- **변경**: 이벤트 ID에서 날짜를 파싱하여 실제 이벤트 시작일 기준으로 Timeline 생성
- 기존: 모든 이벤트가 2024-09-24부터 시작
- 수정: evt_2025012301 → 2025-01-23부터 30일치 생성
- **채널 분포**: 가중치 기반 랜덤 배정으로 변경
- SNS: 45% (최고 비율)
- 우리동네TV: 25%
- 지니TV: 20%
- 링고비즈: 10%
### 2. 이벤트별 API 상세 정보 추가
- **파일**: AnalyticsDashboardResponse.java
- **추가 필드**:
- investment: InvestmentDetails (투자 비용 상세)
- revenue: RevenueDetails (수익 상세)
- costEfficiency: CostEfficiency (비용 효율성)
### 3. 이벤트별 상세 계산 로직 구현
- **파일**: AnalyticsService.java
- **추가 메서드**:
- buildInvestmentDetails(): 투자 비용 상세 계산
- 경품비용 50%, 콘텐츠제작비 30%, 운영비 20%, 채널배포비용(실제)
- buildRevenueDetails(): 수익 상세 계산
- 직접매출 70%, 예상추가매출 30%, 신규고객 40%, 기존고객 60%
- buildCostEfficiency(): 비용 효율성 계산
- 참여자당 비용, 참여자당 수익
### 4. ROI 전용 API 필드 수정
- **파일**: ROICalculator.java
- **수정**: UserRoiAnalyticsService와 동일한 비율 적용
- investmentDetails에 prizeCost, channelCost 추가
- revenueDetails에 newCustomerRevenue, existingCustomerRevenue 추가
- **기존 문제**: null 값 반환
- **해결**: 통합분석과 동일한 계산 로직 적용
## API 응답 구조
### GET /api/v1/events/{eventId}/analytics
```json
{
"investment": {
"total": 5000000,
"prizeCost": 1250000,
"contentCreation": 750000,
"operation": 500000,
"distribution": 2500000,
"channelCost": 2500000
},
"revenue": {
"total": 15000000,
"directSales": 10500000,
"expectedSales": 4500000,
"newCustomerRevenue": 6000000,
"existingCustomerRevenue": 9000000
},
"costEfficiency": {
"costPerParticipant": 50000,
"revenuePerParticipant": 150000
}
}
```
## 테스트 결과
- ✅ Timeline 날짜가 이벤트별로 정확하게 생성됨
- ✅ 채널별 참여자 분포가 가중치대로 배정됨
- ✅ 이벤트별 API에서 상세 투자/수익 정보 제공
- ✅ ROI API에서 null 값 문제 해결
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
98ed508a6f
commit
20e0d24930
@ -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건");
|
||||
|
||||
@ -47,6 +47,21 @@ public class AnalyticsDashboardResponse {
|
||||
*/
|
||||
private RoiSummary roi;
|
||||
|
||||
/**
|
||||
* 투자 비용 상세
|
||||
*/
|
||||
private InvestmentDetails investment;
|
||||
|
||||
/**
|
||||
* 수익 상세
|
||||
*/
|
||||
private RevenueDetails revenue;
|
||||
|
||||
/**
|
||||
* 비용 효율성 분석
|
||||
*/
|
||||
private CostEfficiency costEfficiency;
|
||||
|
||||
/**
|
||||
* 마지막 업데이트 시간
|
||||
*/
|
||||
|
||||
@ -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<ChannelStats> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,43 +60,62 @@ public class ROICalculator {
|
||||
|
||||
/**
|
||||
* 투자 비용 계산
|
||||
*
|
||||
* UserRoiAnalyticsService와 동일한 로직:
|
||||
* - ChannelStats에서 실제 배포 비용 집계
|
||||
* - 나머지 비용 분배: 경품 50%, 콘텐츠 제작 30%, 운영 20%
|
||||
*/
|
||||
private InvestmentDetails calculateInvestment(EventStats eventStats, List<ChannelStats> 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();
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user