Merge pull request #30 from ktds-dg0501/feature/analytics

이벤트별 성과분석 날짜 로직 수정 및 설정 개선
This commit is contained in:
Hyowon Yang 2025-10-30 12:54:56 +09:00 committed by GitHub
commit a3781a279a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 49 additions and 19 deletions

View File

@ -24,7 +24,7 @@
<!-- Kafka Configuration (원격 서버) --> <!-- Kafka Configuration (원격 서버) -->
<entry key="KAFKA_ENABLED" value="true" /> <entry key="KAFKA_ENABLED" value="true" />
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" /> <entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
<entry key="KAFKA_CONSUMER_GROUP_ID" value="analytics-service-consumers" /> <entry key="KAFKA_CONSUMER_GROUP_ID" value="analytics-service-consumers-v3" />
<!-- Sample Data Configuration (MVP Only) --> <!-- Sample Data Configuration (MVP Only) -->
<!-- ⚠️ Kafka Producer로 이벤트 발행 (Consumer가 처리) --> <!-- ⚠️ Kafka Producer로 이벤트 발행 (Consumer가 처리) -->

View File

@ -23,7 +23,7 @@ public class KafkaConsumerConfig {
@Value("${spring.kafka.bootstrap-servers}") @Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers; private String bootstrapServers;
@Value("${spring.kafka.consumer.group-id:analytics-service}") @Value("${spring.kafka.consumer.group-id:analytics-service-consumers-v3}")
private String groupId; private String groupId;
@Bean @Bean

View File

@ -19,6 +19,7 @@ import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaAdmin; import org.springframework.kafka.core.KafkaAdmin;
import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -60,7 +61,8 @@ public class SampleDataLoader implements ApplicationRunner {
private final Random random = new Random(); private final Random random = new Random();
private static final String CONSUMER_GROUP_ID = "analytics-service-consumers-v3"; @Value("${spring.kafka.consumer.group-id}")
private String consumerGroupId;
// Kafka Topic Names (MVP용 샘플 토픽) // Kafka Topic Names (MVP용 샘플 토픽)
private static final String EVENT_CREATED_TOPIC = "sample.event.created"; private static final String EVENT_CREATED_TOPIC = "sample.event.created";
@ -181,7 +183,7 @@ public class SampleDataLoader implements ApplicationRunner {
*/ */
private void resetConsumerOffsets() { private void resetConsumerOffsets() {
try (AdminClient adminClient = AdminClient.create(kafkaAdmin.getConfigurationProperties())) { try (AdminClient adminClient = AdminClient.create(kafkaAdmin.getConfigurationProperties())) {
log.info("🔄 Kafka Consumer Offset 리셋 시작: group={}", CONSUMER_GROUP_ID); log.info("🔄 Kafka Consumer Offset 리셋 시작: group={}", consumerGroupId);
// 모든 토픽의 offset 삭제 // 모든 토픽의 offset 삭제
Set<TopicPartition> partitions = new HashSet<>(); Set<TopicPartition> partitions = new HashSet<>();
@ -195,7 +197,7 @@ public class SampleDataLoader implements ApplicationRunner {
// Consumer Group Offset 삭제 // Consumer Group Offset 삭제
DeleteConsumerGroupOffsetsResult result = adminClient.deleteConsumerGroupOffsets( DeleteConsumerGroupOffsetsResult result = adminClient.deleteConsumerGroupOffsets(
CONSUMER_GROUP_ID, consumerGroupId,
partitions partitions
); );
@ -224,6 +226,8 @@ public class SampleDataLoader implements ApplicationRunner {
.totalInvestment(new BigDecimal("5000000")) .totalInvestment(new BigDecimal("5000000"))
.expectedRevenue(new BigDecimal("15000000")) // 투자 대비 3배 수익 .expectedRevenue(new BigDecimal("15000000")) // 투자 대비 3배 수익
.status("ACTIVE") .status("ACTIVE")
.startDate(java.time.LocalDateTime.of(2025, 1, 23, 0, 0)) // 2025-01-23 시작
.endDate(null) // 진행중
.build(); .build();
publishEvent(EVENT_CREATED_TOPIC, event1); publishEvent(EVENT_CREATED_TOPIC, event1);
@ -235,6 +239,8 @@ public class SampleDataLoader implements ApplicationRunner {
.totalInvestment(new BigDecimal("3500000")) .totalInvestment(new BigDecimal("3500000"))
.expectedRevenue(new BigDecimal("7000000")) // 투자 대비 2배 수익 .expectedRevenue(new BigDecimal("7000000")) // 투자 대비 2배 수익
.status("ACTIVE") .status("ACTIVE")
.startDate(java.time.LocalDateTime.of(2025, 2, 1, 0, 0)) // 2025-02-01 시작
.endDate(null) // 진행중
.build(); .build();
publishEvent(EVENT_CREATED_TOPIC, event2); publishEvent(EVENT_CREATED_TOPIC, event2);
@ -246,6 +252,8 @@ public class SampleDataLoader implements ApplicationRunner {
.totalInvestment(new BigDecimal("2000000")) .totalInvestment(new BigDecimal("2000000"))
.expectedRevenue(new BigDecimal("3000000")) // 투자 대비 1.5배 수익 .expectedRevenue(new BigDecimal("3000000")) // 투자 대비 1.5배 수익
.status("COMPLETED") .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(); .build();
publishEvent(EVENT_CREATED_TOPIC, event3); publishEvent(EVENT_CREATED_TOPIC, event3);

View File

@ -97,6 +97,18 @@ public class EventStats extends BaseTimeEntity {
@Column(length = 20) @Column(length = 20)
private String status; private String status;
/**
* 이벤트 시작일
*/
@Column(name = "start_date")
private java.time.LocalDateTime startDate;
/**
* 이벤트 종료일 (null이면 진행중)
*/
@Column(name = "end_date")
private java.time.LocalDateTime endDate;
/** /**
* 참여자 증가 * 참여자 증가
*/ */

View File

@ -64,11 +64,13 @@ public class EventCreatedConsumer {
.totalInvestment(event.getTotalInvestment()) .totalInvestment(event.getTotalInvestment())
.expectedRevenue(event.getExpectedRevenue() != null ? event.getExpectedRevenue() : BigDecimal.ZERO) .expectedRevenue(event.getExpectedRevenue() != null ? event.getExpectedRevenue() : BigDecimal.ZERO)
.status(event.getStatus()) .status(event.getStatus())
.startDate(event.getStartDate())
.endDate(event.getEndDate())
.build(); .build();
eventStatsRepository.save(eventStats); eventStatsRepository.save(eventStats);
log.info("✅ 이벤트 통계 초기화 완료: eventId={}, userId={}, expectedRevenue={}", log.info("✅ 이벤트 통계 초기화 완료: eventId={}, userId={}, startDate={}, endDate={}",
eventId, eventStats.getUserId(), event.getExpectedRevenue()); eventId, eventStats.getUserId(), event.getStartDate(), event.getEndDate());
// 3. 캐시 무효화 (다음 조회 최신 데이터 반영) // 3. 캐시 무효화 (다음 조회 최신 데이터 반영)
String cacheKey = CACHE_KEY_PREFIX + eventId; String cacheKey = CACHE_KEY_PREFIX + eventId;

View File

@ -6,6 +6,7 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime;
/** /**
* 이벤트 생성 이벤트 * 이벤트 생성 이벤트
@ -45,4 +46,14 @@ public class EventCreatedEvent {
* 이벤트 상태 * 이벤트 상태
*/ */
private String status; private String status;
/**
* 이벤트 시작일
*/
private LocalDateTime startDate;
/**
* 이벤트 종료일 (null이면 진행중)
*/
private LocalDateTime endDate;
} }

View File

@ -146,11 +146,12 @@ public class AnalyticsService {
} }
/** /**
* 기간 정보 구성 (이벤트 생성일 ~ 현재) * 기간 정보 구성 (이벤트 시작일 ~ 종료일 또는 현재)
*/ */
private PeriodInfo buildPeriodInfo(EventStats eventStats) { private PeriodInfo buildPeriodInfo(EventStats eventStats) {
LocalDateTime start = eventStats.getCreatedAt(); LocalDateTime start = eventStats.getStartDate();
LocalDateTime end = LocalDateTime.now(); LocalDateTime end = eventStats.getEndDate() != null ?
eventStats.getEndDate() : LocalDateTime.now();
long durationDays = ChronoUnit.DAYS.between(start, end); long durationDays = ChronoUnit.DAYS.between(start, end);

View File

@ -301,20 +301,17 @@ public class UserAnalyticsService {
/** /**
* 기간 정보 구성 * 기간 정보 구성
*/ *
/** * 전체 이벤트 가장 빠른 시작일 ~ 현재까지의 기간 계산
* 전체 이벤트의 생성/수정 시간 기반으로 period 계산
*/ */
private PeriodInfo buildPeriodFromEvents(List<EventStats> events) { private PeriodInfo buildPeriodFromEvents(List<EventStats> events) {
LocalDateTime start = events.stream() LocalDateTime start = events.stream()
.map(EventStats::getCreatedAt) .map(EventStats::getStartDate)
.filter(Objects::nonNull)
.min(LocalDateTime::compareTo) .min(LocalDateTime::compareTo)
.orElse(LocalDateTime.now()); .orElse(LocalDateTime.now());
LocalDateTime end = events.stream() LocalDateTime end = LocalDateTime.now();
.map(EventStats::getUpdatedAt)
.max(LocalDateTime::compareTo)
.orElse(LocalDateTime.now());
return PeriodInfo.builder() return PeriodInfo.builder()
.startDate(start) .startDate(start)

View File

@ -80,7 +80,6 @@ server:
charset: UTF-8 charset: UTF-8
enabled: true enabled: true
force: true force: true
context-path: /api/v1/analytics
# JWT # JWT
jwt: jwt: