샘플데이터 json저장

This commit is contained in:
Hyowon Yang 2025-10-30 22:16:18 +09:00
parent 9ce62738a1
commit 6280ff8ce1
2 changed files with 401 additions and 188 deletions

View File

@ -11,6 +11,7 @@ import jakarta.annotation.PreDestroy;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.Data;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.DeleteConsumerGroupOffsetsResult;
import org.apache.kafka.clients.admin.ListConsumerGroupOffsetsResult;
@ -18,6 +19,7 @@ import org.apache.kafka.common.TopicPartition;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaAdmin;
@ -25,6 +27,7 @@ import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
@ -69,6 +72,8 @@ public class SampleDataLoader implements ApplicationRunner {
private static final String PARTICIPANT_REGISTERED_TOPIC = "sample.participant.registered";
private static final String DISTRIBUTION_COMPLETED_TOPIC = "sample.distribution.completed";
private SampleDataConfig sampleDataConfig;
@Override
@Transactional
public void run(ApplicationArguments args) {
@ -104,28 +109,36 @@ public class SampleDataLoader implements ApplicationRunner {
}
try {
// 1. EventCreated 이벤트 발행 (3개 이벤트)
// JSON 파일에서 샘플 데이터 로드
log.info("📄 sample-data.json 파일 로드 중...");
sampleDataConfig = loadSampleData();
log.info("✅ sample-data.json 로드 완료: 이벤트 {}건, 배포 {}건, 참여자 패턴 {}건",
sampleDataConfig.getEvents().size(),
sampleDataConfig.getDistributions().size(),
sampleDataConfig.getParticipants().size());
// 1. EventCreated 이벤트 발행
publishEventCreatedEvents();
log.info("⏳ EventStats 생성 대기 중... (5초)");
Thread.sleep(5000); // EventCreatedConsumer가 EventStats 생성할 시간
// 2. DistributionCompleted 이벤트 발행 ( 이벤트당 4개 채널)
// 2. DistributionCompleted 이벤트 발행
publishDistributionCompletedEvents();
log.info("⏳ ChannelStats 생성 대기 중... (3초)");
Thread.sleep(3000); // DistributionCompletedConsumer가 ChannelStats 생성할 시간
// 3. ParticipantRegistered 이벤트 발행 ( 이벤트당 다수 참여자)
publishParticipantRegisteredEvents();
// 3. ParticipantRegistered 이벤트 발행
int totalParticipants = publishParticipantRegisteredEvents();
log.info("⏳ 참여자 등록 이벤트 처리 대기 중... (20초)");
Thread.sleep(20000); // ParticipantRegisteredConsumer가 180개 이벤트 처리할 시간 (비관적 고려)
Thread.sleep(20000); // ParticipantRegisteredConsumer가 이벤트 처리할 시간 (비관적 고려)
log.info("========================================");
log.info("🎉 Kafka 이벤트 발행 완료! (Consumer가 처리 중...)");
log.info("========================================");
log.info("발행된 이벤트:");
log.info(" - EventCreated: 3건");
log.info(" - DistributionCompleted: 3건 (각 이벤트당 4개 채널 배열)");
log.info(" - ParticipantRegistered: 180건 (MVP 테스트용)");
log.info(" - EventCreated: {}건", sampleDataConfig.getEvents().size());
log.info(" - DistributionCompleted: {}건", sampleDataConfig.getDistributions().size());
log.info(" - ParticipantRegistered: {}건", totalParticipants);
log.info("========================================");
// Consumer 처리 대기 (5초)
@ -220,189 +233,135 @@ public class SampleDataLoader implements ApplicationRunner {
}
/**
* EventCreated 이벤트 발행
* EventCreated 이벤트 발행 (JSON 기반)
*/
private void publishEventCreatedEvents() throws Exception {
// 이벤트 1: 신년맞이 할인 이벤트 (진행중, 높은 성과 - ROI 200%)
EventCreatedEvent event1 = EventCreatedEvent.builder()
.eventId("evt_2025012301")
.eventTitle("신년맞이 20% 할인 이벤트")
.storeId("store_001")
.totalInvestment(new BigDecimal("5000000"))
.expectedRevenue(new BigDecimal("15000000")) // 투자 대비 3배 수익
.status("ACTIVE")
.startDate(java.time.LocalDateTime.of(2025, 1, 23, 0, 0)) // 2025-01-23 시작
.endDate(null) // 진행중
.build();
publishEvent(EVENT_CREATED_TOPIC, event1);
for (EventData eventData : sampleDataConfig.getEvents()) {
EventCreatedEvent event = EventCreatedEvent.builder()
.eventId(eventData.getEventId())
.eventTitle(eventData.getEventTitle())
.storeId(eventData.getStoreId())
.totalInvestment(eventData.getTotalInvestment())
.expectedRevenue(eventData.getExpectedRevenue())
.status(eventData.getStatus())
.startDate(parseDateTime(eventData.getStartDate()))
.endDate(eventData.getEndDate() != null ? parseDateTime(eventData.getEndDate()) : null)
.build();
// 이벤트 2: 설날 특가 이벤트 (진행중, 중간 성과 - ROI 100%)
EventCreatedEvent event2 = EventCreatedEvent.builder()
.eventId("evt_2025012302")
.eventTitle("설날 특가 선물세트 이벤트")
.storeId("store_001")
.totalInvestment(new BigDecimal("3500000"))
.expectedRevenue(new BigDecimal("7000000")) // 투자 대비 2배 수익
.status("ACTIVE")
.startDate(java.time.LocalDateTime.of(2025, 2, 1, 0, 0)) // 2025-02-01 시작
.endDate(null) // 진행중
.build();
publishEvent(EVENT_CREATED_TOPIC, event2);
publishEvent(EVENT_CREATED_TOPIC, event);
log.info(" → EventCreated 발행: eventId={}, title={}",
eventData.getEventId(), eventData.getEventTitle());
}
// 이벤트 3: 겨울 신메뉴 런칭 이벤트 (종료, 저조한 성과 - ROI 50%)
EventCreatedEvent event3 = EventCreatedEvent.builder()
.eventId("evt_2025012303")
.eventTitle("겨울 신메뉴 런칭 이벤트")
.storeId("store_001")
.totalInvestment(new BigDecimal("2000000"))
.expectedRevenue(new BigDecimal("3000000")) // 투자 대비 1.5배 수익
.status("COMPLETED")
.startDate(java.time.LocalDateTime.of(2025, 1, 15, 0, 0)) // 2025-01-15 시작
.endDate(java.time.LocalDateTime.of(2025, 1, 31, 23, 59)) // 2025-01-31 종료
.build();
publishEvent(EVENT_CREATED_TOPIC, event3);
log.info("✅ EventCreated 이벤트 3건 발행 완료");
log.info("✅ EventCreated 이벤트 {}건 발행 완료", sampleDataConfig.getEvents().size());
}
/**
* DistributionCompleted 이벤트 발행 (설계서 기준 - 이벤트당 1번 발행, 여러 채널 배열)
* ISO 8601 형식 문자열을 LocalDateTime으로 파싱
*/
private java.time.LocalDateTime parseDateTime(String dateTimeStr) {
return java.time.LocalDateTime.parse(dateTimeStr);
}
/**
* DistributionCompleted 이벤트 발행 (JSON 기반)
*/
private void publishDistributionCompletedEvents() throws Exception {
String[] eventIds = {"evt_2025012301", "evt_2025012302", "evt_2025012303"};
int[][] expectedViews = {
{5000, 10000, 3000, 2000}, // 이벤트1: 우리동네TV, 지니TV, 링고비즈, SNS
{3500, 7000, 2000, 1500}, // 이벤트2
{1500, 3000, 1000, 500} // 이벤트3
};
double channelBudgetRatio = sampleDataConfig.getConfig().getChannelBudgetRatio();
// 이벤트의 투자 금액
BigDecimal[] totalInvestments = {
new BigDecimal("5000000"), // 이벤트1: 500만원
new BigDecimal("3500000"), // 이벤트2: 350만원
new BigDecimal("2000000") // 이벤트3: 200만원
};
for (DistributionData distributionData : sampleDataConfig.getDistributions()) {
String eventId = distributionData.getEventId();
// 채널 배포는 투자의 50% 사용 (나머지는 경품/콘텐츠/운영비용)
double channelBudgetRatio = 0.50;
// 해당 이벤트의 투자 금액 조회
EventData eventData = sampleDataConfig.getEvents().stream()
.filter(e -> e.getEventId().equals(eventId))
.findFirst()
.orElseThrow(() -> new IllegalStateException("이벤트를 찾을 수 없습니다: " + eventId));
// 채널별 비용 비율 (채널 예산 내에서: 우리동네TV 30%, 지니TV 30%, 링고비즈 25%, SNS 15%)
double[] costRatios = {0.30, 0.30, 0.25, 0.15};
for (int i = 0; i < eventIds.length; i++) {
String eventId = eventIds[i];
BigDecimal totalInvestment = totalInvestments[i];
// 채널 배포 예산: 투자의 50%
BigDecimal totalInvestment = eventData.getTotalInvestment();
BigDecimal channelBudget = totalInvestment.multiply(BigDecimal.valueOf(channelBudgetRatio));
// 4개 채널을 배열로
// 채널 배열 생성
List<DistributionCompletedEvent.ChannelDistribution> channels = new ArrayList<>();
// 1. 우리동네TV (TV) - 채널 예산의 30%
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
.channel("우리동네TV")
.channelType("TV")
.status("SUCCESS")
.expectedViews(expectedViews[i][0])
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(costRatios[0])))
.build());
for (ChannelData channelData : distributionData.getChannels()) {
DistributionCompletedEvent.ChannelDistribution channel =
DistributionCompletedEvent.ChannelDistribution.builder()
.channel(channelData.getChannel())
.channelType(channelData.getChannelType())
.status(channelData.getStatus())
.expectedViews(channelData.getExpectedViews())
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(channelData.getDistributionCostRatio())))
.build();
// 2. 지니TV (TV) - 채널 예산의 30%
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
.channel("지니TV")
.channelType("TV")
.status("SUCCESS")
.expectedViews(expectedViews[i][1])
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(costRatios[1])))
.build());
channels.add(channel);
}
// 3. 링고비즈 (CALL) - 채널 예산의 25%
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
.channel("링고비즈")
.channelType("CALL")
.status("SUCCESS")
.expectedViews(expectedViews[i][2])
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(costRatios[2])))
.build());
// 4. SNS (SNS) - 채널 예산의 15%
channels.add(DistributionCompletedEvent.ChannelDistribution.builder()
.channel("SNS")
.channelType("SNS")
.status("SUCCESS")
.expectedViews(expectedViews[i][3])
.distributionCost(channelBudget.multiply(BigDecimal.valueOf(costRatios[3])))
.build());
// 이벤트 발행 (채널 배열 포함)
// 이벤트 발행
DistributionCompletedEvent event = DistributionCompletedEvent.builder()
.eventId(eventId)
.distributedChannels(channels)
.completedAt(java.time.LocalDateTime.now())
.completedAt(parseDateTime(distributionData.getCompletedAt()))
.build();
publishEvent(DISTRIBUTION_COMPLETED_TOPIC, event);
log.info(" → DistributionCompleted 발행: eventId={}, 채널={}개",
eventId, channels.size());
}
log.info("✅ DistributionCompleted 이벤트 3건 발행 완료 (3 이벤트 × 4 채널 배열)");
log.info("✅ DistributionCompleted 이벤트 {}건 발행 완료", sampleDataConfig.getDistributions().size());
}
/**
* ParticipantRegistered 이벤트 발행
*
* 현실적인 참여 패턴 반영:
* - 120명의 고유 참여자 생성
* - 일부 참여자는 여러 이벤트에 중복 참여
* - 이벤트1: 100명 (user001~user100)
* - 이벤트2: 50명 (user051~user100) 50명이 이벤트1과 중복
* - 이벤트3: 30명 (user071~user100) 30명이 이전 이벤트들과 중복
* ParticipantRegistered 이벤트 발행 (JSON 기반)
*/
private void publishParticipantRegisteredEvents() throws Exception {
String[] eventIds = {"evt_2025012301", "evt_2025012302", "evt_2025012303"};
String[] channels = {"우리동네TV", "지니TV", "링고비즈", "SNS"};
// 이벤트별 참여자 범위 (중복 참여 반영)
int[][] participantRanges = {
{1, 100}, // 이벤트1: user001~user100 (100명)
{51, 100}, // 이벤트2: user051~user100 (50명, 이벤트1과 50명 중복)
{71, 100} // 이벤트3: user071~user100 (30명, 모두 중복)
};
private int publishParticipantRegisteredEvents() throws Exception {
String participantIdPrefix = sampleDataConfig.getConfig().getParticipantIdPrefix();
int participantIdPadding = sampleDataConfig.getConfig().getParticipantIdPadding();
int totalPublished = 0;
for (int i = 0; i < eventIds.length; i++) {
String eventId = eventIds[i];
int startUser = participantRanges[i][0];
int endUser = participantRanges[i][1];
for (ParticipantData participantData : sampleDataConfig.getParticipants()) {
String eventId = participantData.getEventId();
int startUser = participantData.getParticipantRange().getStart();
int endUser = participantData.getParticipantRange().getEnd();
int eventParticipants = endUser - startUser + 1;
log.info("이벤트 {} 참여자 발행 시작: user{:03d}~user{:03d} ({}명)",
eventId, startUser, endUser, eventParticipants);
log.info("이벤트 {} 참여자 발행 시작: {}{:0" + participantIdPadding + "d}~{}{:0" + participantIdPadding + "d} ({}명)",
eventId, participantIdPrefix, startUser, participantIdPrefix, endUser, eventParticipants);
// 채널별 가중치 누적 합계 계산 (: SNS=45, 우리동네TV=70, 지니TV=90, 링고비즈=100)
Map<String, Integer> channelWeights = participantData.getChannelWeights();
List<String> channels = new ArrayList<>(channelWeights.keySet());
int[] cumulativeWeights = new int[channels.size()];
int cumulative = 0;
for (int i = 0; i < channels.size(); i++) {
cumulative += channelWeights.get(channels.get(i));
cumulativeWeights[i] = cumulative;
}
// 참여자에 대해 ParticipantRegistered 이벤트 발행
for (int userId = startUser; userId <= endUser; userId++) {
String participantId = String.format("user%03d", userId); // user001, user002, ...
String participantId = String.format("%s%0" + participantIdPadding + "d",
participantIdPrefix, userId);
// 채널별 가중치 기반 랜덤 배정
// 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%
int randomValue = random.nextInt(cumulative);
String channel = channels.get(0); // 기본값
for (int i = 0; i < cumulativeWeights.length; i++) {
if (randomValue < cumulativeWeights[i]) {
channel = channels.get(i);
break;
}
}
ParticipantRegisteredEvent event = ParticipantRegisteredEvent.builder()
.eventId(eventId)
.participantId(participantId)
.channel(channel)
.build();
.eventId(eventId)
.participantId(participantId)
.channel(channel)
.build();
publishEvent(PARTICIPANT_REGISTERED_TOPIC, event);
totalPublished++;
@ -418,24 +377,13 @@ public class SampleDataLoader implements ApplicationRunner {
log.info("========================================");
log.info("✅ ParticipantRegistered 이벤트 {}건 발행 완료", totalPublished);
log.info("📊 참여 패턴:");
log.info(" - 총 고유 참여자: 100명 (user001~user100)");
log.info(" - 이벤트1 참여: 100명");
log.info(" - 이벤트2 참여: 50명 (이벤트1과 50명 중복)");
log.info(" - 이벤트3 참여: 30명 (이벤트1,2와 모두 중복)");
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("========================================");
return totalPublished;
}
/**
* TimelineData 생성 (시간대별 샘플 데이터)
* TimelineData 생성 (시간대별 샘플 데이터) - JSON 기반
*
* - 이벤트마다 30일 × 24시간 = 720시간 hourly 데이터 생성
* - interval=hourly: 시간별 표시 (최근 7일 적합)
@ -445,24 +393,32 @@ public class SampleDataLoader implements ApplicationRunner {
private void createTimelineData() {
log.info("📊 TimelineData 생성 시작...");
String[] eventIds = {"evt_2025012301", "evt_2025012302", "evt_2025012303"};
// 이벤트별 시간당 기준 참여자 계산 (참여자 범위 기반)
List<EventData> events = sampleDataConfig.getEvents();
List<ParticipantData> participants = sampleDataConfig.getParticipants();
// 이벤트별 시간당 기준 참여자 (이벤트 성과에 따라 다름)
int[] baseParticipantsPerHour = {4, 2, 1}; // 이벤트1(높음), 이벤트2(중간), 이벤트3(낮음)
for (int eventIndex = 0; eventIndex < events.size(); eventIndex++) {
EventData event = events.get(eventIndex);
String eventId = event.getEventId();
for (int eventIndex = 0; eventIndex < eventIds.length; eventIndex++) {
String eventId = eventIds[eventIndex];
int baseParticipant = baseParticipantsPerHour[eventIndex];
// 해당 이벤트의 참여자 계산
ParticipantData participantData = participants.stream()
.filter(p -> p.getEventId().equals(eventId))
.findFirst()
.orElse(null);
int totalParticipants = 100; // 기본값
if (participantData != null) {
totalParticipants = participantData.getParticipantRange().getEnd()
- participantData.getParticipantRange().getStart() + 1;
}
// 30일 × 24시간 = 720시간 데이터로 나눔
int baseParticipant = Math.max(1, totalParticipants / (30 * 24));
int cumulativeParticipants = 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
// 이벤트 시작일부터 30일 hourly 데이터 생성
java.time.LocalDateTime startDate = java.time.LocalDateTime.of(year, month, day, 0, 0);
// 이벤트 시작일 파싱
java.time.LocalDateTime startDate = parseDateTime(event.getStartDate());
for (int dayOffset = 0; dayOffset < 30; dayOffset++) {
for (int hour = 0; hour < 24; hour++) {
@ -485,25 +441,26 @@ public class SampleDataLoader implements ApplicationRunner {
// TimelineData 생성
com.kt.event.analytics.entity.TimelineData timelineData =
com.kt.event.analytics.entity.TimelineData.builder()
.eventId(eventId)
.timestamp(timestamp)
.participants(hourlyParticipants)
.views(hourlyViews)
.engagement(hourlyEngagement)
.conversions(hourlyConversions)
.cumulativeParticipants(cumulativeParticipants)
.build();
com.kt.event.analytics.entity.TimelineData.builder()
.eventId(eventId)
.timestamp(timestamp)
.participants(hourlyParticipants)
.views(hourlyViews)
.engagement(hourlyEngagement)
.conversions(hourlyConversions)
.cumulativeParticipants(cumulativeParticipants)
.build();
timelineDataRepository.save(timelineData);
}
}
log.info("✅ TimelineData 생성 완료: eventId={}, 시작일={}-{:02d}-{:02d}, 30일 × 24시간 = 720건",
eventId, year, month, day);
log.info("✅ TimelineData 생성 완료: eventId={}, 시작일={}, 30일 × 24시간 = 720건",
eventId, startDate.toLocalDate());
}
log.info("✅ 전체 TimelineData 생성 완료: 3개 이벤트 × 30일 × 24시간 = 2,160건");
log.info("✅ 전체 TimelineData 생성 완료: {}개 이벤트 × 30일 × 24시간 = {}건",
events.size(), events.size() * 30 * 24);
}
/**
@ -513,4 +470,73 @@ public class SampleDataLoader implements ApplicationRunner {
String jsonMessage = objectMapper.writeValueAsString(event);
kafkaTemplate.send(topic, jsonMessage);
}
/**
* JSON 파일에서 샘플 데이터 로드
*/
private SampleDataConfig loadSampleData() throws IOException {
ClassPathResource resource = new ClassPathResource("sample-data.json");
return objectMapper.readValue(resource.getInputStream(), SampleDataConfig.class);
}
// ========================================
// JSON 데이터 구조 (Inner Classes)
// ========================================
@Data
static class SampleDataConfig {
private List<EventData> events;
private List<DistributionData> distributions;
private List<ParticipantData> participants;
private ConfigData config;
}
@Data
static class EventData {
private String eventId;
private String eventTitle;
private String storeId;
private BigDecimal totalInvestment;
private BigDecimal expectedRevenue;
private String status;
private String startDate; // ISO 8601 형식: "2025-01-23T00:00:00"
private String endDate; // null 가능
private String createdAt;
}
@Data
static class DistributionData {
private String eventId;
private String completedAt;
private List<ChannelData> channels;
}
@Data
static class ChannelData {
private String channel;
private String channelType;
private String status;
private Integer expectedViews;
private Double distributionCostRatio;
}
@Data
static class ParticipantData {
private String eventId;
private ParticipantRange participantRange;
private Map<String, Integer> channelWeights;
}
@Data
static class ParticipantRange {
private Integer start;
private Integer end;
}
@Data
static class ConfigData {
private Double channelBudgetRatio;
private String participantIdPrefix;
private Integer participantIdPadding;
}
}

View File

@ -0,0 +1,187 @@
{
"events": [
{
"eventId": "evt_2025012301",
"eventTitle": "신규 고객 환영 이벤트",
"storeId": "store_001",
"totalInvestment": 5000000,
"expectedRevenue": 15000000,
"status": "ACTIVE",
"startDate": "2025-01-23T00:00:00",
"endDate": "2025-02-23T23:59:59",
"createdAt": "2025-01-23T10:00:00"
},
{
"eventId": "evt_2025011502",
"eventTitle": "재방문 고객 감사 이벤트",
"storeId": "store_001",
"totalInvestment": 3500000,
"expectedRevenue": 7000000,
"status": "ACTIVE",
"startDate": "2025-01-15T00:00:00",
"endDate": "2025-02-15T23:59:59",
"createdAt": "2025-01-15T14:30:00"
},
{
"eventId": "evt_2025010803",
"eventTitle": "신년 특별 할인 이벤트",
"storeId": "store_001",
"totalInvestment": 2000000,
"expectedRevenue": 3000000,
"status": "COMPLETED",
"startDate": "2025-01-01T00:00:00",
"endDate": "2025-01-08T23:59:00",
"createdAt": "2024-12-28T09:00:00"
}
],
"distributions": [
{
"eventId": "evt_2025012301",
"completedAt": "2025-01-23T12:00:00",
"channels": [
{
"channel": "우리동네TV",
"channelType": "TV",
"status": "SUCCESS",
"expectedViews": 5000,
"distributionCostRatio": 0.30
},
{
"channel": "지니TV",
"channelType": "TV",
"status": "SUCCESS",
"expectedViews": 10000,
"distributionCostRatio": 0.30
},
{
"channel": "링고비즈",
"channelType": "CALL",
"status": "SUCCESS",
"expectedViews": 3000,
"distributionCostRatio": 0.25
},
{
"channel": "SNS",
"channelType": "SNS",
"status": "SUCCESS",
"expectedViews": 2000,
"distributionCostRatio": 0.15
}
]
},
{
"eventId": "evt_2025011502",
"completedAt": "2025-02-01T12:00:00",
"channels": [
{
"channel": "우리동네TV",
"channelType": "TV",
"status": "SUCCESS",
"expectedViews": 3500,
"distributionCostRatio": 0.30
},
{
"channel": "지니TV",
"channelType": "TV",
"status": "SUCCESS",
"expectedViews": 7000,
"distributionCostRatio": 0.30
},
{
"channel": "링고비즈",
"channelType": "CALL",
"status": "SUCCESS",
"expectedViews": 2000,
"distributionCostRatio": 0.25
},
{
"channel": "SNS",
"channelType": "SNS",
"status": "SUCCESS",
"expectedViews": 1500,
"distributionCostRatio": 0.15
}
]
},
{
"eventId": "evt_2025010803",
"completedAt": "2025-01-15T12:00:00",
"channels": [
{
"channel": "우리동네TV",
"channelType": "TV",
"status": "SUCCESS",
"expectedViews": 1500,
"distributionCostRatio": 0.30
},
{
"channel": "지니TV",
"channelType": "TV",
"status": "SUCCESS",
"expectedViews": 3000,
"distributionCostRatio": 0.30
},
{
"channel": "링고비즈",
"channelType": "CALL",
"status": "SUCCESS",
"expectedViews": 1000,
"distributionCostRatio": 0.25
},
{
"channel": "SNS",
"channelType": "SNS",
"status": "SUCCESS",
"expectedViews": 500,
"distributionCostRatio": 0.15
}
]
}
],
"participants": [
{
"eventId": "evt_2025012301",
"participantRange": {
"start": 1,
"end": 100
},
"channelWeights": {
"SNS": 45,
"우리동네TV": 25,
"지니TV": 20,
"링고비즈": 10
}
},
{
"eventId": "evt_2025011502",
"participantRange": {
"start": 51,
"end": 100
},
"channelWeights": {
"SNS": 45,
"우리동네TV": 25,
"지니TV": 20,
"링고비즈": 10
}
},
{
"eventId": "evt_2025010803",
"participantRange": {
"start": 71,
"end": 100
},
"channelWeights": {
"SNS": 45,
"우리동네TV": 25,
"지니TV": 20,
"링고비즈": 10
}
}
],
"config": {
"channelBudgetRatio": 0.50,
"participantIdPrefix": "user",
"participantIdPadding": 3
}
}